import { optionContractDatabase } from "../OptionContract/lmdbx.js"; import type { CalendarDatabase } from "./interfaces.js"; /** Largest possible key according to the `ordered-binary` (used by lmdbx) docs. */ const MAXIMUM_KEY = Buffer.from([0xff]); function makeCalendarDatabase(): CalendarDatabase { const getAggregatesSync = ({ key: { symbol, frontExpirationDate, backExpirationDate, strike, type }, date, }) => { const frontOptionContractAggregates = optionContractDatabase.getAggregatesSync({ date, key: { symbol, expirationDate: frontExpirationDate, strike, type }, }); const backOptionContractAggregates = optionContractDatabase.getAggregatesSync({ date, key: { symbol, expirationDate: backExpirationDate, strike, type }, }); const calendarAggregates = []; let i = 0; let j = 0; while ( i < frontOptionContractAggregates.length && j < backOptionContractAggregates.length ) { if ( frontOptionContractAggregates[i].tsStart === backOptionContractAggregates[j].tsStart ) { calendarAggregates.push({ tsStart: frontOptionContractAggregates[i].tsStart, open: backOptionContractAggregates[j].open - frontOptionContractAggregates[i].open, close: backOptionContractAggregates[j].close - frontOptionContractAggregates[i].close, // the high and low are not exactly correct since we don't know if each contract's high and low happened at the same moment as the other: high: backOptionContractAggregates[j].high - frontOptionContractAggregates[i].high, low: backOptionContractAggregates[j].low - frontOptionContractAggregates[i].low, }); i++; j++; } else if ( frontOptionContractAggregates[i].tsStart > backOptionContractAggregates[j].tsStart ) { j++; } else { i++; } } return calendarAggregates; }; const calendarDatabase: Omit = { getKeys: async ({ key: { symbol }, date }) => { const optionContracts = await optionContractDatabase.getOptionContracts({ date, key: { symbol }, }); return optionContracts.flatMap( (frontOptionContract, i, optionContracts) => optionContracts .filter( (potientialBackOptionContract) => frontOptionContract.strike === potientialBackOptionContract.strike && frontOptionContract.type === potientialBackOptionContract.type && frontOptionContract.expirationDate < potientialBackOptionContract.expirationDate ) .map((backOptionContract) => ({ symbol, frontExpirationDate: frontOptionContract.expirationDate, backExpirationDate: backOptionContract.expirationDate, strike: frontOptionContract.strike, type: frontOptionContract.type, })) ); }, getAggregates: async ({ key: { symbol, frontExpirationDate, backExpirationDate, strike, type }, date, }) => getAggregatesSync({ key: { symbol, frontExpirationDate, backExpirationDate, strike, type }, date, }), getAggregatesSync, insertAggregates: async (aggregates) => { // right now, no-op }, getClosingPrice: async ({ key: { symbol, strike, type, frontExpirationDate, backExpirationDate }, }) => { // get unix timestamp, in milliseconds, of the start of the last hour, which is 03:30PM in the `America/New_York` timezone on the front expiration date: const startOfLastHourUnix = new Date( `${frontExpirationDate}T19:30:00Z` ).getTime(); const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000; const frontOptionContractAggregates = ( await optionContractDatabase.getAggregates({ date: frontExpirationDate, key: { symbol, expirationDate: frontExpirationDate, strike, type }, }) ).filter( ({ tsStart }) => tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix ); const backOptionContractAggregates = ( await optionContractDatabase.getAggregates({ date: frontExpirationDate, key: { symbol, expirationDate: backExpirationDate, strike, type }, }) ).filter( ({ tsStart }) => tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix ); let i = 0; let j = 0; let minPrice = 0; while ( i < frontOptionContractAggregates.length && j < backOptionContractAggregates.length ) { if ( frontOptionContractAggregates[i].tsStart === backOptionContractAggregates[j].tsStart ) { const calendarClosePrice = backOptionContractAggregates[j].close - frontOptionContractAggregates[i].close; if (calendarClosePrice < minPrice || minPrice === 0) { minPrice = calendarClosePrice; } i++; j++; } else if ( frontOptionContractAggregates[i].tsStart > backOptionContractAggregates[j].tsStart ) { j++; } else { i++; } } return minPrice; }, getAggregate: async ({ key: { symbol, frontExpirationDate, backExpirationDate, strike, type }, tsStart, }) => { const [frontOptionContractAggregate, backOptionContractAggregate] = await Promise.all([ optionContractDatabase.getAggregate({ key: { symbol, expirationDate: frontExpirationDate, strike, type }, tsStart, }), optionContractDatabase.getAggregate({ key: { symbol, expirationDate: backExpirationDate, strike, type }, tsStart, }), ]); // only return the calendar aggregate if its constituent front and back option contract aggregates exist: if (frontOptionContractAggregate && backOptionContractAggregate) { return { tsStart, open: backOptionContractAggregate.open - frontOptionContractAggregate.open, close: backOptionContractAggregate.close - frontOptionContractAggregate.close, high: backOptionContractAggregate.high - frontOptionContractAggregate.high, low: backOptionContractAggregate.low - frontOptionContractAggregate.low, }; } return undefined; }, getTargetPriceByProbability: async ({ symbol, calendarSpan, strikePercentageFromTheMoney, historicalProbabilityOfSuccess, }) => { return 0.24; }, }; return { ...calendarDatabase, getCalendars: calendarDatabase.getKeys, }; } export const calendarDatabase: CalendarDatabase = makeCalendarDatabase();