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
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+23
View File
@@ -0,0 +1,23 @@
# adapted from example on pnpm.io
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY package.json pnpm-lock.yaml /app/
WORKDIR /app
FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
# install dev dependencies which are needed for building, such as typescript:
FROM base AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
COPY tsconfig.json /app/
COPY src /app/src
RUN pnpm run build
FROM base
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist
WORKDIR /app/dist
CMD [ "node", "index.js" ]
Executable
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
IMAGE_NAME=calendar-optimizer-server
VERSION=prod
docker login registry.sakal.us
if [ $? -eq 0 ]
then
docker build -t "registry.sakal.us/${IMAGE_NAME}:${VERSION}" .
docker push "registry.sakal.us/${IMAGE_NAME}:${VERSION}"
fi
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
kubectl port-forward -n clickhouse clickhouse 8123:8123
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
kubectl rollout restart -n calendar-optimizer deployments/server
+24
View File
@@ -0,0 +1,24 @@
{
"private": true,
"type": "module",
"scripts": {
"build": "esbuild src/*.ts --platform=node --outdir=dist --format=esm",
"build-scripts": "esbuild scripts/*.ts --platform=node --outdir=dist/scripts --format=esm",
"dev:node": "node --watch dist/index.js",
"dev:esbuild": "pnpm run build --watch",
"dev": "run-p dev:*"
},
"dependencies": {
"@clickhouse/client": "^0.2.7",
"@sinclair/typebox": "^0.32.5",
"@trpc/server": "^10.45.0",
"cors": "^2.8.5"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/node": "^20.10.7",
"esbuild": "^0.19.11",
"npm-run-all": "^4.1.5",
"typescript": "^5.3.3"
}
}
+1083
View File
File diff suppressed because it is too large Load Diff
+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;
+100
View File
@@ -0,0 +1,100 @@
CREATE TABLE stock_aggregates
(
symbol LowCardinality(String),
tsStart DateTime32,
open Float64,
close Float64,
low Float64,
high Float64,
volume UInt64,
volume_weighted_price Float64
)
ENGINE MergeTree()
ORDER BY (symbol, tsStart)
CREATE TABLE option_aggregates
(
symbol LowCardinality(String),
expirationDate Date,
optionType Enum('call', 'put'),
strike Float64,
tsStart DateTime32,
open Float64,
close Float64,
low Float64,
high Float64,
volume UInt64,
volumeWeightedPrice Float64
)
ENGINE MergeTree()
ORDER BY (symbol, expirationDate, optionType, strike, tsStart)
ALTER TABLE option_aggregates ADD INDEX idx_expirationDate expirationDate TYPE minmax GRANULARITY 2;
ALTER TABLE option_aggregates ADD INDEX idx_strike strike TYPE minmax GRANULARITY 2;
ALTER TABLE option_aggregates ADD INDEX idx_tsStart tsStart TYPE minmax GRANULARITY 2;
CREATE TABLE option_histories
(
symbol LowCardinality(String),
expirationDate Date,
strike Float64,
tsStart DateTime32,
open Float64,
daysToFront UInt16,
underlyingPrice Float64,
strikePercentageFromUnderlyingPrice Float64
)
ENGINE MergeTree()
ORDER BY (symbol, daysToFront, strikePercentageFromUnderlyingPrice)
CREATE TABLE calendar_histories
(
symbol LowCardinality(String),
tsStart DateTime32,
frontExpirationDate Date,
backExpirationDate Date,
daysToFrontExpiration UInt16,
daysBetweenFrontAndBackExpiration UInt16,
strike Float64,
underlyingPrice Float64,
strikePercentageFromUnderlyingPrice Float64,
calendarPrice Float64
)
ENGINE MergeTree()
PRIMARY KEY (symbol, daysToFrontExpiration, daysBetweenFrontAndBackExpiration, strikePercentageFromUnderlyingPrice)
ORDER BY (symbol, daysToFrontExpiration, daysBetweenFrontAndBackExpiration, strikePercentageFromUnderlyingPrice, tsStart)
-- INSERT INTO calendar_histories
-- SELECT
-- front_option.symbol as symbol,
-- front_option.tsStart as tsStart,
-- front_option.expirationDate as frontExpirationDate,
-- back_option.expirationDate as backExpirationDate,
-- front_option.daysToFront as daysToFrontExpiration,
-- backExpirationDate - frontExpirationDate as daysBetweenFrontAndBackExpiration,
-- front_option.strike as strike,
-- front_option.underlyingPrice as underlyingPrice,
-- front_option.strikePercentageFromUnderlyingPrice,
-- back_option.open - front_option.open as calendarPrice
-- FROM (
-- SELECT
-- symbol,
-- tsStart,
-- expirationDate,
-- strike,
-- open,
-- daysToFront,
-- underlyingPrice,
-- strikePercentageFromUnderlyingPrice
-- FROM option_histories
-- ) AS front_option
-- LEFT JOIN option_aggregates as back_option
-- ON front_option.symbol = back_option.symbol
-- AND front_option.strike = back_option.strike
-- AND front_option.tsStart = back_option.tsStart
-- WHERE back_option.expirationDate > front_option.expirationDate
-- SETTINGS join_algorithm = 'grace_hash', grace_hash_join_initial_buckets = 16, max_bytes_in_join = 536870912
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
"allowJs": true,
"checkJs": true,
"lib": ["es2022"]
},
"include": ["**/*"]
}