Contributing a Safer LimitIfTouchedOrder to Nautilus Trader — A Small Open-Source Win for Rust Trading
Introduction
LimitIfTouchedOrder
(LIT) is a conditional order that sits between a simple limit order and a stop-limit order: it rests inactive until a trigger price is touched, then converts into a plain limit at the specified limit price.
Because it straddles two distinct price levels and multiple conditional flags, robust validation is critical—any silent mismatch can manifest as unwanted executions in live trading.
Pull Request #2533 standardises and hardens the validation logic for LIT orders, bringing it up to the same quality bar as MarketOrder
and LimitOrder
. The PR was merged into develop
on May 1 2025 by @cjdsellers (+207 / −9 across one file). (GitHub, [GitHub][2])
Why the Change Was Needed
- Inconsistent invariants –
quantity
,price
, andtrigger_price
were not always checked for positivity. - Edge-case foot-guns –
TimeInForce::Gtd
could be set with a zeroexpire_time
, silently turning a “good-til-date” order into “good-til-cancel”. - Side/trigger mismatch – A BUY order with a trigger above the limit price (or SELL with trigger below limit) yielded undefined behaviour.
- Developer frustration – Consumers of the SDK had to replicate guard clauses externally; a single canonical constructor removes that burden.
Key Enhancements
Area | Before | After |
---|---|---|
Constructor API | new (panic-on-error) | new_checked (returns Result ) + new now wraps it |
Positivity checks | Only partial | Guaranteed for quantity , price , trigger_price , and optional display_qty |
Display quantity | Not validated | Must be ≤ quantity |
GTD orders | No expire validation | Must supply expire_time when TimeInForce::Gtd |
Side/trigger rule | Undefined | BUY ⇒ trigger ≤ price , SELL ⇒ trigger ≥ price |
Unit-tests | 0 dedicated tests | 5 focused tests (happy-path + 4 failure modes) |
Implementation Highlights
new_checked
– a fallible constructor returninganyhow::Result<Self>
. All invariants live here.- Guard helpers – leverages
check_positive_quantity
,check_positive_price
, andcheck_predicate_false
fromnautilus_core::correctness
. - Legacy behaviour preserved – the original
new
now callsnew_checked().expect("FAILED")
, so downstream crates that relied on panics keep working. - Concise
Display
impl – human-readable string that shows side, quantity, instrument, prices, trigger type, TIF, and status for quick debugging. - Test suite – written with rstest; covers
ok
,quantity_zero
,gtd_without_expire
,buy_trigger_gt_price
, andsell_trigger_lt_price
.
Code diff stats: 207 additions, 9 deletions, affecting crates/model/src/orders/limit_if_touched.rs
. ([GitHub][2])
Impact on Integrators
If you only called LimitIfTouchedOrder::new
nothing breaks—you’ll merely enjoy better error messages if you misuse the API.
For stricter compile-time safety, switch to the new new_checked
constructor and handle Result<T>
explicitly.
let order = LimitIfTouchedOrder::new_checked(
trader_id,
strategy_id,
instrument_id,
client_order_id,
OrderSide::Buy,
qty,
limit_price,
trigger_price,
TriggerType::LastPrice,
TimeInForce::Gtc,
None, // expire_time
false, false, // post_only, reduce_only
false, None, // quote_qty, display_qty
None, None, // emulation_trigger, trigger_instrument_id
None, None, // contingency_type, order_list_id
None, // linked_order_ids
None, // parent_order_id
None, None, // exec_algorithm_id, params
None, // exec_spawn_id
None, // tags
init_id,
ts_init,
)?;
Conclusion
PR [#2533] dramatically reduces the surface area for invalid LIT orders by centralising all domain rules in a single, auditable place. Whether you’re building discretionary tooling or a fully automated strategy on top of Nautilus Trader, you now get fail-fast behaviour with precise error semantics—no more mystery fills in production.
Next steps: adopt
new_checked
, make your own wrappers returnResult
, and enjoy safer trading.