Compare commits

...

43 Commits

Author SHA1 Message Date
avraham c83f2ce0a0 use Slider for lookback period 2025-03-03 21:35:51 -05:00
avraham 4d989f10eb gitignore Bruno files 2025-03-02 17:54:41 -05:00
avraham 906e63eb91 use line on stock price chart; more compact data transfer format 2025-03-02 17:53:59 -05:00
avraham 3732680d52 use CLICKHOUSE_URL envvar instead of CLICKHOUSE_HOST 2025-03-02 09:35:43 -05:00
avraham 3c841f488c attempt to remove outliers from chart data 2025-02-24 09:15:57 -05:00
avraham b6cc25b769 begin with narrower lookback period 2025-02-24 09:15:35 -05:00
avraham 14dff05785 fix: case where calendar exit price data is empty 2025-02-24 09:15:09 -05:00
avraham f05831b5f1 return moniness in percentage, not ratio 2025-02-23 13:20:05 -05:00
avraham b7f9d60715 friendlier variable names; use sliders for input 2025-02-23 13:11:34 -05:00
avraham b2169e1da7 adjust moniness step in form 2025-02-07 09:36:05 -05:00
avraham aeac0e1042 update Dockerfile to use latest corepack to fix npm keys 2025-02-07 09:31:34 -05:00
avraham 904d2da84a send strikePercentageFromUnderlyingPriceRange* as ratio not percentage 2025-02-07 09:30:50 -05:00
avraham 694bb38536 update Dockerfile to use latest corepack to fix npm keys 2025-02-07 09:25:33 -05:00
avraham 64a5172ea8 use new clickhouse tables 2025-02-07 09:23:47 -05:00
avraham ac9554ba21 migrated to mui Grid2 2024-10-06 15:14:30 -04:00
avraham 2597c0f6ac attempt fix: trpc routes not found 2024-09-29 20:19:51 -04:00
avraham add21288be fix: Dockerfile - no longer using postcss and tailwind 2024-09-29 20:03:58 -04:00
avraham db809d7b57 remove unused imports 2024-09-29 20:02:55 -04:00
avraham 8986dc4ea9 fully adopt @mui/material; refactor 2024-09-29 20:01:53 -04:00
avraham ea9bd307f3 refactor 2024-09-26 21:00:58 -04:00
avraham 7bca5e701d refactor 2024-09-26 20:14:11 -04:00
avraham 82915fb0b5 aider: implement simple cli in ink for running backtests 2024-08-11 19:58:40 -04:00
avraham d134385bd7 use tsc for type-checking 2024-08-11 19:52:29 -04:00
avraham 3bc976b63a use standard export name for AggregateDatabase subtype instances 2024-08-11 19:51:32 -04:00
avraham 72f45b81a5 fix: clickhouse-to-lmdbx script imports 2024-08-11 19:16:30 -04:00
avraham d6762fdae5 re-organized code; implemented getAggregate() where it was missing 2024-08-11 18:08:54 -04:00
avraham 15a5d7c67b use lmdbx for stockdb in backtest, and limit to calendars within 5% of the money 2024-08-11 17:39:34 -04:00
avraham fe1265810d fix: the "optiondb-lmdbx" calendardb was returning too many possible strike-front-back permutations due to faulty programming 2024-08-11 17:38:04 -04:00
avraham 8d908521fd fix: getClosingPrice() for two calendardb modules 2024-08-11 17:36:31 -04:00
avraham 5b3e9f85f6 fix: lmdbx optiondb getAggregateSync() returned tsStart = undefined 2024-08-11 17:35:00 -04:00
avraham 666ff16583 add getAggregate method 2024-08-11 17:33:31 -04:00
avraham 35b3278d08 add aider 2024-08-11 17:30:45 -04:00
avraham 704d59a363 use tsx to run 2024-08-11 17:29:08 -04:00
avraham 93d5ac8a30 refactor; backtest using lmdbx 2024-08-07 21:57:03 -04:00
avraham 1d83cd419a fix: Update clickhouse-to-lmdbx script to use retry mechanism and updated utility functions. 2024-08-04 19:26:27 -04:00
avraham cfb207aae8 Revert "feat: Implement retry mechanism for clickhouse timeout errors with exponential backoff strategy in clickhouse-to-lmdbx.ts script"
This reverts commit c749321fe9.
2024-08-04 18:24:28 -04:00
avraham c749321fe9 feat: Implement retry mechanism for clickhouse timeout errors with exponential backoff strategy in clickhouse-to-lmdbx.ts script 2024-08-04 18:23:19 -04:00
avraham eba5344b15 fix biome kvetches 2024-08-02 17:00:35 -04:00
avraham 39bb6c85f8 fix biome kvetches 2024-08-02 17:00:01 -04:00
avraham bf094de461 improve and extract nextDate() function; improve clickhouse-to-lmdbx sync script 2024-08-02 16:41:42 -04:00
avraham 85cafd985d don't use /tmp dir for lmdbx dbs 2024-08-02 16:40:28 -04:00
avraham 70b690ab9d converted frontend from tailwind to material-ui using AI 2024-08-02 13:37:08 -04:00
avraham 3e5e728d92 well-interfaced pluggable databases 2024-07-30 03:46:41 +00:00
57 changed files with 4479 additions and 1801 deletions
+3
View File
@@ -1 +1,4 @@
.pnpm-store
node_modules
.aider*
.env
+2
View File
@@ -1 +1,3 @@
nodejs 20.15.1
python 3.12.4
pnpm 9.7.1
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"organizeImports": {
"enabled": true
},
"formatter": {
"indentWidth": 2,
"indentStyle": "space"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}
+2 -2
View File
@@ -2,14 +2,14 @@
FROM node:20-slim AS build
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN npm install -g corepack@latest && 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 index.css postcss.config.js tailwind.config.js /app/
COPY tsconfig.json vite.config.ts index.html index.css /app/
COPY src /app/src
# Vite injects envvars at build time, not runtime:
ENV VITE_SERVER_BASE_URL=https://calendar-optimizer-server.sakal.us
-4
View File
@@ -1,7 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
+6 -3
View File
@@ -7,20 +7,23 @@
"preview": "vite preview"
},
"dependencies": {
"@mui/icons-material": "^6.1.1",
"@mui/material": "^6.1.1",
"@mui/system": "^6.1.1",
"@mui/x-date-pickers": "^7.18.0",
"@preact/signals": "^1.2.2",
"@trpc/client": "^10.45.0",
"chart.js": "^4.4.1",
"date-fns": "^3.6.0",
"dotenv": "^16.4.1",
"preact": "^10.13.1",
"preact-iso": "^2.3.2",
"preact-render-to-string": "^6.3.1",
"react": "18.3.1",
"react-chartjs-2": "^5.2.0"
},
"devDependencies": {
"@preact/preset-vite": "^2.5.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.4",
"typescript": "^5.3.3",
"vite": "^4.3.2"
}
+645 -723
View File
File diff suppressed because it is too large Load Diff
-6
View File
@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+26 -13
View File
@@ -1,27 +1,40 @@
import { useLocation } from "preact-iso";
import { AppBar, Toolbar, Button, Box } from "@mui/material";
import { styled } from "@mui/system";
const StyledToolbar = styled(Toolbar)({
display: "flex",
justifyContent: "center",
backgroundColor: "#E0E7FF", // Indigo-200 equivalent
});
const StyledButton = styled(Button)(({ theme, active }) => ({
color: theme.palette.text.primary,
fontWeight: active ? "bold" : "normal",
textDecoration: active ? "underline" : "none",
}));
export function Header() {
const { url } = useLocation();
return (
<header class="flex flex-row justify-center">
<nav class="flex flex-row justify-center text-lg gap-4 max-w-5xl py-2 px-3 rounded-full bg-indigo-200">
<a
<AppBar position="static" elevation={0}>
<StyledToolbar>
<Box sx={{ display: "flex", gap: 2 }}>
<StyledButton
href="/"
class={
(url === "/" || url === "/historical-calendar-prices") &&
"underline font-bold"
}
active={url === "/" || url === "/historical-calendar-prices"}
>
Historical Calendar Prices
</a>
<a
</StyledButton>
<StyledButton
href="/calendar-optimizer"
class={url === "/calendar-optimizer" && "underline font-bold"}
active={url === "/calendar-optimizer"}
>
Calendar Optimizer
</a>
</nav>
</header>
</StyledButton>
</Box>
</StyledToolbar>
</AppBar>
);
}
+8 -3
View File
@@ -1,17 +1,21 @@
import _ from "./env";
import { render } from "preact";
import { LocationProvider, Router, Route } from "preact-iso";
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { Header } from "./components/Header.jsx";
import { CalendarOptimizer } from "./pages/CalendarOptimizer.js";
import { NotFound } from "./pages/_404.jsx";
// import './style.css';
import { HistoricalCalendarPrices } from "./pages/HistoricalCalendarPrices.js";
const theme = createTheme();
export function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<LocationProvider>
<div class="flex flex-col justify-start gap-4">
<div>
<Header />
<main>
<Router>
@@ -26,6 +30,7 @@ export function App() {
</main>
</div>
</LocationProvider>
</ThemeProvider>
);
}
+94 -102
View File
@@ -10,7 +10,16 @@ import {
Title,
} from "chart.js";
import { Scatter } from "react-chartjs-2";
// import "./style.css";
import {
Container,
Grid,
Typography,
FormControl,
InputLabel,
Select,
MenuItem,
Paper,
} from "@mui/material";
ChartJS.register(LinearScale, CategoryScale, PointElement, Tooltip, Title);
@@ -77,7 +86,7 @@ function chooseStrike(strike: string) {
.query({
underlying: chosenUnderlying.value,
expirationDate: chosenExpiration.value,
strike: parseFloat(strike),
strike: Number.parseFloat(strike),
})
.then((getOpensForOptionContractResponse) => {
optionContractUplotData.value = getOpensForOptionContractResponse;
@@ -114,98 +123,87 @@ export function CalendarOptimizer() {
useEffect(handleInit, []);
return (
/* container for centering: */
<div class="flex flex-row justify-center">
<div class="flex flex-col justify-start gap-4">
{/* inputs form container: */}
<div class="flex flex-col justify-start gap-1">
<div class="flex flex-row w-160 gap-3">
<div class="text-right w-1/3">
<label>Available Underlyings</label>
</div>
<div class="my-auto w-2/3">
{availableUnderlyings.value.length === 0 ? (
"Loading..."
) : (
<select
<Container maxWidth="lg">
<Grid container spacing={4}>
<Grid item xs={12}>
<Typography variant="h4" gutterBottom>
Calendar Optimizer
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Paper elevation={3} sx={{ p: 3 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel>Available Underlyings</InputLabel>
<Select
value={chosenUnderlying.value || ""}
onChange={handleUnderlyingChange}
class="border border-gray-300 focus:border-blue-400"
label="Available Underlyings"
>
{availableUnderlyings.value.map((availableUnderlying) => (
<option value={availableUnderlying}>
{availableUnderlying}
</option>
{availableUnderlyings.value.map((underlying) => (
<MenuItem key={underlying} value={underlying}>
{underlying}
</MenuItem>
))}
</select>
)}
</div>
</div>
<div class="flex flex-row w-160 gap-3">
<div class="text-right w-1/3">
<label>Available "As-of" Dates</label>
</div>
<div class="my-auto w-2/3">
{availableAsOfDates.value.length === 0 ? (
"Loading..."
) : (
<select
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel>Available "As-of" Dates</InputLabel>
<Select
value={chosenAsOfDate.value || ""}
onChange={handleAsOfDateChange}
class="border border-gray-300 focus:border-blue-400"
label='Available "As-of" Dates'
>
{availableAsOfDates.value.map((availableAsOfDate) => (
<option value={availableAsOfDate}>
{availableAsOfDate}
</option>
{availableAsOfDates.value.map((asOfDate) => (
<MenuItem key={asOfDate} value={asOfDate}>
{asOfDate}
</MenuItem>
))}
</select>
)}
</div>
</div>
<div class="flex flex-row w-160 gap-3">
<div class="text-right w-1/3">
<label>Available Expirations</label>
</div>
<div class="my-auto w-2/3">
{availableExpirations.value.length === 0 ? (
"Loading..."
) : (
<select
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel>Available Expirations</InputLabel>
<Select
value={chosenExpiration.value || ""}
onChange={handleExpirationChange}
class="border border-gray-300 focus:border-blue-400"
label="Available Expirations"
>
{availableExpirations.value.map((availableExpiration) => (
<option value={availableExpiration}>
{availableExpiration}
</option>
{availableExpirations.value.map((expiration) => (
<MenuItem key={expiration} value={expiration}>
{expiration}
</MenuItem>
))}
</select>
)}
</div>
</div>
<div class="flex flex-row w-160 gap-3">
<div class="text-right w-1/3">
<label>Available Strikes</label>
</div>
<div class="my-auto w-2/3">
{availableStrikes.value.length === 0 ? (
"Loading..."
) : (
<select
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel>Available Strikes</InputLabel>
<Select
value={chosenStrike.value || ""}
onChange={handleStrikeChange}
class="border border-gray-300 focus:border-blue-400"
label="Available Strikes"
>
{availableStrikes.value.map((availableStrike) => (
<option value={availableStrike}>{availableStrike}</option>
{availableStrikes.value.map((strike) => (
<MenuItem key={strike} value={strike}>
{strike}
</MenuItem>
))}
</select>
)}
</div>
</div>
</div>
<div className="chart-container">
</Select>
</FormControl>
</Grid>
</Grid>
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper elevation={3} sx={{ p: 3, height: "100%" }}>
{chosenUnderlying.value !== null &&
underlyingUplotData.value.length > 0 ? (
<div className="chart">
<Scatter
data={{
datasets: [
@@ -223,24 +221,20 @@ export function CalendarOptimizer() {
text: "Time",
},
ticks: {
callback: function (value, index, ticks) {
callback: (value, index, ticks) => {
return new Date((value as number) * 1000)
.toISOString()
.substring(0, 10);
},
},
// min: (new Date(chosenLookbackPeriodStart.value)).getTime()/1000,
// max: (new Date(chosenLookbackPeriodEnd.value)).getTime()/1000,
},
y: {
beginAtZero: false,
ticks: {
callback: function (value, index, ticks) {
return "$" + value.toString();
callback: (value, index, ticks) => {
return `$${value.toString()}`;
},
},
// min: 0,
// max: maxChartPrice.value,
},
},
elements: {
@@ -265,16 +259,18 @@ export function CalendarOptimizer() {
maintainAspectRatio: false,
}}
/>
</div>
) : (
<></>
<Typography>Loading Chart...</Typography>
)}
</Paper>
</Grid>
<Grid item xs={12}>
<Paper elevation={3} sx={{ p: 3 }}>
{chosenUnderlying.value !== null &&
chosenAsOfDate.value !== null &&
chosenExpiration.value !== null &&
chosenStrike.value !== null &&
optionContractUplotData.value.length > 0 ? (
<div className="chart">
<Scatter
data={{
datasets: [
@@ -292,24 +288,20 @@ export function CalendarOptimizer() {
text: "Time",
},
ticks: {
callback: function (value, index, ticks) {
callback: (value, index, ticks) => {
return new Date((value as number) * 1000)
.toISOString()
.substring(0, 10);
},
},
// min: (new Date(chosenLookbackPeriodStart.value)).getTime()/1000,
// max: (new Date(chosenLookbackPeriodEnd.value)).getTime()/1000,
},
y: {
beginAtZero: false,
ticks: {
callback: function (value, index, ticks) {
return "$" + value.toString();
callback: (value, index, ticks) => {
return `$${value.toString()}`;
},
},
// min: 0,
// max: maxChartPrice.value,
},
},
elements: {
@@ -334,12 +326,12 @@ export function CalendarOptimizer() {
maintainAspectRatio: false,
}}
/>
</div>
) : (
<></>
<Typography>Loading Chart...</Typography>
)}
</div>
</div>
</div>
</Paper>
</Grid>
</Grid>
</Container>
);
}
+226 -387
View File
@@ -1,4 +1,3 @@
import { signal, computed } from "@preact/signals";
import { useEffect } from "preact/hooks";
import { trpc } from "../trpc.js";
import {
@@ -6,381 +5,160 @@ import {
LinearScale,
CategoryScale,
PointElement,
LineElement,
Tooltip,
Title,
} from "chart.js";
import { Scatter } from "react-chartjs-2";
// import './style.css';
import {
Container,
Grid2,
Typography,
Paper,
Popper,
ClickAwayListener,
Stack,
} from "@mui/material";
import {
availableUnderlyings,
calendarExitPriceChartData,
isPopperOpen,
lookbackPeriodEnd,
lookbackPeriodStart,
maxChartPrice,
maxN,
popperAnchorEl,
popperContent,
similarCalendarPriceChartData,
stockPriceChartData,
underlying,
} from "./HistoricalCalendarPrices/state.js";
import { EditableStrike } from "./HistoricalCalendarPrices/EditableStrike.js";
import {
refreshcalendarExitPriceChartData,
refreshSimilarCalendarPriceChartData,
refreshStockPriceChartData,
} from "./HistoricalCalendarPrices/actions.js";
import { EditableUnderlying } from "./HistoricalCalendarPrices/EditableUnderlying.js";
import { EditableOpenDTE } from "./HistoricalCalendarPrices/EditableOpenDTE.js";
import { EditableExitDTE } from "./HistoricalCalendarPrices/EditableExitDTE.js";
import { EditableSpan } from "./HistoricalCalendarPrices/EditableSpan.js";
import { EditableLookbackPeriod } from "./HistoricalCalendarPrices/EditableLookbackPeriod.js";
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([]);
const chosenLookbackPeriodStart = signal("2022-01-01");
const chosenLookbackPeriodEnd = signal("2024-01-01");
const maxChartPrice = computed(() =>
Math.max(
Math.max.apply(
null,
historicalCalendarQuoteChartData.value.map((d) => d.y)
),
Math.max.apply(
null,
historicalCalendarExitQuoteChartData.value.map((d) => d.y)
)
)
ChartJS.register(
LinearScale,
CategoryScale,
PointElement,
LineElement,
Tooltip,
Title
);
const maxN = computed(() =>
Math.max.apply(
null,
historicalCalendarExitQuoteChartData.value.map((d) => d.n)
)
);
const refreshHistoricalStockQuoteChartData = () => {
historicalStockQuoteChartData.value = [];
trpc.getHistoricalStockQuoteChartData
.query({
underlying: chosenUnderlying.value,
lookbackPeriodStart: chosenLookbackPeriodStart.value,
lookbackPeriodEnd: chosenLookbackPeriodEnd.value,
})
.then((getHistoricalStockQuoteChartDataResponse) => {
historicalStockQuoteChartData.value =
getHistoricalStockQuoteChartDataResponse;
});
};
const refreshHistoricalCalendarQuoteChartData = () => {
historicalCalendarQuoteChartData.value = [];
trpc.getHistoricalCalendarQuoteChartData
.query({
underlying: chosenUnderlying.value,
daysToFrontExpiration: chosenDaysToFrontExpiration.value,
daysBetweenFrontAndBackExpiration:
chosenDaysBetweenFrontAndBackExpiration.value,
strikePercentageFromUnderlyingPriceRangeMin:
chosenStrikePercentageFromUnderlyingPrice.value -
chosenStrikePercentageFromUnderlyingPriceRadius.value,
strikePercentageFromUnderlyingPriceRangeMax:
chosenStrikePercentageFromUnderlyingPrice.value +
chosenStrikePercentageFromUnderlyingPriceRadius.value,
lookbackPeriodStart: chosenLookbackPeriodStart.value,
lookbackPeriodEnd: chosenLookbackPeriodEnd.value,
})
.then((getHistoricalCalendarQuoteChartDataResponse) => {
historicalCalendarQuoteChartData.value =
getHistoricalCalendarQuoteChartDataResponse;
});
};
const refreshHistoricalCalendarExitQuoteChartData = () => {
historicalCalendarExitQuoteChartData.value = [];
trpc.getHistoricalCalendarExitQuoteChartData
.query({
underlying: chosenUnderlying.value,
daysToFrontExpiration: chosenExitToFrontExpiration.value,
daysBetweenFrontAndBackExpiration:
chosenDaysBetweenFrontAndBackExpiration.value,
lookbackPeriodStart: chosenLookbackPeriodStart.value,
lookbackPeriodEnd: chosenLookbackPeriodEnd.value,
})
.then((getHistoricalCalendarExitQuoteChartDataResponse) => {
historicalCalendarExitQuoteChartData.value =
getHistoricalCalendarExitQuoteChartDataResponse;
});
};
const handleInit = () => {
trpc.getAvailableUnderlyings.query().then((availableUnderlyingsResponse) => {
trpc.CalendarCharacteristicsForm.getAvailableUnderlyings
.query()
.then((availableUnderlyingsResponse) => {
availableUnderlyings.value = availableUnderlyingsResponse;
chosenUnderlying.value = availableUnderlyingsResponse[0];
refreshHistoricalStockQuoteChartData();
refreshHistoricalCalendarQuoteChartData();
refreshHistoricalCalendarExitQuoteChartData();
underlying.value = availableUnderlyingsResponse[0];
refreshStockPriceChartData();
refreshSimilarCalendarPriceChartData();
refreshcalendarExitPriceChartData();
});
};
const handleUnderlyingChange = (e) => {
if (chosenUnderlying.value !== e.target.value) {
chosenUnderlying.value = e.target.value;
refreshHistoricalStockQuoteChartData();
refreshHistoricalCalendarQuoteChartData();
refreshHistoricalCalendarExitQuoteChartData();
}
};
const handleDaysToFrontExpirationChange = (e) => {
if (chosenDaysToFrontExpiration.value !== parseInt(e.target.value)) {
chosenDaysToFrontExpiration.value = parseInt(e.target.value);
refreshHistoricalCalendarQuoteChartData();
}
};
const handleDaysBetweenFrontAndBackExpirationChange = (e) => {
if (
chosenDaysBetweenFrontAndBackExpiration.value !== parseInt(e.target.value)
) {
chosenDaysBetweenFrontAndBackExpiration.value = parseInt(e.target.value);
refreshHistoricalCalendarQuoteChartData();
refreshHistoricalCalendarExitQuoteChartData();
}
};
const handleStrikePercentageFromUnderlyingPriceChange = (e) => {
if (
chosenStrikePercentageFromUnderlyingPrice.value !==
parseFloat(e.target.value)
) {
chosenStrikePercentageFromUnderlyingPrice.value = parseFloat(
e.target.value
);
refreshHistoricalCalendarQuoteChartData();
}
};
const handleStrikePercentageFromUnderlyingPriceRadiusChange = (e) => {
if (
chosenStrikePercentageFromUnderlyingPriceRadius.value !==
parseFloat(e.target.value)
) {
chosenStrikePercentageFromUnderlyingPriceRadius.value = parseFloat(
e.target.value
);
refreshHistoricalCalendarQuoteChartData();
}
};
const handleExitToFrontExpirationChange = (e) => {
if (chosenExitToFrontExpiration.value !== parseInt(e.target.value)) {
chosenExitToFrontExpiration.value = parseInt(e.target.value);
refreshHistoricalCalendarExitQuoteChartData();
}
};
const handleLookbackPeriodStartChange = (e) => {
if (chosenLookbackPeriodStart.value !== e.target.value) {
chosenLookbackPeriodStart.value = e.target.value;
refreshHistoricalStockQuoteChartData();
refreshHistoricalCalendarQuoteChartData();
refreshHistoricalCalendarExitQuoteChartData();
}
};
const handleLookbackPeriodEndChange = (e) => {
if (chosenLookbackPeriodEnd.value !== e.target.value) {
chosenLookbackPeriodEnd.value = e.target.value;
refreshHistoricalStockQuoteChartData();
refreshHistoricalCalendarQuoteChartData();
refreshHistoricalCalendarExitQuoteChartData();
}
};
export function HistoricalCalendarPrices() {
useEffect(handleInit, []);
return (
/* container for centering: */
<div class="flex flex-row justify-center">
<div class="flex flex-col justify-start gap-4">
{/* inputs form container: */}
<div class="flex flex-col justify-start gap-1 divide-y">
<div class="flex flex-row w-160 gap-3">
<div class="text-right w-1/3">
<label>Available Underlyings</label>
</div>
<div class="my-auto w-2/3">
{availableUnderlyings.value.length === 0 ? (
"Loading..."
) : (
<select
onChange={handleUnderlyingChange}
class="border border-gray-300 focus:border-blue-400"
<Container maxWidth="lg">
<Grid2 container spacing={4} columns={12}>
{/* <Grid2 size={{ xs: 12 }}>
<Typography variant="h4" gutterBottom>
<EditableUnderlying /> :
<EditableSpan />
-Day Calendar @ <EditableStrike />
%-from-the-money
</Typography>
<Typography variant="h5" gutterBottom sx={{ pl: 1 }}>
Opening at <EditableOpenDTE /> DTE, Closing at <EditableExitDTE />
DTE
</Typography>
<Typography variant="h5" gutterBottom>
<EditableLookbackPeriodStart />-
<EditableLookbackPeriodEnd />
</Typography>
<ClickAwayListener
onClickAway={() => {
isPopperOpen.value = false;
// refreshSimilarCalendarPriceChartData();
console.log("clicked away");
}}
>
{availableUnderlyings.value.map((availableUnderlying) => (
<option value={availableUnderlying}>
{availableUnderlying}
</option>
))}
</select>
)}
</div>
</div>
<div class="flex flex-row w-160 gap-3">
<div class="text-right w-1/3">
<label>Now-to-Front-Month "Days to Expiration"</label>
</div>
<div class="my-auto w-2/3">
<input
type="text"
onBlur={handleDaysToFrontExpirationChange}
value={chosenDaysToFrontExpiration.value}
class="border border-gray-300 focus:border-blue-400"
/>
Days
</div>
</div>
<div class="flex flex-row w-160 gap-3">
<div class="text-right w-1/3">
<label>Front-to-Back-Month "Days to Expiration" Difference</label>
</div>
<div class="my-auto w-2/3">
<input
type="text"
onBlur={handleDaysBetweenFrontAndBackExpirationChange}
value={chosenDaysBetweenFrontAndBackExpiration.value}
class="border border-gray-300 focus:border-blue-400"
/>
Days Difference
</div>
</div>
<div class="flex flex-row w-160 gap-3">
<div class="text-right w-1/3">
<label>"Strike Percentage From Underlying Price" Range</label>
</div>
<div class="my-auto w-2/3">
<input
type="text"
onBlur={handleStrikePercentageFromUnderlyingPriceChange}
value={chosenStrikePercentageFromUnderlyingPrice.value}
class="border border-gray-300 focus:border-blue-400"
/>
% +/-
<input
type="text"
onBlur={handleStrikePercentageFromUnderlyingPriceRadiusChange}
value={chosenStrikePercentageFromUnderlyingPriceRadius.value}
class="border border-gray-300 focus:border-blue-400"
/>
% from ATM
</div>
</div>
<div class="flex flex-row w-160 gap-3">
<div class="text-right w-1/3">
<label>Exit-to-Front-Month "Days to Expiration"</label>
</div>
<div class="my-auto w-2/3">
<input
type="text"
onBlur={handleExitToFrontExpirationChange}
value={chosenExitToFrontExpiration.value}
class="border border-gray-300 focus:border-blue-400"
/>
Days
</div>
</div>
<div class="flex flex-row w-160 gap-3">
<div class="text-right w-1/3">
<label>Lookback Period</label>
</div>
<div class="my-auto w-2/3">
<input
type="text"
onBlur={handleLookbackPeriodStartChange}
value={chosenLookbackPeriodStart.value}
class="border border-gray-300 focus:border-blue-400"
/>
-
<input
type="text"
onBlur={handleLookbackPeriodEndChange}
value={chosenLookbackPeriodEnd.value}
class="border border-gray-300 focus:border-blue-400"
/>
</div>
</div>
</div>
{/* charts container: */}
<div className="flex flex-col justify-start gap-3">
<div className="min-h-96">
{chosenUnderlying.value !== null &&
historicalStockQuoteChartData.value.length > 0 ? (
<div className="h-full">
<Scatter
data={{
datasets: [
{
label: "Stock Open Price",
data: historicalStockQuoteChartData.value,
},
],
<Popper open={isPopperOpen.value} anchorEl={popperAnchorEl.value}>
<Paper elevation={3} sx={{ p: 3 }}>
{popperContent.value}
</Paper>
</Popper>
</ClickAwayListener>
</Grid2> */}
<Grid2 size={{ xs: 12 }}>
<Stack direction="row" spacing={2}>
<Typography gutterBottom minWidth={"8em"}>
Underlying
</Typography>
<EditableUnderlying />
</Stack>
<Stack direction="row" spacing={2}>
<Typography gutterBottom minWidth={"8em"}>
Open DTE
</Typography>
<EditableOpenDTE />
</Stack>
<Stack direction="row" spacing={2}>
<Typography gutterBottom minWidth={"8em"}>
Exit DTE
</Typography>
<EditableExitDTE />
</Stack>
<Stack direction="row" spacing={2}>
<Typography gutterBottom minWidth={"8em"}>
Span
</Typography>
<EditableSpan />
</Stack>
<Stack direction="row" spacing={2}>
<Typography gutterBottom minWidth={"8em"}>
Lookback Period
</Typography>
<EditableLookbackPeriod />
</Stack>
<ClickAwayListener
onClickAway={() => {
isPopperOpen.value = false;
// refreshSimilarCalendarPriceChartData();
console.log("clicked away");
}}
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);
},
},
min:
new Date(chosenLookbackPeriodStart.value).getTime() /
1000,
max:
new Date(chosenLookbackPeriodEnd.value).getTime() /
1000,
},
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,
events: [],
}}
/>
</div>
) : (
<div className="h-full">Loading Chart...</div>
)}
</div>
<div className="min-h-96">
{chosenUnderlying.value !== null &&
historicalCalendarQuoteChartData.value.length > 0 ? (
<div className="h-full">
>
<Popper open={isPopperOpen.value} anchorEl={popperAnchorEl.value}>
<Paper elevation={3} sx={{ p: 3 }}>
{popperContent.value}
</Paper>
</Popper>
</ClickAwayListener>
</Grid2>
<Grid2 size={{ xs: 12, md: 6 }}>
<Paper elevation={3} sx={{ p: 3, minHeight: "28em" }}>
{underlying.value !== null &&
similarCalendarPriceChartData.value.length > 0 ? (
<Scatter
data={{
datasets: [
{
label: "Calendar Open Price",
data: historicalCalendarQuoteChartData.value,
data: similarCalendarPriceChartData.value,
},
],
}}
@@ -392,25 +170,19 @@ export function HistoricalCalendarPrices() {
text: "Time",
},
ticks: {
callback: function (value, index, ticks) {
return new Date((value as number) * 1000)
callback: (value, index, ticks) =>
new Date((value as number) * 1000)
.toISOString()
.substring(0, 10);
.substring(0, 10),
},
},
min:
new Date(chosenLookbackPeriodStart.value).getTime() /
1000,
max:
new Date(chosenLookbackPeriodEnd.value).getTime() /
1000,
min: new Date(lookbackPeriodStart.value).getTime() / 1000,
max: new Date(lookbackPeriodEnd.value).getTime() / 1000,
},
y: {
beginAtZero: true,
ticks: {
callback: function (value, index, ticks) {
return "$" + value.toString();
},
callback: (value, index, ticks) =>
`$${value.toString()}`,
},
min: 0,
max: maxChartPrice.value,
@@ -433,21 +205,21 @@ export function HistoricalCalendarPrices() {
events: [],
}}
/>
</div>
) : (
<div className="h-full">Loading Chart...</div>
<Typography>Loading Chart...</Typography>
)}
</div>
<div className="min-h-96">
{chosenUnderlying.value !== null &&
historicalCalendarQuoteChartData.value.length > 0 ? (
<div className="h-full">
</Paper>
</Grid2>
<Grid2 size={{ xs: 12, md: 6 }}>
<Paper elevation={3} sx={{ p: 3, minHeight: "28em" }}>
{underlying.value !== null &&
calendarExitPriceChartData.value.length > 0 ? (
<Scatter
data={{
datasets: [
{
label: "Calendar Exit Price",
data: historicalCalendarExitQuoteChartData.value,
data: calendarExitPriceChartData.value,
},
],
}}
@@ -461,17 +233,15 @@ export function HistoricalCalendarPrices() {
text: "%-From-the-Money",
},
ticks: {
callback: function (value, index, ticks) {
return value.toString() + "%";
},
callback: (value, index, ticks) =>
`${value.toString()}%`,
},
},
y: {
beginAtZero: true,
ticks: {
callback: function (value, index, ticks) {
return "$" + value.toString();
},
callback: (value, index, ticks) =>
`$${value.toString()}`,
},
min: 0,
max: maxChartPrice.value,
@@ -480,7 +250,7 @@ export function HistoricalCalendarPrices() {
elements: {
point: {
borderWidth: 0,
backgroundColor: function (context) {
backgroundColor: (context) => {
const n = (
context.raw as { x: number; y: number; n: number }
).n;
@@ -509,13 +279,82 @@ export function HistoricalCalendarPrices() {
events: [],
}}
/>
</div>
) : (
<div className="h-full">Loading Chart...</div>
<Typography>Loading Chart...</Typography>
)}
</div>
</div>
</div>
</div>
</Paper>
</Grid2>
<Grid2 size={{ xs: 12 }}>
<Paper elevation={3} sx={{ p: 3, minHeight: "28em", height: "100%" }}>
{underlying.value !== null &&
stockPriceChartData.value.length > 0 ? (
<Scatter
data={{
datasets: [
{
label: "Stock Open Price",
data: stockPriceChartData.value,
},
],
}}
options={{
showLine: true,
normalized: true,
scales: {
x: {
title: {
display: true,
text: "Time",
},
ticks: {
callback: (value, index, ticks) =>
new Date((value as number) * 1000)
.toISOString()
.substring(0, 10),
},
min: new Date(lookbackPeriodStart.value).getTime() / 1000,
max: new Date(lookbackPeriodEnd.value).getTime() / 1000,
},
y: {
beginAtZero: false,
ticks: {
callback: (value, index, ticks) =>
`$${value.toString()}`,
},
},
},
elements: {
point: {
radius: 2,
borderWidth: 0,
},
line: {
borderWidth: 2,
},
},
plugins: {
tooltip: {
enabled: false,
},
legend: {
display: false,
},
title: {
display: true,
text: "Stock Price",
},
},
animation: false,
maintainAspectRatio: false,
events: [],
}}
/>
) : (
<Typography>Loading Chart...</Typography>
)}
</Paper>
</Grid2>
</Grid2>
</Container>
);
}
@@ -0,0 +1,28 @@
import TextField from "@mui/material/TextField";
import { EditableValue } from "./EditableValue";
import { exitDTE } from "./state";
import { refreshcalendarExitPriceChartData } from "./actions";
import Slider from "@mui/material/Slider";
const handleExitDTEChange = (e) => {
if (exitDTE.value !== Number.parseInt(e.target.value)) {
exitDTE.value = Number.parseInt(e.target.value);
refreshcalendarExitPriceChartData();
}
};
export function EditableExitDTE() {
return (
<Slider
value={exitDTE.value}
onChange={handleExitDTEChange}
min={0}
max={5}
step={1}
valueLabelDisplay="on"
/>
// <EditableValue text={exitDTE.value}>
// <ExitToFrontExpirationChooser />
// </EditableValue>
);
}
@@ -0,0 +1,66 @@
import TextField from "@mui/material/TextField";
import {
refreshcalendarExitPriceChartData,
refreshSimilarCalendarPriceChartData,
refreshStockPriceChartData,
} from "./actions";
import { lookbackPeriodEnd, lookbackPeriodStart } from "./state";
import Slider from "@mui/material/Slider";
const handleLookbackPeriodChange = (
e,
[newLookbackPeriodStart, newLookbackPeriodEnd]: [number, number]
) => {
const [lookbackPeriodStartUnixTime, lookbackPeriodEndUnixTime] = [
new Date(lookbackPeriodStart.value).getTime(),
new Date(lookbackPeriodEnd.value).getTime(),
];
if (lookbackPeriodStartUnixTime !== newLookbackPeriodStart) {
lookbackPeriodStart.value = new Date(newLookbackPeriodStart)
.toISOString()
.substring(0, 10);
refreshStockPriceChartData();
refreshSimilarCalendarPriceChartData();
refreshcalendarExitPriceChartData();
}
if (lookbackPeriodEndUnixTime !== newLookbackPeriodEnd) {
lookbackPeriodEnd.value = new Date(newLookbackPeriodEnd)
.toISOString()
.substring(0, 10);
refreshStockPriceChartData();
refreshSimilarCalendarPriceChartData();
refreshcalendarExitPriceChartData();
}
};
const earliestDate = new Date("2022-03-07");
const DAY = 1000 * 60 * 60 * 24;
function addDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result.toISOString().substring(0, 10);
}
function daysBetween(date1, date2) {
return Math.round(Math.abs((date2.getTime() - date1.getTime()) / DAY));
}
export function EditableLookbackPeriod() {
return (
<Slider
value={[
new Date(lookbackPeriodStart.value).getTime(),
new Date(lookbackPeriodEnd.value).getTime(),
]}
onChange={handleLookbackPeriodChange}
valueLabelFormat={(unixTimeMs) =>
new Date(unixTimeMs).toISOString().substring(0, 10)
}
getAriaValueText={(unixTimeMs) =>
new Date(unixTimeMs).toISOString().substring(0, 10)
}
min={earliestDate.getTime()}
max={earliestDate.getTime() + 250 * DAY}
step={DAY}
valueLabelDisplay="on"
/>
);
}
@@ -0,0 +1,38 @@
import TextField from "@mui/material/TextField";
import {
refreshcalendarExitPriceChartData,
refreshSimilarCalendarPriceChartData,
refreshStockPriceChartData,
} from "./actions";
import { lookbackPeriodEnd } from "./state";
import { EditableValue } from "./EditableValue";
const handleLookbackPeriodEndChange = (e) => {
if (lookbackPeriodEnd.value !== e.target.value) {
lookbackPeriodEnd.value = e.target.value;
refreshStockPriceChartData();
refreshSimilarCalendarPriceChartData();
refreshcalendarExitPriceChartData();
}
};
function LookbackPeriodEndChooser() {
return (
<TextField
fullWidth
label="Lookback Period End"
type="date"
value={lookbackPeriodEnd.value}
onChange={handleLookbackPeriodEndChange}
InputLabelProps={{ shrink: true }}
/>
);
}
export function EditableLookbackPeriodEnd() {
return (
<EditableValue text={lookbackPeriodEnd.value}>
<LookbackPeriodEndChooser />
</EditableValue>
);
}
@@ -0,0 +1,38 @@
import TextField from "@mui/material/TextField";
import {
refreshcalendarExitPriceChartData,
refreshSimilarCalendarPriceChartData,
refreshStockPriceChartData,
} from "./actions";
import { lookbackPeriodStart } from "./state";
import { EditableValue } from "./EditableValue";
const handleLookbackPeriodStartChange = (e) => {
if (lookbackPeriodStart.value !== e.target.value) {
lookbackPeriodStart.value = e.target.value;
refreshStockPriceChartData();
refreshSimilarCalendarPriceChartData();
refreshcalendarExitPriceChartData();
}
};
function LookbackPeriodStartChooser() {
return (
<TextField
fullWidth
label="Lookback Period Start"
type="date"
value={lookbackPeriodStart.value}
onChange={handleLookbackPeriodStartChange}
InputLabelProps={{ shrink: true }}
/>
);
}
export function EditableLookbackPeriodStart() {
return (
<EditableValue text={lookbackPeriodStart.value}>
<LookbackPeriodStartChooser />
</EditableValue>
);
}
@@ -0,0 +1,26 @@
import { refreshSimilarCalendarPriceChartData } from "./actions";
import { openDTE } from "./state";
import Slider from "@mui/material/Slider";
const handleOpenDTEChange = (e) => {
if (openDTE.value !== Number.parseInt(e.target.value)) {
openDTE.value = Number.parseInt(e.target.value);
refreshSimilarCalendarPriceChartData();
}
};
export function EditableOpenDTE() {
return (
<Slider
value={openDTE.value}
onChange={handleOpenDTEChange}
min={0}
max={5}
step={1}
valueLabelDisplay="on"
/>
// <EditableValue text={openDTE.value}>
// <DaysToFrontExpirationChooser />
// </EditableValue>
);
}
@@ -0,0 +1,45 @@
import TextField from "@mui/material/TextField";
import {
refreshcalendarExitPriceChartData,
refreshSimilarCalendarPriceChartData,
} from "./actions";
import { span } from "./state";
import { EditableValue } from "./EditableValue";
import Slider from "@mui/material/Slider";
const handleSpanChange = (e) => {
if (span.value !== Number.parseInt(e.target.value)) {
span.value = Number.parseInt(e.target.value);
refreshSimilarCalendarPriceChartData();
refreshcalendarExitPriceChartData();
}
};
function DaysBetweenFrontAndBackExpirationChooser() {
return (
<TextField
fullWidth
label="Front-to-Back-Month Days to Expiration Difference"
type="number"
value={span.value}
onChange={handleSpanChange}
InputProps={{ endAdornment: "Days Difference" }}
/>
);
}
export function EditableSpan() {
return (
<Slider
value={span.value}
onChange={handleSpanChange}
min={3}
max={45}
step={1}
valueLabelDisplay="on"
/>
// <EditableValue text={span.value}>
// <DaysBetweenFrontAndBackExpirationChooser />
// </EditableValue>
);
}
@@ -0,0 +1,65 @@
import Box from "@mui/material/Box";
import { EditableValue } from "./EditableValue";
import { moniness, moninessRadius } from "./state";
import Slider from "@mui/material/Slider";
import { refreshSimilarCalendarPriceChartData } from "./actions";
function MoninessChooser() {
return (
<Slider
fullWidth
label="Strike % From Underlying Price"
value={moniness.value}
valueLabelDisplay="on"
min={0}
max={10}
step={1}
onChange={(e, value) => {
moniness.value = value as number;
}}
onChangeCommitted={(e, value) => {
refreshSimilarCalendarPriceChartData();
}}
InputProps={{ endAdornment: "%" }}
/>
);
}
function MoninessRadiusChooser() {
return (
<Slider
fullWidth
label="Strike % Radius"
value={moninessRadius.value}
valueLabelDisplay="on"
min={0}
max={10}
step={1}
onChange={(e, value) => {
moninessRadius.value = value as number;
}}
onChangeCommitted={(e, value) => {
refreshSimilarCalendarPriceChartData();
}}
InputProps={{ endAdornment: "%" }}
/>
);
}
/** This is its own component so that sliding the slider with the mouse is
* smoother. Preact detects reads from the "slider" signal values, and
* associates them with the component that read them and redraws that component.
* If this was not its own component, it would redraw the entire UI. It was very
* slow. */
export function EditableStrike() {
return (
<EditableValue
text={`${moniness.value.toFixed(1)}±${moninessRadius.value.toFixed(2)}`}
>
<Box sx={{ minWidth: "20em" }}>
<MoninessChooser />
<MoninessRadiusChooser />
</Box>
</EditableValue>
);
}
@@ -0,0 +1,42 @@
import Select from "@mui/material/Select";
import { EditableValue } from "./EditableValue";
import { availableUnderlyings, underlying } from "./state";
import MenuItem from "@mui/material/MenuItem";
import {
refreshcalendarExitPriceChartData,
refreshSimilarCalendarPriceChartData,
refreshStockPriceChartData,
} from "./actions";
const handleUnderlyingChange = (e) => {
if (underlying.value !== e.target.value) {
underlying.value = e.target.value;
refreshStockPriceChartData();
refreshSimilarCalendarPriceChartData();
refreshcalendarExitPriceChartData();
}
};
function UnderlyingChooser() {
return (
<Select
value={underlying.value || ""}
onChange={handleUnderlyingChange}
label="Available Underlyings"
>
{availableUnderlyings.value.map((underlying) => (
<MenuItem key={underlying} value={underlying}>
{underlying}
</MenuItem>
))}
</Select>
);
}
export function EditableUnderlying() {
return (
<EditableValue text={underlying.value}>
<UnderlyingChooser />
</EditableValue>
);
}
@@ -0,0 +1,29 @@
import Button from "@mui/material/Button";
import { isPopperOpen, popperAnchorEl, popperContent } from "./state";
export function EditableValue({ text, children }) {
return (
<Button
variant="text"
size="large"
sx={{
textDecoration: "underline",
textUnderlineOffset: "3px",
fontSize: "1.0em",
}}
onClick={(e) => {
// stop propagation so it's not caught by the ClickAwayListener:
e.stopPropagation();
if (isPopperOpen.value === false) {
isPopperOpen.value = true;
popperAnchorEl.value = e.currentTarget;
popperContent.value = children;
} else {
isPopperOpen.value = false;
}
}}
>
{text}
</Button>
);
}
@@ -0,0 +1,81 @@
import { trpc } from "../../trpc";
import {
calendarExitPriceChartData,
span,
openDTE,
exitDTE,
lookbackPeriodEnd,
lookbackPeriodStart,
similarCalendarPriceChartData,
stockPriceChartData,
moniness,
moninessRadius,
underlying,
} from "./state";
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
function throttle(func, limit) {
let inThrottle;
return function () {
const context = this;
const args = arguments;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
export const refreshStockPriceChartData = throttle(() => {
stockPriceChartData.value = [];
trpc.StockPriceChart.getChartData
.query({
underlying: underlying.value,
lookbackPeriodStart: lookbackPeriodStart.value,
lookbackPeriodEnd: lookbackPeriodEnd.value,
})
.then((getChartDataResponse) => {
stockPriceChartData.value = getChartDataResponse;
});
}, 400);
export const refreshSimilarCalendarPriceChartData = throttle(() => {
similarCalendarPriceChartData.value = [];
trpc.SimilarCalendarPriceChart.getChartData
.query({
underlying: underlying.value,
daysToFrontExpiration: openDTE.value,
daysBetweenFrontAndBackExpiration: span.value,
strikePercentageFromUnderlyingPriceRangeMin:
(moniness.value - moninessRadius.value) / 100,
strikePercentageFromUnderlyingPriceRangeMax:
(moniness.value + moninessRadius.value) / 100,
lookbackPeriodStart: lookbackPeriodStart.value,
lookbackPeriodEnd: lookbackPeriodEnd.value,
})
.then((getChartDataResponse) => {
similarCalendarPriceChartData.value = getChartDataResponse;
});
}, 400);
export const refreshcalendarExitPriceChartData = throttle(() => {
calendarExitPriceChartData.value = [];
trpc.CalendarExitPriceChart.getChartData
.query({
underlying: underlying.value,
daysToFrontExpiration: exitDTE.value,
daysBetweenFrontAndBackExpiration: span.value,
lookbackPeriodStart: lookbackPeriodStart.value,
lookbackPeriodEnd: lookbackPeriodEnd.value,
})
.then((getChartDataResponse) => {
calendarExitPriceChartData.value = getChartDataResponse;
});
}, 400);
@@ -0,0 +1,46 @@
import { computed, signal } from "@preact/signals";
export const isPopperOpen = signal(false);
export const popperAnchorEl = signal(null);
export const popperContent = signal(null);
export const availableUnderlyings = signal([]);
export const underlying = signal(null);
export const openDTE = signal(14);
export const span = signal(14);
export const moniness = signal(1);
export const moninessRadius = signal(1);
export const exitDTE = signal(2);
export const stockPriceChartData = signal<Array<[number, number]>>([]);
export const similarCalendarPriceChartData = signal([]);
export const calendarExitPriceChartData = signal([]);
export const lookbackPeriodStart = signal("2022-03-01");
export const lookbackPeriodEnd = signal("2022-04-01");
export const maxChartPrice = computed(() =>
Math.max(
Math.max.apply(
null,
similarCalendarPriceChartData.value.map((d) => d.y).slice(0, -2)
),
Math.max.apply(
null,
calendarExitPriceChartData.value.map((d) => d.y).slice(0, -2)
)
)
);
export const maxN = computed(() =>
Math.max.apply(
null,
calendarExitPriceChartData.value.map((d) => d.n)
)
);
-13
View File
@@ -1,13 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
width: {
128: "32rem",
160: "40rem",
},
},
},
plugins: [],
};
+5
View File
@@ -0,0 +1,5 @@
{
"devDependencies": {
"@biomejs/biome": "^1.8.3"
}
}
+105
View File
@@ -0,0 +1,105 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@biomejs/biome':
specifier: ^1.8.3
version: 1.8.3
packages:
'@biomejs/biome@1.8.3':
resolution: {integrity: sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@1.8.3':
resolution: {integrity: sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@1.8.3':
resolution: {integrity: sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@1.8.3':
resolution: {integrity: sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-arm64@1.8.3':
resolution: {integrity: sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-x64-musl@1.8.3':
resolution: {integrity: sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-linux-x64@1.8.3':
resolution: {integrity: sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-win32-arm64@1.8.3':
resolution: {integrity: sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@1.8.3':
resolution: {integrity: sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
snapshots:
'@biomejs/biome@1.8.3':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 1.8.3
'@biomejs/cli-darwin-x64': 1.8.3
'@biomejs/cli-linux-arm64': 1.8.3
'@biomejs/cli-linux-arm64-musl': 1.8.3
'@biomejs/cli-linux-x64': 1.8.3
'@biomejs/cli-linux-x64-musl': 1.8.3
'@biomejs/cli-win32-arm64': 1.8.3
'@biomejs/cli-win32-x64': 1.8.3
'@biomejs/cli-darwin-arm64@1.8.3':
optional: true
'@biomejs/cli-darwin-x64@1.8.3':
optional: true
'@biomejs/cli-linux-arm64-musl@1.8.3':
optional: true
'@biomejs/cli-linux-arm64@1.8.3':
optional: true
'@biomejs/cli-linux-x64-musl@1.8.3':
optional: true
'@biomejs/cli-linux-x64@1.8.3':
optional: true
'@biomejs/cli-win32-arm64@1.8.3':
optional: true
'@biomejs/cli-win32-x64@1.8.3':
optional: true
+3
View File
@@ -25,3 +25,6 @@ dist-ssr
*.sw?
.env
*.db
*.db-lck
Calendar tRPC
+17 -12
View File
@@ -2,22 +2,27 @@
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN npm install -g corepack@latest && 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
CMD [ "pnpm", "tsx", "src/index.ts" ]
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" ]
# 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" ]
+2
View File
@@ -0,0 +1,2 @@
- Ingest stock/underlying aggregates from flatfiles
- Create backtesting function to step through each minute of every day.
+17
View File
@@ -0,0 +1,17 @@
import { open } from 'lmdbx'; // or require
const MAXIMUM_KEY = Buffer.from([0xff]);
// or in deno: import { open } from 'https://deno.land/x/lmdbx/mod.ts';
const myDB = open({
path: '/tmp/my.db',
// any options go here, we can turn on compression like this:
compression: true,
});
await myDB.put(["a","b"], "ab");
await myDB.put(["a","c"], "ac");
await myDB.put(["a","d"], "ad");
await myDB.put(["b","a"], "ba");
await myDB.put(["b","c"], "bc");
console.log(Array.from(myDB.getRange({start: ["a"], end: ["a", MAXIMUM_KEY]}).asArray))
+11 -3
View File
@@ -2,31 +2,39 @@
"private": true,
"type": "module",
"scripts": {
"build": "esbuild src/*.ts src/**/*.ts --platform=node --outdir=dist --format=esm",
"build": "esbuild src/*.ts src/*.tsx src/**/*.ts src/**/**/*.ts --platform=node --outdir=dist --format=esm",
"dev:node": "node --watch dist/index.js",
"dev:esbuild": "pnpm run build --watch",
"dev": "run-p dev:*"
"dev": "run-p dev:*",
"cli": "tsx src/cli.tsx"
},
"dependencies": {
"@clickhouse/client": "^0.2.7",
"@clickhouse/client": "^1.4.1",
"@humanwhocodes/env": "^3.0.5",
"@sinclair/typebox": "^0.32.5",
"@trpc/server": "^10.45.0",
"cors": "^2.8.5",
"date-fns": "^3.6.0",
"execa": "^9.3.0",
"ink": "^4.1.0",
"ink-text-input": "^5.0.1",
"lmdbx": "^0.5.0",
"p-all": "^5.0.0",
"p-queue": "^8.0.1",
"p-retry": "^6.2.0",
"p-series": "^3.0.0",
"p-throttle": "^6.1.0",
"react": "^18.2.0",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/node": "^20.10.7",
"@types/react": "^18.0.0",
"esbuild": "^0.19.11",
"npm-run-all": "^4.1.5",
"tsx": "^4.17.0",
"typescript": "^5.3.3"
}
}
+812 -12
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,191 @@
import type { CalendarDatabase, CalendarKey } from "./interfaces.js";
import type { Aggregate } from "../interfaces.js";
import { query } from "../../lib/clickhouse.js";
function makeCalendarDatabase(): CalendarDatabase {
const calendarDatabase: Omit<CalendarDatabase, "getCalendars"> = {
getKeys: async ({ key: { symbol }, date }) => {
const calendarsForSymbolOnDate = await query<
Omit<CalendarKey, "symbol">
>(`
WITH today_option_contracts AS (
SELECT expirationDate, strike, type
FROM option_contract_existences
WHERE symbol = '${symbol}'
AND asOfDate = '${date}'
)
SELECT
front_option_contract.type as type,
front_option_contract.strike as strike,
front_option_contract.expirationDate as frontExpirationDate,
back_option_contract.expirationDate as backExpirationDate
FROM today_option_contracts AS front_option_contract
ASOF INNER JOIN today_option_contracts AS back_option_contract
ON front_option_contract.type = back_option_contract.type
AND front_option_contract.strike = back_option_contract.strike
AND front_option_contract.expirationDate < back_option_contract.expirationDate
`);
return calendarsForSymbolOnDate.map((calendarWithoutSymbol) => ({
...calendarWithoutSymbol,
symbol,
}));
},
getAggregate: async ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
tsStart,
}) => {
const tsStartString = new Date(tsStart).toISOString();
return (
await query<Omit<Aggregate<CalendarKey>, "key">>(`
WITH front_option_contract_candlestick AS (
SELECT
tsStart,
open,
close,
high,
low
FROM option_contract_aggregates
WHERE symbol = '${symbol}'
AND type = '${type}'
AND strike = '${strike}'
AND expirationDate = '${frontExpirationDate}'
AND tsStart = '${tsStartString}'
),
back_option_contract_candlestick AS (
SELECT
tsStart,
open,
close,
high,
low
FROM option_contract_aggregates
WHERE symbol = '${symbol}'
AND type = '${type}'
AND strike = '${strike}'
AND expirationDate = '${backExpirationDate}'
AND tsStart = '${tsStartString}'
)
SELECT
toUnixTimestamp(front_option_contract_candlestick.tsStart) as tsStart,
back_option_contract_candlestick.open - front_option_contract_candlestick.open as open,
back_option_contract_candlestick.close - front_option_contract_candlestick.close as close
FROM front_option_contract_candlestick
INNER JOIN back_option_contract_candlestick
ON front_option_contract_candlestick.tsStart = back_option_contract_candlestick.tsStart
ORDER BY front_option_contract_candlestick.tsStart ASC
`)
).map((aggregate) => ({
...aggregate,
tsStart: aggregate.tsStart * 1000, // unfortunately, `toUnixTimestamp` only returns second-precision
}))[0];
},
getAggregates: async ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
date,
}) => {
return (
await query<Omit<Aggregate<CalendarKey>, "key">>(`
WITH front_option_contract_candlestick AS (
SELECT
tsStart,
open,
close,
high,
low
FROM option_contract_aggregates
WHERE symbol = '${symbol}'
AND type = '${type}'
AND strike = '${strike}'
AND expirationDate = '${frontExpirationDate}'
AND toDate(tsStart) = '${date}'
),
back_option_contract_candlestick AS (
SELECT
tsStart,
open,
close,
high,
low
FROM option_contract_aggregates
WHERE symbol = '${symbol}'
AND type = '${type}'
AND strike = '${strike}'
AND expirationDate = '${backExpirationDate}'
AND toDate(tsStart) = '${date}'
)
SELECT
toUnixTimestamp(front_option_contract_candlestick.tsStart) as tsStart,
back_option_contract_candlestick.open - front_option_contract_candlestick.open as open,
back_option_contract_candlestick.close - front_option_contract_candlestick.close as close
FROM front_option_contract_candlestick
INNER JOIN back_option_contract_candlestick
ON front_option_contract_candlestick.tsStart = back_option_contract_candlestick.tsStart
ORDER BY front_option_contract_candlestick.tsStart ASC
`)
).map((aggregate) => ({
...aggregate,
tsStart: aggregate.tsStart * 1000, // unfortunately, `toUnixTimestamp` only returns second-precision
}));
},
insertAggregates: async (aggregates) => {
// no-op: we insert individual option contracts, not calendars
},
getClosingPrice: async ({
key: { symbol, strike, type, frontExpirationDate, backExpirationDate },
}) => {
return (
await query<{ calendarClosingPrice: number }>(`
WITH front_option_contract_candlestick AS (
SELECT
tsStart,
open,
close,
high,
low
FROM option_contract_aggregates
WHERE symbol = '${symbol}'
AND type = '${type}'
AND strike = '${strike}'
AND expirationDate = '${frontExpirationDate}'
AND toDate(tsStart) = '${frontExpirationDate}'
),
back_option_contract_candlestick AS (
SELECT
tsStart,
open,
close,
high,
low
FROM option_contract_aggregates
WHERE symbol = '${symbol}'
AND type = '${type}'
AND strike = '${strike}'
AND expirationDate = '${backExpirationDate}'
AND toDate(tsStart) = '${frontExpirationDate}'
)
SELECT
min(back_option_contract_candlestick.close - front_option_contract_candlestick.close) as calendarClosingPrice
FROM front_option_contract_candlestick
INNER JOIN back_option_contract_candlestick
ON front_option_contract_candlestick.tsStart = back_option_contract_candlestick.tsStart
`)
)[0]?.calendarClosingPrice;
},
getTargetPriceByProbability: async ({
symbol,
calendarSpan,
strikePercentageFromTheMoney,
historicalProbabilityOfSuccess,
}) => {
return 0.24;
},
};
return {
...calendarDatabase,
getCalendars: calendarDatabase.getKeys,
};
}
export const database: CalendarDatabase = makeCalendarDatabase();
@@ -0,0 +1,24 @@
import type { AggregateDatabase } from "../interfaces.js";
export type CalendarKey = {
symbol: string;
type: "call" | "put";
strike: number;
frontExpirationDate: string;
backExpirationDate: string;
};
export type CalendarDatabase = AggregateDatabase<CalendarKey> & {
getCalendars: AggregateDatabase<CalendarKey>["getKeys"];
getTargetPriceByProbability: ({
symbol,
calendarSpan,
strikePercentageFromTheMoney,
historicalProbabilityOfSuccess,
}: {
symbol: string;
calendarSpan: number;
strikePercentageFromTheMoney: number;
historicalProbabilityOfSuccess: number;
}) => Promise<number>;
};
@@ -0,0 +1,166 @@
import type { CalendarDatabase } from "./interfaces.js";
import { open } from "lmdbx";
const calendarAggregatesDb = open({
path: "./calendar-aggregates.db",
compression: true,
});
const calendarExistenceDb = open({
path: "./calendar-existence.db",
compression: true,
});
/** Largest possible key according to the `ordered-binary` (used by lmdbx) docs. */
const MAXIMUM_KEY = Buffer.from([0xff]);
function makeCalendarDatabase(): CalendarDatabase {
const calendarDatabase: Omit<CalendarDatabase, "getCalendars"> = {
getKeys: async ({ key: { symbol }, date }) => {
return calendarExistenceDb
.getRange({
start: [date, symbol],
end: [date, symbol, MAXIMUM_KEY],
})
.map(({ key }) => ({
symbol,
frontExpirationDate: key[2],
backExpirationDate: key[3],
strike: key[4],
type: key[5],
})).asArray;
},
getAggregates: async ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
date,
}) => {
const startOfDayUnix = new Date(`${date}T00:00:00Z`).valueOf();
const endOfDayUnix = startOfDayUnix + 3600 * 24 * 1000;
return calendarAggregatesDb
.getRange({
start: [
symbol,
frontExpirationDate,
backExpirationDate,
strike,
type,
startOfDayUnix,
],
end: [
symbol,
frontExpirationDate,
backExpirationDate,
strike,
type,
endOfDayUnix,
],
})
.map(({ value }) => ({
tsStart: value.tsStart,
open: value.open,
close: value.close,
high: value.high,
low: value.low,
})).asArray;
},
getAggregate: async ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
tsStart,
}) => {
return await calendarAggregatesDb.get([
symbol,
frontExpirationDate,
backExpirationDate,
strike,
type,
tsStart,
]);
},
insertAggregates: async (aggregates) => {
await calendarExistenceDb.batch(() => {
for (const aggregate of aggregates) {
calendarExistenceDb.put(
[
new Date(aggregate.tsStart).toISOString().substring(0, 10),
aggregate.key.symbol,
aggregate.key.frontExpirationDate,
aggregate.key.backExpirationDate,
aggregate.key.strike,
aggregate.key.type,
],
null
);
}
});
await calendarAggregatesDb.batch(() => {
for (const aggregate of aggregates) {
calendarAggregatesDb.put(
[
aggregate.key.symbol,
aggregate.key.frontExpirationDate,
aggregate.key.backExpirationDate,
aggregate.key.strike,
aggregate.key.type,
aggregate.tsStart,
],
{
open: aggregate.open,
close: aggregate.close,
high: aggregate.high,
low: aggregate.low,
}
);
}
});
},
getClosingPrice: async ({
key: { symbol, strike, type, frontExpirationDate, backExpirationDate },
}) => {
const startOfExpirationDateUnix = new Date(
`${frontExpirationDate}T23:59:59Z`
).valueOf();
const endOfExpirationDateUnix = new Date(
`${frontExpirationDate}T00:00:00Z`
).valueOf();
for (const { value } of calendarAggregatesDb.getRange({
start: [
symbol,
frontExpirationDate,
backExpirationDate,
strike,
type,
startOfExpirationDateUnix,
],
end: [
symbol,
frontExpirationDate,
backExpirationDate,
strike,
type,
endOfExpirationDateUnix,
],
reverse: true,
})) {
if (value.close > 0) {
return value.close;
}
}
return 0;
},
getTargetPriceByProbability: async ({
symbol,
calendarSpan,
strikePercentageFromTheMoney,
historicalProbabilityOfSuccess,
}) => {
return 0.24;
},
};
return {
...calendarDatabase,
getCalendars: calendarDatabase.getKeys,
};
}
export const database: CalendarDatabase = makeCalendarDatabase();
@@ -0,0 +1,207 @@
import { database as optionContractDatabase } from "../OptionContract/lmdbx.js";
import type { CalendarDatabase } from "./interfaces.js";
/** Largest possible key according to the `ordered-binary` (used by lmdbx) docs. */
const MAXIMUM_KEY = Buffer.from([0xff]);
function makeCalendarDatabase(): CalendarDatabase {
const getAggregatesSync = ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
date,
}) => {
const frontOptionContractAggregates =
optionContractDatabase.getAggregatesSync({
date,
key: { symbol, expirationDate: frontExpirationDate, strike, type },
});
const backOptionContractAggregates =
optionContractDatabase.getAggregatesSync({
date,
key: { symbol, expirationDate: backExpirationDate, strike, type },
});
const calendarAggregates = [];
let i = 0;
let j = 0;
while (
i < frontOptionContractAggregates.length &&
j < backOptionContractAggregates.length
) {
if (
frontOptionContractAggregates[i].tsStart ===
backOptionContractAggregates[j].tsStart
) {
calendarAggregates.push({
tsStart: frontOptionContractAggregates[i].tsStart,
open:
backOptionContractAggregates[j].open -
frontOptionContractAggregates[i].open,
close:
backOptionContractAggregates[j].close -
frontOptionContractAggregates[i].close,
// the high and low are not exactly correct since we don't know if each contract's high and low happened at the same moment as the other:
high:
backOptionContractAggregates[j].high -
frontOptionContractAggregates[i].high,
low:
backOptionContractAggregates[j].low -
frontOptionContractAggregates[i].low,
});
i++;
j++;
} else if (
frontOptionContractAggregates[i].tsStart >
backOptionContractAggregates[j].tsStart
) {
j++;
} else {
i++;
}
}
return calendarAggregates;
};
const calendarDatabase: Omit<CalendarDatabase, "getCalendars"> = {
getKeys: async ({ key: { symbol }, date }) => {
const optionContracts = await optionContractDatabase.getOptionContracts({
date,
key: { symbol },
});
return optionContracts.flatMap(
(frontOptionContract, i, optionContracts) =>
optionContracts
.filter(
(potientialBackOptionContract) =>
frontOptionContract.strike ===
potientialBackOptionContract.strike &&
frontOptionContract.type ===
potientialBackOptionContract.type &&
frontOptionContract.expirationDate <
potientialBackOptionContract.expirationDate
)
.map((backOptionContract) => ({
symbol,
frontExpirationDate: frontOptionContract.expirationDate,
backExpirationDate: backOptionContract.expirationDate,
strike: frontOptionContract.strike,
type: frontOptionContract.type,
}))
);
},
getAggregates: async ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
date,
}) =>
getAggregatesSync({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
date,
}),
getAggregatesSync,
insertAggregates: async (aggregates) => {
// right now, no-op
},
getClosingPrice: async ({
key: { symbol, strike, type, frontExpirationDate, backExpirationDate },
}) => {
// get unix timestamp, in milliseconds, of the start of the last hour, which is 03:30PM in the `America/New_York` timezone on the front expiration date:
const startOfLastHourUnix = new Date(
`${frontExpirationDate}T19:30:00Z`
).getTime();
const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000;
const frontOptionContractAggregates = (
await optionContractDatabase.getAggregates({
date: frontExpirationDate,
key: { symbol, expirationDate: frontExpirationDate, strike, type },
})
).filter(
({ tsStart }) =>
tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix
);
const backOptionContractAggregates = (
await optionContractDatabase.getAggregates({
date: frontExpirationDate,
key: { symbol, expirationDate: backExpirationDate, strike, type },
})
).filter(
({ tsStart }) =>
tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix
);
let i = 0;
let j = 0;
let minPrice = 0;
while (
i < frontOptionContractAggregates.length &&
j < backOptionContractAggregates.length
) {
if (
frontOptionContractAggregates[i].tsStart ===
backOptionContractAggregates[j].tsStart
) {
const calendarClosePrice =
backOptionContractAggregates[j].close -
frontOptionContractAggregates[i].close;
if (calendarClosePrice < minPrice || minPrice === 0) {
minPrice = calendarClosePrice;
}
i++;
j++;
} else if (
frontOptionContractAggregates[i].tsStart >
backOptionContractAggregates[j].tsStart
) {
j++;
} else {
i++;
}
}
return minPrice;
},
getAggregate: async ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
tsStart,
}) => {
const [frontOptionContractAggregate, backOptionContractAggregate] =
await Promise.all([
optionContractDatabase.getAggregate({
key: { symbol, expirationDate: frontExpirationDate, strike, type },
tsStart,
}),
optionContractDatabase.getAggregate({
key: { symbol, expirationDate: backExpirationDate, strike, type },
tsStart,
}),
]);
// only return the calendar aggregate if its constituent front and back option contract aggregates exist:
if (frontOptionContractAggregate && backOptionContractAggregate) {
return {
tsStart,
open:
backOptionContractAggregate.open -
frontOptionContractAggregate.open,
close:
backOptionContractAggregate.close -
frontOptionContractAggregate.close,
high:
backOptionContractAggregate.high -
frontOptionContractAggregate.high,
low:
backOptionContractAggregate.low - frontOptionContractAggregate.low,
};
}
return undefined;
},
getTargetPriceByProbability: async ({
symbol,
calendarSpan,
strikePercentageFromTheMoney,
historicalProbabilityOfSuccess,
}) => {
return 0.24;
},
};
return {
...calendarDatabase,
getCalendars: calendarDatabase.getKeys,
};
}
export const database: CalendarDatabase = makeCalendarDatabase();
@@ -0,0 +1,114 @@
import type {
OptionContractDatabase,
OptionContractKey,
} from "./interfaces.js";
import type { Aggregate } from "../interfaces.js";
import { clickhouse, query } from "../../lib/clickhouse.js";
function makeOptionContractDatabase(): OptionContractDatabase {
const optionContractDatabase: Omit<
OptionContractDatabase,
"getOptionContracts"
> = {
getKeys: async ({ key: { symbol }, date }) => {
return (
await query<Omit<OptionContractKey, "symbol">>(`
SELECT expirationDate, strike, type
FROM option_contract_existences
WHERE symbol = '${symbol}'
AND asOfDate = '${date}'
`)
).map((optionContractWithoutKey) => ({
...optionContractWithoutKey,
symbol,
}));
},
getAggregates: async ({
key: { symbol, expirationDate, strike, type },
date,
}) => {
return (
await query<Omit<Aggregate<OptionContractKey>, "key">>(`
SELECT
toUnixTimestamp(tsStart) as tsStart,
open,
close,
high,
low
FROM option_contract_aggregates
WHERE symbol = '${symbol}'
AND type = '${type}'
AND strike = '${strike}'
AND expirationDate = '${expirationDate}'
AND toDate(tsStart) = '${date}'
ORDER BY tsStart ASC
`)
).map((aggregate) => ({
...aggregate,
tsStart: aggregate.tsStart * 1000, // unfortunately, `toUnixTimestamp` only returns second-precision
}));
},
getAggregate: async ({
key: { symbol, expirationDate, strike, type },
tsStart,
}) => {
const tsStartString = new Date(tsStart).toISOString();
return (
await query<Omit<Aggregate<OptionContractKey>, "key">>(`
SELECT
open,
close,
high,
low
FROM option_contract_aggregates
WHERE symbol = '${symbol}'
AND type = '${type}'
AND strike = '${strike}'
AND expirationDate = '${expirationDate}'
AND tsStart = '${tsStartString}'
ORDER BY tsStart ASC
`)
).map((aggregate) => ({
...aggregate,
tsStart,
}))[0];
},
insertAggregates: async (aggregates) => {
// stock existence is taken care of by clickhouse materialized view
await clickhouse.insert({
table: "option_contract_aggregates",
values: aggregates.map(
({
key: { symbol, expirationDate, strike, type },
tsStart,
open,
close,
high,
low,
}) => ({
symbol,
expirationDate,
strike,
type,
tsStart,
open,
close,
high,
low,
})
),
});
},
getClosingPrice: async ({ key }) => {
// no-op: not used since stocks don't have a "closing" price, unlike options.
return 0;
},
};
return {
...optionContractDatabase,
getOptionContracts: optionContractDatabase.getKeys,
};
}
export const database: OptionContractDatabase = makeOptionContractDatabase();
@@ -0,0 +1,12 @@
import type { AggregateDatabase } from "../interfaces.js";
export type OptionContractKey = {
symbol: string;
expirationDate: string;
strike: number;
type: "call" | "put";
};
export type OptionContractDatabase = AggregateDatabase<OptionContractKey> & {
getOptionContracts: AggregateDatabase<OptionContractKey>["getKeys"];
};
@@ -0,0 +1,138 @@
import type { OptionContractDatabase } from "./interfaces.js";
import { open } from "lmdbx";
const optionContractAggregatesDb = open({
path: "./option-contract-aggregates.db",
// any options go here, we can turn on compression like this:
compression: true,
});
const optionContractExistenceDb = open({
path: "./option-contract-existence.db",
// any options go here, we can turn on compression like this:
compression: true,
});
/** Largest possible key according to the `ordered-binary` (used by lmdbx) docs. */
const MAXIMUM_KEY = Buffer.from([0xff]);
function makeOptionContractDatabase(): OptionContractDatabase {
const getAggregatesSync = ({
key: { symbol, expirationDate, strike, type },
date,
}) => {
const startOfDayUnix = new Date(`${date}T00:00:00Z`).valueOf();
const endOfDayUnix = startOfDayUnix + 3600 * 24 * 1000;
return optionContractAggregatesDb
.getRange({
start: [symbol, expirationDate, strike, type, startOfDayUnix],
end: [symbol, expirationDate, strike, type, endOfDayUnix],
})
.map(({ key, value }) => ({
tsStart: key[4],
open: value.open,
close: value.close,
high: value.high,
low: value.low,
})).asArray;
};
const optionContractDatabase: Omit<
OptionContractDatabase,
"getOptionContracts"
> = {
getKeys: async ({ key: { symbol }, date }) => {
return optionContractExistenceDb
.getRange({
start: [date, symbol],
end: [date, symbol, MAXIMUM_KEY],
})
.map(({ key }) => ({
symbol,
expirationDate: key[2],
strike: key[3],
type: key[4],
})).asArray;
},
getAggregatesSync,
getAggregates: async ({
key: { symbol, expirationDate, strike, type },
date,
}) =>
getAggregatesSync({
key: { symbol, expirationDate, strike, type },
date,
}),
insertAggregates: async (aggregates) => {
await optionContractExistenceDb.batch(() => {
for (const aggregate of aggregates) {
optionContractExistenceDb.put(
[
new Date(aggregate.tsStart).toISOString().substring(0, 10),
aggregate.key.symbol,
aggregate.key.expirationDate,
aggregate.key.strike,
aggregate.key.type,
],
null
);
}
});
await optionContractAggregatesDb.batch(() => {
for (const aggregate of aggregates) {
optionContractAggregatesDb.put(
[
aggregate.key.symbol,
aggregate.key.expirationDate,
aggregate.key.strike,
aggregate.key.type,
aggregate.tsStart,
],
{
open: aggregate.open,
close: aggregate.close,
high: aggregate.high,
low: aggregate.low,
}
);
}
});
},
getClosingPrice: async ({
key: { symbol, strike, type, expirationDate },
}) => {
const startOfLastHourUnix = new Date(
`${expirationDate}T00:00:00Z`
).valueOf();
const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000;
let minPrice = 0;
for (const { value } of optionContractAggregatesDb.getRange({
start: [symbol, expirationDate, strike, type, startOfLastHourUnix],
end: [symbol, expirationDate, strike, type, endOfLastHourUnix],
})) {
if (value.close < minPrice || minPrice === 0) {
minPrice = value.close;
}
}
return minPrice;
},
getAggregate: async ({
key: { symbol, expirationDate, strike, type },
tsStart,
}) => {
return await optionContractAggregatesDb.get([
symbol,
expirationDate,
strike,
type,
tsStart,
]);
},
};
return {
...optionContractDatabase,
getOptionContracts: optionContractDatabase.getKeys,
};
}
export const database: OptionContractDatabase = makeOptionContractDatabase();
@@ -0,0 +1,79 @@
import type { StockDatabase, StockKey } from "./interfaces.js";
import type { Aggregate } from "../interfaces.js";
import { clickhouse, query } from "../../lib/clickhouse.js";
function makeStockDatabase(): StockDatabase {
const stockDatabase: Omit<StockDatabase, "getSymbols"> = {
getKeys: async ({ date, key }) => {
if (key?.symbol) {
return [key as StockKey];
}
return await query(`
SELECT DISTINCT symbol FROM stock_aggregates WHERE toDate(tsStart) = '${date}'
`);
},
getAggregates: async ({ key: { symbol }, date }) => {
return (
await query<Omit<Aggregate<StockKey>, "key">>(`
SELECT
toUnixTimestamp(tsStart) as tsStart,
open,
close,
high,
low
FROM stock_aggregates
WHERE symbol = '${symbol}'
AND toDate(tsStart) = '${date}'
ORDER BY tsStart ASC
`)
).map((aggregate) => ({
...aggregate,
tsStart: aggregate.tsStart * 1000, // unfortunately, `toUnixTimestamp` only returns second-precision
}));
},
getAggregate: async ({ key: { symbol }, tsStart }) => {
return (
await query<Omit<Aggregate<StockKey>, "key">>(`
SELECT
open,
close,
high,
low
FROM stock_aggregates
WHERE symbol = '${symbol}'
AND tsStart = '${tsStart}'
`)
).map((aggregate) => ({
...aggregate,
tsStart,
}))[0];
},
insertAggregates: async (aggregates) => {
// stock existence is taken care of by clickhouse materialized view
await clickhouse.insert({
table: "stock_aggregates",
values: aggregates.map(
({ key: { symbol }, tsStart, open, close, high, low }) => ({
symbol,
tsStart,
open,
close,
high,
low,
})
),
});
},
getClosingPrice: async ({ key }) => {
// no-op: not used since stocks don't have a "closing" price, unlike options.
return 0;
},
};
return {
...stockDatabase,
getSymbols: stockDatabase.getKeys,
};
}
export const database: StockDatabase = makeStockDatabase();
@@ -0,0 +1,7 @@
import type { AggregateDatabase } from "../interfaces.js";
export type StockKey = { symbol: string };
export type StockDatabase = AggregateDatabase<StockKey> & {
getSymbols: AggregateDatabase<StockKey>["getKeys"];
};
@@ -0,0 +1,86 @@
import type { StockDatabase, StockKey } from "./interfaces.js";
import { open } from "lmdbx";
const stockAggregatesDb = open({
path: "./stock-aggregates.db",
// any options go here, we can turn on compression like this:
compression: true,
});
const stockExistenceDb = open({
path: "./stock-existence.db",
// any options go here, we can turn on compression like this:
compression: true,
});
/** Largest possible key according to the `ordered-binary` (used by lmdbx) docs. */
const MAXIMUM_KEY = Buffer.from([0xff]);
function makeStockDatabase(): StockDatabase {
const stockDatabase: Omit<StockDatabase, "getSymbols"> = {
getKeys: async ({ date, key }) => {
if (key?.symbol) {
return [key as StockKey];
}
return stockExistenceDb
.getRange({
start: [date],
end: [date, MAXIMUM_KEY],
})
.map(({ key }) => ({ symbol: key[1] })).asArray;
},
getAggregates: async ({ key: { symbol }, date }) => {
const startOfDayUnix = new Date(`${date}T00:00:00Z`).valueOf();
const endOfDayUnix = startOfDayUnix + 3600 * 24 * 1000;
return stockAggregatesDb
.getRange({
start: [symbol, startOfDayUnix],
end: [symbol, endOfDayUnix],
})
.map(({ key, value }) => ({
tsStart: key[1],
open: value.open,
close: value.close,
high: value.high,
low: value.low,
})).asArray;
},
getAggregate: async ({ key: { symbol }, tsStart }) => {
return stockAggregatesDb.get([symbol, tsStart]);
},
insertAggregates: async (aggregates) => {
await stockExistenceDb.batch(() => {
for (const aggregate of aggregates) {
stockExistenceDb.put(
[
new Date(aggregate.tsStart).toISOString().substring(0, 10),
aggregate.key.symbol,
],
null
);
}
});
await stockAggregatesDb.batch(() => {
for (const aggregate of aggregates) {
stockAggregatesDb.put([aggregate.key.symbol, aggregate.tsStart], {
open: aggregate.open,
close: aggregate.close,
high: aggregate.high,
low: aggregate.low,
});
}
});
},
getClosingPrice: async ({ key }) => {
// no-op: not used since stocks don't have a "closing" price, unlike options.
return 0;
},
};
return {
...stockDatabase,
getSymbols: stockDatabase.getKeys,
};
}
export const database: StockDatabase = makeStockDatabase();
@@ -0,0 +1,46 @@
export type Candlestick = {
open: number;
close: number;
high: number;
low: number;
};
export type Aggregate<T> = {
key: T;
/** UNIX time in milliseconds */
tsStart: number;
} & Candlestick;
export type AggregateDatabase<T> = {
getKeys: ({
key,
date,
}: {
key?: T | Partial<T>;
date?: string;
}) => Promise<Array<T>>;
getAggregates: ({
key,
date,
}: {
key: T;
date: string;
}) => Promise<Array<Omit<Aggregate<T>, "key">>>;
/** Since an aggregate may not exist at the specified `tsStart`, return `undefined` if it doesn't exist. */
getAggregate: ({
key,
tsStart,
}: {
key: T;
tsStart: number;
}) => Promise<Omit<Aggregate<T>, "key"> | undefined>;
getAggregatesSync?: ({
key,
date,
}: {
key: T;
date: string;
}) => Array<Omit<Aggregate<T>, "key">>;
insertAggregates: (aggregates: Array<Aggregate<T>>) => Promise<void>;
getClosingPrice: ({ key }: { key: T }) => Promise<number>;
};
+87
View File
@@ -0,0 +1,87 @@
import { query } from "./lib/clickhouse.js";
import { publicProcedure, RpcType, router } from "./trpc.js";
import {
Object as ObjectT,
String as StringT,
Number as NumberT,
} from "@sinclair/typebox";
/** Gets a list of symbols that have at least one option contract */
export const 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 ["SPY"];
});
export const 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);
});
export const 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);
});
export const 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);
});
export default router({
getAvailableUnderlyings,
getAvailableAsOfDates,
getExpirationsForUnderlying,
getStrikesForUnderlying,
});
+53
View File
@@ -0,0 +1,53 @@
import { query } from "./lib/clickhouse.js";
import { publicProcedure, RpcType, router } from "./trpc.js";
import {
Object as ObjectT,
String as StringT,
Number as NumberT,
} from "@sinclair/typebox";
export const getChartData = 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
moniness*100 as x,
FLOOR(price, 1) as y,
sum(number_of_quotes) as n
FROM calendar_stats
WHERE dte = ${daysToFrontExpiration}
AND moniness >= -0.05
AND moniness <= 0.05
AND span = ${daysBetweenFrontAndBackExpiration}
AND date >= '${lookbackPeriodStart}'
AND date <= '${lookbackPeriodEnd}'
GROUP BY x, y
ORDER BY x ASC, y ASC
`,
"JSONEachRow"
);
});
export default router({
getChartData,
});
+54
View File
@@ -0,0 +1,54 @@
import { query } from "./lib/clickhouse.js";
import { publicProcedure, RpcType, router } from "./trpc.js";
import {
Object as ObjectT,
String as StringT,
Number as NumberT,
} from "@sinclair/typebox";
/** Returns prices for all matching calendars (i.e. those with similar
* characteristics to those given) */
export const getChartData = 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(date) as x,
price as y
FROM calendar_stats
WHERE dte = ${daysToFrontExpiration}
AND moniness >= ${strikePercentageFromUnderlyingPriceRangeMin}
AND moniness <= ${strikePercentageFromUnderlyingPriceRangeMax}
AND span = ${daysBetweenFrontAndBackExpiration}
AND date >= '${lookbackPeriodStart}'
AND date <= '${lookbackPeriodEnd}'
`,
"JSONEachRow"
);
});
export default router({
getChartData,
});
+36
View File
@@ -0,0 +1,36 @@
import { query } from "./lib/clickhouse.js";
import { publicProcedure, RpcType, router } from "./trpc.js";
import { Object as ObjectT, String as StringT } from "@sinclair/typebox";
export const getChartData = 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(toStartOfHour(ts)) as x,
avg(price) as y
FROM stock_aggregates_filled
WHERE symbol = '${underlying}'
AND ts >= '${lookbackPeriodStart} 00:00:00'
AND ts <= '${lookbackPeriodEnd} 00:00:00'
GROUP BY x
ORDER BY x ASC
`,
"JSONCompactEachRow"
// "JSONEachRow"
);
});
export default router({
getChartData,
});
+146
View File
@@ -0,0 +1,146 @@
import { database as stockDatabase } from "./AggregateDatabase/Stock/lmdbx.js";
import { database as calendarDatabase } from "./AggregateDatabase/Calendar/optiondb-lmdbx.js";
import type { CalendarKey } from "./AggregateDatabase/Calendar/interfaces.js";
import { nextDate } from "./lib/utils/nextDate.js";
export type BacktestInput = {
symbol: string;
startDate: string;
endDate: string;
/** Between 0 and 1. The frequency that similar calendars have historically ended (i.e. within the last hour) at a higher price than the current calendar's price. */
historicalProbabilityOfSuccess?: number;
initialBuyingPower?: number;
};
export async function backtest({
symbol,
startDate,
endDate,
historicalProbabilityOfSuccess = 0.8,
initialBuyingPower = 2000,
}: BacktestInput) {
let buyingPower = initialBuyingPower;
const portfolio = new Set<CalendarKey>();
// for each day:
for (
let date = startDate, didBuyCalendar = false;
date <= endDate;
date = nextDate(date), didBuyCalendar = false
) {
const calendars = await calendarDatabase.getCalendars({
key: { symbol },
date,
});
const stockAggregates = await stockDatabase.getAggregates({
key: { symbol },
date,
});
// for each minute of that day for which we have a stock candlestick:
for (const stockAggregate of stockAggregates) {
// console.log("Current Time:", new Date(stockAggregate.tsStart));
// filter-out calendars that are far-from-the-money (5%)
const calendarsNearTheMoney = calendars.filter(
({ strike }) =>
Math.abs((stockAggregate.open - strike) / stockAggregate.open) < 0.05
);
console.log(
"Current Date:",
new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
dateStyle: "full",
timeStyle: "long",
}).format(new Date(stockAggregate.tsStart)),
";",
`${calendarsNearTheMoney.length} Calendars Near The Money`
);
// for each relevant calendar on that day:
for (const calendar of calendarsNearTheMoney) {
const strikePercentageFromTheMoney = Math.abs(
(stockAggregate.open - calendar.strike) / stockAggregate.open
);
/** Number of days between the back and front expiration dates. */
const calendarSpanInDays =
(new Date(calendar.backExpirationDate).valueOf() -
new Date(calendar.frontExpirationDate).valueOf()) /
(1000 * 60 * 60 * 24);
const targetCalendarPrice =
await calendarDatabase.getTargetPriceByProbability({
symbol,
calendarSpan: calendarSpanInDays,
strikePercentageFromTheMoney,
historicalProbabilityOfSuccess,
});
const calendarAggregateAtCurrentTime =
await calendarDatabase.getAggregate({
key: {
...calendar,
},
tsStart: stockAggregate.tsStart,
});
// if there exists a matching calendar candlestick for the current minute:
if (calendarAggregateAtCurrentTime) {
// if the current candlestick is a good price (i.e. less than the target price):
const minCalendarPriceInCandlestick = Math.min(
calendarAggregateAtCurrentTime.open,
calendarAggregateAtCurrentTime.close
);
if (
minCalendarPriceInCandlestick < targetCalendarPrice &&
minCalendarPriceInCandlestick >
0.07 /* sometimes the calendar price is zero or negative, which is of course impossible; some institution got a good deal */
) {
// if we can afford to buy the calendar:
if (buyingPower > minCalendarPriceInCandlestick) {
// buy the calendar, and continue to the next day:
portfolio.add(calendar);
buyingPower = buyingPower - minCalendarPriceInCandlestick * 100;
console.log(
"Bought",
calendar,
"for",
minCalendarPriceInCandlestick * 100,
"...$",
buyingPower,
"left"
);
didBuyCalendar = true;
}
}
}
if (didBuyCalendar) {
break;
}
}
if (didBuyCalendar) {
break;
}
}
// for each calendar in portfolio, if today is the last day, close the position:
for (const calendar of portfolio.values()) {
if (calendar.frontExpirationDate === date) {
const calendarClosingPrice = await calendarDatabase.getClosingPrice({
key: {
...calendar,
},
});
portfolio.delete(calendar);
buyingPower = buyingPower + calendarClosingPrice * 100;
console.log(
"Sold",
calendar,
"for",
calendarClosingPrice,
"...$",
buyingPower,
"left"
);
}
}
}
return {
endingBuyingPower: buyingPower,
portfolio: Array.from(portfolio.values()),
};
}
+85
View File
@@ -0,0 +1,85 @@
import React, { useState, useEffect } from "react";
import { render, Box, Text, useInput, useApp } from "ink";
import TextInput from "ink-text-input";
import type { BacktestInput } from "./backtest.js";
import { backtest } from "./backtest.js";
const App = () => {
const [step, setStep] = useState(0);
const [input, setInput] = useState<Partial<BacktestInput>>({});
const [result, setResult] = useState<string>("");
const { exit } = useApp();
const steps = [
{ prompt: "Enter symbol:", key: "symbol" },
{ prompt: "Enter start date (YYYY-MM-DD):", key: "startDate" },
{ prompt: "Enter end date (YYYY-MM-DD):", key: "endDate" },
{
prompt: "Enter historical probability of success (0-1, default 0.8):",
key: "historicalProbabilityOfSuccess",
},
{
prompt: "Enter initial buying power (default 2000):",
key: "initialBuyingPower",
},
];
useInput((input, key) => {
if (key.escape) {
exit();
}
});
useEffect(() => {
if (step === steps.length) {
const runBacktest = async () => {
try {
const backtestResult = await backtest(input as BacktestInput);
setResult(JSON.stringify(backtestResult, null, 2));
} catch (error) {
setResult(`Error: ${error.message}`);
}
};
runBacktest();
}
}, [step, input]);
const handleSubmit = (value: string) => {
const currentStep = steps[step];
let parsedValue: string | number = value;
if (
currentStep.key === "historicalProbabilityOfSuccess" ||
currentStep.key === "initialBuyingPower"
) {
parsedValue = Number.parseFloat(value) || undefined;
}
setInput({ ...input, [currentStep.key]: parsedValue });
setStep(step + 1);
};
if (step < steps.length) {
return (
<Box flexDirection="column">
<Text>{steps[step].prompt}</Text>
<TextInput
value={(input[steps[step].key] as string) || ""}
onChange={(value) =>
setInput((prev) => ({ ...prev, [steps[step].key]: value }))
}
onSubmit={handleSubmit}
/>
</Box>
);
}
return (
<Box flexDirection="column">
<Text>Backtest Results:</Text>
<Text>{result}</Text>
</Box>
);
};
render(<App />);
+21 -250
View File
@@ -1,110 +1,24 @@
import { publicProcedure, router } from "./trpc.js";
import { publicProcedure, router, RpcType } 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,
TSchema,
Number as NumberT,
} from "@sinclair/typebox";
import { TypeCompiler } from "@sinclair/typebox/compiler";
import { TRPCError } from "@trpc/server";
import { createServer } from "http";
import { createServer } from "node:http";
import { Env } from "@humanwhocodes/env";
import StockPriceChart from "./StockPriceChart.js";
import SimilarCalendarPriceChart from "./SimilarCalendarPriceChart.js";
import CalendarExitPriceChart from "./CalendarExitPriceChart.js";
import CalendarCharacteristicsForm from "./CalendarCharacteristicsForm.js";
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
export const getOpensForUnderlying = publicProcedure
.input(
RpcType(
ObjectT({
@@ -125,8 +39,9 @@ const appRouter = router({
`,
"JSONEachRow"
);
}),
getOpensForOptionContract: publicProcedure
});
export const getOpensForOptionContract = publicProcedure
.input(
RpcType(
ObjectT({
@@ -152,160 +67,16 @@ const appRouter = router({
`,
"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"
);
}),
});
const appRouter = router({
CalendarCharacteristicsForm,
StockPriceChart,
SimilarCalendarPriceChart,
CalendarExitPriceChart,
getOpensForUnderlying,
getOpensForOptionContract,
});
// Export type router type signature,
@@ -329,4 +100,4 @@ const server = createServer((req, res) => {
}
});
server.listen(parseInt(LISTEN_PORT));
server.listen(Number.parseInt(LISTEN_PORT));
+15 -8
View File
@@ -1,31 +1,38 @@
import { createClient as createClickhouseClient } from "@clickhouse/client";
import type { DataFormat } from "@clickhouse/client";
import { Env } from "@humanwhocodes/env";
import { retry } from "./utils/retry.js";
const env = new Env();
const { CLICKHOUSE_USER, CLICKHOUSE_PASS } = env.required;
const CLICKHOUSE_HOST = env.get("CLICKHOUSE_HOST", "http://localhost:8123");
const CLICKHOUSE_URL = env.get("CLICKHOUSE_URL", "http://localhost:8123");
export const clickhouse = createClickhouseClient({
host: CLICKHOUSE_HOST,
url: CLICKHOUSE_URL,
username: CLICKHOUSE_USER,
password: CLICKHOUSE_PASS,
keep_alive: {
enabled: true,
// socket_ttl: 2500,
},
});
export async function query<T>(
queryString: string,
format: DataFormat = "JSONEachRow"
): Promise<Array<T>> {
return await (
await clickhouse.query({
return await retry(
async () => {
const result = await clickhouse.query({
query: queryString,
format,
clickhouse_settings: {
output_format_json_quote_64bit_integers: 0,
//output_format_json_quote_64bit_floats: false,
//output_format_json_quote_64bit_decimals: false,
},
})
).json();
});
return await result.json();
},
{ maxRetries: 5 }
);
}
+6
View File
@@ -0,0 +1,6 @@
export function nextDate(date: string) {
const [year, month, day] = date.split('-').map(Number);
const nextDay = new Date(Date.UTC(year, month - 1, day + 1));
const nextDateString = nextDay.toISOString().substring(0, 10);
return nextDateString;
}
+61
View File
@@ -0,0 +1,61 @@
type RetryDecision = {
shouldRetry: boolean;
maxRetries?: number;
delay?: number;
};
type RetryOptions = {
maxRetries?: number;
delay?: number;
shouldRetry?: (error: unknown) => RetryDecision;
};
export async function retry<T>(
fn: () => Promise<T>,
options: RetryOptions = {},
): Promise<T> {
const {
maxRetries: defaultMaxRetries = 3,
delay: defaultDelay = 1000,
shouldRetry = retryOnAnyError,
} = options;
let attempt = 1;
while (true) {
try {
return await fn();
} catch (error) {
const decision = shouldRetry(error);
if (!decision.shouldRetry) throw error;
const currentMaxRetries = decision.maxRetries ?? defaultMaxRetries;
const currentDelay = decision.delay ?? defaultDelay;
if (attempt >= currentMaxRetries) throw error;
console.warn(
`Error occurred, retrying (attempt ${attempt}/${currentMaxRetries})...`,
);
console.error(error);
await new Promise((resolve) => setTimeout(resolve, currentDelay));
attempt++;
}
}
}
export const retryOnAnyError = (): RetryDecision => ({ shouldRetry: true });
export const retryOnTimeout = (error: unknown): RetryDecision =>
error instanceof Error && error.message.includes("timeout")
? { shouldRetry: true, maxRetries: 5, delay: 2000 }
: { shouldRetry: false };
export const retryOnErrorType =
(errorType: new () => Error, options?: Partial<RetryDecision>) =>
(error: unknown) =>
error instanceof errorType
? { shouldRetry: true, ...options }
: { shouldRetry: false };
export const retryOnErrorSubstring =
(substring: string, options?: Partial<RetryDecision>) => (error: unknown) =>
error instanceof Error && error.message.includes(substring)
? { shouldRetry: true, ...options }
: { shouldRetry: false };
+77
View File
@@ -0,0 +1,77 @@
import type { AggregateDatabase } from "../AggregateDatabase/interfaces.js";
import { database as stockDatabaseClickhouse } from "../AggregateDatabase/Stock/clickhouse.js";
import { database as stockDatabaseLmdbx } from "../AggregateDatabase/Stock/lmdbx.js";
// import { optionContractDatabase as optionContractDatabaseClickhouse } from "../optiondb.clickhouse.js";
// import { optionContractDatabase as optionContractDatabaseLmdbx } from "../optiondb.lmdbx.js";
import { nextDate } from "../lib/utils/nextDate.js";
import { retry, retryOnTimeout } from "../lib/utils/retry.js";
import type { OptionContractKey } from "../AggregateDatabase/OptionContract/interfaces.js";
import type { StockKey } from "../AggregateDatabase/Stock/interfaces.js";
async function syncAggregates<T>({
fromDatabase,
toDatabase,
key,
date,
}: {
fromDatabase: AggregateDatabase<T>;
toDatabase: AggregateDatabase<T>;
key: T;
date: string;
}) {
const aggregatesFrom = (await fromDatabase.getAggregates({ key, date })).map(
(aggregateWithoutKey) => ({ ...aggregateWithoutKey, key })
);
await toDatabase.insertAggregates(aggregatesFrom);
}
const symbols = ["AMD", "AAPL", "MSFT", "GOOGL", "NFLX", "NVDA"];
async function run<T extends StockKey | OptionContractKey>({
fromDatabase,
toDatabase,
}: {
fromDatabase: AggregateDatabase<T>;
toDatabase: AggregateDatabase<T>;
}) {
const startDate = process.argv[2];
const endDate = process.argv[3];
if (!startDate || !endDate) {
console.error("Usage: node clickhouse-to-lmdbx.js <startDate> <endDate>");
console.error("Dates should be in YYYY-MM-DD format");
process.exit(1);
}
for (let date = startDate; date <= endDate; date = nextDate(date)) {
// const symbols = await stockDatabaseClickhouse.getSymbols({ date });
for (const symbol of symbols) {
console.log(date, symbol);
const keys = await retry(
() =>
fromDatabase.getKeys({
key: { symbol } as T,
date,
}),
{ shouldRetry: retryOnTimeout }
);
for (const key of keys) {
// console.log(date, symbol, key.expirationDate, key.strike, key.type);
await retry(
() =>
syncAggregates({
fromDatabase,
toDatabase,
key,
date,
}),
{ shouldRetry: retryOnTimeout }
);
}
}
}
}
await run({
fromDatabase: stockDatabaseClickhouse,
toDatabase: stockDatabaseLmdbx,
});
+24 -3
View File
@@ -1,14 +1,35 @@
import { initTRPC } from '@trpc/server';
 
import { initTRPC } from "@trpc/server";
import { TypeCompiler } from "@sinclair/typebox/compiler";
import { TRPCError } from "@trpc/server";
import type { TSchema } from "@sinclair/typebox";
/**
* 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;
/**
* 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",
});
};
}
+39 -57
View File
@@ -82,31 +82,9 @@ CREATE TABLE stock_aggregates
volume UInt64,
volume_weighted_price Float64
)
ENGINE MergeTree()
ENGINE ReplacingMergeTree()
ORDER BY (symbol, tsStart)
CREATE TABLE option_aggregates
(
symbol LowCardinality(String),
expirationDate Date,
strike Float32,
type Enum('call', 'put'),
tsStart DateTime32 CODEC(DoubleDelta(1), ZSTD),
open Float32 CODEC(Delta(2), ZSTD),
close Float32 CODEC(Delta(2), ZSTD),
low Float32 CODEC(Delta(2), ZSTD),
high Float32 CODEC(Delta(2), ZSTD),
volume UInt32 CODEC(T64),
volumeWeightedPrice Float32 CODEC(Delta(2), ZSTD)
)
ENGINE MergeTree()
ORDER BY (symbol, expirationDate, strike, type, 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_contract_aggregates
(
symbol LowCardinality(String),
@@ -125,47 +103,51 @@ CREATE TABLE option_contract_aggregates
ENGINE ReplacingMergeTree()
ORDER BY (symbol, expirationDate, strike, type, tsStart)
CREATE TABLE option_histories_last_day
-- For stats about the character of this stock's options given a certain distance-from-the-money and time-to-expiration:
CREATE TABLE calendar_stats_by_symbol
(
symbol LowCardinality(String),
expirationDate Date,
strike Float64,
type Enum('call', 'put'),
tsStart DateTime32,
open Float64,
minutesToFront UInt16,
underlyingPrice Float64,
strikePercentageFromUnderlyingPrice Float64
calendarSpanInDays UInt16,
tsStart DateTime32 CODEC(Delta, ZSTD), -- included so as to assess the character of the stock's options within a given range of time; for example, if the stock got really hot for a few months.
minutesToExpiration UInt32,
frontMonthOpen Float32,
backMonthOpen Float32,
strikePercentageFromUnderlyingOpen Float64,
frontMonthClose Float32,
backMonthClose Float32,
strikePercentageFromUnderlyingClose Float64
)
ENGINE MergeTree()
ORDER BY (symbol, minutesToFront, strikePercentageFromUnderlyingPrice)
INSERT INTO option_histories_last_day
PRIMARY KEY (symbol, calendarSpanInDays, tsStart)
ORDER BY (symbol, calendarSpanInDays, tsStart, minutesToExpiration);
-- Populate `calendar_stats_by_symbol` by:
-- INSERT INTO calendar_stats_by_symbol
SELECT
option_aggregates.symbol as symbol,
option_aggregates.expirationDate as expirationDate,
option_aggregates.strike as strike,
option_aggregates.type as type,
option_aggregates.tsStart as tsStart,
option_aggregates.open as open,
date_diff('minute', tsStart, timestamp_add(expirationDate, INTERVAL 16 HOUR)) as minutesToFront,
stock_aggregates.open as underlyingPrice Float64,
(strike-underlyingPrice)/underlyingPrice as strikePercentageFromUnderlyingPrice Float64
FROM (
SELECT
symbol,
expirationDate,
strike,
type,
tsStart,
open
FROM option_aggregates
WHERE toDate(tsStart) = expirationDate
) as option_aggregates
frontMonth.symbol,
dateDiff('day', frontMonth.expirationDate, backMonth.expirationDate) as calendarSpanInDays,
frontMonth.tsStart,
dateDiff('minute', frontMonth.tsStart, addMinutes(toDateTime(expirationDate, 'America/New_York'), 60 * 16)) as minutesToExpiration,
frontMonth.open as frontMonthOpen,
backMonth.open as backMonthOpen,
(frontMonth.strike-stock_aggregates.open)/stock_aggregates.open*100.0 as strikePercentageFromUnderlyingOpen,
frontMonth.close as frontMonthClose,
backMonth.close as backMonthClose,
(frontMonth.strike-stock_aggregates.close)/stock_aggregates.close*100.0 as strikePercentageFromUnderlyingClose
FROM option_contract_aggregates as frontMonth
INNER JOIN stock_aggregates
ON option_aggregates.symbol = stock_aggregates.symbol
AND option_aggregates.tsStart = stock_aggregates.tsStart
ON option_contract_aggregates.symbol = stock_aggregates.symbol
AND option_contract_aggregates.tsStart = stock_aggregates.tsStart
INNER JOIN option_contract_aggregates as backMonth
ON frontMonth.symbol = backMonth.symbol
AND frontMonth.strike = backMonth.strike
AND frontMonth.type = backMonth.type
AND frontMonth.tsStart = backMonth.tsStart
WHERE backMonth.expirationDate > frontMonth.expirationDate
AND frontMonth.symbol = 'AAPL'
AND calendarSpanInDays = 14
CREATE TABLE option_histories
(
+3 -2
View File
@@ -1,12 +1,13 @@
{
"compilerOptions": {
"jsx": "react",
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
"allowJs": true,
"checkJs": true,
"checkJs": false,
"lib": ["es2022"]
},
"include": ["**/*"]
"include": ["src/**/*"]
}