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:
co-authored by
Claude Opus 5.5
parent
1782c16145
commit
db1d12327a
@@ -10,8 +10,9 @@ import assert from 'node:assert/strict';
|
||||
import {
|
||||
CME_SPECS, CME_ROOTS, POINT_VALUES, MICRO_OF, tickValue, spreadDollars, dailyMoveDollars, futuresContracts,
|
||||
expectedHoldHours, sessionProgress, drawChances, pickByChance, housekeepingRoot, sanitizeInstruments,
|
||||
resolveAllowlist, MAX_HOLD_HOURS, DRAW_FLOOR,
|
||||
resolveAllowlist, MAX_HOLD_HOURS, DRAW_FLOOR, DEFAULT_MAX_ENTRY_SPREAD, spreadTooWide,
|
||||
} from '../lib/instrument-specs.ts';
|
||||
import { mergeTopstepxQuote, parseTradovateFrame, bookSpread } from '../lib/quote-helpers.ts';
|
||||
|
||||
// Deterministic PRNG so a failure reproduces.
|
||||
function rng(seed) {
|
||||
@@ -159,3 +160,75 @@ test('reading lists: sanitize per family; master list first, the old rules other
|
||||
{ roots: ['ETH/USD', 'XYZ100/USD'], source: 'legacy' },
|
||||
);
|
||||
});
|
||||
|
||||
// ── live quotes and the entry spread limit ──────────────────────────────────
|
||||
|
||||
test('TopstepX quotes: a side the hub leaves out keeps its last price', () => {
|
||||
let book = mergeTopstepxQuote(undefined, { symbol: 'F.US.ENQ', bestBid: 24000.25, bestAsk: 24000.5, lastPrice: 24000.5 });
|
||||
assert.deepEqual(book, { bid: 24000.25, ask: 24000.5 });
|
||||
book = mergeTopstepxQuote(book, { bestAsk: 24001 });
|
||||
assert.deepEqual(book, { bid: 24000.25, ask: 24001 });
|
||||
// Wrapped in { data }, as the user hub's events are; junk prices ignored.
|
||||
assert.deepEqual(mergeTopstepxQuote(book, { data: { bestBid: 24000.75, bestAsk: 0 } }), { bid: 24000.75, ask: 24001 });
|
||||
assert.deepEqual(mergeTopstepxQuote(undefined, null), {});
|
||||
});
|
||||
|
||||
test('Tradovate market-data frames: replies and quotes', () => {
|
||||
const frame = 'a' + JSON.stringify([
|
||||
{ s: 200, i: 1 },
|
||||
{ s: 404, i: 3, d: 'Symbol not found' },
|
||||
{ e: 'md', d: { quotes: [
|
||||
{ contractId: 3267315, timestamp: '2026-09-27T22:10:00Z', entries: { Bid: { price: 24000.25, size: 3 }, Offer: { price: 24000.5, size: 5 }, Trade: { price: 24000.5 } } },
|
||||
{ contractId: 3273057, entries: { Offer: { price: 71.24, size: 2 } } },
|
||||
{ entries: { Bid: { price: 1 } } },
|
||||
] } },
|
||||
]);
|
||||
const parsed = parseTradovateFrame(frame);
|
||||
assert.deepEqual(parsed.responses, [{ id: 1, status: 200, data: undefined }, { id: 3, status: 404, data: 'Symbol not found' }]);
|
||||
assert.deepEqual(parsed.quotes, [
|
||||
{ contractId: 3267315, bid: 24000.25, ask: 24000.5 },
|
||||
{ contractId: 3273057, ask: 71.24 },
|
||||
]);
|
||||
for (const other of ['o', 'h', 'c[1000,"bye"]', 'a[not json', 42]) assert.deepEqual(parseTradovateFrame(other), { responses: [], quotes: [] });
|
||||
});
|
||||
|
||||
test('the spread off the book, in dollars and ticks', () => {
|
||||
const nq = CME_SPECS.NQ;
|
||||
assert.deepEqual(bookSpread({ bid: 24000.25, ask: 24000.5 }, nq.pointValue, nq.tickSize), { dollars: 5, ticks: 1 });
|
||||
assert.deepEqual(bookSpread({ bid: 24000, ask: 24001 }, nq.pointValue, nq.tickSize), { dollars: 20, ticks: 4 });
|
||||
const gc = CME_SPECS.GC;
|
||||
assert.deepEqual(bookSpread({ bid: 3650.1, ask: 3650.3 }, gc.pointValue, gc.tickSize), { dollars: 20, ticks: 2 });
|
||||
// A locked book is zero wide; a crossed or one-sided one isn't a spread.
|
||||
assert.deepEqual(bookSpread({ bid: 71.2, ask: 71.2 }, 1000, 0.01), { dollars: 0, ticks: 0 });
|
||||
assert.equal(bookSpread({ bid: 71.3, ask: 71.2 }, 1000, 0.01), null);
|
||||
assert.equal(bookSpread({ bid: 71.2 }, 1000, 0.01), null);
|
||||
assert.equal(bookSpread(undefined, 1000, 0.01), null);
|
||||
});
|
||||
|
||||
test('the entry limit: $15 by default; one tick of the listed roots fits under it', () => {
|
||||
assert.equal(DEFAULT_MAX_ENTRY_SPREAD, 15);
|
||||
for (const root of ['NQ', 'GC', 'CL', 'ES', 'MNQ']) assert.equal(spreadTooWide(spreadDollars(root), 15), false, root);
|
||||
assert.equal(spreadTooWide(15, 15), false);
|
||||
assert.equal(spreadTooWide(15.01, 15), true);
|
||||
assert.equal(spreadTooWide(20, 15), true);
|
||||
// 0 = no limit; a NaN spread never blocks.
|
||||
assert.equal(spreadTooWide(500, 0), false);
|
||||
assert.equal(spreadTooWide(NaN, 15), false);
|
||||
// One tick of these is over $15 by itself.
|
||||
for (const root of ['SI', 'ZB', 'ZN']) assert.equal(spreadTooWide(spreadDollars(root), 15), true, root);
|
||||
});
|
||||
|
||||
test('the draw leaves out what is over the spread limit', () => {
|
||||
const cands = [
|
||||
{ symbol: 'NQ', spreadDollars: 20, dailyMove: 5164, holdHours: 0.9, wide: true },
|
||||
{ symbol: 'GC', spreadDollars: 10, dailyMove: 4899, holdHours: 1.0 },
|
||||
{ symbol: 'CL', spreadDollars: 10, dailyMove: 3554, holdHours: 1.9 },
|
||||
];
|
||||
const { chances, wide, disabled } = drawChances(cands, 0);
|
||||
assert.deepEqual(wide, ['NQ']);
|
||||
assert.deepEqual(disabled, []);
|
||||
assert.deepEqual(chances.map((c) => c.symbol), ['GC', 'CL']);
|
||||
assert.ok(Math.abs(chances.reduce((s, c) => s + c.chance, 0) - 1) < 1e-9);
|
||||
// All of them wide: nothing to draw.
|
||||
assert.deepEqual(drawChances(cands.map((c) => ({ ...c, wide: true })), 0.5).chances, []);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user