Crypto Trading Systems Bug Report

Stop-loss did not trigger when the chart crossed it: mark price vs last price triggers

A protective stop on a perpetual futures position did not trigger during a sharp wick that clearly crossed the stop on the last-price chart. The order was set to trigger on mark price, which moved much less. We now choose the trigger price type explicitly per order and test both behaviours.

Ryan Pham Ryan Pham @ryan_bugs · March 03, 2026 · 5 min read · 15 Views
Banner: futures stop order not triggered because it used mark price instead of last price

This is about how stop orders are triggered, not about where to place them. Our risk system places a protective stop-market order for every perpetual futures position. One morning a user of the internal dashboard asked why a stop at 2,412.00 on an ETH long had not executed when the price "clearly hit 2,398" during a sudden wick.

The exchange's order record:

{"symbol": "ETHUSDT", "type": "STOP_MARKET", "side": "SELL", "stopPrice": "2412.00",
 "workingType": "MARK_PRICE", "status": "NEW", "reduceOnly": true}

Still NEW. The last traded price had dipped to 2,397.8 for a few seconds; the mark price's low over the same period was 2,415.3.

Reproducing it

We pulled 1-second data for the period: last price and mark price. Last price touched the stop for about 4 seconds; mark price never did. The order was configured to watch mark price.

What we ruled out

  • An exchange malfunction. The order behaved exactly as configured.
  • A stale order. It was live and correctly placed before the wick.

Root cause

Futures stop orders can trigger on different reference prices. On Binance USD-M futures, the workingType parameter selects MARK_PRICE or CONTRACT_PRICE (last price). Our order manager used mark price for all stops by default, a choice someone made long ago to avoid being stopped out by brief wicks, but nobody using the system knew that, and the dashboard showed last-price charts.

What is happening under the hood

  • Last price (contract price): the price of the most recent trade on that contract. It can spike briefly in thin moments.
  • Mark price: derived from a spot index across several venues plus a basis component, designed to be harder to manipulate and to avoid unnecessary liquidations. Exchanges use it for unrealised PnL and liquidation.

A stop on mark price ignores short spikes in the contract's last price; a stop on last price reacts to them. Neither is right in general. The trade-offs:

Trigger Pros Cons
Last price Matches what charts and trades show; reacts to real executions Can be triggered by brief, thin-book wicks
Mark price Robust to manipulation and short spikes May not trigger during real but brief moves; differs from charts

Stop-market orders also have their own slippage once triggered, and some exchanges offer price protection settings that prevent triggering when last and mark prices diverge too much.

Exchanges also change how conditional orders are submitted from time to time, for example moving them to dedicated endpoints or services. The trigger concepts stay the same, but check the current API documentation for the exact endpoint and parameter names before relying on code samples, including the ones below.

Environment

Component Version / value
Exchange Binance USD-M perpetual futures
Orders STOP_MARKET, reduceOnly, workingType=MARK_PRICE by default
Dashboard Last-price candles
Risk system Python 3.12, places protective stops for each position

Minimal reproduction

On a futures testnet, place two reduce-only stop-market orders with the same stop price, one with workingType=MARK_PRICE and one with CONTRACT_PRICE, and observe that they trigger at different times when last and mark prices diverge.

The fix

1. Make the trigger explicit per order type. The risk configuration now states the trigger reference for each kind of protective order, and the order manager refuses to place a stop without it:

protective_stops:
  default_trigger: MARK_PRICE
  catastrophic_stop:
    trigger: CONTRACT_PRICE        # wide stop, must react to real trading
    price_protect: true
params = {
    "symbol": sym, "side": "SELL", "type": "STOP_MARKET",
    "stopPrice": fmt_price(stop), "reduceOnly": "true",
    "workingType": cfg.trigger,              # always explicit
    "priceProtect": "TRUE" if cfg.price_protect else "FALSE",
    "newClientOrderId": cid,
}

2. Show the trigger reference in the dashboard. Stop lines on charts are drawn against the price series they actually watch, and the dashboard can overlay mark price.

3. A second, independent safety check. Our own risk process watches both last and mark prices and can close positions directly if a hard loss limit is breached, regardless of exchange-side stop behaviour.

Alternatives and trade-offs

  • All stops on last price. Simpler mental model, more stop-outs from brief wicks.
  • All stops on mark price. Fewer false triggers, risk of not triggering during short real moves, as in this incident.
  • Two-layer stops (mark-price stop plus a wider last-price stop), which is what we chose for larger positions.

How to test the fix

Testnet tests place both trigger types and simulate diverging prices where possible; unit tests verify that every stop order request contains workingType. We also replayed the morning's 1-second data through our risk process to confirm the independent check would have acted at the configured hard limit.

Confirming in production

  • All open stops now show their trigger type in the dashboard; the team reviewed them and adjusted the configuration per strategy.
  • The independent risk check is exercised in a monthly drill on testnet.

Regression guard

  • Order manager rejects stop orders without an explicit workingType.
  • A daily report lists open stops by trigger type and distance from current prices.

Security implications

Stop behaviour is a risk control. Implicit defaults in risk-critical parameters are dangerous because the people relying on them do not know what they are. This post explains mechanics; deciding stop levels and triggers is a risk decision for each team, not something to copy from a blog.

Prevention checklist

  • Set the stop trigger reference explicitly on every order.
  • Show stops against the price series they watch.
  • Understand price protection settings for diverging prices.
  • Add an independent risk check outside the exchange's order logic.
  • Review defaults in risk-critical code paths regularly.

References

Add a label

#binance-api #futures #stop-orders #mark-price #risk-management
Written by
Ryan Pham @ryan_bugs

I build and run the platforms other engineers ship on: CI/CD pipelines, Kubernetes clusters, GPU inference services, LLM features in production, and the cloud bills that come with all of it. I have also spent years writing and operating automated trading systems, where a rounding bug or a missed websocket message is not a log line but a position you did not intend to hold. Each post here starts from a real failure: the alert, the exact error, what we ruled out, the root cause, and how we proved the fix. Same path every time: Investigate, Explain, Fix, Verify. No hype, no price predictions, just engineering.

Advertisement

Comments

0

No comments yet. Be the first to comment!

Sign In to post a comment.