TopstepX: book Topstep's commission with each fill
TopstepX's gateway reports a fill's exchange and NFA fees only (NQ $1.39 a side, GC $1.66, CL $1.51, MNQ $0.36). Topstep's own commission, $0.50 a side on a full-size contract and $0.25 on a micro, comes off the balance with no trade row carrying it, so our P&L ran $1.00 per full-size round turn above Topstep's. On 2026-10-18 the combines sat $6 to $8 under their books and an XFA $27.50 under, and one combine had its $18 booked as a "withdrawal". Topstep's published round turns (NQ $3.78, MNQ $1.22, GC $4.32, CL $4.02) are the reported fees plus this commission. - lib/topstep-fees.ts: the per-product commission table, and TopstepX contract ids mapped to roots (ENQ, GCE and CLE as seen on real fills, overridden by contracts the client resolves itself; an unknown product pays the full-size rate and is logged once). - Daily P&L, today's realized, and so the measured entry fee a take-profit is placed from, all net the commission. The 90-day refetch rewrites the cached days. - npm run check: the table against the micro flags, the id mapping, and the published round turns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c0261f42ae
commit
aab225a19c
@@ -13,6 +13,7 @@ import {
|
||||
resolveAllowlist, MAX_HOLD_HOURS, DRAW_FLOOR, DEFAULT_MAX_ENTRY_SPREAD, spreadTooWide, sanitizeInstrumentLimits,
|
||||
} from '../lib/instrument-specs.ts';
|
||||
import { mergeTopstepxQuote, parseTradovateFrame, bookSpread } from '../lib/quote-helpers.ts';
|
||||
import { TOPSTEP_COMMISSION_PER_SIDE, topstepxProductCode, topstepxRoot, topstepCommission } from '../lib/topstep-fees.ts';
|
||||
|
||||
// Deterministic PRNG so a failure reproduces.
|
||||
function rng(seed) {
|
||||
@@ -256,3 +257,41 @@ test('instrument limits off the catalog: CME roots, whole numbers 1-1000', () =>
|
||||
assert.deepEqual(sanitizeInstrumentLimits(null), {});
|
||||
assert.deepEqual(sanitizeInstrumentLimits(['NQ']), {});
|
||||
});
|
||||
|
||||
test('Topstep commission: every root, a micro at half the full-size rate', () => {
|
||||
assert.deepEqual(Object.keys(TOPSTEP_COMMISSION_PER_SIDE).sort(), [...CME_ROOTS].sort());
|
||||
for (const root of CME_ROOTS) assert.equal(TOPSTEP_COMMISSION_PER_SIDE[root], CME_SPECS[root].micro ? 0.25 : 0.5, root);
|
||||
});
|
||||
|
||||
test('Topstep commission: TopstepX contract ids to roots', () => {
|
||||
// Codes seen on real fills.
|
||||
assert.equal(topstepxRoot('CON.F.US.ENQ.Z26'), 'NQ');
|
||||
assert.equal(topstepxRoot('CON.F.US.MNQ.Z26'), 'MNQ');
|
||||
assert.equal(topstepxRoot('CON.F.US.GCE.Z26'), 'GC');
|
||||
assert.equal(topstepxRoot('CON.F.US.CLE.X26'), 'CL');
|
||||
assert.equal(topstepxProductCode('CON.F.US.ENQ.Z26'), 'ENQ');
|
||||
// Not a contract id, or a product with no entry.
|
||||
assert.equal(topstepxRoot('NQZ6'), null);
|
||||
assert.equal(topstepxRoot(undefined), null);
|
||||
assert.equal(topstepxRoot('CON.F.US.MBT.Z26'), null);
|
||||
// A contract the client resolved names its own code; a stray key doesn't.
|
||||
assert.equal(topstepxRoot('CON.F.US.MBT.Z26', new Map([['MBT', 'MES']])), 'MES');
|
||||
assert.equal(topstepxRoot('CON.F.US.ENQ.Z26', new Map([['ENQ', '*']])), 'NQ');
|
||||
});
|
||||
|
||||
test('Topstep commission: reported fees plus commission make the published round turn', () => {
|
||||
// TopstepX reports exchange + NFA a side; Topstep's page totals a round turn.
|
||||
const roundTurn = (root, reportedPerSide, contracts) =>
|
||||
Math.round((2 * reportedPerSide * contracts + 2 * topstepCommission(root, contracts)) * 100) / 100;
|
||||
assert.equal(roundTurn('NQ', 1.39, 1), 3.78);
|
||||
assert.equal(roundTurn('MNQ', 0.36, 1), 1.22);
|
||||
assert.equal(roundTurn('GC', 1.66, 1), 4.32);
|
||||
assert.equal(roundTurn('CL', 1.51, 1), 4.02);
|
||||
assert.equal(roundTurn('NQ', 1.39, 3), 11.34);
|
||||
// A sell fill's size is still a cost; an unknown product pays the full-size rate.
|
||||
assert.equal(topstepCommission('NQ', -3), 1.5);
|
||||
assert.equal(topstepCommission(null, 2), 1);
|
||||
// 2026-10-18, an XFA $27.50 under its books: 26 full-size round turns and 3 MNQ.
|
||||
const missing = 26 * 2 * topstepCommission('NQ', 1) + 3 * 2 * topstepCommission('MNQ', 1);
|
||||
assert.equal(missing, 27.5);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user