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
+21
-7
@@ -15,8 +15,8 @@ export interface CmeSpec {
|
||||
/** Dollars per 1.0 of price, per contract. */
|
||||
pointValue: number;
|
||||
tickSize: number;
|
||||
/** Typical bid/ask spread in ticks. There are no live futures quotes, and
|
||||
* a liquid front month is one tick wide. */
|
||||
/** Typical bid/ask spread in ticks — a liquid front month is one tick
|
||||
* wide. Used when no live quote is at hand (lib/live-quotes.ts). */
|
||||
spreadTicks: number;
|
||||
/** Yahoo's continuous-contract ticker, for hourly candles. */
|
||||
yahoo: string;
|
||||
@@ -70,6 +70,15 @@ export function spreadDollars(root: string): number {
|
||||
return s ? s.spreadTicks * s.tickSize * s.pointValue : NaN;
|
||||
}
|
||||
|
||||
/** The most an entry may pay to cross the spread, per contract, when the
|
||||
* master's catalog doesn't say (its Settings page sets it). */
|
||||
export const DEFAULT_MAX_ENTRY_SPREAD = 15;
|
||||
|
||||
/** Whether a spread is over the entry limit. A limit of 0 or less is off. */
|
||||
export function spreadTooWide(spreadDollars: number, maxDollars: number): boolean {
|
||||
return maxDollars > 0 && Number.isFinite(spreadDollars) && spreadDollars > maxDollars + 1e-9;
|
||||
}
|
||||
|
||||
/** Hold cap: an instrument whose next leg is expected to take this long or
|
||||
* longer is out of the draw. */
|
||||
export const MAX_HOLD_HOURS = 12;
|
||||
@@ -151,6 +160,8 @@ export interface DrawCandidate {
|
||||
/** The next leg's expected hold on it; null = unknown. */
|
||||
holdHours: number | null;
|
||||
micro?: boolean;
|
||||
/** Its live spread is over the entry limit right now. */
|
||||
wide?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,14 +175,15 @@ export interface DrawCandidate {
|
||||
* and every drawable instrument gets a floor of DRAW_FLOOR × (1−q).
|
||||
*
|
||||
* Out of the draw: anything whose expected hold is MAX_HOLD_HOURS or more
|
||||
* (`disabled`), and micros unless nothing else is left. An unmeasured
|
||||
* instrument gets the median weight; if none are measured, the draw is even.
|
||||
* (`disabled`), anything whose live spread is over the entry limit (`wide`),
|
||||
* and micros unless nothing else is left. An unmeasured instrument gets the
|
||||
* median weight; if none are measured, the draw is even.
|
||||
*/
|
||||
export function drawChances(
|
||||
candidates: DrawCandidate[],
|
||||
p: number | null,
|
||||
opts: { floor?: number; sharpness?: number; maxHoldHours?: number } = {},
|
||||
): { chances: { symbol: string; chance: number }[]; disabled: string[] } {
|
||||
): { chances: { symbol: string; chance: number }[]; disabled: string[]; wide: string[] } {
|
||||
const floorBase = opts.floor ?? DRAW_FLOOR;
|
||||
const sharpness = opts.sharpness ?? LATE_SHARPNESS;
|
||||
const maxHold = opts.maxHoldHours ?? MAX_HOLD_HOURS;
|
||||
@@ -179,9 +191,10 @@ export function drawChances(
|
||||
const q = progress * progress;
|
||||
|
||||
const disabled = candidates.filter((c) => c.holdHours !== null && c.holdHours >= maxHold).map((c) => c.symbol);
|
||||
let pool = candidates.filter((c) => !disabled.includes(c.symbol));
|
||||
const wide = candidates.filter((c) => c.wide === true).map((c) => c.symbol);
|
||||
let pool = candidates.filter((c) => !disabled.includes(c.symbol) && !wide.includes(c.symbol));
|
||||
if (pool.some((c) => !c.micro)) pool = pool.filter((c) => !c.micro);
|
||||
if (pool.length === 0) return { chances: [], disabled };
|
||||
if (pool.length === 0) return { chances: [], disabled, wide };
|
||||
|
||||
const logWeight = (c: DrawCandidate): number | null => (c.dailyMove !== null && c.dailyMove > 0 && c.spreadDollars > 0
|
||||
? (1 - q) * Math.log(c.dailyMove / c.spreadDollars) + sharpness * q * Math.log(c.dailyMove)
|
||||
@@ -199,6 +212,7 @@ export function drawChances(
|
||||
return {
|
||||
chances: pool.map((c, i) => ({ symbol: c.symbol, chance: floor + (1 - n * floor) * (weights[i] / total) })),
|
||||
disabled,
|
||||
wide,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user