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 };
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 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 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 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 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 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 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 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 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}