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
+1 -2
View File
@@ -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
+3 -36
View File
@@ -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<string, Market>;
__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 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<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 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,
-2
View File
@@ -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;
+1 -1
View File
@@ -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) };
}
/**