Compare commits

...

2 Commits

Author SHA1 Message Date
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
5 changed files with 629 additions and 621 deletions
+11 -10
View File
@@ -86,7 +86,7 @@ function chooseStrike(strike: string) {
.query({ .query({
underlying: chosenUnderlying.value, underlying: chosenUnderlying.value,
expirationDate: chosenExpiration.value, expirationDate: chosenExpiration.value,
strike: parseFloat(strike), strike: Number.parseFloat(strike),
}) })
.then((getOpensForOptionContractResponse) => { .then((getOpensForOptionContractResponse) => {
optionContractUplotData.value = getOpensForOptionContractResponse; optionContractUplotData.value = getOpensForOptionContractResponse;
@@ -201,8 +201,9 @@ export function CalendarOptimizer() {
</Paper> </Paper>
</Grid> </Grid>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<Paper elevation={3} sx={{ p: 3, height: '100%' }}> <Paper elevation={3} sx={{ p: 3, height: "100%" }}>
{chosenUnderlying.value !== null && underlyingUplotData.value.length > 0 ? ( {chosenUnderlying.value !== null &&
underlyingUplotData.value.length > 0 ? (
<Scatter <Scatter
data={{ data={{
datasets: [ datasets: [
@@ -220,7 +221,7 @@ export function CalendarOptimizer() {
text: "Time", text: "Time",
}, },
ticks: { ticks: {
callback: function (value, index, ticks) { callback: (value, index, ticks) => {
return new Date((value as number) * 1000) return new Date((value as number) * 1000)
.toISOString() .toISOString()
.substring(0, 10); .substring(0, 10);
@@ -230,8 +231,8 @@ export function CalendarOptimizer() {
y: { y: {
beginAtZero: false, beginAtZero: false,
ticks: { ticks: {
callback: function (value, index, ticks) { callback: (value, index, ticks) => {
return "$" + value.toString(); return `$${value.toString()}`;
}, },
}, },
}, },
@@ -287,7 +288,7 @@ export function CalendarOptimizer() {
text: "Time", text: "Time",
}, },
ticks: { ticks: {
callback: function (value, index, ticks) { callback: (value, index, ticks) => {
return new Date((value as number) * 1000) return new Date((value as number) * 1000)
.toISOString() .toISOString()
.substring(0, 10); .substring(0, 10);
@@ -297,8 +298,8 @@ export function CalendarOptimizer() {
y: { y: {
beginAtZero: false, beginAtZero: false,
ticks: { ticks: {
callback: function (value, index, ticks) { callback: (value, index, ticks) => {
return "$" + value.toString(); return `$${value.toString()}`;
}, },
}, },
}, },
@@ -333,4 +334,4 @@ export function CalendarOptimizer() {
</Grid> </Grid>
</Container> </Container>
); );
} }
+339 -332
View File
@@ -49,20 +49,20 @@ const maxChartPrice = computed(() =>
Math.max( Math.max(
Math.max.apply( Math.max.apply(
null, null,
historicalCalendarQuoteChartData.value.map((d) => d.y) historicalCalendarQuoteChartData.value.map((d) => d.y),
), ),
Math.max.apply( Math.max.apply(
null, null,
historicalCalendarExitQuoteChartData.value.map((d) => d.y) historicalCalendarExitQuoteChartData.value.map((d) => d.y),
) ),
) ),
); );
const maxN = computed(() => const maxN = computed(() =>
Math.max.apply( Math.max.apply(
null, null,
historicalCalendarExitQuoteChartData.value.map((d) => d.n) historicalCalendarExitQuoteChartData.value.map((d) => d.n),
) ),
); );
const refreshHistoricalStockQuoteChartData = () => { const refreshHistoricalStockQuoteChartData = () => {
@@ -134,16 +134,19 @@ const handleUnderlyingChange = (e) => {
} }
}; };
const handleDaysToFrontExpirationChange = (e) => { const handleDaysToFrontExpirationChange = (e) => {
if (chosenDaysToFrontExpiration.value !== parseInt(e.target.value)) { if (chosenDaysToFrontExpiration.value !== Number.parseInt(e.target.value)) {
chosenDaysToFrontExpiration.value = parseInt(e.target.value); chosenDaysToFrontExpiration.value = Number.parseInt(e.target.value);
refreshHistoricalCalendarQuoteChartData(); refreshHistoricalCalendarQuoteChartData();
} }
}; };
const handleDaysBetweenFrontAndBackExpirationChange = (e) => { const handleDaysBetweenFrontAndBackExpirationChange = (e) => {
if ( if (
chosenDaysBetweenFrontAndBackExpiration.value !== parseInt(e.target.value) chosenDaysBetweenFrontAndBackExpiration.value !==
Number.parseInt(e.target.value)
) { ) {
chosenDaysBetweenFrontAndBackExpiration.value = parseInt(e.target.value); chosenDaysBetweenFrontAndBackExpiration.value = Number.parseInt(
e.target.value,
);
refreshHistoricalCalendarQuoteChartData(); refreshHistoricalCalendarQuoteChartData();
refreshHistoricalCalendarExitQuoteChartData(); refreshHistoricalCalendarExitQuoteChartData();
} }
@@ -151,10 +154,10 @@ const handleDaysBetweenFrontAndBackExpirationChange = (e) => {
const handleStrikePercentageFromUnderlyingPriceChange = (e) => { const handleStrikePercentageFromUnderlyingPriceChange = (e) => {
if ( if (
chosenStrikePercentageFromUnderlyingPrice.value !== chosenStrikePercentageFromUnderlyingPrice.value !==
parseFloat(e.target.value) Number.parseFloat(e.target.value)
) { ) {
chosenStrikePercentageFromUnderlyingPrice.value = parseFloat( chosenStrikePercentageFromUnderlyingPrice.value = Number.parseFloat(
e.target.value e.target.value,
); );
refreshHistoricalCalendarQuoteChartData(); refreshHistoricalCalendarQuoteChartData();
} }
@@ -162,17 +165,17 @@ const handleStrikePercentageFromUnderlyingPriceChange = (e) => {
const handleStrikePercentageFromUnderlyingPriceRadiusChange = (e) => { const handleStrikePercentageFromUnderlyingPriceRadiusChange = (e) => {
if ( if (
chosenStrikePercentageFromUnderlyingPriceRadius.value !== chosenStrikePercentageFromUnderlyingPriceRadius.value !==
parseFloat(e.target.value) Number.parseFloat(e.target.value)
) { ) {
chosenStrikePercentageFromUnderlyingPriceRadius.value = parseFloat( chosenStrikePercentageFromUnderlyingPriceRadius.value = Number.parseFloat(
e.target.value e.target.value,
); );
refreshHistoricalCalendarQuoteChartData(); refreshHistoricalCalendarQuoteChartData();
} }
}; };
const handleExitToFrontExpirationChange = (e) => { const handleExitToFrontExpirationChange = (e) => {
if (chosenExitToFrontExpiration.value !== parseInt(e.target.value)) { if (chosenExitToFrontExpiration.value !== Number.parseInt(e.target.value)) {
chosenExitToFrontExpiration.value = parseInt(e.target.value); chosenExitToFrontExpiration.value = Number.parseInt(e.target.value);
refreshHistoricalCalendarExitQuoteChartData(); refreshHistoricalCalendarExitQuoteChartData();
} }
}; };
@@ -198,320 +201,324 @@ export function HistoricalCalendarPrices() {
useEffect(handleInit, []); useEffect(handleInit, []);
return ( return (
<Container maxWidth="lg"> <Container maxWidth="lg">
<Grid container spacing={4}> <Grid container spacing={4}>
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="h4" gutterBottom> <Typography variant="h4" gutterBottom>
Historical Calendar Prices Historical Calendar Prices
</Typography> </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> </Grid>
</Container> <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: (value, index, ticks) =>
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: (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>
)}
</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: (value, index, ticks) =>
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: (value, index, ticks) =>
`$${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: (value, index, ticks) =>
`${value.toString()}%`,
},
},
y: {
beginAtZero: true,
ticks: {
callback: (value, index, ticks) =>
`$${value.toString()}`,
},
min: 0,
max: maxChartPrice.value,
},
},
elements: {
point: {
borderWidth: 0,
backgroundColor: (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>
); );
} }
+134 -134
View File
@@ -5,143 +5,143 @@ import type { Aggregate } from "./interfaces.js";
import { nextDate } from "./lib/util.js"; import { nextDate } from "./lib/util.js";
type BacktestInput = { type BacktestInput = {
symbol: string; symbol: string;
startDate: string; startDate: string;
endDate: 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. */ /** 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; historicalProbabilityOfSuccess?: number;
initialAvailableValue?: number; initialAvailableValue?: number;
}; };
export async function backtest({ export async function backtest({
symbol, symbol,
startDate, startDate,
endDate, endDate,
historicalProbabilityOfSuccess = 0.8, historicalProbabilityOfSuccess = 0.8,
initialAvailableValue: initialBuyingPower = 2000, initialAvailableValue: initialBuyingPower = 2000,
}: BacktestInput) { }: BacktestInput) {
let buyingPower = initialBuyingPower; let buyingPower = initialBuyingPower;
const portfolio = new Set<CalendarKey>(); const portfolio = new Set<CalendarKey>();
// for each day: // for each day:
for ( for (
let date = startDate, didBuyCalendar = false; let date = startDate, didBuyCalendar = false;
date <= endDate; date <= endDate;
date = nextDate(date), didBuyCalendar = false date = nextDate(date), didBuyCalendar = false
) { ) {
console.log("Current Date:", date); console.log("Current Date:", date);
const calendars = await calendarDatabase.getCalendars({ const calendars = await calendarDatabase.getCalendars({
key: { symbol }, key: { symbol },
date, date,
}); });
const stockAggregates = await stockDatabase.getAggregates({ const stockAggregates = await stockDatabase.getAggregates({
key: symbol, key: symbol,
date, date,
}); });
const calendarsAggregates = new Map< const calendarsAggregates = new Map<
CalendarKey, CalendarKey,
Array<Pick<Aggregate<CalendarKey>, "tsStart" | "open" | "close">> Array<Pick<Aggregate<CalendarKey>, "tsStart" | "open" | "close">>
>(); >();
for (const calendar of calendars) { for (const calendar of calendars) {
calendarsAggregates.set( calendarsAggregates.set(
calendar, calendar,
await calendarDatabase.getAggregates({ await calendarDatabase.getAggregates({
key: { key: {
...calendar, ...calendar,
}, },
date, date,
}), })
); );
} }
// for each minute of that day for which we have a stock candlestick: // for each minute of that day for which we have a stock candlestick:
for (const stockAggregate of stockAggregates) { for (const stockAggregate of stockAggregates) {
// console.log("Current Time:", new Date(stockAggregate.tsStart)); // console.log("Current Time:", new Date(stockAggregate.tsStart));
// filter-out calendars that are far-from-the-money (10%) // filter-out calendars that are far-from-the-money (10%)
const calendarsNearTheMoney = calendars.filter( const calendarsNearTheMoney = calendars.filter(
({ strike }) => ({ strike }) =>
Math.abs((stockAggregate.open - strike) / stockAggregate.open) < 0.1, Math.abs((stockAggregate.open - strike) / stockAggregate.open) < 0.1
); );
// for each relevant calendar on that day: // for each relevant calendar on that day:
for (const calendar of calendarsNearTheMoney) { for (const calendar of calendarsNearTheMoney) {
const strikePercentageFromTheMoney = Math.abs( const strikePercentageFromTheMoney = Math.abs(
(stockAggregate.open - calendar.strike) / stockAggregate.open, (stockAggregate.open - calendar.strike) / stockAggregate.open
); );
/** In days. */ /** In days. */
const calendarSpan = const calendarSpan =
(new Date(calendar.backExpirationDate).valueOf() - (new Date(calendar.backExpirationDate).valueOf() -
new Date(calendar.frontExpirationDate).valueOf()) / new Date(calendar.frontExpirationDate).valueOf()) /
(1000 * 60 * 60 * 24); (1000 * 60 * 60 * 24);
const targetCalendarPrice = const targetCalendarPrice =
await calendarDatabase.getTargetPriceByProbability({ await calendarDatabase.getTargetPriceByProbability({
symbol, symbol,
calendarSpan, calendarSpan,
strikePercentageFromTheMoney, strikePercentageFromTheMoney,
historicalProbabilityOfSuccess, historicalProbabilityOfSuccess,
}); });
const calendarAggregates = calendarsAggregates.get(calendar); const calendarAggregates = calendarsAggregates.get(calendar);
const calendarAggregateAtCurrentTime = calendarAggregates.find( const calendarAggregateAtCurrentTime = calendarAggregates.find(
({ tsStart }) => tsStart === stockAggregate.tsStart, ({ tsStart }) => tsStart === stockAggregate.tsStart
); );
// if there exists a matching calendar candlestick for the current minute: // if there exists a matching calendar candlestick for the current minute:
if (calendarAggregateAtCurrentTime) { if (calendarAggregateAtCurrentTime) {
// if the current candlestick is a good price (i.e. less than the target price): // if the current candlestick is a good price (i.e. less than the target price):
const minCalendarPriceInCandlestick = Math.min( const minCalendarPriceInCandlestick = Math.min(
calendarAggregateAtCurrentTime.open, calendarAggregateAtCurrentTime.open,
calendarAggregateAtCurrentTime.close, calendarAggregateAtCurrentTime.close
); );
if ( if (
minCalendarPriceInCandlestick < targetCalendarPrice && minCalendarPriceInCandlestick < targetCalendarPrice &&
minCalendarPriceInCandlestick > minCalendarPriceInCandlestick >
0.07 /* sometimes the calendar price is zero or negative, which is of course impossible; some institution got a good deal */ 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 we can afford to buy the calendar:
if (buyingPower > minCalendarPriceInCandlestick) { if (buyingPower > minCalendarPriceInCandlestick) {
// buy the calendar, and continue to the next day: // buy the calendar, and continue to the next day:
portfolio.add(calendar); portfolio.add(calendar);
buyingPower = buyingPower - minCalendarPriceInCandlestick * 100; buyingPower = buyingPower - minCalendarPriceInCandlestick * 100;
console.log( console.log(
"Bought", "Bought",
calendar, calendar,
"for", "for",
minCalendarPriceInCandlestick * 100, minCalendarPriceInCandlestick * 100,
"...$", "...$",
buyingPower, buyingPower,
"left", "left"
); );
didBuyCalendar = true; didBuyCalendar = true;
} }
} }
} }
if (didBuyCalendar) { if (didBuyCalendar) {
break; break;
} }
} }
if (didBuyCalendar) { if (didBuyCalendar) {
break; break;
} }
} }
// for each calendar in portfolio, if today is the last day, close the position: // for each calendar in portfolio, if today is the last day, close the position:
for (const calendar of portfolio.values()) { for (const calendar of portfolio.values()) {
if (calendar.frontExpirationDate === date) { if (calendar.frontExpirationDate === date) {
const calendarClosingPrice = await calendarDatabase.getClosingPrice({ const calendarClosingPrice = await calendarDatabase.getClosingPrice({
key: { key: {
...calendar, ...calendar,
}, },
}); });
portfolio.delete(calendar); portfolio.delete(calendar);
buyingPower = buyingPower + calendarClosingPrice * 100; buyingPower = buyingPower + calendarClosingPrice * 100;
console.log( console.log(
"Sold", "Sold",
calendar, calendar,
"for", "for",
calendarClosingPrice, calendarClosingPrice,
"...$", "...$",
buyingPower, buyingPower,
"left", "left"
); );
} }
} }
} }
console.log("Ending Buying Power:", buyingPower); console.log("Ending Buying Power:", buyingPower);
console.log("Portfolio:", portfolio.values()); console.log("Portfolio:", portfolio.values());
} }
+144 -144
View File
@@ -5,151 +5,151 @@ import type { CalendarDatabase } from "./calendardb.interfaces.js";
const MAXIMUM_KEY = Buffer.from([0xff]); const MAXIMUM_KEY = Buffer.from([0xff]);
function makeCalendarDatabase(): CalendarDatabase { function makeCalendarDatabase(): CalendarDatabase {
const calendarDatabase: Omit<CalendarDatabase, "getCalendars"> = { const calendarDatabase: Omit<CalendarDatabase, "getCalendars"> = {
getKeys: async ({ key: { symbol }, date }) => { getKeys: async ({ key: { symbol }, date }) => {
const optionContracts = await optionContractDatabase.getOptionContracts({ const optionContracts = await optionContractDatabase.getOptionContracts({
date, date,
key: { symbol }, key: { symbol },
}); });
return optionContracts.flatMap( return optionContracts.flatMap(
(frontOptionContract, i, optionContracts) => (frontOptionContract, i, optionContracts) =>
optionContracts optionContracts
.filter((_, j) => i !== j) .filter((_, j) => i !== j)
.map((backOptionContract) => ({ .map((backOptionContract) => ({
symbol, symbol,
frontExpirationDate: frontOptionContract.expirationDate, frontExpirationDate: frontOptionContract.expirationDate,
backExpirationDate: backOptionContract.expirationDate, backExpirationDate: backOptionContract.expirationDate,
strike: frontOptionContract.strike, strike: frontOptionContract.strike,
type: frontOptionContract.type, type: frontOptionContract.type,
})), })),
); );
}, },
getAggregates: async ({ getAggregates: async ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type }, key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
date, date,
}) => { }) => {
const frontOptionContractAggregates = const frontOptionContractAggregates =
await optionContractDatabase.getAggregates({ await optionContractDatabase.getAggregates({
date, date,
key: { symbol, expirationDate: frontExpirationDate, strike, type }, key: { symbol, expirationDate: frontExpirationDate, strike, type },
}); });
const backOptionContractAggregates = const backOptionContractAggregates =
await optionContractDatabase.getAggregates({ await optionContractDatabase.getAggregates({
date, date,
key: { symbol, expirationDate: backExpirationDate, strike, type }, key: { symbol, expirationDate: backExpirationDate, strike, type },
}); });
const calendarAggregates = []; const calendarAggregates = [];
let i = 0; let i = 0;
let j = 0; let j = 0;
while ( while (
i < frontOptionContractAggregates.length && i < frontOptionContractAggregates.length &&
j < backOptionContractAggregates.length j < backOptionContractAggregates.length
) { ) {
if ( if (
frontOptionContractAggregates[i].tsStart === frontOptionContractAggregates[i].tsStart ===
backOptionContractAggregates[j].tsStart backOptionContractAggregates[j].tsStart
) { ) {
calendarAggregates.push({ calendarAggregates.push({
tsStart: frontOptionContractAggregates[i].tsStart, tsStart: frontOptionContractAggregates[i].tsStart,
open: open:
backOptionContractAggregates[j].open - backOptionContractAggregates[j].open -
frontOptionContractAggregates[i].open, frontOptionContractAggregates[i].open,
close: close:
backOptionContractAggregates[j].close - backOptionContractAggregates[j].close -
frontOptionContractAggregates[i].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 ata the same moment as the other: // 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: high:
backOptionContractAggregates[j].high - backOptionContractAggregates[j].high -
frontOptionContractAggregates[i].high, frontOptionContractAggregates[i].high,
low: low:
backOptionContractAggregates[j].low - backOptionContractAggregates[j].low -
frontOptionContractAggregates[i].low, frontOptionContractAggregates[i].low,
}); });
i++; i++;
j++; j++;
} else if ( } else if (
frontOptionContractAggregates[i].tsStart > frontOptionContractAggregates[i].tsStart >
backOptionContractAggregates[j].tsStart backOptionContractAggregates[j].tsStart
) { ) {
j++; j++;
} else { } else {
i++; i++;
} }
} }
return calendarAggregates; return calendarAggregates;
}, },
insertAggregates: async (aggregates) => { insertAggregates: async (aggregates) => {
// right now, no-op // right now, no-op
}, },
getClosingPrice: async ({ getClosingPrice: async ({
key: { symbol, strike, type, frontExpirationDate, backExpirationDate }, key: { symbol, strike, type, frontExpirationDate, backExpirationDate },
}) => { }) => {
const startOfLastHourUnix = new Date( const startOfLastHourUnix = new Date(
`${frontExpirationDate}T00:00:00Z`, `${frontExpirationDate}T00:00:00Z`,
).valueOf(); ).valueOf();
const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000; const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000;
const frontOptionContractAggregates = ( const frontOptionContractAggregates = (
await optionContractDatabase.getAggregates({ await optionContractDatabase.getAggregates({
date: frontExpirationDate, date: frontExpirationDate,
key: { symbol, expirationDate: frontExpirationDate, strike, type }, key: { symbol, expirationDate: frontExpirationDate, strike, type },
}) })
).filter( ).filter(
({ tsStart }) => ({ tsStart }) =>
tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix, tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix,
); );
const backOptionContractAggregates = ( const backOptionContractAggregates = (
await optionContractDatabase.getAggregates({ await optionContractDatabase.getAggregates({
date: frontExpirationDate, date: frontExpirationDate,
key: { symbol, expirationDate: backExpirationDate, strike, type }, key: { symbol, expirationDate: backExpirationDate, strike, type },
}) })
).filter( ).filter(
({ tsStart }) => ({ tsStart }) =>
tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix, tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix,
); );
let i = 0; let i = 0;
let j = 0; let j = 0;
let minPrice = 0; let minPrice = 0;
while ( while (
i < frontOptionContractAggregates.length && i < frontOptionContractAggregates.length &&
j < backOptionContractAggregates.length j < backOptionContractAggregates.length
) { ) {
if ( if (
frontOptionContractAggregates[i].tsStart === frontOptionContractAggregates[i].tsStart ===
backOptionContractAggregates[j].tsStart backOptionContractAggregates[j].tsStart
) { ) {
const calendarClosePrice = const calendarClosePrice =
backOptionContractAggregates[j].close - backOptionContractAggregates[j].close -
frontOptionContractAggregates[j].close; frontOptionContractAggregates[j].close;
if (calendarClosePrice < minPrice || minPrice === 0) { if (calendarClosePrice < minPrice || minPrice === 0) {
minPrice = calendarClosePrice; minPrice = calendarClosePrice;
} }
i++; i++;
j++; j++;
} else if ( } else if (
frontOptionContractAggregates[i].tsStart > frontOptionContractAggregates[i].tsStart >
backOptionContractAggregates[j].tsStart backOptionContractAggregates[j].tsStart
) { ) {
j++; j++;
} else { } else {
i++; i++;
} }
} }
return minPrice; return minPrice;
}, },
getTargetPriceByProbability: async ({ getTargetPriceByProbability: async ({
symbol, symbol,
calendarSpan, calendarSpan,
strikePercentageFromTheMoney, strikePercentageFromTheMoney,
historicalProbabilityOfSuccess, historicalProbabilityOfSuccess,
}) => { }) => {
return 0.24; return 0.24;
}, },
}; };
return { return {
...calendarDatabase, ...calendarDatabase,
getCalendars: calendarDatabase.getKeys, getCalendars: calendarDatabase.getKeys,
}; };
} }
export const calendarDatabase: CalendarDatabase = makeCalendarDatabase(); export const calendarDatabase: CalendarDatabase = makeCalendarDatabase();
+1 -1
View File
@@ -17,7 +17,7 @@ async function syncAggregates<T>({
date: string; date: string;
}) { }) {
const aggregatesFrom = (await fromDatabase.getAggregates({ key, date })).map( const aggregatesFrom = (await fromDatabase.getAggregates({ key, date })).map(
(aggregateWithoutKey) => ({ ...aggregateWithoutKey, key }), (aggregateWithoutKey) => ({ ...aggregateWithoutKey, key })
); );
await toDatabase.insertAggregates(aggregatesFrom); await toDatabase.insertAggregates(aggregatesFrom);
} }