Skip to content

Virtual stop-loss

Background

Binance USD-M Futures auto-cancels resting reduce-only LIMIT orders when a position shrinks below the total resting reduce-only quantity (the matching engine enforces the invariant Σ resting reduceOnly ≤ position). This is not Self-Trade Prevention; it's the exchange enforcing the invariant.

When multiple strategies trade the same symbol on one Binance account in one-way (BOTH) position mode and one strategy's STOP_MARKET SL fires inside the matching engine, the position shrinks atomically with no chance for the bot to clean up sibling strategies' TPs first. Sibling TPs become surplus and Binance auto-cancels them. The sibling positions are left unprotected — only the (now-fired) SL gone — until the operator notices and intervenes.

Solution

Move SL evaluation to the bot. The runner watches the book-ticker against each open position's virtual SL level. On crossing, the runner:

  1. Cancels every reduce-only LIMIT TP belonging to the firing position concurrently, awaiting all acks.
  2. Sends a MARKET reduce-only close for the same quantity.

Step 1 removes the strategy's own TPs from the book before the position shrinks. Step 2 then shrinks the position. The Σ resting reduceOnly ≤ position invariant holds throughout, so the auto-cancel never fires.

text
Time →
  cancel TP1 ─┐
  cancel TP2 ─┼─ awaited ──→ MARKET reduce-only close → position drops
  cancel TPN ─┘                  ↑
                       book reduceOnly is already 0 here

Trade-offs vs exchange-side STOP_MARKET

PropertySTOP_MARKET (legacy)Virtual SL
Trigger atomicityinside engine — instantbot-side, ~30–60 ms latency
Survives bot disconnectyesno — SL cannot fire while the WS feed is down
Mid-trigger cancel of sibling LIMITsimpossibleguaranteed
Reduce-only-surplus auto-cancel riskhigh in multi-strategy setupsnone in normal operation
Slippage on hard crasheslowslightly higher

Configuration

Set virtualSl on the strat entry (runner-level flag, not a strategy parameter):

json
{
  "name": "vsl_15S",
  "type": "shot",
  "virtualSl": true
}

Default is false (legacy STOP_MARKET). Recommendation: enable for all strategies on a given symbol or none — mixing modes lets the legacy STOP_MARKET path still trigger the cascade we are eliminating.

Failure modes & WS connectivity

The virtual SL is checked against every book-ticker. If the WS feed stops delivering ticks, the SL cannot fire until the WS recovers — there is no exchange-side STOP_MARKET backstop. Connectivity loss surfaces through the existing WS reconnect / fatal path (auto-pause on disconnect, fatal escalation after repeated reconnect failures), not a separate staleness alert.

Limitations

  • Mixing virtual and legacy SL on the same symbol can re-introduce the cascade when the legacy strategy's STOP_MARKET fires.
  • Bot crash loses virtual SL state. The strategy must re-emit exits on its next on-fill / SetExits after restart for the SL to be re-armed. No startup reconciliation is performed for virtual SLs in this build.
  • The 30–60 ms cancel-then-MARKET window adds slippage relative to engine-side triggers.

Backtesting and paper

  • Paper (isEmulator: true): inherits the fix automatically. Runs through the same runner as live, so virtual_sl: 1 triggers the same cancel-then-MARKET sequence against the paper adapter. The WS-staleness watchdog is harmless in paper because the paper adapter synthesises ticker events itself.
  • Backtest (tradectl backtest ...): the virtual_sl param is silently inert. Backtests model a single strategy against a single position and cannot reproduce the cross-strategy reduce-only-surplus cascade that virtual SL fixes. Reported P&L will be marginally optimistic vs live by the cancel→MARKET latency cost (~30–60 ms slippage on SL fires); use paper for accurate validation before flipping the flag in production.

tradectl — Automate Crypto Trading