/** * Topstep's own commission on TopstepX — the part of a fill's cost the * gateway never reports. * * Trade/search and the user hub carry `fees` on every half-turn, but only the * exchange and NFA fees (NQ $1.39 a side, GC $1.66, CL $1.51, MNQ $0.36). * Topstep's commission — $0.50 a side on a full-size contract, $0.25 on a * micro — comes off the balance with no trade carrying it. Topstep's * "TopstepX Commissions and Fees" page totals both: NQ and ES $3.78 a round * turn, MNQ and MES $1.22, GC $4.32, CL $4.02. Left out, the books ran $1.00 * a full-size round turn above the balance (2026-10-18: an XFA $27.50 off * after 26 NQ/GC round turns and 3 MNQ, each combine $1.00 per round turn). * * No imports, so scripts/check-instruments.mjs can test it directly. */ /** Per contract and side, by CME root. */ export const TOPSTEP_COMMISSION_PER_SIDE: Readonly> = { NQ: 0.5, MNQ: 0.25, ES: 0.5, MES: 0.25, YM: 0.5, MYM: 0.25, RTY: 0.5, M2K: 0.25, GC: 0.5, MGC: 0.25, SI: 0.5, CL: 0.5, MCL: 0.25, NG: 0.5, ZB: 0.5, ZN: 0.5, ZF: 0.5, '6E': 0.5, '6J': 0.5, '6B': 0.5, }; /** A product missing from the table pays the full-size rate, so the books * err toward the lower balance. */ export const TOPSTEP_COMMISSION_FALLBACK = 0.5; /** TopstepX's product code in a contract id → the CME root, where the two * differ. ENQ, GCE and CLE are seen on real fills; the rest follow the same * naming, and contracts the client resolves itself override them. */ const PRODUCT_ROOTS: Readonly> = { ENQ: 'NQ', EP: 'ES', GCE: 'GC', SIE: 'SI', CLE: 'CL', MCLE: 'MCL', NGE: 'NG', USA: 'ZB', TYA: 'ZN', FVA: 'ZF', EU6: '6E', JY6: '6J', BP6: '6B', }; /** "CON.F.US.ENQ.Z26" → "ENQ"; null for anything else. */ export function topstepxProductCode(contractId: unknown): string | null { const parts = String(contractId ?? '').split('.'); return parts.length === 5 && parts[0] === 'CON' && parts[3] ? parts[3] : null; } /** The CME root a TopstepX contract id trades, or null when it's none this * table knows. `learned` maps product codes to roots off the contracts the * client resolved itself; the table covers the rest of the history. */ export function topstepxRoot(contractId: unknown, learned?: ReadonlyMap): string | null { const code = topstepxProductCode(contractId); if (!code) return null; const known = (root: string | undefined) => (root !== undefined && root in TOPSTEP_COMMISSION_PER_SIDE ? root : null); return known(learned?.get(code)) ?? known(PRODUCT_ROOTS[code]) ?? known(code); } /** Topstep's commission on one fill of `size` contracts (one side). */ export function topstepCommission(root: string | null, size: number): number { const perSide = (root !== null ? TOPSTEP_COMMISSION_PER_SIDE[root] : undefined) ?? TOPSTEP_COMMISSION_FALLBACK; return Math.round(perSide * Math.abs(size || 0) * 100) / 100; }