This commit is contained in:
Avraham Sakal
2024-02-04 15:36:28 -05:00
commit dc63f31647
33 changed files with 3423 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
import { createClient as createClickhouseClient } from '@clickhouse/client';
import type { DataFormat } from '@clickhouse/client';
export const clickhouse = createClickhouseClient({
host: process.env.CLICKHOUSE_HOST || "http://localhost:8123",
username:'avraham',
password:'buginoo'
});
export async function query<T>(queryString:string, format:DataFormat='JSONEachRow') : Promise<Array<T>>{
return await (await clickhouse.query({
query: queryString,
format
})).json()
}
+223
View File
@@ -0,0 +1,223 @@
import { publicProcedure, router } from './trpc.js';
import { query } from './clickhouse.js';
import { createHTTPHandler, createHTTPServer } from '@trpc/server/adapters/standalone';
import cors from 'cors';
import { Object as ObjectT, String as StringT, TSchema, Number as NumberT } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';
import { TRPCError } from '@trpc/server';
import { createServer } from 'http';
/**
* 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_contracts
`))
.map(({symbol})=>symbol);
}),
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_contracts
WHERE symbol = '${underlying}'
`))
.map(({asOfDate})=>asOfDate);
}),
getExpirationsForUnderlying: publicProcedure
.input(RpcType(ObjectT({
underlying:StringT(),
asOfDate:StringT()
})))
.query(async (opts)=>{
const {underlying, asOfDate} = opts.input;
return (await query<{expirationDate:string}>(`
SELECT
DISTINCT(expirationDate)
FROM option_contracts
WHERE symbol = '${underlying}'
AND asOfDate = '${asOfDate}'
`))
.map(({expirationDate})=>expirationDate);
}),
getStrikesForUnderlying: publicProcedure
.input(RpcType(ObjectT({
underlying:StringT(),
asOfDate:StringT(),
expirationDate:StringT(),
})))
.query(async (opts)=>{
const {underlying, asOfDate, expirationDate} = opts.input;
return (await query<{strike:string}>(`
SELECT
DISTINCT(strike)
FROM option_contracts
WHERE symbol = '${underlying}'
AND asOfDate = '${asOfDate}'
AND expirationDate = '${expirationDate}'
`))
.map(({strike})=>strike);
}),
getOpensForUnderlying: publicProcedure
.input(RpcType(ObjectT({
underlying:StringT()
})))
.query(async (opts)=>{
const {underlying} = opts.input;
return (await query<[number,number]>(`
SELECT
toUnixTimestamp(tsStart),
open
FROM stock_aggregates
WHERE symbol = '${underlying}'
ORDER BY tsStart ASC
`,'JSONCompactEachRow'))
.reduce((columns, row)=>{ columns[0].push(row[0]); columns[1].push(row[1]); return columns; },[[],[]]);
}),
getOpensForOptionContract: publicProcedure
.input(RpcType(ObjectT({
underlying:StringT(),
expirationDate:StringT(),
strike:NumberT()
})))
.query(async (opts)=>{
const {underlying, expirationDate, strike} = opts.input;
return (await query<[number,number]>(`
SELECT
toUnixTimestamp(tsStart),
open
FROM option_aggregates
WHERE symbol = '${underlying}'
AND expirationDate = '${expirationDate}'
AND strike = ${strike}
AND optionType = 'call'
ORDER BY tsStart ASC
`,'JSONCompactEachRow'))
.reduce((columns, row)=>{ columns[0].push(row[0]); columns[1].push(row[1]); return columns; },[[],[]]);
}),
getHistoricalCalendarPrices: publicProcedure
.input(RpcType(ObjectT({
underlying:StringT(),
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(),
})))
.query(async (opts)=>{
const {underlying, } = opts.input;
return (await query<[number,number]>(`
SELECT
toUnixTimestamp(tsStart) as x,
open as y
FROM stock_aggregates
WHERE symbol = '${underlying}'
ORDER BY x ASC
`,'JSONEachRow'));
}),
getHistoricalCalendarQuoteChartData: publicProcedure
.input(RpcType(ObjectT({
underlying:StringT(),
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 x,
calendarPrice as y
FROM calendar_histories
WHERE symbol = '${underlying}'
AND daysToFrontExpiration = ${daysToFrontExpiration}
AND strikePercentageFromUnderlyingPrice >= ${strikePercentageFromUnderlyingPriceRangeMin}
AND strikePercentageFromUnderlyingPrice <= ${strikePercentageFromUnderlyingPriceRangeMax}
AND daysBetweenFrontAndBackExpiration = ${daysBetweenFrontAndBackExpiration}
`,'JSONEachRow'));
}),
getHistoricalCalendarExitQuoteChartData: publicProcedure
.input(RpcType(ObjectT({
underlying:StringT(),
daysToFrontExpiration:NumberT(),
daysBetweenFrontAndBackExpiration:NumberT(),
})))
.query(async (opts)=>{
const {underlying, daysToFrontExpiration, daysBetweenFrontAndBackExpiration, } = opts.input;
return (await query<[number,number]>(`
SELECT
FLOOR(strikePercentageFromUnderlyingPrice, 1) as x,
calendarPrice as y
FROM calendar_histories
WHERE symbol = '${underlying}'
AND daysToFrontExpiration = ${daysToFrontExpiration}
AND strikePercentageFromUnderlyingPrice >= -5.0
AND strikePercentageFromUnderlyingPrice <= 5.0
AND daysBetweenFrontAndBackExpiration = ${daysBetweenFrontAndBackExpiration}
ORDER BY x 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(parseInt(process.env.LISTEN_PORT) || 3005);
+14
View File
@@ -0,0 +1,14 @@
import { initTRPC } from '@trpc/server';
 
/**
* 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;