You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

333 lines
9.4 KiB
TypeScript

import { publicProcedure, router } 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,
} from "@sinclair/typebox";
import { TypeCompiler } from "@sinclair/typebox/compiler";
import { TRPCError } from "@trpc/server";
import { createServer } from "node:http";
import { Env } from "@humanwhocodes/env";
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<T extends TSchema>(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",
});
};
}
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",
);
}),
});
// Export type router type signature,
// NOT the router itself.
export type AppRouter = typeof appRouter;
const handler = createHTTPHandler({
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);
}
});
server.listen(Number.parseInt(LISTEN_PORT));