use crate::{
error::{Bound, BoundsViolation, OrderError, OrderField, PrecisionLimit, PrecisionViolation},
order::{OrderKind, TrailingOffsetType},
};
use rust_decimal::Decimal;
use rustrade_instrument::Side;
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PreSendRules {
#[default]
Enforce,
Off,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct ValueRule {
pub min: Option<Decimal>,
pub max: Option<Decimal>,
pub step: Option<Decimal>,
}
impl ValueRule {
pub fn new(min: Decimal, max: Decimal, step: Decimal) -> Self {
let on = |value: Decimal| (value > Decimal::ZERO).then_some(value);
Self {
min: on(min),
max: on(max),
step: on(step),
}
}
pub fn is_off(&self) -> bool {
self.min.is_none() && self.max.is_none() && self.step.is_none()
}
#[must_use]
pub fn round_down(&self, value: Decimal) -> Option<Decimal> {
round_down_to(value, self.grid())
}
#[must_use]
pub fn round_up(&self, value: Decimal) -> Option<Decimal> {
round_up_to(value, self.grid())
}
fn grid(&self) -> Option<Decimal> {
self.step.filter(|step| *step > Decimal::ZERO)
}
fn check<AssetKey, InstrumentKey>(
&self,
field: OrderField,
value: Decimal,
) -> Result<(), OrderError<AssetKey, InstrumentKey>> {
if let Some(step) = self.grid()
&& !(value % step).is_zero()
{
return Err(OrderError::InvalidPrecision(PrecisionViolation::new(
field,
value,
PrecisionLimit::NotOnGrid { step },
)));
}
if let Some(min) = self.min.filter(|min| value < *min) {
return Err(OrderError::OutOfBounds(BoundsViolation::new(
field,
Bound::Min,
min,
value,
)));
}
if let Some(max) = self.max.filter(|max| value > *max) {
return Err(OrderError::OutOfBounds(BoundsViolation::new(
field,
Bound::Max,
max,
value,
)));
}
Ok(())
}
}
fn round_down_to(value: Decimal, step: Option<Decimal>) -> Option<Decimal> {
let Some(step) = step else {
return Some(value);
};
let remainder = value % step;
if remainder.is_zero() {
return Some(value);
}
let toward_zero = value - remainder;
let rounded = if remainder.is_sign_negative() {
toward_zero.checked_sub(step)?
} else {
toward_zero
};
on_grid(rounded, step)
}
fn round_up_to(value: Decimal, step: Option<Decimal>) -> Option<Decimal> {
let Some(step) = step else {
return Some(value);
};
let remainder = value % step;
if remainder.is_zero() {
return Some(value);
}
let toward_zero = value - remainder;
let rounded = if remainder.is_sign_negative() {
toward_zero
} else {
toward_zero.checked_add(step)?
};
on_grid(rounded, step)
}
fn on_grid(value: Decimal, step: Decimal) -> Option<Decimal> {
(value % step).is_zero().then_some(value)
}
fn common_step(a: Decimal, b: Decimal) -> Option<Decimal> {
let scale = a.scale().max(b.scale());
let whole = |step: Decimal| {
10_i128
.checked_pow(scale - step.scale())
.and_then(|factor| step.mantissa().checked_mul(factor))
};
let (a, b) = (whole(a)?, whole(b)?);
let (mut x, mut y) = (a, b);
while y != 0 {
(x, y) = (y, x % y);
}
let lcm = (a / x).checked_mul(b)?;
Decimal::try_from_i128_with_scale(lcm, scale)
.ok()
.map(|step| step.normalize())
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct OrderRules {
pub price: ValueRule,
pub quantity: ValueRule,
pub market_quantity: ValueRule,
pub notional: ValueRule,
#[serde(default)]
pub market_notional: ValueRule,
#[serde(default)]
pub trailing_offset_above: ValueRule,
#[serde(default)]
pub trailing_offset_below: ValueRule,
}
impl OrderRules {
pub fn new(
price: ValueRule,
quantity: ValueRule,
market_quantity: ValueRule,
notional: ValueRule,
) -> Self {
Self {
price,
quantity,
market_quantity,
notional,
market_notional: ValueRule::default(),
trailing_offset_above: ValueRule::default(),
trailing_offset_below: ValueRule::default(),
}
}
#[must_use]
pub fn with_market_notional(self, market_notional: ValueRule) -> Self {
Self {
market_notional,
..self
}
}
#[must_use]
pub fn with_trailing_offsets(self, above: ValueRule, below: ValueRule) -> Self {
Self {
trailing_offset_above: above,
trailing_offset_below: below,
..self
}
}
pub fn is_off(&self) -> bool {
self.price.is_off()
&& self.quantity.is_off()
&& self.market_quantity.is_off()
&& self.notional.is_off()
&& self.market_notional.is_off()
&& self.trailing_offset_above.is_off()
&& self.trailing_offset_below.is_off()
}
#[must_use]
pub fn round_quantity_down(&self, kind: OrderKind, quantity: Decimal) -> Option<Decimal> {
round_down_to(quantity, self.quantity_grid(kind)?)
}
#[must_use]
pub fn round_quantity_up(&self, kind: OrderKind, quantity: Decimal) -> Option<Decimal> {
round_up_to(quantity, self.quantity_grid(kind)?)
}
fn quantity_grid(&self, kind: OrderKind) -> Option<Option<Decimal>> {
let quantity = self.quantity.grid();
if !matches!(kind, OrderKind::Market) {
return Some(quantity);
}
match (quantity, self.market_quantity.grid()) {
(Some(a), Some(b)) => common_step(a, b).map(Some),
(step, None) | (None, step) => Some(step),
}
}
pub fn check<AssetKey, InstrumentKey>(
&self,
side: Side,
kind: OrderKind,
price: Option<Decimal>,
quantity: Decimal,
) -> Result<(), OrderError<AssetKey, InstrumentKey>> {
let (limit_price, trigger_price, has_own_limit) = match kind {
OrderKind::Market => (None, None, false),
OrderKind::Limit => (price, None, true),
OrderKind::Stop { trigger_price } | OrderKind::TakeProfit { trigger_price } => {
(None, Some(trigger_price), false)
}
OrderKind::StopLimit { trigger_price }
| OrderKind::TakeProfitLimit { trigger_price } => (price, Some(trigger_price), true),
OrderKind::TrailingStop {
activation_price, ..
} => (None, activation_price, false),
OrderKind::TrailingStopFixedLimit {
activation_price, ..
} => (price, activation_price, true),
OrderKind::TrailingStopLimit { .. } => (None, None, false),
OrderKind::ConditionalUnresolved {
family,
trigger_price,
} => (
price.filter(|_| family.has_limit_price()),
trigger_price,
family.has_limit_price(),
),
};
if let Some(price) = limit_price {
self.price.check(OrderField::Price, price)?;
}
if let Some(trigger) = trigger_price {
self.price.check(OrderField::TriggerPrice, trigger)?;
}
self.check_trailing_offset(side, kind)?;
self.quantity.check(OrderField::Quantity, quantity)?;
if matches!(kind, OrderKind::Market) {
self.market_quantity.check(OrderField::Quantity, quantity)?;
}
if let Some(price) = limit_price.filter(|_| has_own_limit) {
let notional = price.saturating_mul(quantity);
self.notional.check(OrderField::Notional, notional)?;
}
Ok(())
}
fn check_trailing_offset<AssetKey, InstrumentKey>(
&self,
side: Side,
kind: OrderKind,
) -> Result<(), OrderError<AssetKey, InstrumentKey>> {
let (offset, offset_type) = match kind {
OrderKind::TrailingStop {
offset,
offset_type,
..
}
| OrderKind::TrailingStopLimit {
offset,
offset_type,
..
}
| OrderKind::TrailingStopFixedLimit {
offset,
offset_type,
..
} => (offset, offset_type),
OrderKind::Market
| OrderKind::Limit
| OrderKind::Stop { .. }
| OrderKind::StopLimit { .. }
| OrderKind::TakeProfit { .. }
| OrderKind::TakeProfitLimit { .. }
| OrderKind::ConditionalUnresolved { .. } => return Ok(()),
};
let basis_points = match side {
Side::Buy => &self.trailing_offset_above,
Side::Sell => &self.trailing_offset_below,
};
let percent = |bound: Option<Decimal>| bound.map(|bound| bound / Decimal::ONE_HUNDRED);
let range = match offset_type {
TrailingOffsetType::BasisPoints => ValueRule {
step: None,
..*basis_points
},
TrailingOffsetType::Percentage => ValueRule {
min: percent(basis_points.min),
max: percent(basis_points.max),
step: None,
},
TrailingOffsetType::Absolute => return Ok(()),
};
range.check(OrderField::TrailingOffset, offset)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::order::{ConditionalFamily, TrailingOffsetType};
use rust_decimal_macros::dec;
type Error = OrderError<(), ()>;
fn rules() -> OrderRules {
OrderRules::new(
ValueRule::new(dec!(0.01), dec!(1000000), dec!(0.01)),
ValueRule::new(dec!(0.001), dec!(100), dec!(0.001)),
ValueRule::new(dec!(0), dec!(10), dec!(0)),
ValueRule::new(dec!(5), dec!(9000000), dec!(0)),
)
}
fn check(kind: OrderKind, price: Option<Decimal>, quantity: Decimal) -> Result<(), Error> {
rules().check(Side::Buy, kind, price, quantity)
}
fn bounds(field: OrderField, bound: Bound, limit: Decimal, value: Decimal) -> Error {
OrderError::OutOfBounds(BoundsViolation::new(field, bound, limit, value))
}
fn off_grid(field: OrderField, value: Decimal, step: Decimal) -> Error {
OrderError::InvalidPrecision(PrecisionViolation::new(
field,
value,
PrecisionLimit::NotOnGrid { step },
))
}
#[test]
fn zero_or_negative_means_the_part_is_off() {
let rule = ValueRule::new(dec!(0), dec!(-1), dec!(0));
assert!(rule.is_off());
assert!(OrderRules::default().is_off());
assert!(!rules().is_off());
let on = ValueRule::new(dec!(1), dec!(0), dec!(0));
let off = ValueRule::default();
assert!(!OrderRules::new(on, off, off, off).is_off());
assert!(!OrderRules::new(off, on, off, off).is_off());
assert!(!OrderRules::new(off, off, on, off).is_off());
assert!(!OrderRules::new(off, off, off, on).is_off());
assert_eq!(
OrderRules::default().check::<(), ()>(
Side::Buy,
OrderKind::Limit,
Some(dec!(0.0001)),
dec!(7.77)
),
Ok(())
);
}
#[test]
fn a_step_of_zero_set_by_hand_is_no_grid_not_a_panic() {
let mut rules = OrderRules::default();
rules.quantity.step = Some(Decimal::ZERO);
assert_eq!(
rules.check::<(), ()>(Side::Buy, OrderKind::Market, None, dec!(1.5)),
Ok(())
);
}
#[test]
fn a_limit_order_on_the_grid_and_in_range_passes() {
assert_eq!(
check(OrderKind::Limit, Some(dec!(50000.12)), dec!(0.5)),
Ok(())
);
}
#[test]
fn a_price_off_the_tick_grid_is_refused_with_the_step() {
assert_eq!(
check(OrderKind::Limit, Some(dec!(50000.123)), dec!(0.5)),
Err(off_grid(OrderField::Price, dec!(50000.123), dec!(0.01)))
);
}
#[test]
fn a_quantity_off_the_lot_grid_is_refused_with_the_step() {
assert_eq!(
check(OrderKind::Limit, Some(dec!(50000)), dec!(0.0015)),
Err(off_grid(OrderField::Quantity, dec!(0.0015), dec!(0.001)))
);
}
#[test]
fn a_grid_is_counted_from_zero_not_from_the_minimum() {
let rules = OrderRules::new(
ValueRule::default(),
ValueRule::new(dec!(0.002), dec!(0), dec!(0.002)),
ValueRule::default(),
ValueRule::default(),
);
assert_eq!(
rules.check::<(), ()>(Side::Buy, OrderKind::Market, None, dec!(0.003)),
Err(off_grid(OrderField::Quantity, dec!(0.003), dec!(0.002)))
);
assert_eq!(
rules.check::<(), ()>(Side::Buy, OrderKind::Market, None, dec!(0.004)),
Ok(())
);
}
#[test]
fn a_price_beyond_either_bound_is_refused_with_the_limit() {
let rules = OrderRules::new(
ValueRule::new(dec!(1), dec!(10), dec!(0.5)),
ValueRule::default(),
ValueRule::default(),
ValueRule::default(),
);
assert_eq!(
rules.check::<(), ()>(Side::Buy, OrderKind::Limit, Some(dec!(0.5)), dec!(1)),
Err(bounds(OrderField::Price, Bound::Min, dec!(1), dec!(0.5)))
);
assert_eq!(
rules.check::<(), ()>(Side::Buy, OrderKind::Limit, Some(dec!(10.5)), dec!(1)),
Err(bounds(OrderField::Price, Bound::Max, dec!(10), dec!(10.5)))
);
assert_eq!(
rules.check::<(), ()>(Side::Buy, OrderKind::Limit, Some(dec!(1)), dec!(1)),
Ok(())
);
assert_eq!(
rules.check::<(), ()>(Side::Buy, OrderKind::Limit, Some(dec!(10)), dec!(1)),
Ok(())
);
}
#[test]
fn a_quantity_beyond_either_bound_is_refused_and_the_bounds_are_inclusive() {
assert_eq!(
check(OrderKind::Limit, Some(dec!(50000)), dec!(100.001)),
Err(bounds(
OrderField::Quantity,
Bound::Max,
dec!(100),
dec!(100.001)
))
);
assert_eq!(
check(OrderKind::Limit, Some(dec!(50000)), dec!(0)),
Err(bounds(
OrderField::Quantity,
Bound::Min,
dec!(0.001),
dec!(0)
))
);
assert_eq!(
check(OrderKind::Limit, Some(dec!(50000)), dec!(100)),
Ok(())
);
assert_eq!(
check(OrderKind::Limit, Some(dec!(50000)), dec!(0.001)),
Ok(())
);
}
#[test]
fn the_market_quantity_rule_applies_to_a_market_order_only() {
assert_eq!(
check(OrderKind::Market, None, dec!(10.5)),
Err(bounds(
OrderField::Quantity,
Bound::Max,
dec!(10),
dec!(10.5)
))
);
assert_eq!(
check(OrderKind::Limit, Some(dec!(50000)), dec!(10.5)),
Ok(())
);
}
#[test]
fn a_market_order_also_obeys_the_ordinary_quantity_rule() {
assert_eq!(
check(OrderKind::Market, None, dec!(0.0005)),
Err(off_grid(OrderField::Quantity, dec!(0.0005), dec!(0.001)))
);
}
#[test]
fn the_notional_of_a_limit_order_is_its_price_times_its_quantity() {
assert_eq!(
check(OrderKind::Limit, Some(dec!(1000)), dec!(0.004)),
Err(bounds(OrderField::Notional, Bound::Min, dec!(5), dec!(4)))
);
assert_eq!(
check(OrderKind::Limit, Some(dec!(1000)), dec!(0.005)),
Ok(())
);
assert_eq!(
check(OrderKind::Limit, Some(dec!(1000000)), dec!(10)),
Err(bounds(
OrderField::Notional,
Bound::Max,
dec!(9000000),
dec!(10000000)
))
);
}
#[test]
fn a_notional_beyond_the_decimal_range_is_above_the_maximum_not_a_panic() {
let rules = OrderRules::new(
ValueRule::default(),
ValueRule::default(),
ValueRule::default(),
ValueRule::new(dec!(0), dec!(1), dec!(0)),
);
assert!(matches!(
rules.check::<(), ()>(Side::Buy, OrderKind::Limit, Some(Decimal::MAX), Decimal::MAX),
Err(OrderError::OutOfBounds(v)) if v.bound == Bound::Max
));
}
#[test]
fn a_stop_limit_checks_the_price_the_trigger_and_the_notional() {
let kind = |trigger_price| OrderKind::StopLimit { trigger_price };
assert_eq!(
check(kind(dec!(100.001)), Some(dec!(100)), dec!(1)),
Err(off_grid(
OrderField::TriggerPrice,
dec!(100.001),
dec!(0.01)
))
);
assert_eq!(
check(kind(dec!(100)), Some(dec!(100.001)), dec!(1)),
Err(off_grid(OrderField::Price, dec!(100.001), dec!(0.01)))
);
assert_eq!(
check(kind(dec!(100)), Some(dec!(1)), dec!(1)),
Err(bounds(OrderField::Notional, Bound::Min, dec!(5), dec!(1)))
);
assert_eq!(check(kind(dec!(100)), Some(dec!(100)), dec!(1)), Ok(()));
}
#[test]
fn a_take_profit_limit_is_checked_like_a_stop_limit() {
assert_eq!(
check(
OrderKind::TakeProfitLimit {
trigger_price: dec!(100.001)
},
Some(dec!(100)),
dec!(1)
),
Err(off_grid(
OrderField::TriggerPrice,
dec!(100.001),
dec!(0.01)
))
);
}
#[test]
fn a_plain_stop_checks_its_trigger_and_never_a_notional() {
for kind in [
OrderKind::Stop {
trigger_price: dec!(1),
},
OrderKind::TakeProfit {
trigger_price: dec!(1),
},
] {
assert_eq!(check(kind, None, dec!(1)), Ok(()), "{kind:?}");
}
assert_eq!(
check(
OrderKind::Stop {
trigger_price: dec!(1.001)
},
None,
dec!(1)
),
Err(off_grid(OrderField::TriggerPrice, dec!(1.001), dec!(0.01)))
);
}
#[test]
fn a_market_order_never_checks_a_notional_or_a_price() {
assert_eq!(
check(OrderKind::Market, Some(dec!(0.001)), dec!(0.001)),
Ok(())
);
}
#[test]
fn a_trailing_stop_checks_its_activation_price_only() {
let kind = |activation_price| OrderKind::TrailingStop {
offset: dec!(50),
offset_type: TrailingOffsetType::BasisPoints,
activation_price,
};
assert_eq!(check(kind(None), None, dec!(1)), Ok(()));
assert_eq!(check(kind(Some(dec!(1))), None, dec!(1)), Ok(()));
assert_eq!(
check(kind(Some(dec!(1.001))), None, dec!(1)),
Err(off_grid(OrderField::TriggerPrice, dec!(1.001), dec!(0.01)))
);
}
#[test]
fn a_fixed_limit_trailing_stop_checks_its_limit_activation_and_notional() {
let kind = |activation_price| OrderKind::TrailingStopFixedLimit {
offset: dec!(50),
offset_type: TrailingOffsetType::BasisPoints,
activation_price,
};
assert_eq!(
check(kind(None), Some(dec!(100.001)), dec!(1)),
Err(off_grid(OrderField::Price, dec!(100.001), dec!(0.01)))
);
assert_eq!(
check(kind(Some(dec!(1.001))), Some(dec!(100)), dec!(1)),
Err(off_grid(OrderField::TriggerPrice, dec!(1.001), dec!(0.01)))
);
assert_eq!(
check(kind(None), Some(dec!(1)), dec!(1)),
Err(bounds(OrderField::Notional, Bound::Min, dec!(5), dec!(1)))
);
assert_eq!(check(kind(None), Some(dec!(100)), dec!(1)), Ok(()));
}
#[test]
fn an_unresolved_kind_is_checked_as_its_family() {
let kind = |family, trigger_price| OrderKind::ConditionalUnresolved {
family,
trigger_price,
};
let stop_limit = |trigger| kind(ConditionalFamily::StopLimit, trigger);
assert_eq!(
check(stop_limit(Some(dec!(100.001))), Some(dec!(100)), dec!(1)),
Err(off_grid(
OrderField::TriggerPrice,
dec!(100.001),
dec!(0.01)
))
);
assert_eq!(
check(stop_limit(None), Some(dec!(100.001)), dec!(1)),
Err(off_grid(OrderField::Price, dec!(100.001), dec!(0.01)))
);
assert_eq!(
check(stop_limit(None), Some(dec!(1)), dec!(1)),
Err(bounds(OrderField::Notional, Bound::Min, dec!(5), dec!(1)))
);
assert_eq!(check(stop_limit(None), Some(dec!(100)), dec!(1)), Ok(()));
assert_eq!(
check(
kind(ConditionalFamily::Stop, Some(dec!(1))),
Some(dec!(1.001)),
dec!(1)
),
Ok(())
);
}
#[test]
fn a_limit_order_without_a_price_skips_the_price_and_notional_checks() {
assert_eq!(check(OrderKind::Limit, None, dec!(1)), Ok(()));
}
#[test]
fn a_grid_break_is_reported_before_a_range_break_of_the_same_value() {
assert_eq!(
check(OrderKind::Limit, Some(dec!(50000)), dec!(0.0005)),
Err(off_grid(OrderField::Quantity, dec!(0.0005), dec!(0.001)))
);
}
#[test]
fn a_pre_send_rules_deserializes_from_snake_case_and_defaults_to_enforce() {
assert_eq!(PreSendRules::default(), PreSendRules::Enforce);
assert_eq!(
serde_json::from_str::<PreSendRules>("\"off\"").ok(),
Some(PreSendRules::Off)
);
}
#[test]
fn the_refusals_name_the_value_and_the_limit() {
let text = |result: Result<(), Error>| match result {
Err(OrderError::OutOfBounds(violation)) => violation.to_string(),
Err(OrderError::InvalidPrecision(violation)) => violation.to_string(),
other => panic!("expected a refusal, got {other:?}"),
};
assert_eq!(
text(check(OrderKind::Limit, Some(dec!(1000)), dec!(0.004))),
"notional 4.000 is below the minimum 5"
);
assert_eq!(
text(check(OrderKind::Limit, Some(dec!(1000)), dec!(101))),
"quantity 101 is above the maximum 100"
);
assert_eq!(
text(check(OrderKind::Limit, Some(dec!(1000.001)), dec!(1))),
"price 1000.001 is not a multiple of 0.01"
);
}
fn stepped(step: Decimal) -> ValueRule {
ValueRule::new(dec!(0), dec!(0), step)
}
#[test]
fn a_value_on_the_grid_rounds_to_itself_either_way() {
for (step, value) in [
(dec!(0.01), dec!(50000.12)),
(dec!(0.05), dec!(1.15)),
(dec!(0.05), dec!(-1.15)),
(dec!(0.001), dec!(0)),
] {
assert_eq!(
stepped(step).round_down(value),
Some(value),
"{step} {value}"
);
assert_eq!(stepped(step).round_up(value), Some(value), "{step} {value}");
}
}
#[test]
fn a_value_off_the_grid_rounds_to_the_neighbour_below_or_above() {
let rule = stepped(dec!(0.01));
assert_eq!(rule.round_down(dec!(50000.129)), Some(dec!(50000.12)));
assert_eq!(rule.round_up(dec!(50000.121)), Some(dec!(50000.13)));
}
#[test]
fn a_step_that_is_not_a_power_of_ten_rounds_to_its_own_multiples() {
let rule = stepped(dec!(0.05));
assert_eq!(rule.round_down(dec!(1.07)), Some(dec!(1.05)));
assert_eq!(rule.round_up(dec!(1.07)), Some(dec!(1.10)));
assert_eq!(rule.round_down(dec!(0.04)), Some(dec!(0)));
assert_eq!(rule.round_up(dec!(0.01)), Some(dec!(0.05)));
assert_eq!(
rule.round_down(dec!(0.19000))
.map(|value| value.to_string()),
Some("0.15000".to_owned())
);
assert_eq!(
stepped(dec!(0.005))
.round_down(dec!(0.10))
.map(|value| value.to_string()),
Some("0.10".to_owned())
);
}
#[test]
fn a_negative_value_rounds_down_away_from_zero_and_up_toward_it() {
let rule = stepped(dec!(0.05));
assert_eq!(rule.round_down(dec!(-1.07)), Some(dec!(-1.10)));
assert_eq!(rule.round_up(dec!(-1.07)), Some(dec!(-1.05)));
assert_eq!(rule.round_up(dec!(-0.01)), Some(dec!(0)));
}
#[test]
fn with_no_step_a_value_comes_back_unchanged() {
let mut rule = ValueRule::new(dec!(1), dec!(10), dec!(0));
assert_eq!(rule.round_down(dec!(1.2345)), Some(dec!(1.2345)));
assert_eq!(rule.round_up(dec!(1.2345)), Some(dec!(1.2345)));
rule.step = Some(Decimal::ZERO);
assert_eq!(rule.round_down(dec!(1.2345)), Some(dec!(1.2345)));
}
#[test]
fn rounding_does_not_clamp_to_the_range() {
let rule = ValueRule::new(dec!(0.002), dec!(1), dec!(0.002));
assert_eq!(rule.round_down(dec!(0.001)), Some(dec!(0)));
assert_eq!(rule.round_up(dec!(1.001)), Some(dec!(1.002)));
}
#[test]
fn a_result_beyond_the_decimal_range_is_none_not_a_panic() {
let rule = stepped(dec!(2));
assert_eq!(rule.round_up(Decimal::MAX), None);
assert_eq!(rule.round_down(Decimal::MAX), Some(Decimal::MAX - dec!(1)));
assert_eq!(rule.round_down(Decimal::MIN), None);
assert_eq!(rule.round_up(Decimal::MIN), Some(Decimal::MIN + dec!(1)));
}
#[test]
fn a_result_that_needs_more_digits_than_a_decimal_holds_is_none() {
let rule = stepped(dec!(0.3));
assert_eq!(rule.round_down(Decimal::MAX), Some(Decimal::MAX));
assert_eq!(rule.round_down(Decimal::MAX - dec!(1)), None);
assert_eq!(rule.round_up(Decimal::MAX - dec!(1)), None);
}
fn two_quantity_steps() -> OrderRules {
OrderRules::new(
ValueRule::default(),
stepped(dec!(0.05)),
stepped(dec!(0.02)),
ValueRule::default(),
)
}
#[test]
fn a_market_quantity_rounds_to_the_grid_both_quantity_steps_share() {
let rules = two_quantity_steps();
assert_eq!(
rules.round_quantity_down(OrderKind::Market, dec!(0.19)),
Some(dec!(0.1))
);
assert_eq!(
rules.round_quantity_up(OrderKind::Market, dec!(0.11)),
Some(dec!(0.2))
);
assert_eq!(
rules.check::<(), ()>(Side::Buy, OrderKind::Market, None, dec!(0.1)),
Ok(())
);
}
#[test]
fn any_other_quantity_rounds_to_the_quantity_step_alone() {
let rules = two_quantity_steps();
assert_eq!(
rules.round_quantity_down(OrderKind::Limit, dec!(0.19)),
Some(dec!(0.15))
);
assert_eq!(
rules.round_quantity_up(
OrderKind::Stop {
trigger_price: dec!(1)
},
dec!(0.11)
),
Some(dec!(0.15))
);
}
#[test]
fn a_market_quantity_with_one_step_off_rounds_to_the_other() {
let quantity_only = OrderRules::new(
ValueRule::default(),
stepped(dec!(0.05)),
ValueRule::default(),
ValueRule::default(),
);
assert_eq!(
quantity_only.round_quantity_down(OrderKind::Market, dec!(0.19)),
Some(dec!(0.15))
);
let market_only = OrderRules::new(
ValueRule::default(),
ValueRule::default(),
stepped(dec!(0.02)),
ValueRule::default(),
);
assert_eq!(
market_only.round_quantity_down(OrderKind::Market, dec!(0.19)),
Some(dec!(0.18))
);
assert_eq!(
OrderRules::default().round_quantity_up(OrderKind::Market, dec!(0.19)),
Some(dec!(0.19))
);
}
#[test]
fn the_shared_grid_is_the_least_common_multiple_of_the_steps() {
assert_eq!(common_step(dec!(0.05), dec!(0.02)), Some(dec!(0.1)));
assert_eq!(common_step(dec!(0.001), dec!(0.01)), Some(dec!(0.01)));
assert_eq!(common_step(dec!(0.01), dec!(0.001)), Some(dec!(0.01)));
assert_eq!(common_step(dec!(0.4), dec!(0.6)), Some(dec!(1.2)));
assert_eq!(common_step(dec!(0.4), dec!(6)), Some(dec!(6)));
assert_eq!(common_step(dec!(3), dec!(0.5)), Some(dec!(3)));
assert_eq!(common_step(dec!(0.25), dec!(0.25)), Some(dec!(0.25)));
}
#[test]
fn steps_whose_shared_grid_a_decimal_cannot_hold_round_to_none() {
let a = Decimal::from_i128_with_scale(7_922_816_251_426_433_759_354_395_033, 27);
let b = Decimal::from_i128_with_scale(7_922_816_251_426_433_759_354_395_031, 27);
assert_eq!(common_step(a, b), None);
let rules = OrderRules::new(
ValueRule::default(),
stepped(a),
stepped(b),
ValueRule::default(),
);
assert_eq!(rules.round_quantity_down(OrderKind::Market, dec!(1)), None);
assert_eq!(rules.round_quantity_up(OrderKind::Market, dec!(1)), None);
assert_eq!(rules.round_quantity_down(OrderKind::Limit, a), Some(a));
}
fn trailing_rules() -> OrderRules {
OrderRules::default().with_trailing_offsets(
ValueRule::new(dec!(10), dec!(2000), dec!(0)),
ValueRule::new(dec!(20), dec!(1000), dec!(0)),
)
}
fn trailing_stop(offset: Decimal, offset_type: TrailingOffsetType) -> OrderKind {
OrderKind::TrailingStop {
offset,
offset_type,
activation_price: None,
}
}
#[test]
fn a_buy_trailing_offset_is_checked_against_the_range_above_and_a_sell_against_below() {
let check = |side, offset| {
trailing_rules().check::<(), ()>(
side,
trailing_stop(offset, TrailingOffsetType::BasisPoints),
None,
dec!(1),
)
};
assert_eq!(check(Side::Buy, dec!(15)), Ok(()));
assert_eq!(
check(Side::Sell, dec!(15)),
Err(bounds(
OrderField::TrailingOffset,
Bound::Min,
dec!(20),
dec!(15)
))
);
assert_eq!(check(Side::Buy, dec!(1500)), Ok(()));
assert_eq!(
check(Side::Sell, dec!(1500)),
Err(bounds(
OrderField::TrailingOffset,
Bound::Max,
dec!(1000),
dec!(1500)
))
);
assert_eq!(
check(Side::Buy, dec!(9)),
Err(bounds(
OrderField::TrailingOffset,
Bound::Min,
dec!(10),
dec!(9)
))
);
assert_eq!(
check(Side::Buy, dec!(2001)),
Err(bounds(
OrderField::TrailingOffset,
Bound::Max,
dec!(2000),
dec!(2001)
))
);
for (side, offset) in [
(Side::Buy, dec!(10)),
(Side::Buy, dec!(2000)),
(Side::Sell, dec!(20)),
(Side::Sell, dec!(1000)),
] {
assert_eq!(check(side, offset), Ok(()), "{side:?} {offset}");
}
}
#[test]
fn a_percentage_offset_is_checked_and_reported_in_percent() {
let check = |offset| {
trailing_rules().check::<(), ()>(
Side::Buy,
trailing_stop(offset, TrailingOffsetType::Percentage),
None,
dec!(1),
)
};
assert_eq!(check(dec!(0.1)), Ok(()));
assert_eq!(check(dec!(20)), Ok(()));
assert_eq!(
check(dec!(0.09)),
Err(bounds(
OrderField::TrailingOffset,
Bound::Min,
dec!(0.1),
dec!(0.09)
))
);
assert_eq!(
check(dec!(20.5)),
Err(bounds(
OrderField::TrailingOffset,
Bound::Max,
dec!(20),
dec!(20.5)
))
);
}
#[test]
fn an_absolute_offset_is_not_checked() {
assert_eq!(
trailing_rules().check::<(), ()>(
Side::Buy,
trailing_stop(dec!(0.0001), TrailingOffsetType::Absolute),
None,
dec!(1),
),
Ok(())
);
}
#[test]
fn every_trailing_kind_checks_its_offset() {
let offset = dec!(5);
for kind in [
OrderKind::TrailingStopLimit {
offset,
offset_type: TrailingOffsetType::BasisPoints,
stop_price: dec!(100),
limit_offset: dec!(1),
},
OrderKind::TrailingStopFixedLimit {
offset,
offset_type: TrailingOffsetType::BasisPoints,
activation_price: None,
},
] {
assert_eq!(
trailing_rules().check::<(), ()>(Side::Sell, kind, Some(dec!(100)), dec!(1)),
Err(bounds(
OrderField::TrailingOffset,
Bound::Min,
dec!(20),
offset
)),
"{kind:?}"
);
}
}
#[test]
fn a_trailing_offset_is_checked_after_the_prices_and_before_the_quantity() {
let rules = rules().with_trailing_offsets(
ValueRule::new(dec!(10), dec!(0), dec!(0)),
ValueRule::default(),
);
let kind = |activation_price| OrderKind::TrailingStop {
offset: dec!(1),
offset_type: TrailingOffsetType::BasisPoints,
activation_price,
};
assert_eq!(
rules.check::<(), ()>(Side::Buy, kind(Some(dec!(1.001))), None, dec!(0.0005)),
Err(off_grid(OrderField::TriggerPrice, dec!(1.001), dec!(0.01)))
);
assert_eq!(
rules.check::<(), ()>(Side::Buy, kind(None), None, dec!(0.0005)),
Err(bounds(
OrderField::TrailingOffset,
Bound::Min,
dec!(10),
dec!(1)
))
);
}
#[test]
fn a_trailing_rules_step_is_not_used() {
let rules = OrderRules::default().with_trailing_offsets(
ValueRule::new(dec!(0), dec!(0), dec!(10)),
ValueRule::new(dec!(0), dec!(0), dec!(10)),
);
for side in [Side::Buy, Side::Sell] {
assert_eq!(
rules.check::<(), ()>(
side,
trailing_stop(dec!(15), TrailingOffsetType::BasisPoints),
None,
dec!(1)
),
Ok(()),
"{side:?}"
);
}
}
#[test]
fn the_trailing_ranges_leave_other_kinds_alone() {
let rules = OrderRules::default().with_trailing_offsets(
ValueRule::new(dec!(10), dec!(20), dec!(0)),
ValueRule::new(dec!(10), dec!(20), dec!(0)),
);
for kind in [
OrderKind::Market,
OrderKind::Limit,
OrderKind::Stop {
trigger_price: dec!(1),
},
OrderKind::ConditionalUnresolved {
family: ConditionalFamily::Stop,
trigger_price: None,
},
] {
assert_eq!(
rules.check::<(), ()>(Side::Sell, kind, Some(dec!(1)), dec!(1)),
Ok(()),
"{kind:?}"
);
}
}
#[test]
fn the_market_notional_is_never_checked() {
let rules =
OrderRules::default().with_market_notional(ValueRule::new(dec!(5), dec!(10), dec!(0)));
for (kind, price) in [
(OrderKind::Market, Some(dec!(1))),
(OrderKind::Limit, Some(dec!(1))),
] {
assert_eq!(
rules.check::<(), ()>(Side::Buy, kind, price, dec!(1)),
Ok(()),
"{kind:?}"
);
}
}
#[test]
fn the_new_rules_count_towards_being_on_and_new_leaves_them_off() {
let on = ValueRule::new(dec!(1), dec!(0), dec!(0));
let off = ValueRule::default();
assert!(!OrderRules::default().with_market_notional(on).is_off());
assert!(
!OrderRules::default()
.with_trailing_offsets(on, off)
.is_off()
);
assert!(
!OrderRules::default()
.with_trailing_offsets(off, on)
.is_off()
);
let built = rules();
assert!(built.market_notional.is_off());
assert!(built.trailing_offset_above.is_off());
assert!(built.trailing_offset_below.is_off());
let with = built
.with_market_notional(on)
.with_trailing_offsets(on, off);
assert_eq!(with.price, built.price);
assert_eq!(with.notional, built.notional);
assert_eq!(with.market_notional, on);
assert_eq!(with.trailing_offset_above, on);
assert_eq!(with.trailing_offset_below, off);
}
#[test]
fn rules_serialised_without_the_newer_parts_load_with_them_off() {
let rule = r#"{"min":"1","max":null,"step":null}"#;
let json = format!(
r#"{{"price":{rule},"quantity":{rule},"market_quantity":{rule},"notional":{rule}}}"#
);
let loaded: OrderRules = match serde_json::from_str(&json) {
Ok(loaded) => loaded,
Err(error) => panic!("older rules should load: {error}"),
};
let on = ValueRule::new(dec!(1), dec!(0), dec!(0));
assert_eq!(loaded, OrderRules::new(on, on, on, on));
}
}