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.

200 lines
6.6 KiB
TypeScript

import { optionContractDatabase } from "../optiondb/lmdbx.js";
import type { CalendarDatabase } from "./interfaces.js";
/** Largest possible key according to the `ordered-binary` (used by lmdbx) docs. */
const MAXIMUM_KEY = Buffer.from([0xff]);
function makeCalendarDatabase(): CalendarDatabase {
const getAggregatesSync = ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
date,
}) => {
const frontOptionContractAggregates =
optionContractDatabase.getAggregatesSync({
date,
key: { symbol, expirationDate: frontExpirationDate, strike, type },
});
const backOptionContractAggregates =
optionContractDatabase.getAggregatesSync({
date,
key: { symbol, expirationDate: backExpirationDate, strike, type },
});
const calendarAggregates = [];
let i = 0;
let j = 0;
while (
i < frontOptionContractAggregates.length &&
j < backOptionContractAggregates.length
) {
if (
frontOptionContractAggregates[i].tsStart ===
backOptionContractAggregates[j].tsStart
) {
calendarAggregates.push({
tsStart: frontOptionContractAggregates[i].tsStart,
open:
backOptionContractAggregates[j].open -
frontOptionContractAggregates[i].open,
close:
backOptionContractAggregates[j].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 at the same moment as the other:
high:
backOptionContractAggregates[j].high -
frontOptionContractAggregates[i].high,
low:
backOptionContractAggregates[j].low -
frontOptionContractAggregates[i].low,
});
i++;
j++;
} else if (
frontOptionContractAggregates[i].tsStart >
backOptionContractAggregates[j].tsStart
) {
j++;
} else {
i++;
}
}
return calendarAggregates;
};
const calendarDatabase: Omit<CalendarDatabase, "getCalendars"> = {
getKeys: async ({ key: { symbol }, date }) => {
const optionContracts = await optionContractDatabase.getOptionContracts({
date,
key: { symbol },
});
return optionContracts.flatMap(
(frontOptionContract, i, optionContracts) =>
optionContracts
.filter((_, j) => i !== j)
.map((backOptionContract) => ({
symbol,
frontExpirationDate: frontOptionContract.expirationDate,
backExpirationDate: backOptionContract.expirationDate,
strike: frontOptionContract.strike,
type: frontOptionContract.type,
})),
);
},
getAggregates: async ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
date,
}) =>
getAggregatesSync({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
date,
}),
getAggregatesSync,
insertAggregates: async (aggregates) => {
// right now, no-op
},
getClosingPrice: async ({
key: { symbol, strike, type, frontExpirationDate, backExpirationDate },
}) => {
// get unix timestamp, in milliseconds, of the start of the last hour, which is 03:30PM in the `America/New_York` timezone on the front expiration date:
const startOfLastHourUnix = new Date(
`${frontExpirationDate}T19:30:00Z`
).getTime();
const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000;
const frontOptionContractAggregates = (
await optionContractDatabase.getAggregates({
date: frontExpirationDate,
key: { symbol, expirationDate: frontExpirationDate, strike, type },
})
).filter(
({ tsStart }) =>
tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix
);
const backOptionContractAggregates = (
await optionContractDatabase.getAggregates({
date: frontExpirationDate,
key: { symbol, expirationDate: backExpirationDate, strike, type },
})
).filter(
({ tsStart }) =>
tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix
);
let i = 0;
let j = 0;
let minPrice = 0;
while (
i < frontOptionContractAggregates.length &&
j < backOptionContractAggregates.length
) {
if (
frontOptionContractAggregates[i].tsStart ===
backOptionContractAggregates[j].tsStart
) {
const calendarClosePrice =
backOptionContractAggregates[j].close -
frontOptionContractAggregates[i].close;
if (calendarClosePrice < minPrice || minPrice === 0) {
minPrice = calendarClosePrice;
}
i++;
j++;
} else if (
frontOptionContractAggregates[i].tsStart >
backOptionContractAggregates[j].tsStart
) {
j++;
} else {
i++;
}
}
return minPrice;
},
getAggregate: async ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
tsStart,
}) => {
const [frontOptionContractAggregate, backOptionContractAggregate] =
await Promise.all([
optionContractDatabase.getAggregate({
key: { symbol, expirationDate: frontExpirationDate, strike, type },
tsStart,
}),
optionContractDatabase.getAggregate({
key: { symbol, expirationDate: backExpirationDate, strike, type },
tsStart,
}),
]);
// only return the calendar aggregate if its constituent front and back option contract aggregates exist:
if (frontOptionContractAggregate && backOptionContractAggregate) {
return {
tsStart,
open:
backOptionContractAggregate.open -
frontOptionContractAggregate.open,
close:
backOptionContractAggregate.close -
frontOptionContractAggregate.close,
high:
backOptionContractAggregate.high -
frontOptionContractAggregate.high,
low:
backOptionContractAggregate.low - frontOptionContractAggregate.low,
};
}
return undefined;
},
getTargetPriceByProbability: async ({
symbol,
calendarSpan,
strikePercentageFromTheMoney,
historicalProbabilityOfSuccess,
}) => {
return 0.24;
},
};
return {
...calendarDatabase,
getCalendars: calendarDatabase.getKeys,
};
}
export const calendarDatabase: CalendarDatabase = makeCalendarDatabase();