diff --git a/server/src/backtest.ts b/server/src/backtest.ts index 4337abe..9d8e589 100644 --- a/server/src/backtest.ts +++ b/server/src/backtest.ts @@ -5,143 +5,143 @@ import type { Aggregate } from "./interfaces.js"; import { nextDate } from "./lib/util.js"; type BacktestInput = { - symbol: string; - startDate: string; - endDate: string; - /** Between 0 and 1. The frequency that similar calendars have historically ended (i.e. within the last hour) at a higher price than the current calendar's price. */ - historicalProbabilityOfSuccess?: number; - initialAvailableValue?: number; + symbol: string; + startDate: string; + endDate: string; + /** Between 0 and 1. The frequency that similar calendars have historically ended (i.e. within the last hour) at a higher price than the current calendar's price. */ + historicalProbabilityOfSuccess?: number; + initialAvailableValue?: number; }; export async function backtest({ - symbol, - startDate, - endDate, - historicalProbabilityOfSuccess = 0.8, - initialAvailableValue: initialBuyingPower = 2000, + symbol, + startDate, + endDate, + historicalProbabilityOfSuccess = 0.8, + initialAvailableValue: initialBuyingPower = 2000, }: BacktestInput) { - let buyingPower = initialBuyingPower; - const portfolio = new Set(); - // for each day: - for ( - let date = startDate, didBuyCalendar = false; - date <= endDate; - date = nextDate(date), didBuyCalendar = false - ) { - console.log("Current Date:", date); - const calendars = await calendarDatabase.getCalendars({ - key: { symbol }, - date, - }); - const stockAggregates = await stockDatabase.getAggregates({ - key: symbol, - date, - }); - const calendarsAggregates = new Map< - CalendarKey, - Array, "tsStart" | "open" | "close">> - >(); - for (const calendar of calendars) { - calendarsAggregates.set( - calendar, - await calendarDatabase.getAggregates({ - key: { - ...calendar, - }, - date, - }), - ); - } - // for each minute of that day for which we have a stock candlestick: - for (const stockAggregate of stockAggregates) { - // console.log("Current Time:", new Date(stockAggregate.tsStart)); - // filter-out calendars that are far-from-the-money (10%) - const calendarsNearTheMoney = calendars.filter( - ({ strike }) => - Math.abs((stockAggregate.open - strike) / stockAggregate.open) < 0.1, - ); - // for each relevant calendar on that day: - for (const calendar of calendarsNearTheMoney) { - const strikePercentageFromTheMoney = Math.abs( - (stockAggregate.open - calendar.strike) / stockAggregate.open, - ); - /** In days. */ - const calendarSpan = - (new Date(calendar.backExpirationDate).valueOf() - - new Date(calendar.frontExpirationDate).valueOf()) / - (1000 * 60 * 60 * 24); - const targetCalendarPrice = - await calendarDatabase.getTargetPriceByProbability({ - symbol, - calendarSpan, - strikePercentageFromTheMoney, - historicalProbabilityOfSuccess, - }); - const calendarAggregates = calendarsAggregates.get(calendar); - const calendarAggregateAtCurrentTime = calendarAggregates.find( - ({ tsStart }) => tsStart === stockAggregate.tsStart, - ); - // if there exists a matching calendar candlestick for the current minute: - if (calendarAggregateAtCurrentTime) { - // if the current candlestick is a good price (i.e. less than the target price): - const minCalendarPriceInCandlestick = Math.min( - calendarAggregateAtCurrentTime.open, - calendarAggregateAtCurrentTime.close, - ); - if ( - minCalendarPriceInCandlestick < targetCalendarPrice && - minCalendarPriceInCandlestick > - 0.07 /* sometimes the calendar price is zero or negative, which is of course impossible; some institution got a good deal */ - ) { - // if we can afford to buy the calendar: - if (buyingPower > minCalendarPriceInCandlestick) { - // buy the calendar, and continue to the next day: - portfolio.add(calendar); - buyingPower = buyingPower - minCalendarPriceInCandlestick * 100; - console.log( - "Bought", - calendar, - "for", - minCalendarPriceInCandlestick * 100, - "...$", - buyingPower, - "left", - ); - didBuyCalendar = true; - } - } - } - if (didBuyCalendar) { - break; - } - } - if (didBuyCalendar) { - break; - } - } + let buyingPower = initialBuyingPower; + const portfolio = new Set(); + // for each day: + for ( + let date = startDate, didBuyCalendar = false; + date <= endDate; + date = nextDate(date), didBuyCalendar = false + ) { + console.log("Current Date:", date); + const calendars = await calendarDatabase.getCalendars({ + key: { symbol }, + date, + }); + const stockAggregates = await stockDatabase.getAggregates({ + key: symbol, + date, + }); + const calendarsAggregates = new Map< + CalendarKey, + Array, "tsStart" | "open" | "close">> + >(); + for (const calendar of calendars) { + calendarsAggregates.set( + calendar, + await calendarDatabase.getAggregates({ + key: { + ...calendar, + }, + date, + }) + ); + } + // for each minute of that day for which we have a stock candlestick: + for (const stockAggregate of stockAggregates) { + // console.log("Current Time:", new Date(stockAggregate.tsStart)); + // filter-out calendars that are far-from-the-money (10%) + const calendarsNearTheMoney = calendars.filter( + ({ strike }) => + Math.abs((stockAggregate.open - strike) / stockAggregate.open) < 0.1 + ); + // for each relevant calendar on that day: + for (const calendar of calendarsNearTheMoney) { + const strikePercentageFromTheMoney = Math.abs( + (stockAggregate.open - calendar.strike) / stockAggregate.open + ); + /** In days. */ + const calendarSpan = + (new Date(calendar.backExpirationDate).valueOf() - + new Date(calendar.frontExpirationDate).valueOf()) / + (1000 * 60 * 60 * 24); + const targetCalendarPrice = + await calendarDatabase.getTargetPriceByProbability({ + symbol, + calendarSpan, + strikePercentageFromTheMoney, + historicalProbabilityOfSuccess, + }); + const calendarAggregates = calendarsAggregates.get(calendar); + const calendarAggregateAtCurrentTime = calendarAggregates.find( + ({ tsStart }) => tsStart === stockAggregate.tsStart + ); + // if there exists a matching calendar candlestick for the current minute: + if (calendarAggregateAtCurrentTime) { + // if the current candlestick is a good price (i.e. less than the target price): + const minCalendarPriceInCandlestick = Math.min( + calendarAggregateAtCurrentTime.open, + calendarAggregateAtCurrentTime.close + ); + if ( + minCalendarPriceInCandlestick < targetCalendarPrice && + minCalendarPriceInCandlestick > + 0.07 /* sometimes the calendar price is zero or negative, which is of course impossible; some institution got a good deal */ + ) { + // if we can afford to buy the calendar: + if (buyingPower > minCalendarPriceInCandlestick) { + // buy the calendar, and continue to the next day: + portfolio.add(calendar); + buyingPower = buyingPower - minCalendarPriceInCandlestick * 100; + console.log( + "Bought", + calendar, + "for", + minCalendarPriceInCandlestick * 100, + "...$", + buyingPower, + "left" + ); + didBuyCalendar = true; + } + } + } + if (didBuyCalendar) { + break; + } + } + if (didBuyCalendar) { + break; + } + } - // for each calendar in portfolio, if today is the last day, close the position: - for (const calendar of portfolio.values()) { - if (calendar.frontExpirationDate === date) { - const calendarClosingPrice = await calendarDatabase.getClosingPrice({ - key: { - ...calendar, - }, - }); - portfolio.delete(calendar); - buyingPower = buyingPower + calendarClosingPrice * 100; - console.log( - "Sold", - calendar, - "for", - calendarClosingPrice, - "...$", - buyingPower, - "left", - ); - } - } - } + // for each calendar in portfolio, if today is the last day, close the position: + for (const calendar of portfolio.values()) { + if (calendar.frontExpirationDate === date) { + const calendarClosingPrice = await calendarDatabase.getClosingPrice({ + key: { + ...calendar, + }, + }); + portfolio.delete(calendar); + buyingPower = buyingPower + calendarClosingPrice * 100; + console.log( + "Sold", + calendar, + "for", + calendarClosingPrice, + "...$", + buyingPower, + "left" + ); + } + } + } - console.log("Ending Buying Power:", buyingPower); - console.log("Portfolio:", portfolio.values()); + console.log("Ending Buying Power:", buyingPower); + console.log("Portfolio:", portfolio.values()); } diff --git a/server/src/calendardb.optiondb.lmdbx.ts b/server/src/calendardb.optiondb.lmdbx.ts index ac9ee04..5e927c3 100644 --- a/server/src/calendardb.optiondb.lmdbx.ts +++ b/server/src/calendardb.optiondb.lmdbx.ts @@ -5,151 +5,151 @@ import type { CalendarDatabase } from "./calendardb.interfaces.js"; const MAXIMUM_KEY = Buffer.from([0xff]); function makeCalendarDatabase(): CalendarDatabase { - const calendarDatabase: Omit = { - getKeys: async ({ key: { symbol }, date }) => { - const optionContracts = await optionContractDatabase.getOptionContracts({ - date, - key: { symbol }, - }); - return optionContracts.flatMap( - (frontOptionContract, i, optionContracts) => - optionContracts - .filter((_, j) => i !== j) - .map((backOptionContract) => ({ - symbol, - frontExpirationDate: frontOptionContract.expirationDate, - backExpirationDate: backOptionContract.expirationDate, - strike: frontOptionContract.strike, - type: frontOptionContract.type, - })), - ); - }, - getAggregates: async ({ - key: { symbol, frontExpirationDate, backExpirationDate, strike, type }, - date, - }) => { - const frontOptionContractAggregates = - await optionContractDatabase.getAggregates({ - date, - key: { symbol, expirationDate: frontExpirationDate, strike, type }, - }); - const backOptionContractAggregates = - await optionContractDatabase.getAggregates({ - 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 ata 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; - }, - insertAggregates: async (aggregates) => { - // right now, no-op - }, - getClosingPrice: async ({ - key: { symbol, strike, type, frontExpirationDate, backExpirationDate }, - }) => { - const startOfLastHourUnix = new Date( - `${frontExpirationDate}T00:00:00Z`, - ).valueOf(); - 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[j].close; - if (calendarClosePrice < minPrice || minPrice === 0) { - minPrice = calendarClosePrice; - } - i++; - j++; - } else if ( - frontOptionContractAggregates[i].tsStart > - backOptionContractAggregates[j].tsStart - ) { - j++; - } else { - i++; - } - } - return minPrice; - }, - getTargetPriceByProbability: async ({ - symbol, - calendarSpan, - strikePercentageFromTheMoney, - historicalProbabilityOfSuccess, - }) => { - return 0.24; - }, - }; + const calendarDatabase: Omit = { + getKeys: async ({ key: { symbol }, date }) => { + const optionContracts = await optionContractDatabase.getOptionContracts({ + date, + key: { symbol }, + }); + return optionContracts.flatMap( + (frontOptionContract, i, optionContracts) => + optionContracts + .filter((_, j) => i !== j) + .map((backOptionContract) => ({ + symbol, + frontExpirationDate: frontOptionContract.expirationDate, + backExpirationDate: backOptionContract.expirationDate, + strike: frontOptionContract.strike, + type: frontOptionContract.type, + })), + ); + }, + getAggregates: async ({ + key: { symbol, frontExpirationDate, backExpirationDate, strike, type }, + date, + }) => { + const frontOptionContractAggregates = + await optionContractDatabase.getAggregates({ + date, + key: { symbol, expirationDate: frontExpirationDate, strike, type }, + }); + const backOptionContractAggregates = + await optionContractDatabase.getAggregates({ + 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; + }, + insertAggregates: async (aggregates) => { + // right now, no-op + }, + getClosingPrice: async ({ + key: { symbol, strike, type, frontExpirationDate, backExpirationDate }, + }) => { + const startOfLastHourUnix = new Date( + `${frontExpirationDate}T00:00:00Z`, + ).valueOf(); + 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[j].close; + if (calendarClosePrice < minPrice || minPrice === 0) { + minPrice = calendarClosePrice; + } + i++; + j++; + } else if ( + frontOptionContractAggregates[i].tsStart > + backOptionContractAggregates[j].tsStart + ) { + j++; + } else { + i++; + } + } + return minPrice; + }, + getTargetPriceByProbability: async ({ + symbol, + calendarSpan, + strikePercentageFromTheMoney, + historicalProbabilityOfSuccess, + }) => { + return 0.24; + }, + }; - return { - ...calendarDatabase, - getCalendars: calendarDatabase.getKeys, - }; + return { + ...calendarDatabase, + getCalendars: calendarDatabase.getKeys, + }; } export const calendarDatabase: CalendarDatabase = makeCalendarDatabase(); diff --git a/server/src/scripts/clickhouse-to-lmdbx.ts b/server/src/scripts/clickhouse-to-lmdbx.ts index 692913e..c4746aa 100644 --- a/server/src/scripts/clickhouse-to-lmdbx.ts +++ b/server/src/scripts/clickhouse-to-lmdbx.ts @@ -17,7 +17,7 @@ async function syncAggregates({ date: string; }) { const aggregatesFrom = (await fromDatabase.getAggregates({ key, date })).map( - (aggregateWithoutKey) => ({ ...aggregateWithoutKey, key }), + (aggregateWithoutKey) => ({ ...aggregateWithoutKey, key }) ); await toDatabase.insertAggregates(aggregatesFrom); }