Compare commits

..

12 Commits

16 changed files with 370 additions and 232 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
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/
+133 -73
View File
@@ -5,6 +5,7 @@ import {
LinearScale,
CategoryScale,
PointElement,
LineElement,
Tooltip,
Title,
} from "chart.js";
@@ -16,6 +17,7 @@ import {
Paper,
Popper,
ClickAwayListener,
Stack,
} from "@mui/material";
import {
availableUnderlyings,
@@ -38,13 +40,19 @@ import {
refreshStockPriceChartData,
} from "./HistoricalCalendarPrices/actions.js";
import { EditableUnderlying } from "./HistoricalCalendarPrices/EditableUnderlying.js";
import { EditableDaysToFrontExpiration } from "./HistoricalCalendarPrices/EditableDaysToFrontExpiration.js";
import { EditableExitToFrontExpiration } from "./HistoricalCalendarPrices/EditableExitToFrontExpiration.js";
import { EditableDaysBetweenFrontAndBackExpiration } from "./HistoricalCalendarPrices/EditableDaysBetweenFrontAndBackExpiration.js";
import { EditableLookbackPeriodStart } from "./HistoricalCalendarPrices/EditableLookbackPeriodStart.js";
import { EditableLookbackPeriodEnd } from "./HistoricalCalendarPrices/EditableLookbackPeriodEnd.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);
ChartJS.register(
LinearScale,
CategoryScale,
PointElement,
LineElement,
Tooltip,
Title
);
const handleInit = () => {
trpc.CalendarCharacteristicsForm.getAvailableUnderlyings
@@ -64,16 +72,16 @@ export function HistoricalCalendarPrices() {
return (
<Container maxWidth="lg">
<Grid2 container spacing={4} columns={12}>
<Grid2 size={{ xs: 12 }}>
{/* <Grid2 size={{ xs: 12 }}>
<Typography variant="h4" gutterBottom>
<EditableUnderlying /> :
<EditableDaysBetweenFrontAndBackExpiration />
<EditableSpan />
-Day Calendar @ <EditableStrike />
%-from-the-money
</Typography>
<Typography variant="h5" gutterBottom sx={{ pl: 1 }}>
Opening at <EditableDaysToFrontExpiration /> DTE, Closing at{" "}
<EditableExitToFrontExpiration />
Opening at <EditableOpenDTE /> DTE, Closing at <EditableExitDTE />
DTE
</Typography>
<Typography variant="h5" gutterBottom>
@@ -93,72 +101,54 @@ export function HistoricalCalendarPrices() {
</Paper>
</Popper>
</ClickAwayListener>
</Grid2>
</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,
},
],
<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: (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: 1,
borderWidth: 0,
},
},
plugins: {
tooltip: {
enabled: false,
},
legend: {
display: false,
},
title: {
display: true,
text: "Stock Price",
},
},
animation: false,
maintainAspectRatio: false,
events: [],
}}
/>
) : (
<Typography>Loading Chart...</Typography>
)}
>
<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 &&
@@ -223,7 +213,7 @@ export function HistoricalCalendarPrices() {
<Grid2 size={{ xs: 12, md: 6 }}>
<Paper elevation={3} sx={{ p: 3, minHeight: "28em" }}>
{underlying.value !== null &&
similarCalendarPriceChartData.value.length > 0 ? (
calendarExitPriceChartData.value.length > 0 ? (
<Scatter
data={{
datasets: [
@@ -294,6 +284,76 @@ export function HistoricalCalendarPrices() {
)}
</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>
);
@@ -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 { EditableValue } from "./EditableValue";
import {
strikePercentageFromUnderlyingPrice,
strikePercentageFromUnderlyingPriceRadius,
} from "./state";
import { moniness, moninessRadius } from "./state";
import Slider from "@mui/material/Slider";
import { refreshSimilarCalendarPriceChartData } from "./actions";
function StrikePercentageFromUnderlyingPriceChooser() {
function MoninessChooser() {
return (
<Slider
fullWidth
label="Strike % From Underlying Price"
value={strikePercentageFromUnderlyingPrice.value}
value={moniness.value}
valueLabelDisplay="on"
min={0}
max={10}
step={0.1}
step={1}
onChange={(e, value) => {
strikePercentageFromUnderlyingPrice.value = value as number;
moniness.value = value as number;
}}
onChangeCommitted={(e, value) => {
refreshSimilarCalendarPriceChartData();
@@ -28,18 +25,18 @@ function StrikePercentageFromUnderlyingPriceChooser() {
);
}
function StrikePercentageFromUnderlyingPriceRadiusChooser() {
function MoninessRadiusChooser() {
return (
<Slider
fullWidth
label="Strike % Radius"
value={strikePercentageFromUnderlyingPriceRadius.value}
value={moninessRadius.value}
valueLabelDisplay="on"
min={0}
max={0.5}
step={0.05}
max={10}
step={1}
onChange={(e, value) => {
strikePercentageFromUnderlyingPriceRadius.value = value as number;
moninessRadius.value = value as number;
}}
onChangeCommitted={(e, value) => {
refreshSimilarCalendarPriceChartData();
@@ -57,13 +54,11 @@ function StrikePercentageFromUnderlyingPriceRadiusChooser() {
export function EditableStrike() {
return (
<EditableValue
text={`${strikePercentageFromUnderlyingPrice.value.toFixed(
1
)}±${strikePercentageFromUnderlyingPriceRadius.value.toFixed(2)}`}
text={`${moniness.value.toFixed(1)}±${moninessRadius.value.toFixed(2)}`}
>
<Box sx={{ minWidth: "20em" }}>
<StrikePercentageFromUnderlyingPriceChooser />
<StrikePercentageFromUnderlyingPriceRadiusChooser />
<MoninessChooser />
<MoninessRadiusChooser />
</Box>
</EditableValue>
);
@@ -1,19 +1,41 @@
import { trpc } from "../../trpc";
import {
calendarExitPriceChartData,
daysBetweenFrontAndBackExpiration,
daysToFrontExpiration,
exitToFrontExpiration,
span,
openDTE,
exitDTE,
lookbackPeriodEnd,
lookbackPeriodStart,
similarCalendarPriceChartData,
stockPriceChartData,
strikePercentageFromUnderlyingPrice,
strikePercentageFromUnderlyingPriceRadius,
moniness,
moninessRadius,
underlying,
} 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 = [];
trpc.StockPriceChart.getChartData
.query({
@@ -24,40 +46,36 @@ export const refreshStockPriceChartData = () => {
.then((getChartDataResponse) => {
stockPriceChartData.value = getChartDataResponse;
});
};
export const refreshSimilarCalendarPriceChartData = () => {
}, 400);
export const refreshSimilarCalendarPriceChartData = throttle(() => {
similarCalendarPriceChartData.value = [];
trpc.SimilarCalendarPriceChart.getChartData
.query({
underlying: underlying.value,
daysToFrontExpiration: daysToFrontExpiration.value,
daysBetweenFrontAndBackExpiration:
daysBetweenFrontAndBackExpiration.value,
daysToFrontExpiration: openDTE.value,
daysBetweenFrontAndBackExpiration: span.value,
strikePercentageFromUnderlyingPriceRangeMin:
strikePercentageFromUnderlyingPrice.value -
strikePercentageFromUnderlyingPriceRadius.value,
(moniness.value - moninessRadius.value) / 100,
strikePercentageFromUnderlyingPriceRangeMax:
strikePercentageFromUnderlyingPrice.value +
strikePercentageFromUnderlyingPriceRadius.value,
(moniness.value + moninessRadius.value) / 100,
lookbackPeriodStart: lookbackPeriodStart.value,
lookbackPeriodEnd: lookbackPeriodEnd.value,
})
.then((getChartDataResponse) => {
similarCalendarPriceChartData.value = getChartDataResponse;
});
};
export const refreshcalendarExitPriceChartData = () => {
}, 400);
export const refreshcalendarExitPriceChartData = throttle(() => {
calendarExitPriceChartData.value = [];
trpc.CalendarExitPriceChart.getChartData
.query({
underlying: underlying.value,
daysToFrontExpiration: exitToFrontExpiration.value,
daysBetweenFrontAndBackExpiration:
daysBetweenFrontAndBackExpiration.value,
daysToFrontExpiration: exitDTE.value,
daysBetweenFrontAndBackExpiration: span.value,
lookbackPeriodStart: lookbackPeriodStart.value,
lookbackPeriodEnd: lookbackPeriodEnd.value,
})
.then((getChartDataResponse) => {
calendarExitPriceChartData.value = getChartDataResponse;
});
};
}, 400);
@@ -7,33 +7,33 @@ export const popperContent = signal(null);
export const availableUnderlyings = signal([]);
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 strikePercentageFromUnderlyingPriceRadius = signal(0.05);
export const moniness = signal(1);
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 calendarExitPriceChartData = signal([]);
export const lookbackPeriodStart = signal("2022-01-01");
export const lookbackPeriodEnd = signal("2024-01-01");
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)
similarCalendarPriceChartData.value.map((d) => d.y).slice(0, -2)
),
Math.max.apply(
null,
calendarExitPriceChartData.value.map((d) => d.y)
calendarExitPriceChartData.value.map((d) => d.y).slice(0, -2)
)
)
);
+1
View File
@@ -27,3 +27,4 @@ dist-ssr
.env
*.db
*.db-lck
Calendar tRPC
+1 -1
View File
@@ -31,7 +31,7 @@ export const getChartData = publicProcedure
return await query<[number, number, number]>(
`
SELECT
moniness as x,
moniness*100 as x,
FLOOR(price, 1) as y,
sum(number_of_quotes) as n
FROM calendar_stats
+2 -1
View File
@@ -26,7 +26,8 @@ export const getChartData = publicProcedure
GROUP BY x
ORDER BY x ASC
`,
"JSONEachRow"
"JSONCompactEachRow"
// "JSONEachRow"
);
});
+2 -2
View File
@@ -6,10 +6,10 @@ 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({
url: CLICKHOUSE_HOST,
url: CLICKHOUSE_URL,
username: CLICKHOUSE_USER,
password: CLICKHOUSE_PASS,
keep_alive: {