1use crate::{
15 error::{Bound, BoundsViolation, OrderError, OrderField, PrecisionLimit, PrecisionViolation},
16 order::OrderKind,
17};
18use rust_decimal::Decimal;
19use serde::{Deserialize, Serialize};
20
21#[non_exhaustive]
23#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum PreSendRules {
26 #[default]
29 Enforce,
30 Off,
33}
34
35#[non_exhaustive]
40#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
41pub struct ValueRule {
42 pub min: Option<Decimal>,
44 pub max: Option<Decimal>,
46 pub step: Option<Decimal>,
48}
49
50impl ValueRule {
51 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 pub fn is_off(&self) -> bool {
63 self.min.is_none() && self.max.is_none() && self.step.is_none()
64 }
65
66 fn check<AssetKey, InstrumentKey>(
69 &self,
70 field: OrderField,
71 value: Decimal,
72 ) -> Result<(), OrderError<AssetKey, InstrumentKey>> {
73 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#[non_exhaustive]
108#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
109pub struct OrderRules {
110 pub price: ValueRule,
113 pub quantity: ValueRule,
115 pub market_quantity: ValueRule,
117 pub notional: ValueRule,
119}
120
121impl OrderRules {
122 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 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 pub fn check<AssetKey, InstrumentKey>(
166 &self,
167 kind: OrderKind,
168 price: Option<Decimal>,
169 quantity: Decimal,
170 ) -> Result<(), OrderError<AssetKey, InstrumentKey>> {
171 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 OrderKind::TrailingStopLimit { .. } => (None, None, false),
189 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 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 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 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 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 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 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 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 #[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 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 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}