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.
522 lines
18 KiB
TypeScript
522 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 './style.css';
|
|
|
|
ChartJS.register(LinearScale, CategoryScale, PointElement, Tooltip, Title);
|
|
|
|
const availableUnderlyings = signal([]);
|
|
const chosenUnderlying = signal(null);
|
|
|
|
const chosenDaysToFrontExpiration = signal(14);
|
|
|
|
const chosenDaysBetweenFrontAndBackExpiration = signal(14);
|
|
|
|
const chosenStrikePercentageFromUnderlyingPrice = signal(1.4);
|
|
const chosenStrikePercentageFromUnderlyingPriceRadius = signal(0.05);
|
|
|
|
const chosenExitToFrontExpiration = signal(2);
|
|
|
|
const historicalStockQuoteChartData = signal([]);
|
|
|
|
const historicalCalendarQuoteChartData = signal([]);
|
|
|
|
const historicalCalendarExitQuoteChartData = signal([]);
|
|
|
|
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 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"
|
|
>
|
|
{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,
|
|
},
|
|
],
|
|
}}
|
|
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">
|
|
<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: [],
|
|
}}
|
|
/>
|
|
</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">
|
|
<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: [],
|
|
}}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="h-full">Loading Chart...</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|