import type { OptionContractDatabase } from "./interfaces.js"; import { open } from "lmdbx"; const optionContractAggregatesDb = open({ path: "./option-contract-aggregates.db", // any options go here, we can turn on compression like this: compression: true, }); const optionContractExistenceDb = open({ path: "./option-contract-existence.db", // any options go here, we can turn on compression like this: compression: true, }); /** Largest possible key according to the `ordered-binary` (used by lmdbx) docs. */ const MAXIMUM_KEY = Buffer.from([0xff]); function makeOptionContractDatabase(): OptionContractDatabase { const getAggregatesSync = ({ key: { symbol, expirationDate, strike, type }, date, }) => { const startOfDayUnix = new Date(`${date}T00:00:00Z`).valueOf(); const endOfDayUnix = startOfDayUnix + 3600 * 24 * 1000; return optionContractAggregatesDb .getRange({ start: [symbol, expirationDate, strike, type, startOfDayUnix], end: [symbol, expirationDate, strike, type, endOfDayUnix], }) .map(({ key, value }) => ({ tsStart: key[4], open: value.open, close: value.close, high: value.high, low: value.low, })).asArray; }; const optionContractDatabase: Omit< OptionContractDatabase, "getOptionContracts" > = { getKeys: async ({ key: { symbol }, date }) => { return optionContractExistenceDb .getRange({ start: [date, symbol], end: [date, symbol, MAXIMUM_KEY], }) .map(({ key }) => ({ symbol, expirationDate: key[2], strike: key[3], type: key[4], })).asArray; }, getAggregatesSync, getAggregates: async ({ key: { symbol, expirationDate, strike, type }, date, }) => getAggregatesSync({ key: { symbol, expirationDate, strike, type }, date, }), insertAggregates: async (aggregates) => { await optionContractExistenceDb.batch(() => { for (const aggregate of aggregates) { optionContractExistenceDb.put( [ new Date(aggregate.tsStart).toISOString().substring(0, 10), aggregate.key.symbol, aggregate.key.expirationDate, aggregate.key.strike, aggregate.key.type, ], null ); } }); await optionContractAggregatesDb.batch(() => { for (const aggregate of aggregates) { optionContractAggregatesDb.put( [ aggregate.key.symbol, aggregate.key.expirationDate, aggregate.key.strike, aggregate.key.type, aggregate.tsStart, ], { open: aggregate.open, close: aggregate.close, high: aggregate.high, low: aggregate.low, } ); } }); }, getClosingPrice: async ({ key: { symbol, strike, type, expirationDate }, }) => { const startOfLastHourUnix = new Date( `${expirationDate}T00:00:00Z` ).valueOf(); const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000; let minPrice = 0; for (const { value } of optionContractAggregatesDb.getRange({ start: [symbol, expirationDate, strike, type, startOfLastHourUnix], end: [symbol, expirationDate, strike, type, endOfLastHourUnix], })) { if (value.close < minPrice || minPrice === 0) { minPrice = value.close; } } return minPrice; }, getAggregate: async ({ key: { symbol, expirationDate, strike, type }, tsStart, }) => { return await optionContractAggregatesDb.get([ symbol, expirationDate, strike, type, tsStart, ]); }, }; return { ...optionContractDatabase, getOptionContracts: optionContractDatabase.getKeys, }; } export const database: OptionContractDatabase = makeOptionContractDatabase();