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:
Brandon Li
2026-10-02 13:49:59 -05:00
co-authored by Claude Opus 5.5
parent d6bb5a7221
commit 8747dd6b08
3 changed files with 98 additions and 13 deletions
+19 -2
View File
@@ -1085,11 +1085,28 @@ export function percentFee(pct: number, price: number, pointValue: number, contr
return Math.ceil(raw * 100 - 1e-9) / 100;
}
/**
* A price on the tick grid, free of float fuzz. 4217.6 − 87 × 0.1 is
* 4208.900000000001 in floating point, and TopstepX refuses a price off the
* grid ("Invalid price. Price is outside allowed range") — a GC short rode
* with no take-profit on 2026-10-02. An averaged multi-contract fill can sit
* between ticks too (by 1/contracts of a tick or more), so an off-grid price
* goes `mode`'s way; fuzz within a millionth of a tick is never pushed a
* whole tick.
*/
export function onTick(price: number, tickSize: number, mode: 'up' | 'down'): number {
if (!(tickSize > 0)) return price;
const n = price / tickSize;
const k = mode === 'up' ? Math.ceil(n - 1e-6) : Math.floor(n + 1e-6);
return Math.round(k * tickSize * 1e10) / 1e10;
}
/**
* Where to put the take-profit so the leg nets at least `leg` after fees.
*
* Measured-fee firms (commissionPct 0): the exit fee is taken to equal the
* entry fee — the same formula the fire path always used, reproduced exactly.
* entry fee — the same formula the fire path always used, reproduced exactly,
* then put on the tick grid away from the entry (onTick).
*
* Percent-fee firms: the exit fee depends on the exit price itself, so solve
* for it. With r = pct/100 and k = pointValue × contracts, a long nets
@@ -1116,7 +1133,7 @@ export function takeProfitPlan(o: {
const grossTarget = o.leg + totalCommission;
const ticks = Math.ceil(grossTarget / k / o.tickSize);
return {
exitPrice: o.entry + dir * ticks * o.tickSize,
exitPrice: onTick(o.entry + dir * ticks * o.tickSize, o.tickSize, o.side === 'Buy' ? 'up' : 'down'),
ticks,
entryFee: o.entryCommission,
exitFee: o.entryCommission,