Instruments: no fee lookup — commissions barely differ between instruments

Entry cost is one tick of spread; fees can be added later if they matter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-26 19:51:56 -05:00
co-authored by Claude Opus 5.5
parent ee4550512c
commit ff9b107452
7 changed files with 13 additions and 52 deletions
+2 -2
View File
@@ -114,7 +114,7 @@ the batch firm's list, at random but weighted:
- **Efficiency** — one contract's typical daily move ÷ one tick of spread - **Efficiency** — one contract's typical daily move ÷ one tick of spread
(from Yahoo's hourly candles over the past week, measured once a day). (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 - **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 weights follow efficiency; toward the last entry (14:55 CT) the floor fades
and the weight moves sharply to the fastest movers. 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). listed product (MNQ when NQ is listed).
Every draw logs its odds. `GET /api/instruments` shows each firm's list with 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 with no orders), and the master's Firms page shows the same next to each
instrument. instrument.
+3 -5
View File
@@ -1,5 +1,4 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { getClients } from '@/lib/clients';
import { getCachedCatalog } from '@/lib/firm-catalog'; import { getCachedCatalog } from '@/lib/firm-catalog';
import { import {
ensureMarket, firmInstruments, futuresRootsHere, instrumentStatsFor, instrumentsSource, isFuturesFirm, simulateDraws, 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 * 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 * futures firms, how the Auto draw sees every instrument right now: a tick
* cost, one contract's daily move, the hold of a $1,000 leg, the chance. * 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. * ?simulate=N adds N draws from the real draw code — no orders.
*/ */
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
await ensureMarket(CME_ROOTS, 8_000); 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 n = Math.min(100_000, Math.max(0, Math.floor(Number(req.nextUrl.searchParams.get('simulate')) || 0)));
const firms = getCachedCatalog().map((firm) => { const firms = getCachedCatalog().map((firm) => {
const instruments = firmInstruments(firm); const instruments = firmInstruments(firm);
@@ -30,7 +28,7 @@ export async function GET(req: NextRequest) {
platform: firm.platform, platform: firm.platform,
source: instrumentsSource(firm), source: instrumentsSource(firm),
instruments, 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) } : {}), ...(futures && n > 0 ? { simulated: simulateDraws(instruments, EXAMPLE_LEG, n) } : {}),
}; };
}); });
+3 -4
View File
@@ -20,7 +20,6 @@ interface InstrumentsOverview {
symbol: string; symbol: string;
listed: boolean; listed: boolean;
spreadDollars: number; spreadDollars: number;
feesPerSide: number | null;
dailyMoveDollars: number | null; dailyMoveDollars: number | null;
holdHours: number | null; holdHours: number | null;
disabled: boolean; disabled: boolean;
@@ -470,7 +469,7 @@ export default function SettingsPage() {
<tr className="border-b border-slate-100 text-xs uppercase tracking-wider text-slate-400"> <tr className="border-b border-slate-100 text-xs uppercase tracking-wider text-slate-400">
<th className="px-4 py-2 text-left font-semibold">Symbol</th> <th className="px-4 py-2 text-left font-semibold">Symbol</th>
<th className="px-4 py-2 text-left font-semibold">Contract</th> <th className="px-4 py-2 text-left font-semibold">Contract</th>
<th className="px-4 py-2 text-right font-semibold">Entry</th> <th className="px-4 py-2 text-right font-semibold">Spread</th>
<th className="px-4 py-2 text-right font-semibold">Daily move</th> <th className="px-4 py-2 text-right font-semibold">Daily move</th>
<th className="px-4 py-2 text-right font-semibold">Hold</th> <th className="px-4 py-2 text-right font-semibold">Hold</th>
<th className="px-4 py-2 text-right font-semibold">Chance now</th> <th className="px-4 py-2 text-right font-semibold">Chance now</th>
@@ -487,8 +486,8 @@ export default function SettingsPage() {
{c?.name ?? '—'} {c?.name ?? '—'}
{c?.alternative && <span className="ml-2 text-xs bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded font-sans">rolled from {c.alternative}</span>} {c?.alternative && <span className="ml-2 text-xs bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded font-sans">rolled from {c.alternative}</span>}
</td> </td>
<td className="px-4 py-2 text-right tabular-nums text-slate-600" title="Per contract: one tick of spread, plus the firm's fees per side when known"> <td className="px-4 py-2 text-right tabular-nums text-slate-600" title="One tick of spread on one contract — what entering costs, commissions aside">
{x ? `$${x.spreadDollars.toFixed(2)}${x.feesPerSide !== null ? ` + $${x.feesPerSide.toFixed(2)}` : ''}` : '—'} {x ? `$${x.spreadDollars.toFixed(2)}` : '—'}
</td> </td>
<td className="px-4 py-2 text-right tabular-nums text-slate-600"> <td className="px-4 py-2 text-right tabular-nums text-slate-600">
{x?.dailyMoveDollars != null ? `$${x.dailyMoveDollars.toLocaleString()}` : '—'} {x?.dailyMoveDollars != null ? `$${x.dailyMoveDollars.toLocaleString()}` : '—'}
+1 -2
View File
@@ -22,7 +22,7 @@ import {
} from './trading-logic'; } from './trading-logic';
import { getSetting, setSetting } from './db'; import { getSetting, setSetting } from './db';
import { POINT_VALUES, housekeepingRoot } from './instrument-specs'; 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'; import { errorMessage, isClientReady, isOrderNotPlaced, type PlatformClient, type PlatformContract, type PickContext } from './platform-client';
// ── process-wide state ─────────────────────────────────────────────────────── // ── process-wide state ───────────────────────────────────────────────────────
@@ -1230,7 +1230,6 @@ async function fireLeg(
await new Promise(r => setTimeout(r, 1000)); await new Promise(r => setTimeout(r, 1000));
const updatedCash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 }; const updatedCash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 };
measured = entryCommissionMeasured(item.cash.realizedPnL, updatedCash.realizedPnL, contracts); 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; entryFee = measured ?? FALLBACK_COMMISSION_PER_CONTRACT * contracts;
} }
// The stop. On a measured-fee firm whose fee didn't show, the daily-loss // The stop. On a measured-fee firm whose fee didn't show, the daily-loss
+3 -36
View File
@@ -10,8 +10,8 @@
* near the last entry, nothing expected to hold 12 hours or more. * 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 * 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: * date); the spread is one tick. Commissions play no part — they barely
* Tradovate's fee parameters, or TopstepX's last measured fill. * differ between instruments.
*/ */
import axios from 'axios'; import axios from 'axios';
@@ -76,8 +76,6 @@ const CONCURRENCY = 4;
const g = globalThis as typeof globalThis & { const g = globalThis as typeof globalThis & {
__futuresMarket?: Map<string, Market>; __futuresMarket?: Map<string, Market>;
__futuresMarketInflight?: Map<string, Promise<void>>; __futuresMarketInflight?: Map<string, Promise<void>>;
__futuresFees?: Map<string, { tradeDate: string; fees: Record<string, number>; at: number }>;
__futuresMeasuredFees?: Map<string, number>;
}; };
const market = (g.__futuresMarket ??= new Map()); const market = (g.__futuresMarket ??= new Map());
const inflight = (g.__futuresMarketInflight ??= 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; 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<string, number>) => 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 draw ────────────────────────────────────────────────────────────────
/** The batch's leg, for the hold estimate: its dollars, its stop (Infinity = /** 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 * 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 * next leg — the listed ones' holds, the ones over the cap, and each one's
* chance. * chance.
*/ */
export function instrumentStatsFor( export function instrumentStatsFor(
firm: FirmLike & { id: string }, firm: FirmLike & { id: string },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
client: any,
lead: (LegShape & { account: string }) | null, lead: (LegShape & { account: string }) | null,
): InstrumentStats { ): InstrumentStats {
refreshFees({ id: firm.id, platform: firm.platform }, client);
const listed = firmInstruments(firm); const listed = firmInstruments(firm);
const board = futuresBoard(listed, lead); const board = futuresBoard(listed, lead);
const disabled = new Set(board.disabled); const disabled = new Set(board.disabled);
@@ -269,7 +237,6 @@ export function instrumentStatsFor(
listed: listed.includes(root), listed: listed.includes(root),
contract: getCachedContract(root)?.name ?? null, contract: getCachedContract(root)?.name ?? null,
spreadDollars: spreadDollars(root), spreadDollars: spreadDollars(root),
feesPerSide: feesPerSide(firm.id, root),
dailyMoveDollars: dailyMove === null ? null : Math.round(dailyMove), dailyMoveDollars: dailyMove === null ? null : Math.round(dailyMove),
holdHours: holdHours === null ? null : Math.round(holdHours * 10) / 10, holdHours: holdHours === null ? null : Math.round(holdHours * 10) / 10,
disabled: listed.includes(root) ? disabled.has(root) : holdHours !== null && holdHours >= MAX_HOLD_HOURS, disabled: listed.includes(root) ? disabled.has(root) : holdHours !== null && holdHours >= MAX_HOLD_HOURS,
-2
View File
@@ -138,8 +138,6 @@ export interface InstrumentStats {
contract: string | null; contract: string | null;
/** One tick of spread on one contract, in dollars. */ /** One tick of spread on one contract, in dollars. */
spreadDollars: number; 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. */ /** One contract's typical daily move (1σ), in dollars. */
dailyMoveDollars: number | null; dailyMoveDollars: number | null;
holdHours: number | null; holdHours: number | null;
+1 -1
View File
@@ -427,7 +427,7 @@ function instrumentStatsBlock(
const shape = accountLegShape(client, acc, tradeDate); const shape = accountLegShape(client, acc, tradeDate);
if (shape) { lead = { ...shape, account: acc.name }; break; } if (shape) { lead = { ...shape, account: acc.name }; break; }
} }
return { instrumentStats: instrumentStatsFor(f, client, lead) }; return { instrumentStats: instrumentStatsFor(f, lead) };
} }
/** /**