Add synthetic print and structure features
- Export synthetic market types - Track special print conditions and derived cluster features - Add same-size leg symmetry to structure packets
This commit is contained in:
parent
8dcbcd2201
commit
9076d3b395
4 changed files with 103 additions and 0 deletions
|
|
@ -2,3 +2,4 @@ export * from "./events";
|
|||
export * from "./live";
|
||||
export * from "./options-flow";
|
||||
export * from "./sp500";
|
||||
export * from "./synthetic-market";
|
||||
|
|
|
|||
|
|
@ -271,6 +271,14 @@ type ClusterState = {
|
|||
totalPremium: number;
|
||||
firstPrice: number;
|
||||
lastPrice: number;
|
||||
conditions: Set<string>;
|
||||
specialPrintCount: number;
|
||||
firstExecutionIv: number | null;
|
||||
lastExecutionIv: number | null;
|
||||
minExecutionIv: number | null;
|
||||
maxExecutionIv: number | null;
|
||||
firstUnderlyingMid: number | null;
|
||||
lastUnderlyingMid: number | null;
|
||||
placements: NbboPlacementCounts;
|
||||
flushed: boolean;
|
||||
};
|
||||
|
|
@ -329,6 +337,29 @@ const createPlacementCounts = (): NbboPlacementCounts => ({
|
|||
stale: 0
|
||||
});
|
||||
|
||||
const SPECIAL_PRINT_CONDITIONS = new Set(["AUCTION", "CROSS", "OPENING", "CLOSING", "COMPLEX", "SPREAD"]);
|
||||
const SYNTHETIC_EVENT_CONDITION_RE = /^EVENT_(\d+)D$/i;
|
||||
|
||||
const normalizeConditions = (conditions: readonly string[] | undefined): string[] =>
|
||||
(conditions ?? []).map((condition) => condition.trim().toUpperCase()).filter(Boolean);
|
||||
|
||||
const hasSpecialCondition = (conditions: readonly string[] | undefined): boolean =>
|
||||
normalizeConditions(conditions).some((condition) => SPECIAL_PRINT_CONDITIONS.has(condition));
|
||||
|
||||
const parseSyntheticEventOffsetDays = (conditions: Iterable<string>): number | null => {
|
||||
for (const condition of conditions) {
|
||||
const match = SYNTHETIC_EVENT_CONDITION_RE.exec(condition);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
const days = Number(match[1]);
|
||||
if (Number.isFinite(days) && days > 0) {
|
||||
return days;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const recordPlacement = (counts: NbboPlacementCounts, placement: NbboPlacement): void => {
|
||||
switch (placement) {
|
||||
case "AA":
|
||||
|
|
@ -569,6 +600,12 @@ const applyDeliverPolicy = (
|
|||
|
||||
const buildCluster = (print: OptionPrint): ClusterState => {
|
||||
const placements = createPlacementCounts();
|
||||
const normalizedConditions = normalizeConditions(print.conditions);
|
||||
const executionIv = typeof print.execution_iv === "number" && Number.isFinite(print.execution_iv) ? print.execution_iv : null;
|
||||
const executionUnderlyingMid =
|
||||
typeof print.execution_underlying_mid === "number" && Number.isFinite(print.execution_underlying_mid)
|
||||
? print.execution_underlying_mid
|
||||
: null;
|
||||
recordPlacement(placements, classifyPlacement(print.price, selectNbbo(print.option_contract_id, print.ts)));
|
||||
return {
|
||||
contractId: print.option_contract_id,
|
||||
|
|
@ -585,6 +622,14 @@ const buildCluster = (print: OptionPrint): ClusterState => {
|
|||
totalPremium: print.price * print.size,
|
||||
firstPrice: print.price,
|
||||
lastPrice: print.price,
|
||||
conditions: new Set(normalizedConditions),
|
||||
specialPrintCount: hasSpecialCondition(print.conditions) ? 1 : 0,
|
||||
firstExecutionIv: executionIv,
|
||||
lastExecutionIv: executionIv,
|
||||
minExecutionIv: executionIv,
|
||||
maxExecutionIv: executionIv,
|
||||
firstUnderlyingMid: executionUnderlyingMid,
|
||||
lastUnderlyingMid: executionUnderlyingMid,
|
||||
placements,
|
||||
flushed: false
|
||||
};
|
||||
|
|
@ -607,6 +652,25 @@ const updateCluster = (cluster: ClusterState, print: OptionPrint): ClusterState
|
|||
cluster.totalSize += print.size;
|
||||
cluster.totalPremium += print.price * print.size;
|
||||
cluster.lastPrice = print.price;
|
||||
for (const condition of normalizeConditions(print.conditions)) {
|
||||
cluster.conditions.add(condition);
|
||||
}
|
||||
if (hasSpecialCondition(print.conditions)) {
|
||||
cluster.specialPrintCount += 1;
|
||||
}
|
||||
if (typeof print.execution_iv === "number" && Number.isFinite(print.execution_iv)) {
|
||||
cluster.lastExecutionIv = print.execution_iv;
|
||||
cluster.minExecutionIv =
|
||||
cluster.minExecutionIv === null ? print.execution_iv : Math.min(cluster.minExecutionIv, print.execution_iv);
|
||||
cluster.maxExecutionIv =
|
||||
cluster.maxExecutionIv === null ? print.execution_iv : Math.max(cluster.maxExecutionIv, print.execution_iv);
|
||||
}
|
||||
if (typeof print.execution_underlying_mid === "number" && Number.isFinite(print.execution_underlying_mid)) {
|
||||
if (cluster.firstUnderlyingMid === null) {
|
||||
cluster.firstUnderlyingMid = print.execution_underlying_mid;
|
||||
}
|
||||
cluster.lastUnderlyingMid = print.execution_underlying_mid;
|
||||
}
|
||||
recordPlacement(
|
||||
cluster.placements,
|
||||
classifyPlacement(print.price, selectNbbo(print.option_contract_id, print.ts))
|
||||
|
|
@ -836,6 +900,27 @@ const flushCluster = async (
|
|||
if (cluster.isEtf !== null) {
|
||||
features.is_etf = cluster.isEtf;
|
||||
}
|
||||
if (cluster.conditions.size > 0) {
|
||||
features.conditions = Array.from(cluster.conditions).sort().join(",");
|
||||
}
|
||||
if (cluster.specialPrintCount > 0) {
|
||||
features.special_print_count = cluster.specialPrintCount;
|
||||
}
|
||||
if (cluster.minExecutionIv !== null && cluster.maxExecutionIv !== null) {
|
||||
features.execution_iv_shock = roundTo(Math.max(0, cluster.maxExecutionIv - cluster.minExecutionIv));
|
||||
}
|
||||
if (
|
||||
cluster.firstUnderlyingMid !== null &&
|
||||
cluster.lastUnderlyingMid !== null &&
|
||||
cluster.firstUnderlyingMid > 0
|
||||
) {
|
||||
const moveBps = ((cluster.lastUnderlyingMid - cluster.firstUnderlyingMid) / cluster.firstUnderlyingMid) * 10_000;
|
||||
features.underlying_move_bps = roundTo(moveBps);
|
||||
}
|
||||
const syntheticEventOffsetDays = parseSyntheticEventOffsetDays(cluster.conditions);
|
||||
if (syntheticEventOffsetDays !== null) {
|
||||
features.corporate_event_ts = cluster.endTs + syntheticEventOffsetDays * 86_400_000;
|
||||
}
|
||||
|
||||
const placementTotal =
|
||||
cluster.placements.aa +
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export type StructurePacketPlan = {
|
|||
nbboAggressiveBuyRatio: number;
|
||||
nbboAggressiveSellRatio: number;
|
||||
nbboAggressiveRatio: number;
|
||||
sameSizeLegSymmetry: number;
|
||||
source_ts: number;
|
||||
ingest_ts: number;
|
||||
seq: number;
|
||||
|
|
@ -132,6 +133,19 @@ const dayDiff = (from: string | null, to: string | null): number | null => {
|
|||
return Math.round(diffMs / 86_400_000);
|
||||
};
|
||||
|
||||
const sameSizeLegSymmetry = (legs: LegEvidence[]): number => {
|
||||
const sizes = legs.map((leg) => leg.totalSize).filter((value) => Number.isFinite(value) && value > 0);
|
||||
if (sizes.length < 2) {
|
||||
return 0;
|
||||
}
|
||||
const min = Math.min(...sizes);
|
||||
const max = Math.max(...sizes);
|
||||
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return min / max;
|
||||
};
|
||||
|
||||
export const shouldEmitStructurePacket = (legs: LegEvidence[], currentLegContractId: string): boolean => {
|
||||
if (legs.length < 2) {
|
||||
return false;
|
||||
|
|
@ -250,6 +264,7 @@ export const planStructurePacket = (
|
|||
nbboAggressiveBuyRatio,
|
||||
nbboAggressiveSellRatio,
|
||||
nbboAggressiveRatio,
|
||||
sameSizeLegSymmetry: roundTo(sameSizeLegSymmetry(legs)),
|
||||
source_ts: Number.isFinite(source_ts) ? source_ts : 0,
|
||||
ingest_ts,
|
||||
seq
|
||||
|
|
@ -320,6 +335,7 @@ export const buildStructureFlowPacket = (
|
|||
features.nbbo_aggressive_buy_ratio = roundTo(plan.nbboAggressiveBuyRatio);
|
||||
features.nbbo_aggressive_sell_ratio = roundTo(plan.nbboAggressiveSellRatio);
|
||||
features.nbbo_aggressive_ratio = roundTo(plan.nbboAggressiveRatio);
|
||||
features.same_size_leg_symmetry = roundTo(plan.sameSizeLegSymmetry);
|
||||
|
||||
const join_quality: Record<string, number> = {
|
||||
nbbo_coverage_ratio: roundTo(plan.nbboCoverageRatio)
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ describe("structure packet planning", () => {
|
|||
expect(packet.features.nbbo_bb_count).toBe(1);
|
||||
expect(packet.features.nbbo_mid_count).toBe(1);
|
||||
expect(packet.features.nbbo_coverage_ratio).toBeCloseTo(1, 6);
|
||||
expect(packet.features.same_size_leg_symmetry).toBeCloseTo(0.5, 4);
|
||||
|
||||
// 2 aggressive (AA + BB) out of 3 classified (AA + BB + MID)
|
||||
expect(packet.features.nbbo_aggressive_ratio).toBeCloseTo(2 / 3, 4);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue