From ff9b107452d00cd6797235e996ecec340ffc6e3b Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Sat, 26 Sep 2026 19:51:56 -0500 Subject: [PATCH] =?UTF-8?q?Instruments:=20no=20fee=20lookup=20=E2=80=94=20?= =?UTF-8?q?commissions=20barely=20differ=20between=20instruments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entry cost is one tick of spread; fees can be added later if they matter. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- app/api/instruments/route.ts | 8 +++----- app/settings/page.tsx | 7 +++---- lib/auto-trade.ts | 3 +-- lib/instruments.ts | 39 +++--------------------------------- lib/master-protocol.ts | 2 -- lib/reporter.ts | 2 +- 7 files changed, 13 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 5ae87ec..0d292ef 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ the batch firm's list, at random but weighted: - **Efficiency** — one contract's typical daily move ÷ one tick of spread (from Yahoo's hourly candles over the past week, measured once a day). - Commissions don't count; they're shown, never used to choose. + Commissions don't count — they barely differ between instruments. - **Session timing** — at the open every instrument gets a 10% floor and the weights follow efficiency; toward the last entry (14:55 CT) the floor fades and the weight moves sharply to the fastest movers. @@ -126,7 +126,7 @@ the batch firm's list, at random but weighted: listed product (MNQ when NQ is listed). Every draw logs its odds. `GET /api/instruments` shows each firm's list with -entry cost, daily move, hold and chance (`?simulate=10000` runs the real draw +spread, daily move, hold and chance (`?simulate=10000` runs the real draw with no orders), and the master's Firms page shows the same next to each instrument. diff --git a/app/api/instruments/route.ts b/app/api/instruments/route.ts index 3f3caf7..7d17c05 100644 --- a/app/api/instruments/route.ts +++ b/app/api/instruments/route.ts @@ -1,5 +1,4 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getClients } from '@/lib/clients'; import { getCachedCatalog } from '@/lib/firm-catalog'; import { ensureMarket, firmInstruments, futuresRootsHere, instrumentStatsFor, instrumentsSource, isFuturesFirm, simulateDraws, @@ -13,13 +12,12 @@ const EXAMPLE_LEG = { leg: 1000, stopDollars: Infinity, maxPositionSize: 0 }; /** * GET /api/instruments — what each firm trades (the master's lists) and, for - * futures firms, how the Auto draw sees every instrument right now: entry - * cost, one contract's daily move, the hold of a $1,000 leg, the chance. + * futures firms, how the Auto draw sees every instrument right now: a tick + * of spread, one contract's daily move, the hold of a $1,000 leg, the chance. * ?simulate=N adds N draws from the real draw code — no orders. */ export async function GET(req: NextRequest) { await ensureMarket(CME_ROOTS, 8_000); - const clients = getClients(); const n = Math.min(100_000, Math.max(0, Math.floor(Number(req.nextUrl.searchParams.get('simulate')) || 0))); const firms = getCachedCatalog().map((firm) => { const instruments = firmInstruments(firm); @@ -30,7 +28,7 @@ export async function GET(req: NextRequest) { platform: firm.platform, source: instrumentsSource(firm), instruments, - ...(futures ? { stats: instrumentStatsFor(firm, clients.get(firm.id), { ...EXAMPLE_LEG, account: 'a $1,000 leg' }) } : {}), + ...(futures ? { stats: instrumentStatsFor(firm, { ...EXAMPLE_LEG, account: 'a $1,000 leg' }) } : {}), ...(futures && n > 0 ? { simulated: simulateDraws(instruments, EXAMPLE_LEG, n) } : {}), }; }); diff --git a/app/settings/page.tsx b/app/settings/page.tsx index e13654e..08d972c 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -20,7 +20,6 @@ interface InstrumentsOverview { symbol: string; listed: boolean; spreadDollars: number; - feesPerSide: number | null; dailyMoveDollars: number | null; holdHours: number | null; disabled: boolean; @@ -470,7 +469,7 @@ export default function SettingsPage() { Symbol Contract - Entry + Spread Daily move Hold Chance now @@ -487,8 +486,8 @@ export default function SettingsPage() { {c?.name ?? '—'} {c?.alternative && rolled from {c.alternative}} - - {x ? `$${x.spreadDollars.toFixed(2)}${x.feesPerSide !== null ? ` + $${x.feesPerSide.toFixed(2)}` : ''}` : '—'} + + {x ? `$${x.spreadDollars.toFixed(2)}` : '—'} {x?.dailyMoveDollars != null ? `$${x.dailyMoveDollars.toLocaleString()}` : '—'} diff --git a/lib/auto-trade.ts b/lib/auto-trade.ts index 9cd2865..b90a38e 100644 --- a/lib/auto-trade.ts +++ b/lib/auto-trade.ts @@ -22,7 +22,7 @@ import { } from './trading-logic'; import { getSetting, setSetting } from './db'; import { POINT_VALUES, housekeepingRoot } from './instrument-specs'; -import { firmInstruments, drawFuturesInstrument, recordMeasuredFee, type LegShape } from './instruments'; +import { firmInstruments, drawFuturesInstrument, type LegShape } from './instruments'; import { errorMessage, isClientReady, isOrderNotPlaced, type PlatformClient, type PlatformContract, type PickContext } from './platform-client'; // ── process-wide state ─────────────────────────────────────────────────────── @@ -1230,7 +1230,6 @@ async function fireLeg( await new Promise(r => setTimeout(r, 1000)); const updatedCash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 }; measured = entryCommissionMeasured(item.cash.realizedPnL, updatedCash.realizedPnL, contracts); - if (measured !== null && !item.drain) recordMeasuredFee(item.firm.id, symbolLabel, measured / contracts); entryFee = measured ?? FALLBACK_COMMISSION_PER_CONTRACT * contracts; } // The stop. On a measured-fee firm whose fee didn't show, the daily-loss diff --git a/lib/instruments.ts b/lib/instruments.ts index 9df6ca6..a9cbe78 100644 --- a/lib/instruments.ts +++ b/lib/instruments.ts @@ -10,8 +10,8 @@ * near the last entry, nothing expected to hold 12 hours or more. * * Movement comes from Yahoo's hourly candles (7 days, measured once per trade - * date); the spread is one tick. Fees are shown, never used to choose: - * Tradovate's fee parameters, or TopstepX's last measured fill. + * date); the spread is one tick. Commissions play no part — they barely + * differ between instruments. */ import axios from 'axios'; @@ -76,8 +76,6 @@ const CONCURRENCY = 4; const g = globalThis as typeof globalThis & { __futuresMarket?: Map; __futuresMarketInflight?: Map>; - __futuresFees?: Map; at: number }>; - __futuresMeasuredFees?: Map; }; const market = (g.__futuresMarket ??= new Map()); const inflight = (g.__futuresMarketInflight ??= new Map()); @@ -143,33 +141,6 @@ export function marketOf(root: string): { price: number | null; dailyMove: numbe return m ? { price: m.price, dailyMove: m.dailyMove, error: m.error } : null; } -// ── fees (shown, never used to choose) ───────────────────────────────────── - -const feeCache = (g.__futuresFees ??= new Map()); -const measuredFees = (g.__futuresMeasuredFees ??= new Map()); - -/** A fill's commission per contract per side, for platforms with no fee - * lookup (TopstepX). */ -export function recordMeasuredFee(firmId: string, root: string, perContract: number): void { - if (Number.isFinite(perContract) && perContract >= 0) measuredFees.set(`${firmId}|${root}`, Math.round(perContract * 100) / 100); -} - -/** Tradovate's fee parameters, fetched once per trade date per firm. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function refreshFees(firm: { id: string; platform: string }, client: any): void { - if (firm.platform !== 'tradovate' || typeof client?.fetchInstrumentFees !== 'function') return; - const cached = feeCache.get(firm.id); - if (cached && cached.tradeDate === currentTradeDateCT() && (Object.keys(cached.fees).length > 0 || Date.now() - cached.at < RETRY_MS)) return; - feeCache.set(firm.id, { tradeDate: currentTradeDateCT(), fees: cached?.fees ?? {}, at: Date.now() }); - void Promise.resolve(client.fetchInstrumentFees([...CME_ROOTS])) - .then((fees: Record) => feeCache.set(firm.id, { tradeDate: currentTradeDateCT(), fees: fees ?? {}, at: Date.now() })) - .catch((err: unknown) => console.warn(`[instruments] fee lookup failed: ${errorMessage(err)}`)); -} - -export function feesPerSide(firmId: string, root: string): number | null { - return feeCache.get(firmId)?.fees[root] ?? measuredFees.get(`${firmId}|${root}`) ?? null; -} - // ── the draw ──────────────────────────────────────────────────────────────── /** The batch's leg, for the hold estimate: its dollars, its stop (Infinity = @@ -240,17 +211,14 @@ export function simulateDraws(roots: readonly string[], leg: LegShape | null, n: /** * A futures firm's instruments as the draw sees them right now: every CME - * root with its entry cost, movement and contract, and — sized for `lead`'s + * root with its spread, movement and contract, and — sized for `lead`'s * next leg — the listed ones' holds, the ones over the cap, and each one's * chance. */ export function instrumentStatsFor( firm: FirmLike & { id: string }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - client: any, lead: (LegShape & { account: string }) | null, ): InstrumentStats { - refreshFees({ id: firm.id, platform: firm.platform }, client); const listed = firmInstruments(firm); const board = futuresBoard(listed, lead); const disabled = new Set(board.disabled); @@ -269,7 +237,6 @@ export function instrumentStatsFor( listed: listed.includes(root), contract: getCachedContract(root)?.name ?? null, spreadDollars: spreadDollars(root), - feesPerSide: feesPerSide(firm.id, root), dailyMoveDollars: dailyMove === null ? null : Math.round(dailyMove), holdHours: holdHours === null ? null : Math.round(holdHours * 10) / 10, disabled: listed.includes(root) ? disabled.has(root) : holdHours !== null && holdHours >= MAX_HOLD_HOURS, diff --git a/lib/master-protocol.ts b/lib/master-protocol.ts index 552fa4d..dc935f7 100644 --- a/lib/master-protocol.ts +++ b/lib/master-protocol.ts @@ -138,8 +138,6 @@ export interface InstrumentStats { contract: string | null; /** One tick of spread on one contract, in dollars. */ spreadDollars: number; - /** The firm's fees per contract per side; null when unknown. */ - feesPerSide: number | null; /** One contract's typical daily move (1σ), in dollars. */ dailyMoveDollars: number | null; holdHours: number | null; diff --git a/lib/reporter.ts b/lib/reporter.ts index 1ef970e..1215d0f 100644 --- a/lib/reporter.ts +++ b/lib/reporter.ts @@ -427,7 +427,7 @@ function instrumentStatsBlock( const shape = accountLegShape(client, acc, tradeDate); if (shape) { lead = { ...shape, account: acc.name }; break; } } - return { instrumentStats: instrumentStatsFor(f, client, lead) }; + return { instrumentStats: instrumentStatsFor(f, lead) }; } /**