init
commit
dc63f31647
@ -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,22 @@
|
|||||||
|
# adapted from example on pnpm.io
|
||||||
|
FROM node:20-slim AS build
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
RUN corepack enable
|
||||||
|
WORKDIR /app
|
||||||
|
# copy what's necessary to install dependencies:
|
||||||
|
COPY package.json pnpm-lock.yaml /app/
|
||||||
|
# install dependencies:
|
||||||
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||||
|
# copy what's necessary to build:
|
||||||
|
COPY tsconfig.json vite.config.ts index.html /app/
|
||||||
|
COPY src /app/src
|
||||||
|
# Vite injects envvars at build time, not runtime:
|
||||||
|
ENV VITE_SERVER_BASE_URL=https://calendar-optimizer-server.sakal.us
|
||||||
|
# run the build:
|
||||||
|
RUN pnpm run build
|
||||||
|
# hack to get spa-to-http to answer `/healthz` query:
|
||||||
|
RUN echo OK > ./dist/healthz
|
||||||
|
|
||||||
|
FROM devforth/spa-to-http:latest
|
||||||
|
COPY --from=build /app/dist .
|
@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
IMAGE_NAME=calendar-optimizer-frontend
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
kubectl rollout restart -n calendar-optimizer deployments/frontend
|
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
<title>Vite + Preact</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@preact/signals": "^1.2.2",
|
||||||
|
"@trpc/client": "^10.45.0",
|
||||||
|
"chart.js": "^4.4.1",
|
||||||
|
"preact": "^10.13.1",
|
||||||
|
"preact-iso": "^2.3.2",
|
||||||
|
"preact-render-to-string": "^6.3.1",
|
||||||
|
"react-chartjs-2": "^5.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@preact/preset-vite": "^2.5.0",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^4.3.2"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="27.68" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 296"><path fill="#673AB8" d="m128 0l128 73.9v147.8l-128 73.9L0 221.7V73.9z"></path><path fill="#FFF" d="M34.865 220.478c17.016 21.78 71.095 5.185 122.15-34.704c51.055-39.888 80.24-88.345 63.224-110.126c-17.017-21.78-71.095-5.184-122.15 34.704c-51.055 39.89-80.24 88.346-63.224 110.126Zm7.27-5.68c-5.644-7.222-3.178-21.402 7.573-39.253c11.322-18.797 30.541-39.548 54.06-57.923c23.52-18.375 48.303-32.004 69.281-38.442c19.922-6.113 34.277-5.075 39.92 2.148c5.644 7.223 3.178 21.403-7.573 39.254c-11.322 18.797-30.541 39.547-54.06 57.923c-23.52 18.375-48.304 32.004-69.281 38.441c-19.922 6.114-34.277 5.076-39.92-2.147Z"></path><path fill="#FFF" d="M220.239 220.478c17.017-21.78-12.169-70.237-63.224-110.126C105.96 70.464 51.88 53.868 34.865 75.648c-17.017 21.78 12.169 70.238 63.224 110.126c51.055 39.889 105.133 56.485 122.15 34.704Zm-7.27-5.68c-5.643 7.224-19.998 8.262-39.92 2.148c-20.978-6.437-45.761-20.066-69.28-38.441c-23.52-18.376-42.74-39.126-54.06-57.923c-10.752-17.851-13.218-32.03-7.575-39.254c5.644-7.223 19.999-8.261 39.92-2.148c20.978 6.438 45.762 20.067 69.281 38.442c23.52 18.375 42.739 39.126 54.06 57.923c10.752 17.85 13.218 32.03 7.574 39.254Z"></path><path fill="#FFF" d="M127.552 167.667c10.827 0 19.603-8.777 19.603-19.604c0-10.826-8.776-19.603-19.603-19.603c-10.827 0-19.604 8.777-19.604 19.603c0 10.827 8.777 19.604 19.604 19.604Z"></path></svg>
|
After Width: | Height: | Size: 1.6 KiB |
@ -0,0 +1,24 @@
|
|||||||
|
import { useLocation } from 'preact-iso';
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
const { url } = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<a href="/" class={url == '/' && 'active'}>
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
<a href="/calendar-optimizer" class={url == '/calendar-optimizer' && 'active'}>
|
||||||
|
Calendar Optimizer
|
||||||
|
</a>
|
||||||
|
<a href="/historical-calendar-prices" class={url == '/historical-calendar-prices' && 'active'}>
|
||||||
|
Historical Calendar Prices
|
||||||
|
</a>
|
||||||
|
<a href="/404" class={url == '/404' && 'active'}>
|
||||||
|
404
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { render } from 'preact';
|
||||||
|
import { LocationProvider, Router, Route } from 'preact-iso';
|
||||||
|
|
||||||
|
import { Header } from './components/Header.jsx';
|
||||||
|
import { Home } from './pages/Home/index.jsx';
|
||||||
|
import { CalendarOptimizer } from './pages/CalendarOptimizer/index.jsx';
|
||||||
|
import { NotFound } from './pages/_404.jsx';
|
||||||
|
import './style.css';
|
||||||
|
import { HistoricalCalendarPrices } from './pages/HistoricalCalendarPrices/HistoricalCalendarPrices.js';
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
return (
|
||||||
|
<LocationProvider>
|
||||||
|
<Header />
|
||||||
|
<main>
|
||||||
|
<Router>
|
||||||
|
<Route path="/" component={Home} />
|
||||||
|
<Route path="/calendar-optimizer" component={CalendarOptimizer} />
|
||||||
|
<Route path="/historical-calendar-prices" component={HistoricalCalendarPrices} />
|
||||||
|
<Route default component={NotFound} />
|
||||||
|
</Router>
|
||||||
|
</main>
|
||||||
|
</LocationProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<App />, document.getElementById('app'));
|
@ -0,0 +1,130 @@
|
|||||||
|
import { signal } from "@preact/signals";
|
||||||
|
import { useCallback, useEffect } from "preact/hooks";
|
||||||
|
import {trpc} from '../../trpc.js';
|
||||||
|
|
||||||
|
const availableUnderlyings = signal([]);
|
||||||
|
const chosenUnderlying = signal(null);
|
||||||
|
|
||||||
|
const availableAsOfDates = signal([]);
|
||||||
|
const chosenAsOfDate = signal(null);
|
||||||
|
|
||||||
|
const availableExpirations = signal([]);
|
||||||
|
const chosenExpiration = signal(null);
|
||||||
|
|
||||||
|
const availableStrikes = signal([]);
|
||||||
|
const chosenStrike = signal(null);
|
||||||
|
|
||||||
|
const optionContractUplotData = signal([]);
|
||||||
|
const underlyingUplotData = signal([]);
|
||||||
|
|
||||||
|
export function CalendarOptimizer(){
|
||||||
|
const handleInit = useCallback(()=>{
|
||||||
|
trpc.getAvailableUnderlyings
|
||||||
|
.query()
|
||||||
|
.then((availableUnderlyingsResponse)=>{
|
||||||
|
availableUnderlyings.value = availableUnderlyingsResponse;
|
||||||
|
});
|
||||||
|
},[]);
|
||||||
|
const handleUnderlyingChange = useCallback((e)=>{
|
||||||
|
console.log(`Chose Underlying: ${e.target.value}`);
|
||||||
|
chosenUnderlying.value = e.target.value;
|
||||||
|
trpc.getAvailableAsOfDates
|
||||||
|
.query({underlying:e.target.value})
|
||||||
|
.then((getAvailableAsOfDatesResponse)=>{
|
||||||
|
availableAsOfDates.value = getAvailableAsOfDatesResponse;
|
||||||
|
});
|
||||||
|
trpc.getOpensForUnderlying
|
||||||
|
.query({underlying:e.target.value})
|
||||||
|
.then((getOpensForUnderlyingResponse)=>{
|
||||||
|
underlyingUplotData.value = getOpensForUnderlyingResponse;
|
||||||
|
});
|
||||||
|
},[]);
|
||||||
|
const handleAsOfDateChange = useCallback((e)=>{
|
||||||
|
console.log(`Chose Date: ${e.target.value}`);
|
||||||
|
chosenAsOfDate.value = e.target.value;
|
||||||
|
trpc.getExpirationsForUnderlying
|
||||||
|
.query({underlying:chosenUnderlying.value, asOfDate:chosenAsOfDate.value})
|
||||||
|
.then((getExpirationsForUnderlyingResponse)=>{
|
||||||
|
availableExpirations.value = getExpirationsForUnderlyingResponse;
|
||||||
|
});
|
||||||
|
},[]);
|
||||||
|
const handleExpirationChange = useCallback((e)=>{
|
||||||
|
console.log(`Chose Expiration: ${e.target.value}`);
|
||||||
|
chosenExpiration.value = e.target.value;
|
||||||
|
trpc.getStrikesForUnderlying
|
||||||
|
.query({underlying:chosenUnderlying.value, asOfDate:chosenAsOfDate.value, expirationDate: e.target.value})
|
||||||
|
.then((getStrikesForUnderlyingResponse)=>{
|
||||||
|
availableStrikes.value = getStrikesForUnderlyingResponse;
|
||||||
|
});
|
||||||
|
},[]);
|
||||||
|
const handleStrikeChange = useCallback((e)=>{
|
||||||
|
console.log(`Chose Strike: ${e.target.value}`);
|
||||||
|
chosenStrike.value = e.target.value;
|
||||||
|
trpc.getOpensForOptionContract
|
||||||
|
.query({underlying:chosenUnderlying.value, expirationDate:chosenExpiration.value, strike:parseFloat(e.target.value)})
|
||||||
|
.then((getOpensForOptionContractResponse)=>{
|
||||||
|
optionContractUplotData.value = getOpensForOptionContractResponse;
|
||||||
|
});
|
||||||
|
},[]);
|
||||||
|
|
||||||
|
useEffect(handleInit, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label>Available Underlyings</label>
|
||||||
|
{
|
||||||
|
availableUnderlyings.value.length === 0
|
||||||
|
? "Loading..."
|
||||||
|
: <select onChange={handleUnderlyingChange}>
|
||||||
|
{availableUnderlyings.value.map((availableUnderlying)=>(
|
||||||
|
<option value={availableUnderlying}>{availableUnderlying}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Available "As-of" Dates</label>
|
||||||
|
{
|
||||||
|
availableAsOfDates.value.length === 0
|
||||||
|
? "Loading..."
|
||||||
|
: <select onChange={handleAsOfDateChange}>
|
||||||
|
{availableAsOfDates.value.map((availableAsOfDate)=>(
|
||||||
|
<option value={availableAsOfDate}>{availableAsOfDate}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Available Expirations</label>
|
||||||
|
{
|
||||||
|
availableExpirations.value.length === 0
|
||||||
|
? "Loading..."
|
||||||
|
: <select onChange={handleExpirationChange}>
|
||||||
|
{availableExpirations.value.map((availableExpiration)=>(
|
||||||
|
<option value={availableExpiration}>{availableExpiration}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Available Strikes</label>
|
||||||
|
{
|
||||||
|
availableStrikes.value.length === 0
|
||||||
|
? "Loading..."
|
||||||
|
: <select onChange={handleStrikeChange}>
|
||||||
|
{availableStrikes.value.map((availableStrike)=>(
|
||||||
|
<option value={availableStrike}>{availableStrike}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{/* {chosenUnderlying.value!==null && underlyingUplotData.value.length>0
|
||||||
|
? <UPlot data={underlyingUplotData.value} title="Underlying" opts={uplotOpts}/>
|
||||||
|
: <></>}
|
||||||
|
{chosenUnderlying.value!==null && chosenAsOfDate.value!==null && chosenExpiration.value!==null && chosenStrike.value!==null && optionContractUplotData.value.length>0
|
||||||
|
? <UPlot data={optionContractUplotData.value} title="Option Contract" opts={uplotOpts}/>
|
||||||
|
: <></>} */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,321 @@
|
|||||||
|
import { signal } from "@preact/signals";
|
||||||
|
import { useCallback, useEffect } from "preact/hooks";
|
||||||
|
import {trpc} from '../../trpc.js';
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
LinearScale,
|
||||||
|
CategoryScale,
|
||||||
|
PointElement,
|
||||||
|
Tooltip,
|
||||||
|
Title,
|
||||||
|
} from 'chart.js';
|
||||||
|
import { Scatter } from 'react-chartjs-2';
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
ChartJS.register(LinearScale, CategoryScale, PointElement, Tooltip, Title,);
|
||||||
|
|
||||||
|
const availableUnderlyings = signal([]);
|
||||||
|
const chosenUnderlying = signal(null);
|
||||||
|
|
||||||
|
const chosenDaysToFrontExpiration = signal(14);
|
||||||
|
|
||||||
|
const chosenDaysBetweenFrontAndBackExpiration = signal(14);
|
||||||
|
|
||||||
|
const chosenStrikePercentageFromUnderlyingPrice = signal(1.4);
|
||||||
|
const chosenStrikePercentageFromUnderlyingPriceRadius = signal(0.05);
|
||||||
|
|
||||||
|
const chosenExitToFrontExpiration = signal(2);
|
||||||
|
|
||||||
|
const historicalStockQuoteChartData = signal([]);
|
||||||
|
|
||||||
|
const historicalCalendarQuoteChartData = signal([]);
|
||||||
|
|
||||||
|
const historicalCalendarExitQuoteChartData = signal([]);
|
||||||
|
|
||||||
|
|
||||||
|
export function HistoricalCalendarPrices(){
|
||||||
|
const refreshHistoricalStockQuoteChartData = useCallback(()=>{
|
||||||
|
trpc.getHistoricalStockQuoteChartData
|
||||||
|
.query({
|
||||||
|
underlying:chosenUnderlying.value,
|
||||||
|
})
|
||||||
|
.then((getHistoricalStockQuoteChartDataResponse)=>{
|
||||||
|
historicalStockQuoteChartData.value = getHistoricalStockQuoteChartDataResponse;
|
||||||
|
})
|
||||||
|
},[]);
|
||||||
|
const refreshHistoricalCalendarQuoteChartData = useCallback(()=>{
|
||||||
|
trpc.getHistoricalCalendarQuoteChartData
|
||||||
|
.query({
|
||||||
|
underlying:chosenUnderlying.value,
|
||||||
|
daysToFrontExpiration:chosenDaysToFrontExpiration.value,
|
||||||
|
daysBetweenFrontAndBackExpiration:chosenDaysBetweenFrontAndBackExpiration.value,
|
||||||
|
strikePercentageFromUnderlyingPriceRangeMin:chosenStrikePercentageFromUnderlyingPrice.value - chosenStrikePercentageFromUnderlyingPriceRadius.value,
|
||||||
|
strikePercentageFromUnderlyingPriceRangeMax:chosenStrikePercentageFromUnderlyingPrice.value + chosenStrikePercentageFromUnderlyingPriceRadius.value,
|
||||||
|
})
|
||||||
|
.then((getHistoricalCalendarQuoteChartDataResponse)=>{
|
||||||
|
historicalCalendarQuoteChartData.value = getHistoricalCalendarQuoteChartDataResponse;
|
||||||
|
})
|
||||||
|
},[]);
|
||||||
|
const refreshHistoricalCalendarExitQuoteChartData = useCallback(()=>{
|
||||||
|
trpc.getHistoricalCalendarExitQuoteChartData
|
||||||
|
.query({
|
||||||
|
underlying:chosenUnderlying.value,
|
||||||
|
daysToFrontExpiration:chosenExitToFrontExpiration.value,
|
||||||
|
daysBetweenFrontAndBackExpiration:chosenDaysBetweenFrontAndBackExpiration.value,
|
||||||
|
})
|
||||||
|
.then((getHistoricalCalendarExitQuoteChartDataResponse)=>{
|
||||||
|
historicalCalendarExitQuoteChartData.value = getHistoricalCalendarExitQuoteChartDataResponse;
|
||||||
|
})
|
||||||
|
},[]);
|
||||||
|
const handleInit = useCallback(()=>{
|
||||||
|
trpc.getAvailableUnderlyings
|
||||||
|
.query()
|
||||||
|
.then((availableUnderlyingsResponse)=>{
|
||||||
|
availableUnderlyings.value = availableUnderlyingsResponse;
|
||||||
|
chosenUnderlying.value = availableUnderlyingsResponse[0];
|
||||||
|
refreshHistoricalStockQuoteChartData();
|
||||||
|
refreshHistoricalCalendarQuoteChartData();
|
||||||
|
refreshHistoricalCalendarExitQuoteChartData();
|
||||||
|
});
|
||||||
|
},[]);
|
||||||
|
const handleUnderlyingChange = useCallback((e)=>{
|
||||||
|
chosenUnderlying.value = e.target.value;
|
||||||
|
refreshHistoricalStockQuoteChartData();
|
||||||
|
refreshHistoricalCalendarQuoteChartData();
|
||||||
|
refreshHistoricalCalendarExitQuoteChartData();
|
||||||
|
},[]);
|
||||||
|
const handleDaysToFrontExpirationChange = useCallback((e)=>{
|
||||||
|
chosenDaysToFrontExpiration.value = parseInt(e.target.value);
|
||||||
|
refreshHistoricalCalendarQuoteChartData();
|
||||||
|
},[]);
|
||||||
|
const handleDaysBetweenFrontAndBackExpirationChange = useCallback((e)=>{
|
||||||
|
chosenDaysBetweenFrontAndBackExpiration.value = parseInt(e.target.value);
|
||||||
|
refreshHistoricalCalendarQuoteChartData();
|
||||||
|
refreshHistoricalCalendarExitQuoteChartData();
|
||||||
|
},[]);
|
||||||
|
const handleStrikePercentageFromUnderlyingPriceChange = useCallback((e)=>{
|
||||||
|
chosenStrikePercentageFromUnderlyingPrice.value = parseFloat(e.target.value);
|
||||||
|
refreshHistoricalCalendarQuoteChartData();
|
||||||
|
},[]);
|
||||||
|
const handleStrikePercentageFromUnderlyingPriceRadiusChange = useCallback((e)=>{
|
||||||
|
chosenStrikePercentageFromUnderlyingPriceRadius.value = parseFloat(e.target.value);
|
||||||
|
refreshHistoricalCalendarQuoteChartData();
|
||||||
|
},[]);
|
||||||
|
const handleExitToFrontExpirationChange = useCallback((e)=>{
|
||||||
|
chosenExitToFrontExpiration.value = parseInt(e.target.value);
|
||||||
|
refreshHistoricalCalendarExitQuoteChartData();
|
||||||
|
},[]);
|
||||||
|
|
||||||
|
useEffect(handleInit, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label>Available Underlyings</label>
|
||||||
|
{
|
||||||
|
availableUnderlyings.value.length === 0
|
||||||
|
? "Loading..."
|
||||||
|
: <select onChange={handleUnderlyingChange}>
|
||||||
|
{availableUnderlyings.value.map((availableUnderlying)=>(
|
||||||
|
<option value={availableUnderlying}>{availableUnderlying}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Now-to-Front-Month "Days to Expiration"</label>
|
||||||
|
<input type="text" onChange={handleDaysToFrontExpirationChange} value={chosenDaysToFrontExpiration.value} />
|
||||||
|
Days
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Front-to-Back-Month "Days to Expiration" Difference</label>
|
||||||
|
<input type="text" onChange={handleDaysBetweenFrontAndBackExpirationChange} value={chosenDaysBetweenFrontAndBackExpiration.value} />
|
||||||
|
Days Difference
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>"Strike Percentage From Underlying Price" Range</label>
|
||||||
|
<input type="text" onChange={handleStrikePercentageFromUnderlyingPriceChange} value={chosenStrikePercentageFromUnderlyingPrice.value} />
|
||||||
|
%
|
||||||
|
+/-
|
||||||
|
<input type="text" onChange={handleStrikePercentageFromUnderlyingPriceRadiusChange} value={chosenStrikePercentageFromUnderlyingPriceRadius.value} />
|
||||||
|
% from ATM
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Exit-to-Front-Month "Days to Expiration"</label>
|
||||||
|
<input type="text" onChange={handleExitToFrontExpirationChange} value={chosenExitToFrontExpiration.value} />
|
||||||
|
Days
|
||||||
|
</div>
|
||||||
|
<div className="chart-container">
|
||||||
|
{chosenUnderlying.value!==null && historicalStockQuoteChartData.value.length>0
|
||||||
|
? <div className="chart">
|
||||||
|
<Scatter
|
||||||
|
data={{
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Stock Open Price",
|
||||||
|
data: historicalStockQuoteChartData.value
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "Time"
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback: function(value, index, ticks) {
|
||||||
|
return (new Date(value as number*1000)).toISOString().substring(0,10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: false,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value, index, ticks) {
|
||||||
|
return "$"+value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
radius: 1,
|
||||||
|
borderWidth: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "Stock Price"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: false,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
: <div>Loading Chart...</div>}
|
||||||
|
</div>
|
||||||
|
<div className="chart-container">
|
||||||
|
{chosenUnderlying.value!==null && historicalCalendarQuoteChartData.value.length>0
|
||||||
|
? <div className="chart">
|
||||||
|
<Scatter
|
||||||
|
data={{
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Calendar Open Price",
|
||||||
|
data: historicalCalendarQuoteChartData.value
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "Time"
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback: function(value, index, ticks) {
|
||||||
|
return (new Date(value as number*1000)).toISOString().substring(0,10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value, index, ticks) {
|
||||||
|
return "$"+value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "Calendar Price (Under Like Conditions)"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: false,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
: <div>Loading Chart...</div>}
|
||||||
|
|
||||||
|
{chosenUnderlying.value!==null && historicalCalendarQuoteChartData.value.length>0
|
||||||
|
? <div className="chart">
|
||||||
|
<Scatter
|
||||||
|
data={{
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Calendar Exit Price",
|
||||||
|
data: historicalCalendarExitQuoteChartData.value
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'linear',
|
||||||
|
beginAtZero: false,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "%-From-the-Money"
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback: function(value, index, ticks) {
|
||||||
|
return value.toString()+"%";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value, index, ticks) {
|
||||||
|
return "$"+value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
borderWidth: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: ["Calendar Prices at Exit","by %-age from-the-money"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: false,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
: <div>Loading Chart...</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
.chart-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: 1800px;
|
||||||
|
height: 480px;
|
||||||
|
}
|
||||||
|
.chart-container > .chart {
|
||||||
|
width: 880px;
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import preactLogo from '../../assets/preact.svg';
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
export function Logo(){
|
||||||
|
return (
|
||||||
|
<a href="https://preactjs.com" target="_blank">
|
||||||
|
<img src={preactLogo} alt="Preact logo" height="160" width="160" />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Home() {
|
||||||
|
return (
|
||||||
|
<div class="home">
|
||||||
|
<Logo/>
|
||||||
|
<h1>Get Started with the Calendar Optimizer</h1>
|
||||||
|
<a href="/calendar-optimizer" target="_blank">Calendar Optimizer</a>
|
||||||
|
<a href="/historical-calendar-prices" target="_blank">Historical Calendar Prices</a>
|
||||||
|
<section>
|
||||||
|
<Resource
|
||||||
|
title="Learn Preact"
|
||||||
|
description="If you're new to Preact, try the interactive tutorial to learn important concepts"
|
||||||
|
href="https://preactjs.com/tutorial"
|
||||||
|
/>
|
||||||
|
<Resource
|
||||||
|
title="Differences to React"
|
||||||
|
description="If you're coming from React, you may want to check out our docs to see where Preact differs"
|
||||||
|
href="https://preactjs.com/guide/v10/differences-to-react"
|
||||||
|
/>
|
||||||
|
<Resource
|
||||||
|
title="Learn Vite"
|
||||||
|
description="To learn more about Vite and how you can customize it to fit your needs, take a look at their excellent documentation"
|
||||||
|
href="https://vitejs.dev"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resource(props) {
|
||||||
|
return (
|
||||||
|
<a href={props.href} target="_blank" class="resource">
|
||||||
|
<h2>{props.title}</h2>
|
||||||
|
<p>{props.description}</p>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
img {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #673ab8aa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home section {
|
||||||
|
margin-top: 5rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
column-gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #222;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource:hover {
|
||||||
|
border: 1px solid #000;
|
||||||
|
box-shadow: 0 25px 50px -12px #673ab888;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
.home section {
|
||||||
|
margin-top: 5rem;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
row-gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.resource {
|
||||||
|
color: #ccc;
|
||||||
|
background-color: #161616;
|
||||||
|
}
|
||||||
|
.resource:hover {
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
export function NotFound() {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h1>404: Not Found</h1>
|
||||||
|
<p>It's gone :(</p>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color: #222;
|
||||||
|
background-color: #ffffff;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
background-color: #673ab8;
|
||||||
|
}
|
||||||
|
|
||||||
|
header nav {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
header a {
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
header a.active {
|
||||||
|
background-color: #0005;
|
||||||
|
}
|
||||||
|
|
||||||
|
header a:hover {
|
||||||
|
background-color: #0008;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
max-width: 1280px;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
main {
|
||||||
|
margin: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
color: #ccc;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
|
||||||
|
import type { AppRouter } from '../../server/src/index';
|
||||||
|
|
||||||
|
export const trpc = createTRPCProxyClient<AppRouter>({
|
||||||
|
links: [
|
||||||
|
httpBatchLink({
|
||||||
|
// `import.meta.env` is what Vite uses to expose envvars:
|
||||||
|
url: import.meta.env.VITE_SERVER_BASE_URL || 'https://calendar-optimizer-server.sakal.us',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
transformer: {
|
||||||
|
serialize: (x)=>x,
|
||||||
|
deserialize: (x)=>x,
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"noEmit": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
|
||||||
|
/* Preact Config */
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "preact",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"paths": {
|
||||||
|
"react": ["./node_modules/preact/compat/"],
|
||||||
|
"react-dom": ["./node_modules/preact/compat/"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["node_modules/vite/client.d.ts", "**/*"]
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import preact from '@preact/preset-vite';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [preact()],
|
||||||
|
});
|
@ -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" ]
|
@ -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
|
||||||
|
|
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
kubectl port-forward -n clickhouse clickhouse 8123:8123
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
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,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": ["**/*"]
|
||||||
|
}
|
Loading…
Reference in New Issue