Skip to main content

Contributing a Safer MarketIfTouchedOrder to Nautilus Trader — Hardening Conditional Orders in Rust

· 3 min read
Vadim Nicolai
Senior Software Engineer at Vitrifi

TL;DR – PR #2577 introduces a fallible constructor, complete domain-level checks, and four focussed tests for MarketIfTouchedOrder, thereby closing long-standing Issue #2529 on order-validation consistency.


1 Background

MarketIfTouchedOrder (MIT) is effectively the reverse of a stop-market order: it lies dormant until price touches a trigger, then fires as an immediate market order.
Because a latent trigger feeds straight into an instant fill path, robust validation is non-negotiable—any silent mismatch becomes a live trade.


2 Why the Change Was Necessary

ProblemImpact
Partial positivity checks on quantity, trigger_price, display_qtyInvalid values propagated deep into matching engines before exploding
TimeInForce::Gtd accepted expire_time = NoneProgrammer thought they had “good-til-date”; engine treated it as GTC
No check that display_qty ≤ quantityIceberg slice could exceed total size, leaking full inventory
Legacy new API only panickedCall-site couldn’t surface errors cleanly

Issue #2529 demanded uniform, fail-fast checks across all order types; MIT was first in line.


3 What PR #2577 Delivers

AreaBefore (v0)After (v1)
Constructornew → panic on errornew_checkedanyhow::Result<Self>; new now wraps it
Positivity checksPartialGuaranteed for quantity, trigger_price, (optional) display_qty
GTD ordersexpire_time optionalRequired when TIF == GTD
Iceberg ruleNonedisplay_qty ≤ quantity
Error channelOpaque panicsPrecise anyhow::Error variants
Tests04 rstest cases (happy-path + 3 failure modes)

Diff stats: +159 / −13 – one file crates/model/src/orders/market_if_touched.rs.


4 File Walkthrough Highlights

  1. new_checked – all domain guards live here; returns Result.
  2. Guard helpers – re-uses check_positive_quantity, check_positive_price, check_predicate_false.
  3. Legacy compatibilitynew() simply calls Self::new_checked(...).expect(FAILED).
  4. apply() tweak – slippage is recomputed immediately after a fill event.
  5. Testsok, quantity_zero, gtd_without_expire, display_qty_gt_quantity.

6 Order-Lifecycle Diagram


7 Using the New API

let mit = MarketIfTouchedOrder::new_checked(
trader_id,
strategy_id,
instrument_id,
client_order_id,
OrderSide::Sell,
qty,
trigger_price,
TriggerType::LastPrice,
TimeInForce::Gtc,
None, // expire_time
false, false, // reduce_only, quote_quantity
None, None, // display_qty, emulation_trigger
None, None, // trigger_instrument_id, contingency_type
None, None, // order_list_id, linked_order_ids
None, // parent_order_id
None, None, // exec_algorithm_id, params
None, // exec_spawn_id
None, // tags
init_id,
ts_init,
)?;

Prefer new_checked in production code; if you stick with new, you’ll still get clearer panic messages.


8 Impact & Next Steps

  • Fail-fast safety – all invariants enforced before the order leaves your code.
  • Granular error reporting – propagate Result outward instead of catching panics.
  • Zero breaking changes – legacy code continues to compile.

Action items: migrate to new_checked, bubble the Result, and sleep better during live trading.


9 References

TypeLink
Pull Request #2577https://github.com/nautechsystems/nautilus_trader/pull/2577
Issue #2529https://github.com/nautechsystems/nautilus_trader/issues/2529

Happy (and safer) trading!