Live futures spreads off one client, and the master's max entry spread

One quote feed per machine: a TopstepX client's market hub, else a
Tradovate client's market-data socket, subscribed to every listed root's
front month and the drain micro. A supervisor keeps it on a working client,
dropping one that fails or goes quiet while the market trades.

The Auto draw weighs each instrument by its live spread (the typical tick
when no fresh quote is at hand). No futures entry pays more than the
master's max entry spread ($15 until set) to cross the book: the draw
leaves such an instrument out, an explicit symbol sits the tick out, and
fireLeg and the extra-day trade check it last before sending. The Settings
page and the master's stats show the live spreads and the feed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-27 15:06:44 -05:00
co-authored by Claude Opus 5.5
parent 1782c16145
commit db1d12327a
13 changed files with 747 additions and 33 deletions
+79
View File
@@ -0,0 +1,79 @@
/**
* Pure helpers for the live futures quote feed (lib/live-quotes.ts): reading
* what TopstepX's market hub and Tradovate's market-data socket send, and
* turning the top of the book into the spread an entry pays.
*
* No imports, so scripts/check-instruments.mjs can test them directly.
*/
export interface BookTop {
bid?: number;
ask?: number;
}
const price = (v: unknown): number | undefined => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined);
/**
* A TopstepX GatewayQuote laid over the last one. The hub sends only the
* fields that changed, so a side it leaves out keeps its last price.
*/
export function mergeTopstepxQuote(prev: BookTop | undefined, raw: unknown): BookTop {
const r = raw as Record<string, unknown> | null;
const data = r && typeof r === 'object' && 'data' in r && typeof r.data === 'object' && r.data !== null
? (r.data as Record<string, unknown>)
: r;
const bid = price(data?.bestBid);
const ask = price(data?.bestAsk);
return { ...prev, ...(bid !== undefined ? { bid } : {}), ...(ask !== undefined ? { ask } : {}) };
}
export interface TradovateFrame {
/** Replies to requests: request id, HTTP-like status, payload. */
responses: { id: number; status: number; data?: unknown }[];
/** Quote updates by contract id; a side left out didn't change. */
quotes: { contractId: number; bid?: number; ask?: number }[];
}
/** One frame off Tradovate's market-data socket ('a[...]'; 'o', 'h' and 'c'
* frames carry nothing to read). */
export function parseTradovateFrame(frame: unknown): TradovateFrame {
const out: TradovateFrame = { responses: [], quotes: [] };
if (typeof frame !== 'string' || frame[0] !== 'a') return out;
let items: unknown;
try { items = JSON.parse(frame.slice(1)); } catch { return out; }
if (!Array.isArray(items)) return out;
for (const item of items) {
if (!item || typeof item !== 'object') continue;
const it = item as { e?: unknown; d?: unknown; i?: unknown; s?: unknown };
if (typeof it.i === 'number' && typeof it.s === 'number') {
out.responses.push({ id: it.i, status: it.s, data: it.d });
continue;
}
if (it.e !== 'md') continue;
const quotes = (it.d as { quotes?: unknown } | undefined)?.quotes;
if (!Array.isArray(quotes)) continue;
for (const q of quotes) {
const quote = q as { contractId?: unknown; entries?: Record<string, { price?: unknown } | undefined> } | null;
if (typeof quote?.contractId !== 'number') continue;
const bid = price(quote.entries?.Bid?.price);
const ask = price(quote.entries?.Offer?.price);
out.quotes.push({ contractId: quote.contractId, ...(bid !== undefined ? { bid } : {}), ...(ask !== undefined ? { ask } : {}) });
}
}
return out;
}
/**
* What crossing the book costs on one contract: the width between bid and
* ask, in dollars and in ticks. null for a book missing a side or crossed
* (ask below bid) — not a spread anyone can enter on.
*/
export function bookSpread(book: BookTop | undefined, pointValue: number, tickSize: number): { dollars: number; ticks: number } | null {
if (!book || book.bid === undefined || book.ask === undefined || book.ask < book.bid) return null;
if (!(pointValue > 0)) return null;
const width = book.ask - book.bid;
return {
dollars: Math.round(width * pointValue * 100) / 100,
ticks: tickSize > 0 ? Math.round(width / tickSize) : NaN,
};
}