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
|
- **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.
|
||||||
|
|
||||||
|
|||||||
@@ -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) } : {}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user