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:
co-authored by
Claude Opus 5.5
parent
ee4550512c
commit
ff9b107452
@@ -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.
|
||||
|
||||
|
||||
@@ -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) } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
<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">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">Hold</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?.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 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">
|
||||
{x ? `$${x.spreadDollars.toFixed(2)}${x.feesPerSide !== null ? ` + $${x.feesPerSide.toFixed(2)}` : ''}` : '—'}
|
||||
<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)}` : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right tabular-nums text-slate-600">
|
||||
{x?.dailyMoveDollars != null ? `$${x.dailyMoveDollars.toLocaleString()}` : '—'}
|
||||
|
||||
+1
-2
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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) };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user