|
| 1 | +--- |
| 2 | +slug: nautilus-limitiftouched-validation |
| 3 | +title: Contributing a Safer `LimitIfTouchedOrder` to Nautilus Trader — A Small Open-Source Win for Rust Trading |
| 4 | +date: 2025-05-03 |
| 5 | +authors: [nicolad] |
| 6 | +--- |
| 7 | + |
| 8 | +## Introduction |
| 9 | + |
| 10 | +`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**. |
| 11 | +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. |
| 12 | + |
| 13 | +Pull Request [#2533](https://github.com/nautechsystems/nautilus_trader/pull/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][1], [GitHub][2]) |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## Why the Change Was Needed |
| 18 | + |
| 19 | +- **Inconsistent invariants** – `quantity`, `price`, and `trigger_price` were _not_ always checked for positivity. |
| 20 | +- **Edge-case foot-guns** – `TimeInForce::Gtd` could be set with a zero `expire_time`, silently turning a “good-til-date” order into “good-til-cancel”. |
| 21 | +- **Side/trigger mismatch** – A BUY order with a trigger _above_ the limit price (or SELL with trigger _below_ limit) yielded undefined behaviour. |
| 22 | +- **Developer frustration** – Consumers of the SDK had to replicate guard clauses externally; a single canonical constructor removes that burden. |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## Key Enhancements |
| 27 | + |
| 28 | +| Area | Before | After | |
| 29 | +| ----------------- | ---------------------- | ------------------------------------------------------------------------------- | |
| 30 | +| Constructor API | `new` (panic-on-error) | `new_checked` (returns `Result`) + `new` now wraps it | |
| 31 | +| Positivity checks | Only partial | Guaranteed for `quantity`, `price`, `trigger_price`, and optional `display_qty` | |
| 32 | +| Display quantity | Not validated | Must be ≤ `quantity` | |
| 33 | +| GTD orders | No expire validation | Must supply `expire_time` when `TimeInForce::Gtd` | |
| 34 | +| Side/trigger rule | Undefined | `BUY ⇒ trigger ≤ price`, `SELL ⇒ trigger ≥ price` | |
| 35 | +| Unit-tests | 0 dedicated tests | 5 focused tests (happy-path + 4 failure modes) | |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## Implementation Highlights |
| 40 | + |
| 41 | +1. **`new_checked`** – a fallible constructor returning `anyhow::Result<Self>`. All invariants live here. |
| 42 | +2. **Guard helpers** – leverages `check_positive_quantity`, `check_positive_price`, and `check_predicate_false` from `nautilus_core::correctness`. |
| 43 | +3. **Legacy behaviour preserved** – the original `new` now calls `new_checked().expect("FAILED")`, so downstream crates that relied on panics keep working. |
| 44 | +4. **Concise `Display` impl** – human-readable string that shows side, quantity, instrument, prices, trigger type, TIF, and status for quick debugging. |
| 45 | +5. **Test suite** – written with _rstest_; covers `ok`, `quantity_zero`, `gtd_without_expire`, `buy_trigger_gt_price`, and `sell_trigger_lt_price`. |
| 46 | + |
| 47 | +Code diff stats: **207 additions**, **9 deletions**, affecting `crates/model/src/orders/limit_if_touched.rs`. ([GitHub][2]) |
| 48 | + |
| 49 | +--- |
| 50 | + |
| 51 | +## Impact on Integrators |
| 52 | + |
| 53 | +_If you only called_ `LimitIfTouchedOrder::new` **nothing breaks**—you’ll merely enjoy better error messages if you misuse the API. |
| 54 | +For stricter compile-time safety, switch to the new `new_checked` constructor and handle `Result<T>` explicitly. |
| 55 | + |
| 56 | +```rust |
| 57 | +let order = LimitIfTouchedOrder::new_checked( |
| 58 | + trader_id, |
| 59 | + strategy_id, |
| 60 | + instrument_id, |
| 61 | + client_order_id, |
| 62 | + OrderSide::Buy, |
| 63 | + qty, |
| 64 | + limit_price, |
| 65 | + trigger_price, |
| 66 | + TriggerType::LastPrice, |
| 67 | + TimeInForce::Gtc, |
| 68 | + None, // expire_time |
| 69 | + false, false, // post_only, reduce_only |
| 70 | + false, None, // quote_qty, display_qty |
| 71 | + None, None, // emulation_trigger, trigger_instrument_id |
| 72 | + None, None, // contingency_type, order_list_id |
| 73 | + None, // linked_order_ids |
| 74 | + None, // parent_order_id |
| 75 | + None, None, // exec_algorithm_id, params |
| 76 | + None, // exec_spawn_id |
| 77 | + None, // tags |
| 78 | + init_id, |
| 79 | + ts_init, |
| 80 | +)?; |
| 81 | +``` |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | +## Conclusion |
| 86 | + |
| 87 | +PR \[#2533] dramatically reduces the surface area for invalid LIT orders by centralising all domain rules in a single, auditable place. |
| 88 | +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. |
| 89 | + |
| 90 | +> **Next steps:** adopt `new_checked`, make your own wrappers return `Result`, and enjoy safer trading. |
| 91 | +
|
| 92 | +--- |
| 93 | + |
| 94 | +[1]: https://github.com/nautechsystems/nautilus_trader/pull/2533 "Improve validations for LimitIfTouchedOrder by nicolad · Pull Request #2533 · nautechsystems/nautilus_trader · GitHub" |
0 commit comments