use sqlite for sync state; use sync.ts lib instead of scripts
							parent
							
								
									ad66397639
								
							
						
					
					
						commit
						e0a2bc395e
					
				@ -0,0 +1,2 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
kubectl exec -it -n clickhouse clickhouse -- clickhouse-client -u avraham --password buginoo
 | 
			
		||||
											
												
													File diff suppressed because it is too large
													Load Diff
												
											
										
									
								@ -1,4 +1,4 @@
 | 
			
		||||
import _ from "./env.js";
 | 
			
		||||
import _ from "../env.js";
 | 
			
		||||
import { createClient as createClickhouseClient } from "@clickhouse/client";
 | 
			
		||||
import type { DataFormat } from "@clickhouse/client";
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,138 @@
 | 
			
		||||
// import pThrottle from "p-throttle";
 | 
			
		||||
import pRetry from "p-retry";
 | 
			
		||||
 | 
			
		||||
const apiKey = "H95NTsatM1iTWLUwDLxM2J5zhUVYdCEz";
 | 
			
		||||
// export const getApiKey = pThrottle({ limit: 5, interval: 60000 })(() => apiKey);
 | 
			
		||||
export const getApiKey = () => apiKey;
 | 
			
		||||
 | 
			
		||||
export const optionContractToTicker = ({
 | 
			
		||||
  symbol,
 | 
			
		||||
  expirationDate,
 | 
			
		||||
  strike,
 | 
			
		||||
  type,
 | 
			
		||||
}: {
 | 
			
		||||
  symbol: string;
 | 
			
		||||
  expirationDate: string;
 | 
			
		||||
  strike: number;
 | 
			
		||||
  type: "call" | "put";
 | 
			
		||||
}) =>
 | 
			
		||||
  `O:${symbol}${expirationDate.substring(2, 4)}${expirationDate.substring(
 | 
			
		||||
    5,
 | 
			
		||||
    7
 | 
			
		||||
  )}${expirationDate.substring(8, 10)}${
 | 
			
		||||
    type === "call" ? "C" : "P"
 | 
			
		||||
  }${Math.floor(strike * 1000)
 | 
			
		||||
    .toString()
 | 
			
		||||
    .padStart(8, "0")}`;
 | 
			
		||||
 | 
			
		||||
type PolygonOptionContractsResponse = {
 | 
			
		||||
  next_url?: string;
 | 
			
		||||
  results: Array<{
 | 
			
		||||
    ticker: string;
 | 
			
		||||
    expiration_date: string;
 | 
			
		||||
    strike_price: number;
 | 
			
		||||
    contract_type: "call" | "put";
 | 
			
		||||
  }>;
 | 
			
		||||
};
 | 
			
		||||
