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 { createClient as createClickhouseClient } from "@clickhouse/client";
|
||||||
import type { DataFormat } 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