Skip to main content

rustrade_execution/
rules.rs

1//! An instrument's order rules (tick, lot step, minimums and maximums) and the check of an order
2//! against them before it is sent.
3//!
4//! A venue publishes, per instrument, the grid a price and a quantity must sit on and the range
5//! each must fall in. A client that loads those rules refuses an order that breaks them without
6//! sending it, so the caller gets a typed error that says which value and which limit
7//! ([`OrderError::InvalidPrecision`] for the grid, [`OrderError::OutOfBounds`] for the range)
8//! rather than a venue's text after a round trip. It never rounds: rounding would place a
9//! different order from the one asked for.
10//!
11//! A pass is not a guarantee. The check covers the rules a client has loaded, from the last time
12//! it loaded them, and only the ones that can be read off the request. The venue has the last say.
13
14use crate::{
15    error::{Bound, BoundsViolation, OrderError, OrderField, PrecisionLimit, PrecisionViolation},
16    order::OrderKind,
17};
18use rust_decimal::Decimal;
19use serde::{Deserialize, Serialize};
20
21/// Whether a client checks an order against its instrument's [`OrderRules`] before sending it.
22#[non_exhaustive]
23#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum PreSendRules {
26    /// Refuse an order that breaks a rule, unsent. The default: a refusal costs no request, and
27    /// carries the limit that was broken.
28    #[default]
29    Enforce,
30    /// Send every order and leave the rules to the venue. Choose it when the loaded rules could
31    /// be wrong, for example because the venue changed them since they were loaded.
32    Off,
33}
34
35/// The grid and range of one kind of value: a price, a quantity or a notional.
36///
37/// Each part is optional. A venue that publishes a zero for one means the rule is off, and
38/// [`ValueRule::new`] reads zero that way.
39#[non_exhaustive]
40#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
41pub struct ValueRule {
42    /// The smallest value allowed.
43    pub min: Option<Decimal>,
44    /// The largest value allowed.
45    pub max: Option<Decimal>,
46    /// The value must be a whole multiple of this, counted from zero.
47    pub step: Option<Decimal>,
48}
49
50impl ValueRule {
51    /// A rule from the venue's `min`, `max` and `step`, each off when zero or negative.
52    pub fn new(min: Decimal, max: Decimal, step: Decimal) -> Self {
53        let on = |value: Decimal| (value > Decimal::ZERO).then_some(value);
54        Self {
55            min: on(min),
56            max: on(max),
57            step: on(step),
58        }
59    }
60
61    /// Whether every part is off.
62    pub fn is_off(&self) -> bool {
63        self.min.is_none() && self.max.is_none() && self.step.is_none()
64    }
65
66    /// `Ok` when `value` is on the grid and in the range, else the first rule it breaks, grid
67    /// before range.
68    fn check<AssetKey, InstrumentKey>(
69        &self,
70        field: OrderField,
71        value: Decimal,
72    ) -> Result<(), OrderError<AssetKey, InstrumentKey>> {
73        // A step set to zero by hand, not through `new`, is no grid: `%` would panic on it.
74        if let Some(step) = self.step.filter(|step| *step > Decimal::ZERO)
75            && !(value % step).is_zero()
76        {
77            return Err(OrderError::InvalidPrecision(PrecisionViolation::new(
78                field,
79                value,
80                PrecisionLimit::NotOnGrid { step },
81            )));
82        }
83        if let Some(min) = self.min.filter(|min| value < *min) {
84            return Err(OrderError::OutOfBounds(BoundsViolation::new(
85                field,
86                Bound::Min,
87                min,
88                value,
89            )));
90        }
91        if let Some(max) = self.max.filter(|max| value > *max) {
92            return Err(OrderError::OutOfBounds(BoundsViolation::new(
93                field,
94                Bound::Max,
95                max,
96                value,
97            )));
98        }
99        Ok(())
100    }
101}
102
103/// One instrument's order rules.
104///
105/// Venue-agnostic: a client fills it from its venue's rules and calls [`check`](Self::check).
106/// [`Default`] is no rules at all, which passes every order.
107#[non_exhaustive]
108#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
109pub struct OrderRules {
110    /// The grid and range of a limit, stop and trigger price, and of a trailing order's
111    /// activation price.
112    pub price: ValueRule,
113    /// The grid and range of every order's quantity.
114    pub quantity: ValueRule,
115    /// The grid and range of a market order's quantity, on top of [`quantity`](Self::quantity).
116    pub market_quantity: ValueRule,
117    /// The range of the notional, a price times a quantity. Its `step` is not used.
118    pub notional: ValueRule,
119}
120
121impl OrderRules {
122    /// Rules with `price`, `quantity`, `market_quantity` and `notional` as given.
123    pub fn new(
124        price: ValueRule,
125        quantity: ValueRule,
126        market_quantity: ValueRule,
127        notional: ValueRule,
128    ) -> Self {
129        Self {
130            price,
131            quantity,
132            market_quantity,
133            notional,
134        }
135    }
136
137    /// Whether these rules check nothing.
138    pub fn is_off(&self) -> bool {
139        self.price.is_off()
140            && self.quantity.is_off()
141            && self.market_quantity.is_off()
142            && self.notional.is_off()
143    }
144
145    /// Check an order of `kind`, with its limit `price` and `quantity`, against the rules.
146    ///
147    /// Returns the first rule the order breaks, or `Ok` if it breaks none, checking in this order:
148    /// - **Prices**, the grid and then the range, each on its own value: the limit `price` of a
149    ///   limit, stop-limit, take-profit-limit or fixed-limit trailing order; the trigger price of
150    ///   a stop, stop-limit, take-profit or take-profit-limit order; and a trailing order's
151    ///   activation price when it has one. A market order has no price to check.
152    /// - **Quantity**, against [`quantity`](Self::quantity) and, for a market order, then
153    ///   [`market_quantity`](Self::market_quantity).
154    /// - **Notional**, `price × quantity`, only where the price is the order's own limit: a limit,
155    ///   stop-limit, take-profit-limit or fixed-limit trailing order. Never a market, plain stop
156    ///   or trailing-stop order: the venue prices them at a reference price this check cannot
157    ///   know. An order with no `price` skips it.
158    ///
159    /// A closing order is checked like any other: a venue that enforces a minimum notional
160    /// enforces it on a close too, so an exemption would move the refusal to the venue.
161    ///
162    /// # Errors
163    /// [`OrderError::InvalidPrecision`] with [`PrecisionLimit::NotOnGrid`] for a value off its
164    /// grid, [`OrderError::OutOfBounds`] for one outside its range.
165    pub fn check<AssetKey, InstrumentKey>(
166        &self,
167        kind: OrderKind,
168        price: Option<Decimal>,
169        quantity: Decimal,
170    ) -> Result<(), OrderError<AssetKey, InstrumentKey>> {
171        // A match with no wildcard, so that a new `OrderKind` fails to compile until it is
172        // classified here.
173        let (limit_price, trigger_price, has_own_limit) = match kind {
174            OrderKind::Market => (None, None, false),
175            OrderKind::Limit => (price, None, true),
176            OrderKind::Stop { trigger_price } | OrderKind::TakeProfit { trigger_price } => {
177                (None, Some(trigger_price), false)
178            }
179            OrderKind::StopLimit { trigger_price }
180            | OrderKind::TakeProfitLimit { trigger_price } => (price, Some(trigger_price), true),
181            OrderKind::TrailingStop {
182                activation_price, ..
183            } => (None, activation_price, false),
184            OrderKind::TrailingStopFixedLimit {
185                activation_price, ..
186            } => (price, activation_price, true),
187            // The venue prices it from its stop and offset: there is no price to read.
188            OrderKind::TrailingStopLimit { .. } => (None, None, false),
189        };
190
191        if let Some(price) = limit_price {
192            self.price.check(OrderField::Price, price)?;
193        }
194        if let Some(trigger) = trigger_price {
195            self.price.check(OrderField::TriggerPrice, trigger)?;
196        }
197        self.quantity.check(OrderField::Quantity, quantity)?;
198        if matches!(kind, OrderKind::Market) {
199            self.market_quantity.check(OrderField::Quantity, quantity)?;
200        }
201        if let Some(price) = limit_price.filter(|_| has_own_limit) {
202            // A product beyond `Decimal`'s range is beyond any maximum too.
203            let notional = price.saturating_mul(quantity);
204            self.notional.check(OrderField::Notional, notional)?;
205        }
206        Ok(())
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::order::TrailingOffsetType;
214    use rust_decimal_macros::dec;
215
216    type Error = OrderError<(), ()>;
217
218    /// BTCUSDT-like: tick 0.01, lot 0.001 in 0.001..=100, market lot up to 10, notional 5..=9e6.
219    fn rules() -> OrderRules {
220        OrderRules::new(
221            ValueRule::new(dec!(0.01), dec!(1000000), dec!(0.01)),
222            ValueRule::new(dec!(0.001), dec!(100), dec!(0.001)),
223            ValueRule::new(dec!(0), dec!(10), dec!(0)),
224            ValueRule::new(dec!(5), dec!(9000000), dec!(0)),
225        )
226    }
227
228    fn check(kind: OrderKind, price: Option<Decimal>, quantity: Decimal) -> Result<(), Error> {
229        rules().check(kind, price, quantity)
230    }
231
232    fn bounds(field: OrderField, bound: Bound, limit: Decimal, value: Decimal) -> Error {
233        OrderError::OutOfBounds(BoundsViolation::new(field, bound, limit, value))
234    }
235
236    fn off_grid(field: OrderField, value: Decimal, step: Decimal) -> Error {
237        OrderError::InvalidPrecision(PrecisionViolation::new(
238            field,
239            value,
240            PrecisionLimit::NotOnGrid { step },
241        ))
242    }
243
244    #[test]
245    fn zero_or_negative_means_the_part_is_off() {
246        let rule = ValueRule::new(dec!(0), dec!(-1), dec!(0));
247        assert!(rule.is_off());
248        assert!(OrderRules::default().is_off());
249        assert!(!rules().is_off());
250        // One part on is enough for the rules to be on.
251        let on = ValueRule::new(dec!(1), dec!(0), dec!(0));
252        let off = ValueRule::default();
253        assert!(!OrderRules::new(on, off, off, off).is_off());
254        assert!(!OrderRules::new(off, on, off, off).is_off());
255        assert!(!OrderRules::new(off, off, on, off).is_off());
256        assert!(!OrderRules::new(off, off, off, on).is_off());
257        assert_eq!(
258            OrderRules::default().check::<(), ()>(OrderKind::Limit, Some(dec!(0.0001)), dec!(7.77)),
259            Ok(())
260        );
261    }
262
263    #[test]
264    fn a_step_of_zero_set_by_hand_is_no_grid_not_a_panic() {
265        let mut rules = OrderRules::default();
266        rules.quantity.step = Some(Decimal::ZERO);
267        assert_eq!(
268            rules.check::<(), ()>(OrderKind::Market, None, dec!(1.5)),
269            Ok(())
270        );
271    }
272
273    #[test]
274    fn a_limit_order_on_the_grid_and_in_range_passes() {
275        assert_eq!(
276            check(OrderKind::Limit, Some(dec!(50000.12)), dec!(0.5)),
277            Ok(())
278        );
279    }
280
281    #[test]
282    fn a_price_off_the_tick_grid_is_refused_with_the_step() {
283        assert_eq!(
284            check(OrderKind::Limit, Some(dec!(50000.123)), dec!(0.5)),
285            Err(off_grid(OrderField::Price, dec!(50000.123), dec!(0.01)))
286        );
287    }
288
289    #[test]
290    fn a_quantity_off_the_lot_grid_is_refused_with_the_step() {
291        assert_eq!(
292            check(OrderKind::Limit, Some(dec!(50000)), dec!(0.0015)),
293            Err(off_grid(OrderField::Quantity, dec!(0.0015), dec!(0.001)))
294        );
295    }
296
297    #[test]
298    fn a_grid_is_counted_from_zero_not_from_the_minimum() {
299        // 0.003 is 1.5 steps of 0.002: off a grid counted from zero, though a minimum of 0.002
300        // would put an origin-at-minimum grid's next point at 0.004 as well.
301        let rules = OrderRules::new(
302            ValueRule::default(),
303            ValueRule::new(dec!(0.002), dec!(0), dec!(0.002)),
304            ValueRule::default(),
305            ValueRule::default(),
306        );
307        assert_eq!(
308            rules.check::<(), ()>(OrderKind::Market, None, dec!(0.003)),
309            Err(off_grid(OrderField::Quantity, dec!(0.003), dec!(0.002)))
310        );
311        assert_eq!(
312            rules.check::<(), ()>(OrderKind::Market, None, dec!(0.004)),
313            Ok(())
314        );
315    }
316
317    #[test]
318    fn a_price_beyond_either_bound_is_refused_with_the_limit() {
319        // On the grid, but beyond a bound.
320        let rules = OrderRules::new(
321            ValueRule::new(dec!(1), dec!(10), dec!(0.5)),
322            ValueRule::default(),
323            ValueRule::default(),
324            ValueRule::default(),
325        );
326        assert_eq!(
327            rules.check::<(), ()>(OrderKind::Limit, Some(dec!(0.5)), dec!(1)),
328            Err(bounds(OrderField::Price, Bound::Min, dec!(1), dec!(0.5)))
329        );
330        assert_eq!(
331            rules.check::<(), ()>(OrderKind::Limit, Some(dec!(10.5)), dec!(1)),
332            Err(bounds(OrderField::Price, Bound::Max, dec!(10), dec!(10.5)))
333        );
334        assert_eq!(
335            rules.check::<(), ()>(OrderKind::Limit, Some(dec!(1)), dec!(1)),
336            Ok(())
337        );
338        assert_eq!(
339            rules.check::<(), ()>(OrderKind::Limit, Some(dec!(10)), dec!(1)),
340            Ok(())
341        );
342    }
343
344    #[test]
345    fn a_quantity_beyond_either_bound_is_refused_and_the_bounds_are_inclusive() {
346        assert_eq!(
347            check(OrderKind::Limit, Some(dec!(50000)), dec!(100.001)),
348            Err(bounds(
349                OrderField::Quantity,
350                Bound::Max,
351                dec!(100),
352                dec!(100.001)
353            ))
354        );
355        assert_eq!(
356            check(OrderKind::Limit, Some(dec!(50000)), dec!(0)),
357            Err(bounds(
358                OrderField::Quantity,
359                Bound::Min,
360                dec!(0.001),
361                dec!(0)
362            ))
363        );
364        assert_eq!(
365            check(OrderKind::Limit, Some(dec!(50000)), dec!(100)),
366            Ok(())
367        );
368        assert_eq!(
369            check(OrderKind::Limit, Some(dec!(50000)), dec!(0.001)),
370            Ok(())
371        );
372    }
373
374    #[test]
375    fn the_market_quantity_rule_applies_to_a_market_order_only() {
376        assert_eq!(
377            check(OrderKind::Market, None, dec!(10.5)),
378            Err(bounds(
379                OrderField::Quantity,
380                Bound::Max,
381                dec!(10),
382                dec!(10.5)
383            ))
384        );
385        assert_eq!(
386            check(OrderKind::Limit, Some(dec!(50000)), dec!(10.5)),
387            Ok(())
388        );
389    }
390
391    #[test]
392    fn a_market_order_also_obeys_the_ordinary_quantity_rule() {
393        assert_eq!(
394            check(OrderKind::Market, None, dec!(0.0005)),
395            Err(off_grid(OrderField::Quantity, dec!(0.0005), dec!(0.001)))
396        );
397    }
398
399    #[test]
400    fn the_notional_of_a_limit_order_is_its_price_times_its_quantity() {
401        assert_eq!(
402            check(OrderKind::Limit, Some(dec!(1000)), dec!(0.004)),
403            Err(bounds(OrderField::Notional, Bound::Min, dec!(5), dec!(4)))
404        );
405        assert_eq!(
406            check(OrderKind::Limit, Some(dec!(1000)), dec!(0.005)),
407            Ok(())
408        );
409        assert_eq!(
410            check(OrderKind::Limit, Some(dec!(1000000)), dec!(10)),
411            Err(bounds(
412                OrderField::Notional,
413                Bound::Max,
414                dec!(9000000),
415                dec!(10000000)
416            ))
417        );
418    }
419
420    #[test]
421    fn a_notional_beyond_the_decimal_range_is_above_the_maximum_not_a_panic() {
422        let rules = OrderRules::new(
423            ValueRule::default(),
424            ValueRule::default(),
425            ValueRule::default(),
426            ValueRule::new(dec!(0), dec!(1), dec!(0)),
427        );
428        assert!(matches!(
429            rules.check::<(), ()>(OrderKind::Limit, Some(Decimal::MAX), Decimal::MAX),
430            Err(OrderError::OutOfBounds(v)) if v.bound == Bound::Max
431        ));
432    }
433
434    #[test]
435    fn a_stop_limit_checks_the_price_the_trigger_and_the_notional() {
436        let kind = |trigger_price| OrderKind::StopLimit { trigger_price };
437        assert_eq!(
438            check(kind(dec!(100.001)), Some(dec!(100)), dec!(1)),
439            Err(off_grid(
440                OrderField::TriggerPrice,
441                dec!(100.001),
442                dec!(0.01)
443            ))
444        );
445        assert_eq!(
446            check(kind(dec!(100)), Some(dec!(100.001)), dec!(1)),
447            Err(off_grid(OrderField::Price, dec!(100.001), dec!(0.01)))
448        );
449        assert_eq!(
450            check(kind(dec!(100)), Some(dec!(1)), dec!(1)),
451            Err(bounds(OrderField::Notional, Bound::Min, dec!(5), dec!(1)))
452        );
453        assert_eq!(check(kind(dec!(100)), Some(dec!(100)), dec!(1)), Ok(()));
454    }
455
456    #[test]
457    fn a_take_profit_limit_is_checked_like_a_stop_limit() {
458        assert_eq!(
459            check(
460                OrderKind::TakeProfitLimit {
461                    trigger_price: dec!(100.001)
462                },
463                Some(dec!(100)),
464                dec!(1)
465            ),
466            Err(off_grid(
467                OrderField::TriggerPrice,
468                dec!(100.001),
469                dec!(0.01)
470            ))
471        );
472    }
473
474    #[test]
475    fn a_plain_stop_checks_its_trigger_and_never_a_notional() {
476        // A 1 USDT order would break the minimum notional if the trigger priced it.
477        for kind in [
478            OrderKind::Stop {
479                trigger_price: dec!(1),
480            },
481            OrderKind::TakeProfit {
482                trigger_price: dec!(1),
483            },
484        ] {
485            assert_eq!(check(kind, None, dec!(1)), Ok(()), "{kind:?}");
486        }
487        assert_eq!(
488            check(
489                OrderKind::Stop {
490                    trigger_price: dec!(1.001)
491                },
492                None,
493                dec!(1)
494            ),
495            Err(off_grid(OrderField::TriggerPrice, dec!(1.001), dec!(0.01)))
496        );
497    }
498
499    #[test]
500    fn a_market_order_never_checks_a_notional_or_a_price() {
501        // A request price on a market order is ignored.
502        assert_eq!(
503            check(OrderKind::Market, Some(dec!(0.001)), dec!(0.001)),
504            Ok(())
505        );
506    }
507
508    #[test]
509    fn a_trailing_stop_checks_its_activation_price_only() {
510        let kind = |activation_price| OrderKind::TrailingStop {
511            offset: dec!(50),
512            offset_type: TrailingOffsetType::BasisPoints,
513            activation_price,
514        };
515        assert_eq!(check(kind(None), None, dec!(1)), Ok(()));
516        assert_eq!(check(kind(Some(dec!(1))), None, dec!(1)), Ok(()));
517        assert_eq!(
518            check(kind(Some(dec!(1.001))), None, dec!(1)),
519            Err(off_grid(OrderField::TriggerPrice, dec!(1.001), dec!(0.01)))
520        );
521    }
522
523    #[test]
524    fn a_fixed_limit_trailing_stop_checks_its_limit_activation_and_notional() {
525        let kind = |activation_price| OrderKind::TrailingStopFixedLimit {
526            offset: dec!(50),
527            offset_type: TrailingOffsetType::BasisPoints,
528            activation_price,
529        };
530        assert_eq!(
531            check(kind(None), Some(dec!(100.001)), dec!(1)),
532            Err(off_grid(OrderField::Price, dec!(100.001), dec!(0.01)))
533        );
534        assert_eq!(
535            check(kind(Some(dec!(1.001))), Some(dec!(100)), dec!(1)),
536            Err(off_grid(OrderField::TriggerPrice, dec!(1.001), dec!(0.01)))
537        );
538        assert_eq!(
539            check(kind(None), Some(dec!(1)), dec!(1)),
540            Err(bounds(OrderField::Notional, Bound::Min, dec!(5), dec!(1)))
541        );
542        assert_eq!(check(kind(None), Some(dec!(100)), dec!(1)), Ok(()));
543    }
544
545    #[test]
546    fn a_limit_order_without_a_price_skips_the_price_and_notional_checks() {
547        assert_eq!(check(OrderKind::Limit, None, dec!(1)), Ok(()));
548    }
549
550    #[test]
551    fn a_grid_break_is_reported_before_a_range_break_of_the_same_value() {
552        // 0.0005 is below the minimum quantity and off the grid: the grid is reported.
553        assert_eq!(
554            check(OrderKind::Limit, Some(dec!(50000)), dec!(0.0005)),
555            Err(off_grid(OrderField::Quantity, dec!(0.0005), dec!(0.001)))
556        );
557    }
558
559    #[test]
560    fn a_pre_send_rules_deserializes_from_snake_case_and_defaults_to_enforce() {
561        assert_eq!(PreSendRules::default(), PreSendRules::Enforce);
562        assert_eq!(
563            serde_json::from_str::<PreSendRules>("\"off\"").ok(),
564            Some(PreSendRules::Off)
565        );
566    }
567
568    #[test]
569    fn the_refusals_name_the_value_and_the_limit() {
570        let text = |result: Result<(), Error>| match result {
571            Err(OrderError::OutOfBounds(violation)) => violation.to_string(),
572            Err(OrderError::InvalidPrecision(violation)) => violation.to_string(),
573            other => panic!("expected a refusal, got {other:?}"),
574        };
575        assert_eq!(
576            text(check(OrderKind::Limit, Some(dec!(1000)), dec!(0.004))),
577            "notional 4.000 is below the minimum 5"
578        );
579        assert_eq!(
580            text(check(OrderKind::Limit, Some(dec!(1000)), dec!(101))),
581            "quantity 101 is above the maximum 100"
582        );
583        assert_eq!(
584            text(check(OrderKind::Limit, Some(dec!(1000.001)), dec!(1))),
585            "price 1000.001 is not a multiple of 0.01"
586        );
587    }
588}