Compare commits

..

16 Commits

21 changed files with 445 additions and 294 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
FROM node:20-slim AS build FROM node:20-slim AS build
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable RUN npm install -g corepack@latest && corepack enable
WORKDIR /app WORKDIR /app
# copy what's necessary to install dependencies: # copy what's necessary to install dependencies:
COPY package.json pnpm-lock.yaml /app/ COPY package.json pnpm-lock.yaml /app/
+144 -84
View File
@@ -5,17 +5,19 @@ import {
LinearScale, LinearScale,
CategoryScale, CategoryScale,
PointElement, PointElement,
LineElement,
Tooltip, Tooltip,
Title, Title,
} from "chart.js"; } from "chart.js";
import { Scatter } from "react-chartjs-2"; import { Scatter } from "react-chartjs-2";
import { import {
Container, Container,
Grid, Grid2,
Typography, Typography,
Paper, Paper,
Popper, Popper,
ClickAwayListener, ClickAwayListener,
Stack,
} from "@mui/material"; } from "@mui/material";
import { import {
availableUnderlyings, availableUnderlyings,
@@ -38,13 +40,19 @@ import {
refreshStockPriceChartData, refreshStockPriceChartData,
} from "./HistoricalCalendarPrices/actions.js"; } from "./HistoricalCalendarPrices/actions.js";
import { EditableUnderlying } from "./HistoricalCalendarPrices/EditableUnderlying.js"; import { EditableUnderlying } from "./HistoricalCalendarPrices/EditableUnderlying.js";
import { EditableDaysToFrontExpiration } from "./HistoricalCalendarPrices/EditableDaysToFrontExpiration.js"; import { EditableOpenDTE } from "./HistoricalCalendarPrices/EditableOpenDTE.js";
import { EditableExitToFrontExpiration } from "./HistoricalCalendarPrices/EditableExitToFrontExpiration.js"; import { EditableExitDTE } from "./HistoricalCalendarPrices/EditableExitDTE.js";
import { EditableDaysBetweenFrontAndBackExpiration } from "./HistoricalCalendarPrices/EditableDaysBetweenFrontAndBackExpiration.js"; import { EditableSpan } from "./HistoricalCalendarPrices/EditableSpan.js";
import { EditableLookbackPeriodStart } from "./HistoricalCalendarPrices/EditableLookbackPeriodStart.js"; import { EditableLookbackPeriod } from "./HistoricalCalendarPrices/EditableLookbackPeriod.js";
import { EditableLookbackPeriodEnd } from "./HistoricalCalendarPrices/EditableLookbackPeriodEnd.js";
ChartJS.register(LinearScale, CategoryScale, PointElement, Tooltip, Title); ChartJS.register(
LinearScale,
CategoryScale,
PointElement,
LineElement,
Tooltip,
Title
);
const handleInit = () => { const handleInit = () => {
trpc.CalendarCharacteristicsForm.getAvailableUnderlyings trpc.CalendarCharacteristicsForm.getAvailableUnderlyings
@@ -63,17 +71,17 @@ export function HistoricalCalendarPrices() {
return ( return (
<Container maxWidth="lg"> <Container maxWidth="lg">
<Grid container spacing={4}> <Grid2 container spacing={4} columns={12}>
<Grid item xs={12}> {/* <Grid2 size={{ xs: 12 }}>
<Typography variant="h4" gutterBottom> <Typography variant="h4" gutterBottom>
<EditableUnderlying /> : <EditableUnderlying /> :
<EditableDaysBetweenFrontAndBackExpiration /> <EditableSpan />
-Day Calendar @ <EditableStrike /> -Day Calendar @ <EditableStrike />
%-from-the-money %-from-the-money
</Typography> </Typography>
<Typography variant="h5" gutterBottom sx={{ pl: 1 }}> <Typography variant="h5" gutterBottom sx={{ pl: 1 }}>
Opening at <EditableDaysToFrontExpiration /> DTE, Closing at{" "} Opening at <EditableOpenDTE /> DTE, Closing at <EditableExitDTE />
<EditableExitToFrontExpiration />
DTE DTE
</Typography> </Typography>
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
@@ -93,73 +101,55 @@ export function HistoricalCalendarPrices() {
</Paper> </Paper>
</Popper> </Popper>
</ClickAwayListener> </ClickAwayListener>
</Grid> </Grid2> */}
<Grid item xs={12}>
<Paper elevation={3} sx={{ p: 3, minHeight: "28em", height: "100%" }}> <Grid2 size={{ xs: 12 }}>
{underlying.value !== null && <Stack direction="row" spacing={2}>
stockPriceChartData.value.length > 0 ? ( <Typography gutterBottom minWidth={"8em"}>
<Scatter Underlying
data={{ </Typography>
datasets: [ <EditableUnderlying />
{ </Stack>
label: "Stock Open Price", <Stack direction="row" spacing={2}>
data: stockPriceChartData.value, <Typography gutterBottom minWidth={"8em"}>
}, Open DTE
], </Typography>
}} <EditableOpenDTE />
options={{ </Stack>
scales: { <Stack direction="row" spacing={2}>
x: { <Typography gutterBottom minWidth={"8em"}>
title: { Exit DTE
display: true, </Typography>
text: "Time", <EditableExitDTE />
}, </Stack>
ticks: { <Stack direction="row" spacing={2}>
callback: (value, index, ticks) => <Typography gutterBottom minWidth={"8em"}>
new Date((value as number) * 1000) Span
.toISOString() </Typography>
.substring(0, 10), <EditableSpan />
}, </Stack>
min: new Date(lookbackPeriodStart.value).getTime() / 1000, <Stack direction="row" spacing={2}>
max: new Date(lookbackPeriodEnd.value).getTime() / 1000, <Typography gutterBottom minWidth={"8em"}>
}, Lookback Period
y: { </Typography>
beginAtZero: false, <EditableLookbackPeriod />
ticks: { </Stack>
callback: (value, index, ticks) => <ClickAwayListener
`$${value.toString()}`, onClickAway={() => {
}, isPopperOpen.value = false;
}, // refreshSimilarCalendarPriceChartData();
}, console.log("clicked away");
elements: { }}
point: { >
radius: 1, <Popper open={isPopperOpen.value} anchorEl={popperAnchorEl.value}>
borderWidth: 0, <Paper elevation={3} sx={{ p: 3 }}>
}, {popperContent.value}
}, </Paper>
plugins: { </Popper>
tooltip: { </ClickAwayListener>
enabled: false, </Grid2>
},
legend: { <Grid2 size={{ xs: 12, md: 6 }}>
display: false,
},
title: {
display: true,
text: "Stock Price",
},
},
animation: false,
maintainAspectRatio: false,
events: [],
}}
/>
) : (
<Typography>Loading Chart...</Typography>
)}
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper elevation={3} sx={{ p: 3, minHeight: "28em" }}> <Paper elevation={3} sx={{ p: 3, minHeight: "28em" }}>
{underlying.value !== null && {underlying.value !== null &&
similarCalendarPriceChartData.value.length > 0 ? ( similarCalendarPriceChartData.value.length > 0 ? (
@@ -219,11 +209,11 @@ export function HistoricalCalendarPrices() {
<Typography>Loading Chart...</Typography> <Typography>Loading Chart...</Typography>
)} )}
</Paper> </Paper>
</Grid> </Grid2>
<Grid item xs={12} md={6}> <Grid2 size={{ xs: 12, md: 6 }}>
<Paper elevation={3} sx={{ p: 3, minHeight: "28em" }}> <Paper elevation={3} sx={{ p: 3, minHeight: "28em" }}>
{underlying.value !== null && {underlying.value !== null &&
similarCalendarPriceChartData.value.length > 0 ? ( calendarExitPriceChartData.value.length > 0 ? (
<Scatter <Scatter
data={{ data={{
datasets: [ datasets: [
@@ -293,8 +283,78 @@ export function HistoricalCalendarPrices() {
<Typography>Loading Chart...</Typography> <Typography>Loading Chart...</Typography>
)} )}
</Paper> </Paper>
</Grid> </Grid2>
</Grid> <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> </Container>
); );
} }
@@ -1,38 +0,0 @@
import TextField from "@mui/material/TextField";
import {
refreshcalendarExitPriceChartData,
refreshSimilarCalendarPriceChartData,
} from "./actions";
import { daysBetweenFrontAndBackExpiration } from "./state";
import { EditableValue } from "./EditableValue";
const handleDaysBetweenFrontAndBackExpirationChange = (e) => {
if (
daysBetweenFrontAndBackExpiration.value !== Number.parseInt(e.target.value)
) {
daysBetweenFrontAndBackExpiration.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={daysBetweenFrontAndBackExpiration.value}
onChange={handleDaysBetweenFrontAndBackExpirationChange}
InputProps={{ endAdornment: "Days Difference" }}
/>
);
}
export function EditableDaysBetweenFrontAndBackExpiration() {
return (
<EditableValue text={daysBetweenFrontAndBackExpiration.value}>
<DaysBetweenFrontAndBackExpirationChooser />
</EditableValue>
);
}
@@ -1,32 +0,0 @@
import TextField from "@mui/material/TextField";
import { refreshSimilarCalendarPriceChartData } from "./actions";
import { EditableValue } from "./EditableValue";
import { daysToFrontExpiration } from "./state";
const handleDaysToFrontExpirationChange = (e) => {
if (daysToFrontExpiration.value !== Number.parseInt(e.target.value)) {
daysToFrontExpiration.value = Number.parseInt(e.target.value);
refreshSimilarCalendarPriceChartData();
}
};
function DaysToFrontExpirationChooser() {
return (
<TextField
fullWidth
label="Now-to-Front-Month Days to Expiration"
type="number"
value={daysToFrontExpiration.value}
onChange={handleDaysToFrontExpirationChange}
InputProps={{ endAdornment: "Days" }}
/>
);
}
export function EditableDaysToFrontExpiration() {
return (
<EditableValue text={daysToFrontExpiration.value}>
<DaysToFrontExpirationChooser />
</EditableValue>
);
}
@@ -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>
);
}
@@ -1,32 +0,0 @@
import TextField from "@mui/material/TextField";
import { EditableValue } from "./EditableValue";
import { exitToFrontExpiration } from "./state";
import { refreshcalendarExitPriceChartData } from "./actions";
const handleExitToFrontExpirationChange = (e) => {
if (exitToFrontExpiration.value !== Number.parseInt(e.target.value)) {
exitToFrontExpiration.value = Number.parseInt(e.target.value);
refreshcalendarExitPriceChartData();
}
};
function ExitToFrontExpirationChooser() {
return (
<TextField
fullWidth
label="Exit-to-Front-Month Days to Expiration"
type="number"
value={exitToFrontExpiration.value}
onChange={handleExitToFrontExpirationChange}
InputProps={{ endAdornment: "Days" }}
/>
);
}
export function EditableExitToFrontExpiration() {
return (
<EditableValue text={exitToFrontExpiration.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,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>
);
}
@@ -1,24 +1,21 @@
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { EditableValue } from "./EditableValue"; import { EditableValue } from "./EditableValue";
import { import { moniness, moninessRadius } from "./state";
strikePercentageFromUnderlyingPrice,
strikePercentageFromUnderlyingPriceRadius,
} from "./state";
import Slider from "@mui/material/Slider"; import Slider from "@mui/material/Slider";
import { refreshSimilarCalendarPriceChartData } from "./actions"; import { refreshSimilarCalendarPriceChartData } from "./actions";
function StrikePercentageFromUnderlyingPriceChooser() { function MoninessChooser() {
return ( return (
<Slider <Slider
fullWidth fullWidth
label="Strike % From Underlying Price" label="Strike % From Underlying Price"
value={strikePercentageFromUnderlyingPrice.value} value={moniness.value}
valueLabelDisplay="on" valueLabelDisplay="on"
min={0} min={0}
max={10} max={10}
step={0.1} step={1}
onChange={(e, value) => { onChange={(e, value) => {
strikePercentageFromUnderlyingPrice.value = value as number; moniness.value = value as number;
}} }}
onChangeCommitted={(e, value) => { onChangeCommitted={(e, value) => {
refreshSimilarCalendarPriceChartData(); refreshSimilarCalendarPriceChartData();
@@ -28,18 +25,18 @@ function StrikePercentageFromUnderlyingPriceChooser() {
); );
} }
function StrikePercentageFromUnderlyingPriceRadiusChooser() { function MoninessRadiusChooser() {
return ( return (
<Slider <Slider
fullWidth fullWidth
label="Strike % Radius" label="Strike % Radius"
value={strikePercentageFromUnderlyingPriceRadius.value} value={moninessRadius.value}
valueLabelDisplay="on" valueLabelDisplay="on"
min={0} min={0}
max={0.5} max={10}
step={0.05} step={1}
onChange={(e, value) => { onChange={(e, value) => {
strikePercentageFromUnderlyingPriceRadius.value = value as number; moninessRadius.value = value as number;
}} }}
onChangeCommitted={(e, value) => { onChangeCommitted={(e, value) => {
refreshSimilarCalendarPriceChartData(); refreshSimilarCalendarPriceChartData();
@@ -57,13 +54,11 @@ function StrikePercentageFromUnderlyingPriceRadiusChooser() {
export function EditableStrike() { export function EditableStrike() {
return ( return (
<EditableValue <EditableValue
text={`${strikePercentageFromUnderlyingPrice.value.toFixed( text={`${moniness.value.toFixed(1)}±${moninessRadius.value.toFixed(2)}`}
1
)}±${strikePercentageFromUnderlyingPriceRadius.value.toFixed(2)}`}
> >
<Box sx={{ minWidth: "20em" }}> <Box sx={{ minWidth: "20em" }}>
<StrikePercentageFromUnderlyingPriceChooser /> <MoninessChooser />
<StrikePercentageFromUnderlyingPriceRadiusChooser /> <MoninessRadiusChooser />
</Box> </Box>
</EditableValue> </EditableValue>
); );
@@ -1,19 +1,41 @@
import { trpc } from "../../trpc"; import { trpc } from "../../trpc";
import { import {
calendarExitPriceChartData, calendarExitPriceChartData,
daysBetweenFrontAndBackExpiration, span,
daysToFrontExpiration, openDTE,
exitToFrontExpiration, exitDTE,
lookbackPeriodEnd, lookbackPeriodEnd,
lookbackPeriodStart, lookbackPeriodStart,
similarCalendarPriceChartData, similarCalendarPriceChartData,
stockPriceChartData, stockPriceChartData,
strikePercentageFromUnderlyingPrice, moniness,
strikePercentageFromUnderlyingPriceRadius, moninessRadius,
underlying, underlying,
} from "./state"; } from "./state";
export const refreshStockPriceChartData = () => { 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 = []; stockPriceChartData.value = [];
trpc.StockPriceChart.getChartData trpc.StockPriceChart.getChartData
.query({ .query({
@@ -24,40 +46,36 @@ export const refreshStockPriceChartData = () => {
.then((getChartDataResponse) => { .then((getChartDataResponse) => {
stockPriceChartData.value = getChartDataResponse; stockPriceChartData.value = getChartDataResponse;
}); });
}; }, 400);
export const refreshSimilarCalendarPriceChartData = () => { export const refreshSimilarCalendarPriceChartData = throttle(() => {
similarCalendarPriceChartData.value = []; similarCalendarPriceChartData.value = [];
trpc.SimilarCalendarPriceChart.getChartData trpc.SimilarCalendarPriceChart.getChartData
.query({ .query({
underlying: underlying.value, underlying: underlying.value,
daysToFrontExpiration: daysToFrontExpiration.value, daysToFrontExpiration: openDTE.value,
daysBetweenFrontAndBackExpiration: daysBetweenFrontAndBackExpiration: span.value,
daysBetweenFrontAndBackExpiration.value,
strikePercentageFromUnderlyingPriceRangeMin: strikePercentageFromUnderlyingPriceRangeMin:
strikePercentageFromUnderlyingPrice.value - (moniness.value - moninessRadius.value) / 100,
strikePercentageFromUnderlyingPriceRadius.value,
strikePercentageFromUnderlyingPriceRangeMax: strikePercentageFromUnderlyingPriceRangeMax:
strikePercentageFromUnderlyingPrice.value + (moniness.value + moninessRadius.value) / 100,
strikePercentageFromUnderlyingPriceRadius.value,
lookbackPeriodStart: lookbackPeriodStart.value, lookbackPeriodStart: lookbackPeriodStart.value,
lookbackPeriodEnd: lookbackPeriodEnd.value, lookbackPeriodEnd: lookbackPeriodEnd.value,
}) })
.then((getChartDataResponse) => { .then((getChartDataResponse) => {
similarCalendarPriceChartData.value = getChartDataResponse; similarCalendarPriceChartData.value = getChartDataResponse;
}); });
}; }, 400);
export const refreshcalendarExitPriceChartData = () => { export const refreshcalendarExitPriceChartData = throttle(() => {
calendarExitPriceChartData.value = []; calendarExitPriceChartData.value = [];
trpc.CalendarExitPriceChart.getChartData trpc.CalendarExitPriceChart.getChartData
.query({ .query({
underlying: underlying.value, underlying: underlying.value,
daysToFrontExpiration: exitToFrontExpiration.value, daysToFrontExpiration: exitDTE.value,
daysBetweenFrontAndBackExpiration: daysBetweenFrontAndBackExpiration: span.value,
daysBetweenFrontAndBackExpiration.value,
lookbackPeriodStart: lookbackPeriodStart.value, lookbackPeriodStart: lookbackPeriodStart.value,
lookbackPeriodEnd: lookbackPeriodEnd.value, lookbackPeriodEnd: lookbackPeriodEnd.value,
}) })
.then((getChartDataResponse) => { .then((getChartDataResponse) => {
calendarExitPriceChartData.value = getChartDataResponse; calendarExitPriceChartData.value = getChartDataResponse;
}); });
}; }, 400);
@@ -7,33 +7,33 @@ export const popperContent = signal(null);
export const availableUnderlyings = signal([]); export const availableUnderlyings = signal([]);
export const underlying = signal(null); export const underlying = signal(null);
export const daysToFrontExpiration = signal(14); export const openDTE = signal(14);
export const daysBetweenFrontAndBackExpiration = signal(14); export const span = signal(14);
export const strikePercentageFromUnderlyingPrice = signal(1.4); export const moniness = signal(1);
export const strikePercentageFromUnderlyingPriceRadius = signal(0.05); export const moninessRadius = signal(1);
export const exitToFrontExpiration = signal(2); export const exitDTE = signal(2);
export const stockPriceChartData = signal([]); export const stockPriceChartData = signal<Array<[number, number]>>([]);
export const similarCalendarPriceChartData = signal([]); export const similarCalendarPriceChartData = signal([]);
export const calendarExitPriceChartData = signal([]); export const calendarExitPriceChartData = signal([]);
export const lookbackPeriodStart = signal("2022-01-01"); export const lookbackPeriodStart = signal("2022-03-01");
export const lookbackPeriodEnd = signal("2024-01-01"); export const lookbackPeriodEnd = signal("2022-04-01");
export const maxChartPrice = computed(() => export const maxChartPrice = computed(() =>
Math.max( Math.max(
Math.max.apply( Math.max.apply(
null, null,
similarCalendarPriceChartData.value.map((d) => d.y) similarCalendarPriceChartData.value.map((d) => d.y).slice(0, -2)
), ),
Math.max.apply( Math.max.apply(
null, null,
calendarExitPriceChartData.value.map((d) => d.y) calendarExitPriceChartData.value.map((d) => d.y).slice(0, -2)
) )
) )
); );
+2 -1
View File
@@ -26,4 +26,5 @@ dist-ssr
.env .env
*.db *.db
*.db-lck *.db-lck
Calendar tRPC
+17 -12
View File
@@ -2,22 +2,27 @@
FROM node:20-slim AS base FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable RUN npm install -g corepack@latest && corepack enable
COPY package.json pnpm-lock.yaml /app/ COPY package.json pnpm-lock.yaml /app/
WORKDIR /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 RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
COPY tsconfig.json /app/ COPY tsconfig.json /app/
COPY src /app/src COPY src /app/src
RUN pnpm run build CMD [ "pnpm", "tsx", "src/index.ts" ]
FROM base # FROM base AS prod-deps
COPY --from=prod-deps /app/node_modules /app/node_modules # RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
COPY --from=build /app/dist /app/dist
WORKDIR /app/dist # # install dev dependencies which are needed for building, such as typescript:
CMD [ "node", "index.js" ] # 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 -1
View File
@@ -9,11 +9,12 @@
"cli": "tsx src/cli.tsx" "cli": "tsx src/cli.tsx"
}, },
"dependencies": { "dependencies": {
"@clickhouse/client": "^0.2.7", "@clickhouse/client": "^1.4.1",
"@humanwhocodes/env": "^3.0.5", "@humanwhocodes/env": "^3.0.5",
"@sinclair/typebox": "^0.32.5", "@sinclair/typebox": "^0.32.5",
"@trpc/server": "^10.45.0", "@trpc/server": "^10.45.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"date-fns": "^3.6.0",
"execa": "^9.3.0", "execa": "^9.3.0",
"ink": "^4.1.0", "ink": "^4.1.0",
"ink-text-input": "^5.0.1", "ink-text-input": "^5.0.1",
+17 -9
View File
@@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@clickhouse/client': '@clickhouse/client':
specifier: ^0.2.7 specifier: ^1.4.1
version: 0.2.7 version: 1.4.1
'@humanwhocodes/env': '@humanwhocodes/env':
specifier: ^3.0.5 specifier: ^3.0.5
version: 3.0.5 version: 3.0.5
@@ -23,6 +23,9 @@ importers:
cors: cors:
specifier: ^2.8.5 specifier: ^2.8.5
version: 2.8.5 version: 2.8.5
date-fns:
specifier: ^3.6.0
version: 3.6.0
execa: execa:
specifier: ^9.3.0 specifier: ^9.3.0
version: 9.3.0 version: 9.3.0
@@ -88,11 +91,11 @@ packages:
resolution: {integrity: sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==} resolution: {integrity: sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==}
engines: {node: '>=14.13.1'} engines: {node: '>=14.13.1'}
'@clickhouse/client-common@0.2.7': '@clickhouse/client-common@1.4.1':
resolution: {integrity: sha512-vgZm+8c5Cu1toIx1/xplF5dEHlCPw+7pJDOOEtLv2CIUVZ0Bl6nGVZ43EWxRdHeah9ivTfoRWhN1zI1PxjH0xQ==} resolution: {integrity: sha512-f5eoTrUSDplrMoi3ddeZ0MzGTn0iGMByEQ8j63eVMoBSOI2+F6jEIPcW2tWofT79Rvnn3RRlveYcShiaIiCJyw==}
'@clickhouse/client@0.2.7': '@clickhouse/client@1.4.1':
resolution: {integrity: sha512-ZiyarrGngHc+f5AjZSA7mkQfvnE/71jgXk304B0ps8V+aBpE2CsFB6AQmE/Mk2YkP5j+8r/JfG+m0AZWmE27ig==} resolution: {integrity: sha512-12iV+MeykxdQySRFHwaVU+hKUv3JP6kdwOI+z3zzyfPVYHynTlV8emJjjGZR0+VfRaj3PCMuQfryfsJ82nh9WQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
'@esbuild/aix-ppc64@0.19.11': '@esbuild/aix-ppc64@0.19.11':
@@ -610,6 +613,9 @@ packages:
csstype@3.1.3: csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
date-fns@3.6.0:
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
debug@4.3.5: debug@4.3.5:
resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@@ -1626,11 +1632,11 @@ snapshots:
ansi-styles: 6.2.1 ansi-styles: 6.2.1
is-fullwidth-code-point: 4.0.0 is-fullwidth-code-point: 4.0.0
'@clickhouse/client-common@0.2.7': {} '@clickhouse/client-common@1.4.1': {}
'@clickhouse/client@0.2.7': '@clickhouse/client@1.4.1':
dependencies: dependencies:
'@clickhouse/client-common': 0.2.7 '@clickhouse/client-common': 1.4.1
'@esbuild/aix-ppc64@0.19.11': '@esbuild/aix-ppc64@0.19.11':
optional: true optional: true
@@ -2021,6 +2027,8 @@ snapshots:
csstype@3.1.3: {} csstype@3.1.3: {}
date-fns@3.6.0: {}
debug@4.3.5: debug@4.3.5:
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
+1 -1
View File
@@ -13,7 +13,7 @@ export const getAvailableUnderlyings = publicProcedure.query(async (opts) => {
// SELECT DISTINCT(symbol) as symbol FROM option_contract_existences WHERE asOfDate = (SELECT max(asOfDate) FROM option_contract_existences) // SELECT DISTINCT(symbol) as symbol FROM option_contract_existences WHERE asOfDate = (SELECT max(asOfDate) FROM option_contract_existences)
// `) // `)
// ).map(({ symbol }) => symbol); // ).map(({ symbol }) => symbol);
return ["AAPL", "AMD", "GOOGL", "MSFT", "NFLX"]; return ["SPY"];
}); });
export const getAvailableAsOfDates = publicProcedure export const getAvailableAsOfDates = publicProcedure
+11 -12
View File
@@ -30,18 +30,17 @@ export const getChartData = publicProcedure
} = opts.input; } = opts.input;
return await query<[number, number, number]>( return await query<[number, number, number]>(
` `
SELECT SELECT
FLOOR(strikePercentageFromUnderlyingPrice, 1) as x, moniness*100 as x,
FLOOR(calendarPrice, 1) as y, FLOOR(price, 1) as y,
count(*) as n sum(number_of_quotes) as n
FROM calendar_histories FROM calendar_stats
WHERE symbol = '${underlying}' WHERE dte = ${daysToFrontExpiration}
AND daysToFrontExpiration = ${daysToFrontExpiration} AND moniness >= -0.05
AND strikePercentageFromUnderlyingPrice >= -5.0 AND moniness <= 0.05
AND strikePercentageFromUnderlyingPrice <= 5.0 AND span = ${daysBetweenFrontAndBackExpiration}
AND daysBetweenFrontAndBackExpiration = ${daysBetweenFrontAndBackExpiration} AND date >= '${lookbackPeriodStart}'
AND tsStart >= '${lookbackPeriodStart} 00:00:00' AND date <= '${lookbackPeriodEnd}'
AND tsStart <= '${lookbackPeriodEnd} 00:00:00'
GROUP BY x, y GROUP BY x, y
ORDER BY x ASC, y ASC ORDER BY x ASC, y ASC
`, `,
+10 -11
View File
@@ -34,17 +34,16 @@ export const getChartData = publicProcedure
} = opts.input; } = opts.input;
return await query<[number, number]>( return await query<[number, number]>(
` `
SELECT SELECT
toUnixTimestamp(tsStart) as x, toUnixTimestamp(date) as x,
truncate(calendarPrice, 2) as y price as y
FROM calendar_histories FROM calendar_stats
WHERE symbol = '${underlying}' WHERE dte = ${daysToFrontExpiration}
AND daysToFrontExpiration = ${daysToFrontExpiration} AND moniness >= ${strikePercentageFromUnderlyingPriceRangeMin}
AND strikePercentageFromUnderlyingPrice >= ${strikePercentageFromUnderlyingPriceRangeMin} AND moniness <= ${strikePercentageFromUnderlyingPriceRangeMax}
AND strikePercentageFromUnderlyingPrice <= ${strikePercentageFromUnderlyingPriceRangeMax} AND span = ${daysBetweenFrontAndBackExpiration}
AND daysBetweenFrontAndBackExpiration = ${daysBetweenFrontAndBackExpiration} AND date >= '${lookbackPeriodStart}'
AND tsStart >= '${lookbackPeriodStart} 00:00:00' AND date <= '${lookbackPeriodEnd}'
AND tsStart <= '${lookbackPeriodEnd} 00:00:00'
`, `,
"JSONEachRow" "JSONEachRow"
); );
+8 -6
View File
@@ -17,15 +17,17 @@ export const getChartData = publicProcedure
return await query<[number, number]>( return await query<[number, number]>(
` `
SELECT SELECT
toUnixTimestamp(tsStart) as x, toUnixTimestamp(toStartOfHour(ts)) as x,
open as y avg(price) as y
FROM stock_aggregates FROM stock_aggregates_filled
WHERE symbol = '${underlying}' WHERE symbol = '${underlying}'
AND tsStart >= '${lookbackPeriodStart} 00:00:00' AND ts >= '${lookbackPeriodStart} 00:00:00'
AND tsStart <= '${lookbackPeriodEnd} 00:00:00' AND ts <= '${lookbackPeriodEnd} 00:00:00'
GROUP BY x
ORDER BY x ASC ORDER BY x ASC
`, `,
"JSONEachRow" "JSONCompactEachRow"
// "JSONEachRow"
); );
}); });
+5 -5
View File
@@ -6,21 +6,21 @@ import { retry } from "./utils/retry.js";
const env = new Env(); const env = new Env();
const { CLICKHOUSE_USER, CLICKHOUSE_PASS } = env.required; 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({ export const clickhouse = createClickhouseClient({
host: CLICKHOUSE_HOST, url: CLICKHOUSE_URL,
username: CLICKHOUSE_USER, username: CLICKHOUSE_USER,
password: CLICKHOUSE_PASS, password: CLICKHOUSE_PASS,
keep_alive: { keep_alive: {
enabled: true, enabled: true,
socket_ttl: 2500, // socket_ttl: 2500,
}, },
}); });
export async function query<T>( export async function query<T>(
queryString: string, queryString: string,
format: DataFormat = "JSONEachRow", format: DataFormat = "JSONEachRow"
): Promise<Array<T>> { ): Promise<Array<T>> {
return await retry( return await retry(
async () => { async () => {
@@ -33,6 +33,6 @@ export async function query<T>(
}); });
return await result.json(); return await result.json();
}, },
{ maxRetries: 5 }, { maxRetries: 5 }
); );
} }