init
This commit is contained in:
@@ -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?
|
||||
@@ -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
@@ -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
|
||||
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
kubectl port-forward -n clickhouse clickhouse 8123:8123
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
kubectl rollout restart -n calendar-optimizer deployments/server
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
+1083
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"noEmit": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"lib": ["es2022"]
|
||||
},
|
||||
"include": ["**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user