From 7bca5e701d6ef6ebe0b494b8495873c793bce702 Mon Sep 17 00:00:00 2001 From: avraham Date: Thu, 26 Sep 2024 20:14:11 -0400 Subject: [PATCH] refactor --- .tool-versions | 3 +- server/src/CalendarCharacteristicsForm.ts | 87 +++++ server/src/CalendarExitPriceChart.ts | 54 +++ server/src/SimilarCalendarPriceChart.ts | 55 ++++ server/src/UnderlyingPriceChart.ts | 34 ++ server/src/index.ts | 381 +++++----------------- server/src/trpc.ts | 29 +- 7 files changed, 333 insertions(+), 310 deletions(-) create mode 100644 server/src/CalendarCharacteristicsForm.ts create mode 100644 server/src/CalendarExitPriceChart.ts create mode 100644 server/src/SimilarCalendarPriceChart.ts create mode 100644 server/src/UnderlyingPriceChart.ts diff --git a/.tool-versions b/.tool-versions index 14a291a..6191b0c 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,3 @@ nodejs 20.15.1 -python 3.12.4 \ No newline at end of file +python 3.12.4 +pnpm 9.7.1 \ No newline at end of file diff --git a/server/src/CalendarCharacteristicsForm.ts b/server/src/CalendarCharacteristicsForm.ts new file mode 100644 index 0000000..9b2da06 --- /dev/null +++ b/server/src/CalendarCharacteristicsForm.ts @@ -0,0 +1,87 @@ +import { query } from "./lib/clickhouse"; +import { publicProcedure, RpcType, router } from "./trpc"; +import { + Object as ObjectT, + String as StringT, + Number as NumberT, +} from "@sinclair/typebox"; + +/** Gets a list of symbols that have at least one option contract */ +export const getAvailableUnderlyings = publicProcedure.query(async (opts) => { + // return ( + // await query<{ symbol: string }>(` + // SELECT DISTINCT(symbol) as symbol FROM option_contract_existences WHERE asOfDate = (SELECT max(asOfDate) FROM option_contract_existences) + // `) + // ).map(({ symbol }) => symbol); + return ["AAPL", "AMD", "GOOGL", "MSFT", "NFLX"]; +}); + +export const getAvailableAsOfDates = publicProcedure + .input(RpcType(ObjectT({ underlying: StringT() }))) + .query(async (opts) => { + const underlying = opts.input.underlying; + return ( + await query<{ asOfDate: string }>(` + SELECT + DISTINCT(asOfDate) as asOfDate + FROM option_contract_existences + WHERE symbol = '${underlying}' + ORDER BY asOfDate + `) + ).map(({ asOfDate }) => asOfDate); + }); + +export const getExpirationsForUnderlying = publicProcedure + .input( + RpcType( + ObjectT({ + underlying: StringT({ maxLength: 5 }), + asOfDate: StringT(), + }) + ) + ) + .query(async (opts) => { + const { underlying, asOfDate } = opts.input; + return ( + await query<{ expirationDate: string }>(` + SELECT + DISTINCT(expirationDate) as expirationDate + FROM option_contract_existences + WHERE symbol = '${underlying}' + AND asOfDate = '${asOfDate}' + ORDER BY expirationDate + `) + ).map(({ expirationDate }) => expirationDate); + }); + +export const getStrikesForUnderlying = publicProcedure + .input( + RpcType( + ObjectT({ + underlying: StringT({ maxLength: 5 }), + asOfDate: StringT(), + expirationDate: StringT(), + }) + ) + ) + .query(async (opts) => { + const { underlying, asOfDate, expirationDate } = opts.input; + return ( + await query<{ strike: string }>(` + SELECT + DISTINCT(strike) as strike + FROM option_contract_existences + WHERE symbol = '${underlying}' + AND asOfDate = '${asOfDate}' + AND expirationDate = '${expirationDate}' + ORDER BY strike + `) + ).map(({ strike }) => strike); + }); + +export default router({ + getAvailableUnderlyings, + getAvailableAsOfDates, + getExpirationsForUnderlying, + getStrikesForUnderlying, +}); diff --git a/server/src/CalendarExitPriceChart.ts b/server/src/CalendarExitPriceChart.ts new file mode 100644 index 0000000..b16e0d9 --- /dev/null +++ b/server/src/CalendarExitPriceChart.ts @@ -0,0 +1,54 @@ +import { query } from "./lib/clickhouse"; +import { publicProcedure, RpcType, router } from "./trpc"; +import { + Object as ObjectT, + String as StringT, + Number as NumberT, +} from "@sinclair/typebox"; + +export const getChartData = publicProcedure + .input( + RpcType( + ObjectT({ + underlying: StringT({ maxLength: 5 }), + daysToFrontExpiration: NumberT(), + daysBetweenFrontAndBackExpiration: NumberT(), + lookbackPeriodStart: StringT({ + pattern: "[0-9]{4}-[0-9]{2}-[0-9]{2}", + }), + lookbackPeriodEnd: StringT({ pattern: "[0-9]{4}-[0-9]{2}-[0-9]{2}" }), + }) + ) + ) + .query(async (opts) => { + const { + underlying, + daysToFrontExpiration, + daysBetweenFrontAndBackExpiration, + lookbackPeriodStart, + lookbackPeriodEnd, + } = opts.input; + return await query<[number, number, number]>( + ` + SELECT + FLOOR(strikePercentageFromUnderlyingPrice, 1) as x, + FLOOR(calendarPrice, 1) as y, + count(*) as n + FROM calendar_histories + WHERE symbol = '${underlying}' + AND daysToFrontExpiration = ${daysToFrontExpiration} + AND strikePercentageFromUnderlyingPrice >= -5.0 + AND strikePercentageFromUnderlyingPrice <= 5.0 + AND daysBetweenFrontAndBackExpiration = ${daysBetweenFrontAndBackExpiration} + AND tsStart >= '${lookbackPeriodStart} 00:00:00' + AND tsStart <= '${lookbackPeriodEnd} 00:00:00' + GROUP BY x, y + ORDER BY x ASC, y ASC + `, + "JSONEachRow" + ); + }); + +export default router({ + getChartData, +}); diff --git a/server/src/SimilarCalendarPriceChart.ts b/server/src/SimilarCalendarPriceChart.ts new file mode 100644 index 0000000..ce4b990 --- /dev/null +++ b/server/src/SimilarCalendarPriceChart.ts @@ -0,0 +1,55 @@ +import { query } from "./lib/clickhouse"; +import { publicProcedure, RpcType, router } from "./trpc"; +import { + Object as ObjectT, + String as StringT, + Number as NumberT, +} from "@sinclair/typebox"; + +/** Returns prices for all matching calendars (i.e. those with similar + * characteristics to those given) */ +export const getChartData = publicProcedure + .input( + RpcType( + ObjectT({ + underlying: StringT({ maxLength: 5 }), + daysToFrontExpiration: NumberT(), + daysBetweenFrontAndBackExpiration: NumberT(), + strikePercentageFromUnderlyingPriceRangeMin: NumberT(), + strikePercentageFromUnderlyingPriceRangeMax: NumberT(), + lookbackPeriodStart: StringT(), + lookbackPeriodEnd: StringT(), + }) + ) + ) + .query(async (opts) => { + const { + underlying, + daysToFrontExpiration, + daysBetweenFrontAndBackExpiration, + strikePercentageFromUnderlyingPriceRangeMin, + strikePercentageFromUnderlyingPriceRangeMax, + lookbackPeriodStart, + lookbackPeriodEnd, + } = opts.input; + return await query<[number, number]>( + ` + SELECT + toUnixTimestamp(tsStart) as x, + truncate(calendarPrice, 2) as y + FROM calendar_histories + WHERE symbol = '${underlying}' + AND daysToFrontExpiration = ${daysToFrontExpiration} + AND strikePercentageFromUnderlyingPrice >= ${strikePercentageFromUnderlyingPriceRangeMin} + AND strikePercentageFromUnderlyingPrice <= ${strikePercentageFromUnderlyingPriceRangeMax} + AND daysBetweenFrontAndBackExpiration = ${daysBetweenFrontAndBackExpiration} + AND tsStart >= '${lookbackPeriodStart} 00:00:00' + AND tsStart <= '${lookbackPeriodEnd} 00:00:00' + `, + "JSONEachRow" + ); + }); + +export default router({ + getChartData, +}); diff --git a/server/src/UnderlyingPriceChart.ts b/server/src/UnderlyingPriceChart.ts new file mode 100644 index 0000000..86e04ca --- /dev/null +++ b/server/src/UnderlyingPriceChart.ts @@ -0,0 +1,34 @@ +import { query } from "./lib/clickhouse"; +import { publicProcedure, RpcType, router } from "./trpc"; +import { Object as ObjectT, String as StringT } from "@sinclair/typebox"; + +export const getChartData = publicProcedure + .input( + RpcType( + ObjectT({ + underlying: StringT({ maxLength: 5 }), + lookbackPeriodStart: StringT(), + lookbackPeriodEnd: StringT(), + }) + ) + ) + .query(async (opts) => { + const { underlying, lookbackPeriodStart, lookbackPeriodEnd } = opts.input; + return await query<[number, number]>( + ` + SELECT + toUnixTimestamp(tsStart) as x, + open as y + FROM stock_aggregates + WHERE symbol = '${underlying}' + AND tsStart >= '${lookbackPeriodStart} 00:00:00' + AND tsStart <= '${lookbackPeriodEnd} 00:00:00' + ORDER BY x ASC + `, + "JSONEachRow" + ); + }); + +export default router({ + getChartData, +}); diff --git a/server/src/index.ts b/server/src/index.ts index 2bd4eec..9643f8b 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,311 +1,82 @@ -import { publicProcedure, router } from "./trpc.js"; +import { publicProcedure, router, RpcType } from "./trpc.js"; import { query } from "./lib/clickhouse.js"; import { createHTTPHandler } from "@trpc/server/adapters/standalone"; import cors from "cors"; import { - Object as ObjectT, - String as StringT, - type TSchema, - Number as NumberT, + Object as ObjectT, + String as StringT, + Number as NumberT, } from "@sinclair/typebox"; -import { TypeCompiler } from "@sinclair/typebox/compiler"; -import { TRPCError } from "@trpc/server"; import { createServer } from "node:http"; import { Env } from "@humanwhocodes/env"; +import UnderlyingPriceChart from "./UnderlyingPriceChart.js"; +import SimilarCalendarPriceChart from "./SimilarCalendarPriceChart.js"; +import CalendarExitPriceChart from "./CalendarExitPriceChart.js"; +import CalendarCharacteristicsForm from "./CalendarCharacteristicsForm.js"; const env = new Env(); const LISTEN_PORT = env.get("LISTEN_PORT", 3005); -/** - * Generate a TRPC-compatible validator function given a Typebox schema. - * This was copied from [https://github.com/sinclairzx81/typebox/blob/6cfcdc02cc813af2f1be57407c771fc4fadfc34a/example/trpc/readme.md]. - * @param schema A Typebox schema - * @returns A TRPC-compatible validator function - */ -export function RpcType(schema: T) { - const check = TypeCompiler.Compile(schema); - return (value: unknown) => { - if (check.Check(value)) return value; - const { path, message } = check.Errors(value).First(); - throw new TRPCError({ - message: `${message} for ${path}`, - code: "BAD_REQUEST", - }); - }; -} +export const getOpensForUnderlying = publicProcedure + .input( + RpcType( + ObjectT({ + underlying: StringT({ maxLength: 5 }), + }) + ) + ) + .query(async (opts) => { + const { underlying } = opts.input; + return await query<{ x: number; y: number }>( + ` + SELECT + toUnixTimestamp(tsStart) as x, + open as y + FROM stock_aggregates + WHERE symbol = '${underlying}' + ORDER BY tsStart ASC + `, + "JSONEachRow" + ); + }); + +export const getOpensForOptionContract = publicProcedure + .input( + RpcType( + ObjectT({ + underlying: StringT({ maxLength: 5 }), + expirationDate: StringT(), + strike: NumberT(), + }) + ) + ) + .query(async (opts) => { + const { underlying, expirationDate, strike } = opts.input; + return await query<{ x: number; y: number }>( + ` + SELECT + toUnixTimestamp(tsStart) as x, + open as y + FROM option_contract_aggregates + WHERE symbol = '${underlying}' + AND expirationDate = '${expirationDate}' + AND strike = ${strike} + AND type = 'call' + ORDER BY tsStart ASC + `, + "JSONEachRow" + ); + }); const appRouter = router({ - getAvailableUnderlyings: publicProcedure.query(async (opts) => { - // return ( - // await query<{ symbol: string }>(` - // SELECT DISTINCT(symbol) as symbol FROM option_contract_existences WHERE asOfDate = (SELECT max(asOfDate) FROM option_contract_existences) - // `) - // ).map(({ symbol }) => symbol); - return ["AAPL", "AMD", "GOOGL", "MSFT", "NFLX"]; - }), - getAvailableAsOfDates: publicProcedure - .input(RpcType(ObjectT({ underlying: StringT() }))) - .query(async (opts) => { - const underlying = opts.input.underlying; - return ( - await query<{ asOfDate: string }>(` - SELECT - DISTINCT(asOfDate) as asOfDate - FROM option_contract_existences - WHERE symbol = '${underlying}' - ORDER BY asOfDate - `) - ).map(({ asOfDate }) => asOfDate); - }), - getExpirationsForUnderlying: publicProcedure - .input( - RpcType( - ObjectT({ - underlying: StringT({ maxLength: 5 }), - asOfDate: StringT(), - }), - ), - ) - .query(async (opts) => { - const { underlying, asOfDate } = opts.input; - return ( - await query<{ expirationDate: string }>(` - SELECT - DISTINCT(expirationDate) as expirationDate - FROM option_contract_existences - WHERE symbol = '${underlying}' - AND asOfDate = '${asOfDate}' - ORDER BY expirationDate - `) - ).map(({ expirationDate }) => expirationDate); - }), - getStrikesForUnderlying: publicProcedure - .input( - RpcType( - ObjectT({ - underlying: StringT({ maxLength: 5 }), - asOfDate: StringT(), - expirationDate: StringT(), - }), - ), - ) - .query(async (opts) => { - const { underlying, asOfDate, expirationDate } = opts.input; - return ( - await query<{ strike: string }>(` - SELECT - DISTINCT(strike) as strike - FROM option_contract_existences - WHERE symbol = '${underlying}' - AND asOfDate = '${asOfDate}' - AND expirationDate = '${expirationDate}' - ORDER BY strike - `) - ).map(({ strike }) => strike); - }), - getOpensForUnderlying: publicProcedure - .input( - RpcType( - ObjectT({ - underlying: StringT({ maxLength: 5 }), - }), - ), - ) - .query(async (opts) => { - const { underlying } = opts.input; - return await query<{ x: number; y: number }>( - ` - SELECT - toUnixTimestamp(tsStart) as x, - open as y - FROM stock_aggregates - WHERE symbol = '${underlying}' - ORDER BY tsStart ASC - `, - "JSONEachRow", - ); - }), - getOpensForOptionContract: publicProcedure - .input( - RpcType( - ObjectT({ - underlying: StringT({ maxLength: 5 }), - expirationDate: StringT(), - strike: NumberT(), - }), - ), - ) - .query(async (opts) => { - const { underlying, expirationDate, strike } = opts.input; - return await query<{ x: number; y: number }>( - ` - SELECT - toUnixTimestamp(tsStart) as x, - open as y - FROM option_contract_aggregates - WHERE symbol = '${underlying}' - AND expirationDate = '${expirationDate}' - AND strike = ${strike} - AND type = 'call' - ORDER BY tsStart ASC - `, - "JSONEachRow", - ); - }), - getHistoricalCalendarPrices: publicProcedure - .input( - RpcType( - ObjectT({ - underlying: StringT({ maxLength: 5 }), - daysToFrontExpiration: NumberT(), - daysBetweenFrontAndBackExpiration: NumberT(), - strikePercentageFromUnderlyingPriceRangeMin: NumberT(), - strikePercentageFromUnderlyingPriceRangeMax: NumberT(), - }), - ), - ) - .query(async (opts) => { - const { - underlying, - daysToFrontExpiration, - daysBetweenFrontAndBackExpiration, - strikePercentageFromUnderlyingPriceRangeMin, - strikePercentageFromUnderlyingPriceRangeMax, - } = opts.input; - return ( - await query<[number, number]>( - ` - SELECT - toUnixTimestamp(tsStart) as asOfTs, - calendarPrice - FROM calendar_histories - WHERE symbol = '${underlying}' - AND daysToFrontExpiration = ${daysToFrontExpiration} - AND strikePercentageFromUnderlyingPrice >= ${strikePercentageFromUnderlyingPriceRangeMin} - AND strikePercentageFromUnderlyingPrice <= ${strikePercentageFromUnderlyingPriceRangeMax} - AND daysBetweenFrontAndBackExpiration = ${daysBetweenFrontAndBackExpiration} - `, - "JSONCompactEachRow", - ) - ).reduce( - (columns, row) => { - columns[0].push(row[0]); - columns[1].push(row[1]); - return columns; - }, - [[], []], - ); - }), - getHistoricalStockQuoteChartData: publicProcedure - .input( - RpcType( - ObjectT({ - underlying: StringT({ maxLength: 5 }), - lookbackPeriodStart: StringT(), - lookbackPeriodEnd: StringT(), - }), - ), - ) - .query(async (opts) => { - const { underlying, lookbackPeriodStart, lookbackPeriodEnd } = opts.input; - return await query<[number, number]>( - ` - SELECT - toUnixTimestamp(tsStart) as x, - open as y - FROM stock_aggregates - WHERE symbol = '${underlying}' - AND tsStart >= '${lookbackPeriodStart} 00:00:00' - AND tsStart <= '${lookbackPeriodEnd} 00:00:00' - ORDER BY x ASC - `, - "JSONEachRow", - ); - }), - getHistoricalCalendarQuoteChartData: publicProcedure - .input( - RpcType( - ObjectT({ - underlying: StringT({ maxLength: 5 }), - daysToFrontExpiration: NumberT(), - daysBetweenFrontAndBackExpiration: NumberT(), - strikePercentageFromUnderlyingPriceRangeMin: NumberT(), - strikePercentageFromUnderlyingPriceRangeMax: NumberT(), - lookbackPeriodStart: StringT(), - lookbackPeriodEnd: StringT(), - }), - ), - ) - .query(async (opts) => { - const { - underlying, - daysToFrontExpiration, - daysBetweenFrontAndBackExpiration, - strikePercentageFromUnderlyingPriceRangeMin, - strikePercentageFromUnderlyingPriceRangeMax, - lookbackPeriodStart, - lookbackPeriodEnd, - } = opts.input; - return await query<[number, number]>( - ` - SELECT - toUnixTimestamp(tsStart) as x, - truncate(calendarPrice, 2) as y - FROM calendar_histories - WHERE symbol = '${underlying}' - AND daysToFrontExpiration = ${daysToFrontExpiration} - AND strikePercentageFromUnderlyingPrice >= ${strikePercentageFromUnderlyingPriceRangeMin} - AND strikePercentageFromUnderlyingPrice <= ${strikePercentageFromUnderlyingPriceRangeMax} - AND daysBetweenFrontAndBackExpiration = ${daysBetweenFrontAndBackExpiration} - AND tsStart >= '${lookbackPeriodStart} 00:00:00' - AND tsStart <= '${lookbackPeriodEnd} 00:00:00' - `, - "JSONEachRow", - ); - }), - getHistoricalCalendarExitQuoteChartData: publicProcedure - .input( - RpcType( - ObjectT({ - underlying: StringT({ maxLength: 5 }), - daysToFrontExpiration: NumberT(), - daysBetweenFrontAndBackExpiration: NumberT(), - lookbackPeriodStart: StringT({ - pattern: "[0-9]{4}-[0-9]{2}-[0-9]{2}", - }), - lookbackPeriodEnd: StringT({ pattern: "[0-9]{4}-[0-9]{2}-[0-9]{2}" }), - }), - ), - ) - .query(async (opts) => { - const { - underlying, - daysToFrontExpiration, - daysBetweenFrontAndBackExpiration, - lookbackPeriodStart, - lookbackPeriodEnd, - } = opts.input; - return await query<[number, number, number]>( - ` - SELECT - FLOOR(strikePercentageFromUnderlyingPrice, 1) as x, - FLOOR(calendarPrice, 1) as y, - count(*) as n - FROM calendar_histories - WHERE symbol = '${underlying}' - AND daysToFrontExpiration = ${daysToFrontExpiration} - AND strikePercentageFromUnderlyingPrice >= -5.0 - AND strikePercentageFromUnderlyingPrice <= 5.0 - AND daysBetweenFrontAndBackExpiration = ${daysBetweenFrontAndBackExpiration} - AND tsStart >= '${lookbackPeriodStart} 00:00:00' - AND tsStart <= '${lookbackPeriodEnd} 00:00:00' - GROUP BY x, y - ORDER BY x ASC, y ASC - `, - "JSONEachRow", - ); - }), + CalendarCharacteristicsForm, + UnderlyingPriceChart, + SimilarCalendarPriceChart, + CalendarExitPriceChart, + + getOpensForUnderlying, + getOpensForOptionContract, }); // Export type router type signature, @@ -313,20 +84,20 @@ const appRouter = router({ export type AppRouter = typeof appRouter; const handler = createHTTPHandler({ - middleware: cors(), - router: appRouter, - createContext() { - return {}; - }, + middleware: cors(), + router: appRouter, + createContext() { + return {}; + }, }); const server = createServer((req, res) => { - if (req.url.startsWith("/healthz")) { - res.statusCode = 200; - res.end("OK"); - } else { - handler(req, res); - } + if (req.url.startsWith("/healthz")) { + res.statusCode = 200; + res.end("OK"); + } else { + handler(req, res); + } }); server.listen(Number.parseInt(LISTEN_PORT)); diff --git a/server/src/trpc.ts b/server/src/trpc.ts index 02a8c08..1c5fa35 100644 --- a/server/src/trpc.ts +++ b/server/src/trpc.ts @@ -1,14 +1,35 @@ -import { initTRPC } from '@trpc/server'; -  +import { initTRPC } from "@trpc/server"; +import { TypeCompiler } from "@sinclair/typebox/compiler"; +import { TRPCError } from "@trpc/server"; +import type { TSchema } from "@sinclair/typebox"; + /** * Initialization of tRPC backend * Should be done only once per backend! */ const t = initTRPC.create(); -  + /** * Export reusable router and procedure helpers * that can be used throughout the router */ export const router = t.router; -export const publicProcedure = t.procedure; \ No newline at end of file +export const publicProcedure = t.procedure; + +/** + * Generate a TRPC-compatible validator function given a Typebox schema. + * This was copied from [https://github.com/sinclairzx81/typebox/blob/6cfcdc02cc813af2f1be57407c771fc4fadfc34a/example/trpc/readme.md]. + * @param schema A Typebox schema + * @returns A TRPC-compatible validator function + */ +export function RpcType(schema: T) { + const check = TypeCompiler.Compile(schema); + return (value: unknown) => { + if (check.Check(value)) return value; + const { path, message } = check.Errors(value).First(); + throw new TRPCError({ + message: `${message} for ${path}`, + code: "BAD_REQUEST", + }); + }; +}