Exit prices on the tick grid, and a refused take-profit flattens
4217.6 − 87 × 0.1 is 4208.900000000001 in floating point. TopstepX refused
that take-profit ("Invalid price. Price is outside allowed range") on two
GC shorts on 2026-10-02, and with no stop on the leg nothing else was
working: both rode far past their target with no exit. ES and NQ never hit
it because 0.25 is exact in binary; GC and CL ticks aren't.
- onTick puts the measured-fee take-profit and every stop on the grid —
the take-profit away from the entry, the stop toward it — so an averaged
multi-contract fill between ticks is handled the same way.
- A leg's lone take-profit (no stop, or the fallback after a refused
bracket) that the broker refuses now flattens the position and benches
the account for the day, like a refused bracket. An unknown outcome still
places nothing more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d6bb5a7221
commit
8747dd6b08
@@ -9,7 +9,7 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
percentFee, takeProfitPlan, obfuscationStopDollars, dllRoom, dllStopDollars, dllViable, stopTicks,
|
||||
percentFee, takeProfitPlan, onTick, obfuscationStopDollars, dllRoom, dllStopDollars, dllViable, stopTicks,
|
||||
botStopsActive, sizeNotional, isNoTradeTime, isPastSessionClose, nextTradingDate, effectiveCombine, DEFAULT_TARGET_HOLD_HOURS,
|
||||
projectPerfectPath, cycleCompletion, DLL_RESERVE_FRACTION, DLL_MIN_STOP, notionalCap, expectedSwapToWin, MARGIN_HEADROOM,
|
||||
dllReserve, DLL_SLIPPAGE_FRACTION, maxLossRoom, lastStand, breachDistance, LAST_STAND_FRACTION, planLegSize, rankForTarget,
|
||||
@@ -28,6 +28,38 @@ function rng(seed) {
|
||||
};
|
||||
}
|
||||
|
||||
// ── prices on the tick grid ─────────────────────────────────────────────────
|
||||
|
||||
test('onTick: 2026-10-02 GC short — 87 ticks below 4217.6 is 4208.9, not 4208.900000000001', () => {
|
||||
assert.notEqual(4217.6 - 87 * 0.1, 4208.9); // the fuzz TopstepX refused
|
||||
const tp = takeProfitPlan({ side: 'Sell', entry: 4217.6, leg: 861.4, pointValue: 100, contracts: 1, tickSize: 0.1, commissionPct: 0, entryCommission: 2.8 });
|
||||
assert.equal(tp.ticks, 87);
|
||||
assert.equal(tp.exitPrice, 4208.9);
|
||||
assert.equal(JSON.stringify(tp.exitPrice), '4208.9');
|
||||
});
|
||||
|
||||
test('onTick: measured-fee take-profits always land on the grid, as the shortest decimal', () => {
|
||||
const r = rng(11);
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
const side = r() < 0.5 ? 'Buy' : 'Sell';
|
||||
const [pv, tick] = [[100, 0.1], [1000, 0.01], [10, 0.1], [12.5, 0.0001], [1000, 0.03125], [50, 0.25]][Math.floor(r() * 6)];
|
||||
const decimals = String(tick).split('.')[1].length;
|
||||
const entry = Number((Math.round((50 + r() * 20000) / tick) * tick).toFixed(decimals));
|
||||
const tp = takeProfitPlan({ side, entry, leg: 5 + r() * 2500, pointValue: pv, contracts: 1 + Math.floor(r() * 4), tickSize: tick, commissionPct: 0, entryCommission: Math.round(r() * 1000) / 100 });
|
||||
assert.equal(tp.exitPrice, Number(tp.exitPrice.toFixed(decimals)), `${tp.exitPrice} has more decimals than tick ${tick}`);
|
||||
assert.equal(Math.round(Math.abs(tp.exitPrice - entry) / tick), tp.ticks);
|
||||
}
|
||||
});
|
||||
|
||||
test('onTick: an averaged fill between ticks goes the given way; fuzz never moves a tick', () => {
|
||||
assert.equal(onTick(7749.125, 0.25, 'up'), 7749.25); // 2 ES filled at .00 and .25
|
||||
assert.equal(onTick(7749.125, 0.25, 'down'), 7749);
|
||||
assert.equal(onTick(4208.900000000001, 0.1, 'up'), 4208.9);
|
||||
assert.equal(onTick(4208.899999999999, 0.1, 'down'), 4208.9);
|
||||
assert.equal(onTick(62.07, 0.01, 'up'), 62.07);
|
||||
assert.equal(onTick(123.5, 0, 'up'), 123.5); // no tick size known: untouched
|
||||
});
|
||||
|
||||
// ── parity with the pre-refactor inline code ────────────────────────────────
|
||||
|
||||
/** The take-profit exactly as auto-trade.ts computed it before takeProfitPlan. */
|
||||
@@ -51,7 +83,8 @@ test('takeProfitPlan reproduces the old formula when fees are measured', () => {
|
||||
const fee = Math.round(r() * 1000) / 100;
|
||||
const got = takeProfitPlan({ side, entry, leg, pointValue: pv, contracts, tickSize: tick, commissionPct: 0, entryCommission: fee });
|
||||
const want = oldTakeProfit(side, entry, leg, pv, contracts, tick, fee);
|
||||
assert.equal(got.exitPrice, want.exitPrice);
|
||||
// The same price, minus the old formula's float fuzz.
|
||||
assert.equal(got.exitPrice, Math.round(want.exitPrice * 1e10) / 1e10);
|
||||
assert.equal(got.ticks, want.ticks);
|
||||
assert.equal(got.totalCommission, want.totalCommission);
|
||||
assert.equal(got.grossTarget, want.grossTarget);
|
||||
|
||||
Reference in New Issue
Block a user