export async function* makeGetOptionContractsIterator(
 | 
			
		||||
  symbol: string,
 | 
			
		||||
  date: string
 | 
			
		||||
) {
 | 
			
		||||
  let latestBatchResponse = (await (
 | 
			
		||||
    await fetch(
 | 
			
		||||
      `https://api.polygon.io/v3/reference/options/contracts?underlying_ticker=${symbol}&as_of=${date}&sort=ticker&limit=1000&apiKey=${await getApiKey()}`
 | 
			
		||||
    )
 | 
			
		||||
  ).json()) as PolygonOptionContractsResponse;
 | 
			
		||||
  yield latestBatchResponse.results.map((result) => ({
 | 
			
		||||
    asOfDate: date,
 | 
			
		||||
    symbol,
 | 
			
		||||
    expirationDate: result.expiration_date,
 | 
			
		||||
    strike: result.strike_price,
 | 
			
		||||
    type: result.contract_type,
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  // as long as there's a `next_url`, call that:
 | 
			
		||||
  while (latestBatchResponse.hasOwnProperty("next_url")) {
 | 
			
		||||
    latestBatchResponse = (await (
 | 
			
		||||
      await fetch(`${latestBatchResponse.next_url}&apiKey=${await getApiKey()}`)
 | 
			
		||||
    ).json()) as PolygonOptionContractsResponse;
 | 
			
		||||
    yield latestBatchResponse.results?.map((result) => ({
 | 
			
		||||
      asOfDate: date,
 | 
			
		||||
      symbol,
 | 
			
		||||
      expirationDate: result.expiration_date,
 | 
			
		||||
      strike: result.strike_price,
 | 
			
		||||
      type: result.contract_type,
 | 
			
		||||
    })) || [];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PolygonOptionContractAggregatesResponse = {
 | 
			
		||||
  next_url?: string;
 | 
			
		||||
  status: string;
 | 
			
		||||
  resultsCount: number;
 | 
			
		||||
  results: Array<{
 | 
			
		||||
    c: number;
 | 
			
		||||
    h: number;
 | 
			
		||||
    n: number;
 | 
			
		||||
    l: number;
 | 
			
		||||
    o: number;
 | 
			
		||||
    t: number;
 | 
			
		||||
    v: number;
 | 
			
		||||
    vw: number;
 | 
			
		||||
  }>;
 | 
			
		||||
};
 | 
			
		||||
export type OptionContract = {
 | 
			
		||||
  symbol: string;
 | 
			
		||||
  expirationDate: string;
 | 
			
		||||
  strike: number;
 | 
			
		||||
  type: "call" | "put";
 | 
			
		||||
};
 | 
			
		||||
export async function* makeGetOptionContractAggregatesIterator({
 | 
			
		||||
  symbol,
 | 
			
		||||
  expirationDate,
 | 
			
		||||
  strike,
 | 
			
		||||
  type,
 | 
			
		||||
  firstDate,
 | 
			
		||||
}: OptionContract & { firstDate: string }) {
 | 
			
		||||
  const optionContractTicker = optionContractToTicker({
 | 
			
		||||
    symbol: symbol,
 | 
			
		||||
    expirationDate,
 | 
			
		||||
    strike,
 | 
			
		||||
    type,
 | 
			
		||||
  });
 | 
			
		||||
  const expirationDateAsDateObject = new Date(expirationDate);
 | 
			
		||||
  const currentDateAsDateObject = new Date(firstDate);
 | 
			
		||||
  while (currentDateAsDateObject <= expirationDateAsDateObject) {
 | 
			
		||||
    const asOfDate = currentDateAsDateObject.toISOString().substring(0, 10);
 | 
			
		||||
    let latestBatchResponse = await pRetry(
 | 
			
		||||
      async () =>
 | 
			
		||||
        (await (
 | 
			
		||||
          await fetch(
 | 
			
		||||
            `https://api.polygon.io/v2/aggs/ticker/${optionContractTicker}/range/1/minute/${asOfDate}/${asOfDate}?adjusted=false&sort=asc&limit=50000&apiKey=${await getApiKey()}`
 | 
			
		||||
          )
 | 
			
		||||
        ).json()) as PolygonOptionContractAggregatesResponse,
 | 
			
		||||
      { retries: 5, factor: 2, minTimeout: 1000, maxTimeout: 60 * 1000 }
 | 
			
		||||
    );
 | 
			
		||||
    if (latestBatchResponse.status.toLowerCase() !== "ok") {
 | 
			
		||||
      console.error(latestBatchResponse);
 | 
			
		||||
      throw new Error(
 | 
			
		||||
        `error fetching option contract aggregate ${optionContractTicker}`
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    yield latestBatchResponse.results?.map((result) => ({
 | 
			
		||||
      symbol,
 | 
			
		||||
      expirationDate,
 | 
			
		||||
      strike,
 | 
			
		||||
      type,
 | 
			
		||||
 | 
			
		||||
      tsStart: (result.t || 0) / 1000,
 | 
			
		||||
      open: result.o,
 | 
			
		||||
      close: result.c,
 | 
			
		||||
      low: result.l,
 | 
			
		||||
      high: result.h,
 | 
			
		||||
    })) || [];
 | 
			
		||||
    currentDateAsDateObject.setUTCDate(
 | 
			
		||||
      currentDateAsDateObject.getUTCDate() + 1
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,243 @@
 | 
			
		||||
import * as Polygon from "./polygon.js";
 | 
			
		||||
import sqlite3 from "sqlite3";
 | 
			
		||||
import { open } from "sqlite";
 | 
			
		||||
import { clickhouse, query } from "./clickhouse.js";
 | 
			
		||||
import { OptionContract } from "./polygon.js";
 | 
			
		||||
 | 
			
		||||
const sqliteDb = await open({
 | 
			
		||||
  filename: "/tmp/sync-state.db",
 | 
			
		||||
  driver: sqlite3.Database,
 | 
			
		||||
});
 | 
			
		||||
await sqliteDb.exec(`
 | 
			
		||||
  CREATE TABLE IF NOT EXISTS option_contract_sync_states (
 | 
			
		||||
    symbol TEXT,
 | 
			
		||||
    date TEXT,
 | 
			
		||||
    status TINYINT,
 | 
			
		||||
    UNIQUE (symbol, date) ON CONFLICT REPLACE
 | 
			
		||||
  )
 | 
			
		||||
`);
 | 
			
		||||
await sqliteDb.exec(`
 | 
			
		||||
  CREATE TABLE IF NOT EXISTS option_contract_aggregates_sync_states (
 | 
			
		||||
    ticker TEXT,
 | 
			
		||||
    status TINYINT,
 | 
			
		||||
    UNIQUE (ticker) ON CONFLICT REPLACE
 | 
			
		||||
  )
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
export async function getPullOptionContractsState(
 | 
			
		||||
  symbol: string,
 | 
			
		||||
  date: string
 | 
			
		||||
) {
 | 
			
		||||
  const state = await sqliteDb.get(
 | 
			
		||||
    `
 | 
			
		||||
    SELECT
 | 
			
		||||
      *
 | 
			
		||||
    FROM option_contract_sync_states
 | 
			
		||||
    WHERE symbol = :symbol
 | 
			
		||||
    AND date = :date
 | 
			
		||||
  `,
 | 
			
		||||
    {
 | 
			
		||||
      ":symbol": symbol,
 | 
			
		||||
      ":date": date,
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
  return state;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const enum OptionContractSyncStatus {
 | 
			
		||||
  STARTED = 0,
 | 
			
		||||
  COMPLETED = 1,
 | 
			
		||||
}
 | 
			
		||||
type OptionContractSyncState = {
 | 
			
		||||
  status: OptionContractSyncStatus;
 | 
			
		||||
};
 | 
			
		||||
export async function setPullOptionContractsState(
 | 
			
		||||
  symbol: string,
 | 
			
		||||
  date: string,
 | 
			
		||||
  state: OptionContractSyncState
 | 
			
		||||
) {
 | 
			
		||||
  await sqliteDb.run(
 | 
			
		||||
    `
 | 
			
		||||
    INSERT INTO option_contract_sync_states (symbol, date, status) VALUES (:symbol, :date, :status)
 | 
			
		||||
  `,
 | 
			
		||||
    {
 | 
			
		||||
      ":symbol": symbol,
 | 
			
		||||
      ":date": date,
 | 
			
		||||
      ":status": state.status,
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getPullOptionContractAggregatesState(ticker: string) {
 | 
			
		||||
  const state = await sqliteDb.get(
 | 
			
		||||
    `
 | 
			
		||||
    SELECT
 | 
			
		||||
      *
 | 
			
		||||
    FROM option_contract_aggregates_sync_states
 | 
			
		||||
    WHERE ticker = :ticker
 | 
			
		||||
  `,
 | 
			
		||||
    {
 | 
			
		||||
      ":ticker": ticker,
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
  return state;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const enum OptionContractAggregatesSyncStatus {
 | 
			
		||||
  STARTED = 0,
 | 
			
		||||
  COMPLETED = 1,
 | 
			
		||||
}
 | 
			
		||||
type OptionContractAggregatesSyncState = {
 | 
			
		||||
  status: OptionContractAggregatesSyncStatus;
 | 
			
		||||
};
 | 
			
		||||
export async function setPullOptionContractAggregatesState(
 | 
			
		||||
  ticker: string,
 | 
			
		||||
  state: OptionContractAggregatesSyncState
 | 
			
		||||
) {
 | 
			
		||||
  await sqliteDb.run(
 | 
			
		||||
    `
 | 
			
		||||
    INSERT INTO option_contract_aggregates_sync_states (ticker, status) VALUES (:ticker, :status)
 | 
			
		||||
  `,
 | 
			
		||||
    {
 | 
			
		||||
      ":ticker": ticker,
 | 
			
		||||
      ":status": state.status,
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function pullOptionContracts(symbol: string, date: string) {
 | 
			
		||||
  // check if sync was completed:
 | 
			
		||||
  if (
 | 
			
		||||
    (await getPullOptionContractsState(symbol, date))?.status !==
 | 
			
		||||
    OptionContractSyncStatus.COMPLETED
 | 
			
		||||
  ) {
 | 
			
		||||
    await setPullOptionContractsState(symbol, date, {
 | 
			
		||||
      status: OptionContractSyncStatus.STARTED,
 | 
			
		||||
    });
 | 
			
		||||
    for await (const batch of Polygon.makeGetOptionContractsIterator(
 | 
			
		||||
      symbol,
 | 
			
		||||
      date
 | 
			
		||||
    )) {
 | 
			
		||||
      console.log(batch.length);
 | 
			
		||||
      await clickhouse.insert({
 | 
			
		||||
        table: "option_contract_existences",
 | 
			
		||||
        values: batch,
 | 
			
		||||
        format: "JSONEachRow",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    await setPullOptionContractsState(symbol, date, {
 | 
			
		||||
      status: OptionContractSyncStatus.COMPLETED,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function pullOptionContractAggregates(
 | 
			
		||||
  optionContract: OptionContract
 | 
			
		||||
) {
 | 
			
		||||
  const ticker = Polygon.optionContractToTicker(optionContract);
 | 
			
		||||
  // check if sync was completed:
 | 
			
		||||
  if (
 | 
			
		||||
    (await getPullOptionContractAggregatesState(ticker))?.status !==
 | 
			
		||||
    OptionContractAggregatesSyncStatus.COMPLETED
 | 
			
		||||
  ) {
 | 
			
		||||
    await setPullOptionContractAggregatesState(ticker, {
 | 
			
		||||
      status: OptionContractAggregatesSyncStatus.STARTED,
 | 
			
		||||
    });
 | 
			
		||||
    const { firstDate } = await getOptionContractDateRange(optionContract);
 | 
			
		||||
    for await (const batch of Polygon.makeGetOptionContractAggregatesIterator({
 | 
			
		||||
      ...optionContract,
 | 
			
		||||
      firstDate,
 | 
			
		||||
    })) {
 | 
			
		||||
      console.log(batch.length);
 | 
			
		||||
      await clickhouse.insert({
 | 
			
		||||
        table: "option_contract_aggregates",
 | 
			
		||||
        values: batch,
 | 
			
		||||
        format: "JSONEachRow",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    await setPullOptionContractAggregatesState(ticker, {
 | 
			
		||||
      status: OptionContractAggregatesSyncStatus.COMPLETED,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function* makeGetOptionContractsIterator(
 | 
			
		||||
  symbol: string,
 | 
			
		||||
  asOfDate: string
 | 
			
		||||
) {
 | 
			
		||||
  const limit = 2000;
 | 
			
		||||
  let offset = 0;
 | 
			
		||||
  let batch;
 | 
			
		||||
  do {
 | 
			
		||||
    batch = await query(`
 | 
			
		||||
      SELECT
 | 
			
		||||
        *
 | 
			
		||||
      FROM option_contract_existences
 | 
			
		||||
      WHERE asOfDate = '${asOfDate}' 
 | 
			
		||||
      AND symbol = '${symbol}'
 | 
			
		||||
      ORDER BY expirationDate ASC, strike ASC, type ASC
 | 
			
		||||
      LIMIT ${limit}
 | 
			
		||||
      OFFSET ${offset}
 | 
			
		||||
    `);
 | 
			
		||||
    yield batch;
 | 
			
		||||
    offset = offset + limit;
 | 
			
		||||
  } while (batch.length === limit);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function pullOptionContractsSince(
 | 
			
		||||
  symbol: string,
 | 
			
		||||
  firstDate: string
 | 
			
		||||
) {
 | 
			
		||||
  const currentDateAsDateObject = new Date(firstDate);
 | 
			
		||||
  const yesterdayAsDateObject = new Date();
 | 
			
		||||
  yesterdayAsDateObject.setUTCDate(yesterdayAsDateObject.getUTCDate() - 1);
 | 
			
		||||
  while (currentDateAsDateObject <= yesterdayAsDateObject) {
 | 
			
		||||
    const currentDate = currentDateAsDateObject.toISOString().substring(0, 10);
 | 
			
		||||
    console.log(`Date: ${currentDate}:`);
 | 
			
		||||
    await pullOptionContracts(symbol, currentDate);
 | 
			
		||||
    currentDateAsDateObject.setUTCDate(
 | 
			
		||||
      currentDateAsDateObject.getUTCDate() + 1
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function pullOptionContractAggregatesSince(
 | 
			
		||||
  symbol: string,
 | 
			
		||||
  firstDate: string
 | 
			
		||||
) {
 | 
			
		||||
  const currentDateAsDateObject = new Date(firstDate);
 | 
			
		||||
  const yesterdayAsDateObject = new Date();
 | 
			
		||||
  yesterdayAsDateObject.setUTCDate(yesterdayAsDateObject.getUTCDate() - 1);
 | 
			
		||||
  while (currentDateAsDateObject <= yesterdayAsDateObject) {
 | 
			
		||||
    const currentDate = currentDateAsDateObject.toISOString().substring(0, 10);
 | 
			
		||||
    for await (const optionContracts of makeGetOptionContractsIterator(
 | 
			
		||||
      symbol,
 | 
			
		||||
      currentDate
 | 
			
		||||
    )) {
 | 
			
		||||
      await pullOptionContractAggregates(optionContracts);
 | 
			
		||||
    }
 | 
			
		||||
    console.log(`Date: ${currentDate}`);
 | 
			
		||||
    currentDateAsDateObject.setUTCDate(
 | 
			
		||||
      currentDateAsDateObject.getUTCDate() + 1
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getOptionContractDateRange({
 | 
			
		||||
  symbol,
 | 
			
		||||
  expirationDate,
 | 
			
		||||
  strike,
 | 
			
		||||
  type,
 | 
			
		||||
}: OptionContract) {
 | 
			
		||||
  const rows = await query<{ firstDate: string; lastDate: string }>(`
 | 
			
		||||
    SELECT
 | 
			
		||||
      min(asOfDate) AS firstDate,
 | 
			
		||||
      max(asOfDate) AS lastDate
 | 
			
		||||
    FROM option_contract_existences
 | 
			
		||||
    WHERE symbol = '${symbol}'
 | 
			
		||||
    AND expirationDate = '${expirationDate}'
 | 
			
		||||
    AND strike = ${strike}
 | 
			
		||||
    AND type = '${type}'
 | 
			
		||||
  `);
 | 
			
		||||
  return rows[0] || { firstDate: null, lastDate: null };
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
//import pThrottle from 'p-throttle';
 | 
			
		||||
 | 
			
		||||
const apiKey = "H95NTsatM1iTWLUwDLxM2J5zhUVYdCEz";
 | 
			
		||||
//export const getApiKey = pThrottle({limit: 5, interval: 60000})(()=>apiKey);
 | 
			
		||||
export const getApiKey = () => apiKey;
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue