Files
Brandon LiandClaude Opus 5.5 1e844178de Trades: record every firm's trades, not just Vest's, and list them on each
TopstepX and Tradovate report fills, so a trade is built from them, flat
to flat (lib/fill-trades.ts), off the window each sync already reads:
TopstepX's two days of Trade/search, Tradovate's sync snapshot. Neither
says who placed an order, so every order the bot places is noted in
own_orders, and a trade is the bot's when its opening fill's order is
one; its stop or take-profit closing it reads as such. A contract is
written only when the position before its window comes out the same
two passes running, so a fill landing between the position and history
reads can't build the wrong trades.

DXtrade reports positions: its record groups the last two days' filled
orders by position code, with each close's SETTLEMENT and COMMISSION.
The bot's own orders carry af- codes, so nothing is kept locally. Only
accounts with something going on are read, at most every 5 minutes.

The Trades button shows on every firm with a login, and the account
page lists any account's trades.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 01:27:07 -05:00

191 lines
8.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Checks for building trades out of fills (lib/fill-trades.ts — TopstepX and
// Tradovate) and out of DXtrade's order history (lib/dxtrade-helpers.ts).
//
// npm run check
//
// The files import only Node built-ins, so Node strips their types and loads
// them directly; .mjs keeps this file out of Next's type check.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { fillsToTrades, startingNet, contractRoot, byFillTime } from '../lib/fill-trades.ts';
import { topstepxSymbol } from '../lib/topstep-fees.ts';
import { parseHistoryOrder, dxTrades } from '../lib/dxtrade-helpers.ts';
let n = 0;
const fill = (side, qty, price, extra = {}) => {
n += 1;
return { id: String(1000 + n), orderId: String(5000 + n), at: 1_000 + n * 1_000, side, qty, price, fee: 1, pnl: null, ...extra };
};
test('a round trip, with the platform’s P&L', () => {
const a = fill('buy', 2, 100);
const b = fill('sell', 2, 101, { pnl: 40 });
const [t, ...rest] = fillsToTrades([b, a], { startNet: 0, pointValue: null });
assert.equal(rest.length, 0);
assert.deepEqual(t, {
id: a.id, side: 'long', quantity: 2, openPrice: 100, closePrice: 101, pnl: 40, fee: 2,
openedAt: a.at, closedAt: b.at, openOrderId: a.orderId, closeOrderId: b.orderId, carried: false,
});
});
test('P&L from the prices when the platform gives none; adds and partial closes stay one trade', () => {
const fills = [fill('buy', 1, 100), fill('buy', 1, 102), fill('sell', 1, 103), fill('sell', 1, 104)];
const [t, ...rest] = fillsToTrades(fills, { startNet: 0, pointValue: 20 });
assert.equal(rest.length, 0);
assert.equal(t.quantity, 2);
assert.equal(t.openPrice, 101);
assert.equal(t.closePrice, 103.5);
assert.equal(t.pnl, 100);
assert.equal(t.fee, 4);
assert.equal(t.closeOrderId, fills[3].orderId);
// No point value and no platform P&L: unknown, not zero.
assert.equal(fillsToTrades(fills, { startNet: 0, pointValue: null })[0].pnl, null);
});
test('a short', () => {
const fills = [fill('sell', 3, 50.5), fill('buy', 3, 50)];
const [t] = fillsToTrades(fills, { startNet: 0, pointValue: 1000 });
assert.equal(t.side, 'short');
assert.equal(t.pnl, 1500);
});
test('a fill through zero closes one trade and opens the next', () => {
const a = fill('buy', 1, 100);
const b = fill('sell', 3, 105, { fee: 3, pnl: 100 });
const [first, second, ...rest] = fillsToTrades([a, b], { startNet: 0, pointValue: 20 });
assert.equal(rest.length, 0);
assert.equal(first.side, 'long');
assert.equal(first.pnl, 100);
assert.equal(first.fee, 2); // its own buy, and a third of the reversing fill
assert.equal(first.closeOrderId, b.orderId);
assert.equal(second.id, b.id);
assert.equal(second.side, 'short');
assert.equal(second.quantity, 2);
assert.equal(second.openPrice, 105);
assert.equal(second.fee, 2);
assert.equal(second.closedAt, null);
assert.equal(second.pnl, null);
assert.equal(second.closePrice, null);
});
test('fills that start inside a position: passed over, or carried on', () => {
const close = fill('sell', 1, 110, { pnl: 200 });
const a = fill('buy', 1, 100);
const b = fill('sell', 1, 101, { pnl: 20 });
// Nothing stored: the leading close belongs to nothing on record.
const plain = fillsToTrades([close, a, b], { startNet: 1, pointValue: 20 });
assert.equal(plain.length, 1);
assert.equal(plain[0].id, a.id);
// The stored open trade carries on and closes.
const carry = { id: 'tsx:42', side: 'long', quantity: 1, openPrice: 100, fee: 0.5, openedAt: 10 };
const [carried, next] = fillsToTrades([close, a, b], { startNet: 1, pointValue: 20, carry });
assert.equal(carried.id, 'tsx:42');
assert.equal(carried.carried, true);
assert.equal(carried.openedAt, 10);
assert.equal(carried.openOrderId, null);
assert.equal(carried.pnl, 200);
assert.equal(carried.fee, 1.5);
assert.equal(carried.closedAt, close.at);
assert.equal(next.id, a.id);
// A carry on the other side isn't this position.
assert.equal(fillsToTrades([close], { startNet: 1, pointValue: 20, carry: { ...carry, side: 'short' } }).length, 0);
});
test('the position before the fills', () => {
const buy = fill('buy', 1, 1);
const sell = fill('sell', 1, 1);
assert.equal(startingNet([buy, sell], 0), 0);
assert.equal(startingNet([buy], 1), 0);
assert.equal(startingNet([sell], 0), 1);
assert.equal(startingNet([], -2), -2);
});
test('fills sort by time, then numerically by id', () => {
const a = { id: '9', at: 5 };
const b = { id: '10', at: 5 };
const c = { id: '1', at: 6 };
assert.deepEqual([c, b, a].sort(byFillTime).map((f) => f.id), ['9', '10', '1']);
});
test('contract roots and TopstepX names', () => {
assert.equal(contractRoot('MNQZ6'), 'MNQ');
assert.equal(contractRoot('ZNZ6'), 'ZN');
assert.equal(contractRoot('NGZ6'), 'NG');
assert.equal(contractRoot('6EZ6'), '6E');
assert.equal(contractRoot('M2KH27'), 'M2K');
assert.equal(contractRoot('CL'), 'CL');
assert.equal(topstepxSymbol('CON.F.US.ENQ.Z26'), 'NQZ6');
assert.equal(topstepxSymbol('CON.F.US.MNQ.H27'), 'MNQH7');
assert.equal(topstepxSymbol('CON.F.US.XYZ.M26'), 'XYZM6');
assert.equal(topstepxSymbol('something'), 'something');
});
// DXtrade: a bot round trip on BTC/USD, as /orders/history and /transfers
// answer (position-based account: the opening order's id is the position).
const openOrder = {
account: 'default:T247-1', orderId: 555, orderCode: 'af-mh2-abc-e', type: 'MARKET', instrument: 'BTC/USD',
status: 'COMPLETED', finalStatus: true,
legs: [{ instrument: 'BTC/USD', positionEffect: 'OPEN', side: 'BUY', quantity: 0.5, filledQuantity: 0.5, averagePrice: 60000 }],
issueTime: '2026-10-26T01:00:00.000Z', transactionTime: '2026-10-26T01:00:01.000Z',
executions: [{ transactionTime: '2026-10-26T01:00:00.500Z', lastQuantity: 0.5, averagePrice: 60000 }],
};
const stopOrder = {
account: 'default:T247-1', orderId: 556, orderCode: 'af-mh3-def-sl', type: 'STOP', instrument: 'BTC/USD',
status: 'COMPLETED', finalStatus: true,
legs: [{ instrument: 'BTC/USD', positionEffect: 'CLOSE', positionCode: '555', side: 'SELL', filledQuantity: 0.5, averagePrice: 59000 }],
issueTime: '2026-10-26T01:00:02.000Z', transactionTime: '2026-10-26T02:00:00.000Z', executions: [],
};
const txn = (type, value, orderCode) => ({ key: `${orderCode}|${type}`, account: 'default:T247-1', type, value, time: 0, orderCode });
test('DXtrade order history rows', () => {
const o = parseHistoryOrder(openOrder);
assert.equal(o.orderCode, 'af-mh2-abc-e');
assert.equal(o.effect, 'OPEN');
assert.equal(o.side, 'BUY');
assert.equal(o.positionCode, undefined);
assert.equal(o.qty, 0.5);
assert.equal(o.price, 60000);
assert.equal(o.at, Date.parse('2026-10-26T01:00:00.500Z'));
const s = parseHistoryOrder(stopOrder);
assert.equal(s.positionCode, '555');
assert.equal(s.at, Date.parse('2026-10-26T02:00:00.000Z'));
// Never filled: not a trade's.
assert.equal(parseHistoryOrder({ ...openOrder, legs: [{ ...openOrder.legs[0], filledQuantity: 0 }] }), null);
assert.equal(parseHistoryOrder({}), null);
});
test('DXtrade positions from their orders and cash', () => {
const orders = [parseHistoryOrder(stopOrder), parseHistoryOrder(openOrder)];
const txns = [txn('SETTLEMENT', -500, 'af-mh3-def-sl'), txn('COMMISSION', -12, 'af-mh2-abc-e'), txn('COMMISSION', -11.8, 'af-mh3-def-sl')];
const [t, ...rest] = dxTrades(orders, txns, new Map(), new Map());
assert.equal(rest.length, 0);
assert.equal(t.positionCode, '555');
assert.equal(t.side, 'long');
assert.equal(t.quantity, 0.5);
assert.equal(t.openPrice, 60000);
assert.equal(t.closePrice, 59000);
assert.equal(t.pnl, -500);
assert.equal(t.fee, 23.8);
assert.equal(t.openCode, 'af-mh2-abc-e');
assert.equal(t.closeCode, 'af-mh3-def-sl');
assert.equal(t.closeType, 'STOP');
// Still held: open, whatever the history says.
const held = new Map([['555', { symbol: 'BTC/USD', side: 'long', quantity: 0.5, openPrice: 60000, openedAt: 1 }]]);
const [open] = dxTrades([parseHistoryOrder(openOrder)], [], held, new Map());
assert.equal(open.closedAt, null);
assert.equal(open.pnl, null);
// Opened before the history: completed from what's stored, or left out.
const [late] = dxTrades([parseHistoryOrder(stopOrder)], txns, new Map(), held);
assert.equal(late.openedAt, 1);
assert.equal(late.openCode, null);
assert.equal(late.pnl, -500);
assert.equal(dxTrades([parseHistoryOrder(stopOrder)], txns, new Map(), new Map()).length, 0);
// A position open with no orders in the window still shows.
const [quiet] = dxTrades([], [], held, new Map());
assert.equal(quiet.positionCode, '555');
assert.equal(quiet.openedAt, 1);
});