From signal to a filled order that actually exists
A strategy that backtests well has solved the easy half. The other half is that a stop loss is not one thing, an open-orders endpoint may not mean what its name says, and no exchange lets you move an exit atomically.
- Surface
- Live execution across two exchange routes
- Written in
- Node.js and a Python execution service
- Verified
- Live, against real fills
- Bugs from one distinction
- Four
A stop loss is two different objects
Set a stop at the same time as the entry and the exchange attaches it to the position. Place or move one afterwards and you get a standalone conditional order. They look identical in the interface. They behave differently in four ways, and one of those ways can open a position you did not ask for.
I wrote this table after the fourth bug. Every one of them had been filed separately, and every one of them was this.
| Attached | Standalone | |
|---|---|---|
| Cleared when position closes | yes | no |
| Listed in open orders | no | yes |
| Reported on the position | yes | no |
| Moved by | position endpoint | order patch |
Verified live across both connection routes, not read from documentation.
Four bugs, one root cause
The dangerous one was the first. Moving a stop went through a patch that dropped the reduce-only flag on the replacement, so a moved exit quietly became an ordinary order — one that outlived the position it was protecting and could open a fresh one when it triggered.
That flag is not shown anywhere in the exchange's own web interface. The only way to see it was to read the order back over the API and look.
The rest fell out of the same distinction. The order list omitted attached brackets because the request never asked for conditional orders. Fixing that duplicated every exit line on the chart, since exits now arrived from both the order list and the position. And dragging an exit went to the wrong endpoint, because the code matched against the order list first — position-attached exits have to be matched first, since the order patch refuses them outright.
// the replacement order, read back: { "type": "STOP_MARKET", "triggerPrice": "62.29", "reduceOnly": false // <- was true } // reduceOnly false means this order // can OPEN a position on trigger. // the exchange UI displays it // identically to the correct one.
The one that cancelled someone else's orders
A restart brought several strategies back up on one shared account. One of them reconciled its safety-order ladder, enumerated the open orders, decided three of them sat beyond its stop loss and were therefore unreachable, and cancelled them.
They belonged to a different strategy. On a different symbol.
The open-orders endpoint on that account is not reliably scoped to the symbol you asked about, and the reconcile had trusted that it was. Every reconcile path now filters to its own symbol before anything is adopted or cancelled, and both paths have a regression test that reproduces the original cancellation.
This is the class of bug that makes people distrust automation, and it is invisible in every backtest ever written, because in a backtest yours is the only strategy alive.
Resting limit orders belonging to another strategy, cancelled by a reconcile that assumed the exchange had filtered by symbol.
Both unscoped fetches found and fixed, each pinned by a test that fails against the original code.
What I tell clients before they ask
Moving an exit is cancel-then-place. It is not atomic, and it cannot be — the exchange has no native modify and rejects a second exit while the first is alive.
That means a failed replacement leaves you with no exit at all, not an unchanged one. So both routes report which of the two steps happened, and the caller is told the difference rather than left to infer it from a silence.
Position sizing is the same kind of honesty problem. A percentage of capital is not a position size once leverage is involved, so the capital guards divide by leverage and the sizing model treats its fraction as margin rather than notional. Liquidation distance is read from the exchange's own maintenance-margin tiers, not approximated from a formula that happens to be close at low leverage.
A live position whose take profit and stop loss were set at entry, so the exchange holds them against the position itself and reports both back as fields on it — the attached form from the table above, and the reason they survive a restart without the trading bot having to remember them. Entry, mark, leverage and both trigger prices are read back over the API, not from local state. Watch a trade placed and managed by hand.