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            // Never placed, but its prices are the venue's and so on its grids.
190            OrderKind::ConditionalUnresolved {
191                family,
192                trigger_price,
193            } => (
194                price.filter(|_| family.has_limit_price()),
195                trigger_price,
196                family.has_limit_price(),
197            ),
198        };
199
200        if let Some(price) = limit_price {
201            self.price.check(OrderField::Price, price)?;
202        }
203        if let Some(trigger) = trigger_price {
204            self.price.check(OrderField::TriggerPrice, trigger)?;
205        }
206        self.quantity.check(OrderField::Quantity, quantity)?;
207        if matches!(kind, OrderKind::Market) {
208            self.market_quantity.check(OrderField::Quantity, quantity)?;
209        }
210        if let Some(price) = limit_price.filter(|_| has_own_limit) {
211            // A product beyond `Decimal`'s range is beyond any maximum too.
212            let notional = price.saturating_mul(quantity);
213            self.notional.check(OrderField::Notional, notional)?;
214        }
215        Ok(())
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::order::{ConditionalFamily, TrailingOffsetType};
223    use rust_decimal_macros::dec;
224
225    type Error = OrderError<(), ()>;
226
227    /// BTCUSDT-like: tick 0.01, lot 0.001 in 0.001..=100, market lot up to 10, notional 5..=9e6.
228    fn rules() -> OrderRules {
229        OrderRules::new(
230            ValueRule::new(dec!(0.01), dec!(1000000), dec!(0.01)),
231            ValueRule::new(dec!(0.001), dec!(100), dec!(0.001)),
232            ValueRule::new(dec!(0), dec!(10), dec!(0)),
233            ValueRule::new(dec!(5), dec!(9000000), dec!(0)),
234        )
235    }
236
237    fn check(kind: OrderKind, price: Option<Decimal>, quantity: Decimal) -> Result<(), Error> {
238        rules().check(kind, price, quantity)
239    }
240
241    fn bounds(field: OrderField, bound: Bound, limit: Decimal, value: Decimal) -> Error {
242        OrderError::OutOfBounds(BoundsViolation::new(field, bound, limit, value))
243    }
244
245    fn off_grid(field: OrderField, value: Decimal, step: Decimal) -> Error {
246        OrderError::InvalidPrecision(PrecisionViolation::new(
247            field,
248            value,
249            PrecisionLimit::NotOnGrid { step },
250        ))
251    }
252
253    #[test]
254    fn zero_or_negative_means_the_part_is_off() {
255        let rule = ValueRule::new(dec!(0), dec!(-1), dec!(0));
256        assert!(rule.is_off());
257        assert!(OrderRules::default().is_off());
258        assert!(!rules().is_off());
259        // One part on is enough for the rules to be on.
260        let on = ValueRule::new(dec!(1), dec!(0), dec!(0));
261        let off = ValueRule::default();
262        assert!(!OrderRules::new(on, off, off, off).is_off());
263        assert!(!OrderRules::new(off, on, off, off).is_off());
264        assert!(!OrderRules::new(off, off, on, off).is_off());
265        assert!(!OrderRules::new(off, off, off, on).is_off());
266        assert_eq!(
267            OrderRules::default().check::<(), ()>(OrderKind::Limit, Some(dec!(0.0001)), dec!(7.77)),
268            Ok(())
269        );
270    }
271
272    #[test]
273    fn a_step_of_zero_set_by_hand_is_no_grid_not_a_panic() {
274        let mut rules = OrderRules::default();
275        rules.quantity.step = Some(Decimal::ZERO);
276        assert_eq!(
277            rules.check::<(), ()>(OrderKind::Market, None, dec!(1.5)),
278            Ok(())
279        );
280    }
281
282    #[test]
283    fn a_limit_order_on_the_grid_and_in_range_passes() {
284        assert_eq!(
285            check(OrderKind::Limit, Some(dec!(50000.12)), dec!(0.5)),
286            Ok(())
287        );
288    }
289
290    #[test]
291    fn a_price_off_the_tick_grid_is_refused_with_the_step() {
292        assert_eq!(
293            check(OrderKind::Limit, Some(dec!(50000.123)), dec!(0.5)),
294            Err(off_grid(OrderField::Price, dec!(50000.123), dec!(0.01)))
295        );
296    }
297
298    #[test]
299    fn a_quantity_off_the_lot_grid_is_refused_with_the_step() {
300        assert_eq!(
301            check(OrderKind::Limit, Some(dec!(50000)), dec!(0.0015)),
302            Err(off_grid(OrderField::Quantity, dec!(0.0015), dec!(0.001)))
303        );
304    }
305
306    #[test]
307    fn a_grid_is_counted_from_zero_not_from_the_minimum() {
308        // 0.003 is 1.5 steps of 0.002: off a grid counted from zero, though a minimum of 0.002
309        // would put an origin-at-minimum grid's next point at 0.004 as well.
310        let rules = OrderRules::new(
311            ValueRule::default(),
312            ValueRule::new(dec!(0.002), dec!(0), dec!(0.002)),
313            ValueRule::default(),
314            ValueRule::default(),
315        );
316        assert_eq!(
317            rules.check::<(), ()>(OrderKind::Market, None, dec!(0.003)),
318            Err(off_grid(OrderField::Quantity, dec!(0.003), dec!(0.002)))
319        );
320        assert_eq!(
321            rules.check::<(), ()>(OrderKind::Market, None, dec!(0.004)),
322            Ok(())
323        );
324    }
325
326    #[test]
327    fn a_price_beyond_either_bound_is_refused_with_the_limit() {
328        // On the grid, but beyond a bound.
329        let rules = OrderRules::new(
330            ValueRule::new(dec!(1), dec!(10), dec!(0.5)),
331            ValueRule::default(),
332            ValueRule::default(),
333            ValueRule::default(),
334        );
335        assert_eq!(
336            rules.check::<(), ()>(OrderKind::Limit, Some(dec!(0.5)), dec!(1)),
337            Err(bounds(OrderField::Price, Bound::Min, dec!(1), dec!(0.5)))
338        );
339        assert_eq!(
340            rules.check::<(), ()>(OrderKind::Limit, Some(dec!(10.5)), dec!(1)),
341            Err(bounds(OrderField::Price, Bound::Max, dec!(10), dec!(10.5)))
342        );
343        assert_eq!(
344            rules.check::<(), ()>(OrderKind::Limit, Some(dec!(1)), dec!(1)),
345            Ok(())
346        );
347        assert_eq!(
348            rules.check::<(), ()>(OrderKind::Limit, Some(dec!(10)), dec!(1)),
349            Ok(())
350        );
351    }
352
353    #[test]
354    fn a_quantity_beyond_either_bound_is_refused_and_the_bounds_are_inclusive() {
355        assert_eq!(
356            check(OrderKind::Limit, Some(dec!(50000)), dec!(100.001)),
357            Err(bounds(
358                OrderField::Quantity,
359                Bound::Max,
360                dec!(100),
361                dec!(100.001)
362            ))
363        );
364        assert_eq!(
365            check(OrderKind::Limit, Some(dec!(50000)), dec!(0)),
366            Err(bounds(
367                OrderField::Quantity,
368                Bound::Min,
369                dec!(0.001),
370                dec!(0)
371            ))
372        );
373        assert_eq!(
374            check(OrderKind::Limit, Some(dec!(50000)), dec!(100)),
375            Ok(())
376        );
377        assert_eq!(
378            check(OrderKind::Limit, Some(dec!(50000)), dec!(0.001)),
379            Ok(())
380        );
381    }
382
383    #[test]
384    fn the_market_quantity_rule_applies_to_a_market_order_only() {
385        assert_eq!(
386            check(OrderKind::Market, None, dec!(10.5)),
387            Err(bounds(
388                OrderField::Quantity,
389                Bound::Max,
390                dec!(10),
391                dec!(10.5)
392            ))
393        );
394        assert_eq!(
395            check(OrderKind::Limit, Some(dec!(50000)), dec!(10.5)),
396            Ok(())
397        );
398    }
399
400    #[test]
401    fn a_market_order_also_obeys_the_ordinary_quantity_rule() {
402        assert_eq!(
403            check(OrderKind::Market, None, dec!(0.0005)),
404            Err(off_grid(OrderField::Quantity, dec!(0.0005), dec!(0.001)))
405        );
406    }
407
408    #[test]
409    fn the_notional_of_a_limit_order_is_its_price_times_its_quantity() {
410        assert_eq!(
411            check(OrderKind::Limit, Some(dec!(1000)), dec!(0.004)),
412            Err(bounds(OrderField::Notional, Bound::Min, dec!(5), dec!(4)))
413        );
414        assert_eq!(
415            check(OrderKind::Limit, Some(dec!(1000)), dec!(0.005)),
416            Ok(())
417        );
418        assert_eq!(
419            check(OrderKind::Limit, Some(dec!(1000000)), dec!(10)),
420            Err(bounds(
421                OrderField::Notional,
422                Bound::Max,
423                dec!(9000000),
424                dec!(10000000)
425            ))
426        );
427    }
428
429    #[test]
430    fn a_notional_beyond_the_decimal_range_is_above_the_maximum_not_a_panic() {
431        let rules = OrderRules::new(
432            ValueRule::default(),
433            ValueRule::default(),
434            ValueRule::default(),
435            ValueRule::new(dec!(0), dec!(1), dec!(0)),
436        );
437        assert!(matches!(
438            rules.check::<(), ()>(OrderKind::Limit, Some(Decimal::MAX), Decimal::MAX),
439            Err(OrderError::OutOfBounds(v)) if v.bound == Bound::Max
440        ));
441    }
442
443    #[test]
444    fn a_stop_limit_checks_the_price_the_trigger_and_the_notional() {
445        let kind = |trigger_price| OrderKind::StopLimit { trigger_price };
446        assert_eq!(
447            check(kind(dec!(100.001)), Some(dec!(100)), dec!(1)),
448            Err(off_grid(
449                OrderField::TriggerPrice,
450                dec!(100.001),
451                dec!(0.01)
452            ))
453        );
454        assert_eq!(
455            check(kind(dec!(100)), Some(dec!(100.001)), dec!(1)),
456            Err(off_grid(OrderField::Price, dec!(100.001), dec!(0.01)))
457        );
458        assert_eq!(
459            check(kind(dec!(100)), Some(dec!(1)), dec!(1)),
460            Err(bounds(OrderField::Notional, Bound::Min, dec!(5), dec!(1)))
461        );
462        assert_eq!(check(kind(dec!(100)), Some(dec!(100)), dec!(1)), Ok(()));
463    }
464
465    #[test]
466    fn a_take_profit_limit_is_checked_like_a_stop_limit() {
467        assert_eq!(
468            check(
469                OrderKind::TakeProfitLimit {
470                    trigger_price: dec!(100.001)
471                },
472                Some(dec!(100)),
473                dec!(1)
474            ),
475            Err(off_grid(
476                OrderField::TriggerPrice,
477                dec!(100.001),
478                dec!(0.01)
479            ))
480        );
481    }
482
483    #[test]
484    fn a_plain_stop_checks_its_trigger_and_never_a_notional() {
485        // A 1 USDT order would break the minimum notional if the trigger priced it.
486        for kind in [
487            OrderKind::Stop {
488                trigger_price: dec!(1),
489            },
490            OrderKind::TakeProfit {
491                trigger_price: dec!(1),
492            },
493        ] {
494            assert_eq!(check(kind, None, dec!(1)), Ok(()), "{kind:?}");
495        }
496        assert_eq!(
497            check(
498                OrderKind::Stop {
499                    trigger_price: dec!(1.001)
500                },
501                None,
502                dec!(1)
503            ),
504            Err(off_grid(OrderField::TriggerPrice, dec!(1.001), dec!(0.01)))
505        );
506    }
507
508    #[test]
509    fn a_market_order_never_checks_a_notional_or_a_price() {
510        // A request price on a market order is ignored.
511        assert_eq!(
512            check(OrderKind::Market, Some(dec!(0.001)), dec!(0.001)),
513            Ok(())
514        );
515    }
516
517    #[test]
518    fn a_trailing_stop_checks_its_activation_price_only() {
519        let kind = |activation_price| OrderKind::TrailingStop {
520            offset: dec!(50),
521            offset_type: TrailingOffsetType::BasisPoints,
522            activation_price,
523        };
524        assert_eq!(check(kind(None), None, dec!(1)), Ok(()));
525        assert_eq!(check(kind(Some(dec!(1))), None, dec!(1)), Ok(()));
526        assert_eq!(
527            check(kind(Some(dec!(1.001))), None, dec!(1)),
528            Err(off_grid(OrderField::TriggerPrice, dec!(1.001), dec!(0.01)))
529        );
530    }
531
532    #[test]
533    fn a_fixed_limit_trailing_stop_checks_its_limit_activation_and_notional() {
534        let kind = |activation_price| OrderKind::TrailingStopFixedLimit {
535            offset: dec!(50),
536            offset_type: TrailingOffsetType::BasisPoints,
537            activation_price,
538        };
539        assert_eq!(
540            check(kind(None), Some(dec!(100.001)), dec!(1)),
541            Err(off_grid(OrderField::Price, dec!(100.001), dec!(0.01)))
542        );
543        assert_eq!(
544            check(kind(Some(dec!(1.001))), Some(dec!(100)), dec!(1)),
545            Err(off_grid(OrderField::TriggerPrice, dec!(1.001), dec!(0.01)))
546        );
547        assert_eq!(
548            check(kind(None), Some(dec!(1)), dec!(1)),
549            Err(bounds(OrderField::Notional, Bound::Min, dec!(5), dec!(1)))
550        );
551        assert_eq!(check(kind(None), Some(dec!(100)), dec!(1)), Ok(()));
552    }
553
554    /// An unresolved kind is checked as its family: a limit family's price and notional, and
555    /// any trigger. A stop family ignores a request price.
556    #[test]
557    fn an_unresolved_kind_is_checked_as_its_family() {
558        let kind = |family, trigger_price| OrderKind::ConditionalUnresolved {
559            family,
560            trigger_price,
561        };
562        let stop_limit = |trigger| kind(ConditionalFamily::StopLimit, trigger);
563        assert_eq!(
564            check(stop_limit(Some(dec!(100.001))), Some(dec!(100)), dec!(1)),
565            Err(off_grid(
566                OrderField::TriggerPrice,
567                dec!(100.001),
568                dec!(0.01)
569            ))
570        );
571        assert_eq!(
572            check(stop_limit(None), Some(dec!(100.001)), dec!(1)),
573            Err(off_grid(OrderField::Price, dec!(100.001), dec!(0.01)))
574        );
575        assert_eq!(
576            check(stop_limit(None), Some(dec!(1)), dec!(1)),
577            Err(bounds(OrderField::Notional, Bound::Min, dec!(5), dec!(1)))
578        );
579        assert_eq!(check(stop_limit(None), Some(dec!(100)), dec!(1)), Ok(()));
580        // A stop family carries no limit: an off-grid request price is not read, and no notional.
581        assert_eq!(
582            check(
583                kind(ConditionalFamily::Stop, Some(dec!(1))),
584                Some(dec!(1.001)),
585                dec!(1)
586            ),
587            Ok(())
588        );
589    }
590
591    #[test]
592    fn a_limit_order_without_a_price_skips_the_price_and_notional_checks() {
593        assert_eq!(check(OrderKind::Limit, None, dec!(1)), Ok(()));
594    }
595
596    #[test]
597    fn a_grid_break_is_reported_before_a_range_break_of_the_same_value() {
598        // 0.0005 is below the minimum quantity and off the grid: the grid is reported.
599        assert_eq!(
600            check(OrderKind::Limit, Some(dec!(50000)), dec!(0.0005)),
601            Err(off_grid(OrderField::Quantity, dec!(0.0005), dec!(0.001)))
602        );
603    }
604
605    #[test]
606    fn a_pre_send_rules_deserializes_from_snake_case_and_defaults_to_enforce() {
607        assert_eq!(PreSendRules::default(), PreSendRules::Enforce);
608        assert_eq!(
609            serde_json::from_str::<PreSendRules>("\"off\"").ok(),
610            Some(PreSendRules::Off)
611        );
612    }
613
614    #[test]
615    fn the_refusals_name_the_value_and_the_limit() {
616        let text = |result: Result<(), Error>| match result {
617            Err(OrderError::OutOfBounds(violation)) => violation.to_string(),
618            Err(OrderError::InvalidPrecision(violation)) => violation.to_string(),
619            other => panic!("expected a refusal, got {other:?}"),
620        };
621        assert_eq!(
622            text(check(OrderKind::Limit, Some(dec!(1000)), dec!(0.004))),
623            "notional 4.000 is below the minimum 5"
624        );
625        assert_eq!(
626            text(check(OrderKind::Limit, Some(dec!(1000)), dec!(101))),
627            "quantity 101 is above the maximum 100"
628        );
629        assert_eq!(
630            text(check(OrderKind::Limit, Some(dec!(1000.001)), dec!(1))),
631            "price 1000.001 is not a multiple of 0.01"
632        );
633    }
634}