You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

518 lines
18 KiB
TypeScript

import { signal, computed } from "@preact/signals";
import { useEffect } from "preact/hooks";
import { trpc } from "../trpc.js";
import {
Chart as ChartJS,
LinearScale,
CategoryScale,
PointElement,
Tooltip,
Title,
} from "chart.js";
import { Scatter } from "react-chartjs-2";
import {
Container,
Grid,
Typography,
TextField,
Select,
MenuItem,
InputLabel,
FormControl,
Paper,
} from "@mui/material";
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)
)
)
);
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) => {
availableUnderlyings.value = availableUnderlyingsResponse;
chosenUnderlying.value = availableUnderlyingsResponse[0];
refreshHistoricalStockQuoteChartData();
refreshHistoricalCalendarQuoteChartData();
refreshHistoricalCalendarExitQuoteChartData();
});
};
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 maxWidth="lg">
<Grid container spacing={4}>
<Grid item xs={12}>
<Typography variant="h4" gutterBottom>
Historical Calendar Prices
</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}
label="Available Underlyings"
>
{availableUnderlyings.value.map((underlying) => (
<MenuItem key={underlying} value={underlying}>
{underlying}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Now-to-Front-Month Days to Expiration"
type="number"
value={chosenDaysToFrontExpiration.value}
onChange={handleDaysToFrontExpirationChange}
InputProps={{ endAdornment: "Days" }}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Front-to-Back-Month Days to Expiration Difference"
type="number"
value={chosenDaysBetweenFrontAndBackExpiration.value}
onChange={handleDaysBetweenFrontAndBackExpirationChange}
InputProps={{ endAdornment: "Days Difference" }}
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label="Strike % From Underlying Price"
type="number"
value={chosenStrikePercentageFromUnderlyingPrice.value}
onChange={handleStrikePercentageFromUnderlyingPriceChange}
InputProps={{ endAdornment: "%" }}
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label="Strike % Radius"
type="number"
value={chosenStrikePercentageFromUnderlyingPriceRadius.value}
onChange={handleStrikePercentageFromUnderlyingPriceRadiusChange}
InputProps={{ endAdornment: "%" }}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Exit-to-Front-Month Days to Expiration"
type="number"
value={chosenExitToFrontExpiration.value}
onChange={handleExitToFrontExpirationChange}
InputProps={{ endAdornment: "Days" }}
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label="Lookback Period Start"
type="date"
value={chosenLookbackPeriodStart.value}
onChange={(e) => handleLookbackPeriodStartChange({ target: { value: e.target.value } })}
InputLabelProps={{ shrink: true }}
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label="Lookback Period End"
type="date"
value={chosenLookbackPeriodEnd.value}
onChange={(e) => handleLookbackPeriodEndChange({ target: { value: e.target.value } })}
InputLabelProps={{ shrink: true }}
/>
</Grid>
</Grid>
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper elevation={3} sx={{ p: 3, height: '100%' }}>
{chosenUnderlying.value !== null &&
historicalStockQuoteChartData.value.length > 0 ? (
<Scatter
data={{
datasets: [
{
label: "Stock Open Price",
data: historicalStockQuoteChartData.value,
},
],
}}
options={{
scales: {
x: {
title: {
display: true,
text: "Time",
},
ticks: {
callback: function (value, index, ticks) {
return new Date((value as number) * 1000)
.toISOString()
.substring(0, 10);
},
},
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: [],
}}
/>
) : (
<Typography>Loading Chart...</Typography>
)}
</Paper>
</Grid>
<Grid item xs={12}>
<Paper elevation={3} sx={{ p: 3 }}>
{chosenUnderlying.value !== null &&
historicalCalendarQuoteChartData.value.length > 0 ? (
<Scatter
data={{
datasets: [
{
label: "Calendar Open Price",
data: historicalCalendarQuoteChartData.value,
},
],
}}
options={{
scales: {
x: {
title: {
display: true,
text: "Time",
},
ticks: {
callback: function (value, index, ticks) {
return new Date((value as number) * 1000)
.toISOString()
.substring(0, 10);
},
},
min:
new Date(chosenLookbackPeriodStart.value).getTime() /
1000,
max:
new Date(chosenLookbackPeriodEnd.value).getTime() /
1000,
},
y: {
beginAtZero: true,
ticks: {
callback: function (value, index, ticks) {
return "$" + value.toString();
},
},
min: 0,
max: maxChartPrice.value,
},
},
plugins: {
tooltip: {
enabled: false,
},
legend: {
display: false,
},
title: {
display: true,
text: "Calendar Price (Under Like Conditions)",
},
},
animation: false,
maintainAspectRatio: false,
events: [],
}}
/>
) : (
<Typography>Loading Chart...</Typography>
)}
</Paper>
</Grid>
<Grid item xs={12}>
<Paper elevation={3} sx={{ p: 3 }}>
{chosenUnderlying.value !== null &&
historicalCalendarQuoteChartData.value.length > 0 ? (
<Scatter
data={{
datasets: [
{
label: "Calendar Exit Price",
data: historicalCalendarExitQuoteChartData.value,
},
],
}}
options={{
scales: {
x: {
type: "linear",
beginAtZero: false,
title: {
display: true,
text: "%-From-the-Money",
},
ticks: {
callback: function (value, index, ticks) {
return value.toString() + "%";
},
},
},
y: {
beginAtZero: true,
ticks: {
callback: function (value, index, ticks) {
return "$" + value.toString();
},
},
min: 0,
max: maxChartPrice.value,
},
},
elements: {
point: {
borderWidth: 0,
backgroundColor: function (context) {
const n = (
context.raw as { x: number; y: number; n: number }
).n;
const alpha = n / maxN.value;
return `rgba(0, 0, 0, ${alpha})`;
},
},
},
plugins: {
tooltip: {
enabled: false,
},
legend: {
display: false,
},
title: {
display: true,
text: [
"Calendar Prices at Exit",
"by %-age from-the-money",
],
},
},
animation: false,
maintainAspectRatio: false,
events: [],
}}
/>
) : (
<Typography>Loading Chart...</Typography>
)}
</Paper>
</Grid>
</Grid>
</Container>
);
}