use super::ExecutionVenue;
use crate::Dir;
use derive_more::Display;
use rust_decimal::Decimal;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use strum_macros::{EnumString, IntoStaticStr};
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ExecutionInfo {
pub execution_venue: ExecutionVenue,
pub exchange_symbol: Option<String>,
pub tick_size: TickSize,
pub step_size: Decimal,
pub min_order_quantity: Decimal,
pub min_order_quantity_unit: MinOrderQuantityUnit,
pub is_delisted: bool,
pub initial_margin: Option<Decimal>,
pub maintenance_margin: Option<Decimal>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TickSize {
#[schemars(title = "Simple|Decimal")]
Simple(Decimal),
#[schemars(title = "Varying")]
Varying { thresholds: Vec<(Decimal, Decimal)> },
}
impl TickSize {
pub fn simple(tick_size: Decimal) -> Self {
Self::Simple(tick_size)
}
pub fn increment(&self, mut price: Decimal, mut n: i32) -> Option<Decimal> {
if n == 0 {
return Some(price);
} else if n < 0 {
return self.decrement(price, -n);
}
let thresholds = match self {
TickSize::Simple(step) => {
return Some(price + *step * Decimal::from(n));
}
TickSize::Varying { thresholds } => thresholds,
};
if thresholds.is_empty() {
return None;
}
let mut i = thresholds.len() - 1;
let mut t_bound;
let mut t_step;
let mut u_bound;
loop {
(t_bound, t_step) = thresholds[i];
u_bound = thresholds.get(i + 1).map(|(b, _)| *b);
if price >= t_bound {
break;
}
if i == 0 {
return None;
}
i -= 1;
}
while n > 0 {
price += t_step;
if u_bound.is_some_and(|u| price >= u) {
i += 1;
(_, t_step) = thresholds[i];
u_bound = thresholds.get(i + 1).map(|(b, _)| *b);
}
n -= 1;
}
Some(price)
}
pub fn decrement(&self, mut price: Decimal, mut n: i32) -> Option<Decimal> {
if n == 0 {
return Some(price);
} else if n < 0 {
return self.increment(price, -n);
}
let thresholds = match self {
TickSize::Simple(step) => {
return Some(price - *step * Decimal::from(n));
}
TickSize::Varying { thresholds } => thresholds,
};
if thresholds.is_empty() {
return None;
}
let mut i = thresholds.len() - 1;
let mut t_bound;
let mut t_step;
loop {
(t_bound, t_step) = thresholds[i];
if price >= t_bound {
break;
}
if i == 0 {
return None;
}
i -= 1;
}
while n > 0 {
if price == t_bound {
if i == 0 {
return None;
}
i -= 1;
(t_bound, t_step) = thresholds[i];
}
price -= t_step;
n -= 1;
}
Some(price)
}
pub fn round_aggressive(&self, price: Decimal, dir: Dir) -> Option<Decimal> {
match dir {
Dir::Buy => self.round_up(price),
Dir::Sell => self.round_down(price),
}
}
pub fn round_passive(&self, price: Decimal, dir: Dir) -> Option<Decimal> {
match dir {
Dir::Buy => self.round_down(price),
Dir::Sell => self.round_up(price),
}
}
pub fn round_up(&self, price: Decimal) -> Option<Decimal> {
match self {
TickSize::Simple(tick_size) => {
if tick_size.is_zero() {
return None;
}
let remainder = price % tick_size;
if remainder.is_zero() {
Some(price)
} else if remainder > Decimal::ZERO {
Some(price - remainder + tick_size)
} else {
Some(price - remainder)
}
}
TickSize::Varying { thresholds } => {
if thresholds.is_empty() {
return None;
}
let mut tick_size = thresholds[0].1;
for (threshold, size) in thresholds {
if price >= *threshold {
tick_size = *size;
} else {
break;
}
}
if tick_size.is_zero() {
return None;
}
let remainder = price % tick_size;
if remainder.is_zero() {
Some(price)
} else if remainder > Decimal::ZERO {
Some(price - remainder + tick_size)
} else {
Some(price - remainder)
}
}
}
}
pub fn round_down(&self, price: Decimal) -> Option<Decimal> {
match self {
TickSize::Simple(tick_size) => {
if tick_size.is_zero() {
return None;
}
let remainder = price % tick_size;
if remainder.is_zero() {
Some(price)
} else if remainder > Decimal::ZERO {
Some(price - remainder)
} else {
Some(price - remainder - tick_size)
}
}
TickSize::Varying { thresholds } => {
if thresholds.is_empty() {
return None;
}
let mut tick_size = thresholds[0].1;
for (threshold, size) in thresholds {
if price >= *threshold {
tick_size = *size;
} else {
break;
}
}
if tick_size.is_zero() {
return None;
}
let remainder = price % tick_size;
if remainder.is_zero() {
Some(price)
} else if remainder > Decimal::ZERO {
Some(price - remainder)
} else {
Some(price - remainder - tick_size)
}
}
}
}
pub fn signed_tick_distance(&self, from: Decimal, to: Decimal) -> Option<Decimal> {
if from == to {
return Some(Decimal::ZERO);
} else if from > to {
return self.signed_tick_distance(to, from).map(|d| -d);
}
match self {
TickSize::Simple(step) => {
if step.is_zero() {
None
} else {
Some((to - from) / *step)
}
}
TickSize::Varying { thresholds } => {
let mut ticks = Decimal::ZERO;
let mut price = from;
let mut i = thresholds.iter();
let mut step = None;
while let Some((lower, lower_step)) = i.next() {
if price >= *lower {
step = Some(*lower_step);
break;
}
}
let mut step = step?; loop {
if step.is_zero() {
return None;
}
match i.next() {
Some((upper, next_step)) => {
if to >= *upper {
ticks += (upper - price) / step;
price = *upper;
step = *next_step;
} else {
ticks += (to - price) / step;
break;
}
}
None => {
ticks += (to - price) / step;
break;
}
}
}
Some(ticks)
}
}
}
}
impl ExecutionInfo {
pub fn min_quantity_in_base_units(&self, price: Option<Decimal>) -> Option<Decimal> {
match self.min_order_quantity_unit {
MinOrderQuantityUnit::Base => Some(self.min_order_quantity),
MinOrderQuantityUnit::Quote => {
if let Some(p) = price {
if p.is_zero() {
None
} else {
Some(self.min_order_quantity / p)
}
} else {
None
}
}
}
}
pub fn round_quantity(&self, quantity: Decimal) -> Decimal {
round_quantity_with_step(quantity, self.step_size)
}
pub fn round_quantity_up(&self, quantity: Decimal) -> Decimal {
round_quantity_up_with_step(quantity, self.step_size)
}
pub fn round_quantity_down(&self, quantity: Decimal) -> Decimal {
round_quantity_down_with_step(quantity, self.step_size)
}
}
pub fn round_quantity_with_step(quantity: Decimal, step_size: Decimal) -> Decimal {
if step_size.is_zero() {
return quantity;
}
let remainder = quantity % step_size;
if remainder.is_zero() {
quantity
} else {
let half_step = step_size / Decimal::from(2);
if remainder >= half_step {
quantity - remainder + step_size
} else {
quantity - remainder
}
}
}
pub fn round_quantity_up_with_step(quantity: Decimal, step_size: Decimal) -> Decimal {
if step_size.is_zero() {
return quantity;
}
let remainder = quantity % step_size;
if remainder.is_zero() {
quantity
} else {
quantity - remainder + step_size
}
}
pub fn round_quantity_down_with_step(quantity: Decimal, step_size: Decimal) -> Decimal {
if step_size.is_zero() {
return quantity;
}
let remainder = quantity % step_size;
if remainder.is_zero() {
quantity
} else {
quantity - remainder
}
}
#[derive(
Default,
Debug,
Display,
Clone,
Copy,
EnumString,
IntoStaticStr,
Serialize,
Deserialize,
JsonSchema,
)]
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
#[serde(tag = "unit", rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum MinOrderQuantityUnit {
#[default]
#[schemars(title = "Base")]
Base,
#[schemars(title = "Quote")]
Quote,
}
#[cfg(feature = "postgres")]
crate::to_sql_str!(MinOrderQuantityUnit);
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_round_quantity() {
let step_size = dec!(0.1);
assert_eq!(round_quantity_with_step(dec!(1.24), step_size), dec!(1.2));
assert_eq!(round_quantity_with_step(dec!(1.25), step_size), dec!(1.3));
assert_eq!(round_quantity_with_step(dec!(0.04), step_size), dec!(0.0));
assert_eq!(round_quantity_up_with_step(dec!(1.21), step_size), dec!(1.3));
assert_eq!(round_quantity_up_with_step(dec!(0.01), step_size), dec!(0.1));
assert_eq!(round_quantity_up_with_step(dec!(0.0), step_size), dec!(0.0));
assert_eq!(round_quantity_down_with_step(dec!(1.29), step_size), dec!(1.2));
assert_eq!(round_quantity_down_with_step(dec!(0.09), step_size), dec!(0.0));
assert_eq!(round_quantity_down_with_step(dec!(0.0), step_size), dec!(0.0));
}
#[test]
fn test_tick_size_decrement() {
let tick = TickSize::Simple(dec!(0.01));
assert_eq!(tick.increment(dec!(100.00), -1), Some(dec!(99.99)));
let tick_varying = TickSize::Varying {
thresholds: vec![
(dec!(0), dec!(0.01)),
(dec!(100), dec!(0.05)),
(dec!(500), dec!(0.10)),
],
};
assert_eq!(tick_varying.increment(dec!(100.00), -1), Some(dec!(99.99)));
assert_eq!(tick_varying.increment(dec!(100.05), -1), Some(dec!(100)));
assert_eq!(tick_varying.increment(dec!(500.00), -1), Some(dec!(499.95)));
}
#[test]
fn test_tick_size_zero() {
let tick = TickSize::Varying {
thresholds: vec![
(dec!(0), dec!(0.01)),
(dec!(100), dec!(0.05)),
(dec!(500), dec!(0.10)),
],
};
assert_eq!(tick.increment(dec!(100.00), 0), Some(dec!(100.00)));
assert_eq!(tick.increment(dec!(150.00), 0), Some(dec!(150.00)));
assert_eq!(tick.increment(dec!(50.00), 0), Some(dec!(50.00)));
}
#[test]
fn test_tick_size_boundary_behavior() {
let tick = TickSize::Varying {
thresholds: vec![
(dec!(0), dec!(0.01)),
(dec!(100), dec!(0.05)),
(dec!(500), dec!(0.10)),
],
};
assert_eq!(tick.increment(dec!(100.00), 1), Some(dec!(100.05)));
assert_eq!(tick.increment(dec!(500.00), 1), Some(dec!(500.10)));
assert_eq!(tick.increment(dec!(100.00), -1), Some(dec!(99.99)));
assert_eq!(tick.increment(dec!(500.00), -1), Some(dec!(499.95)));
}
#[test]
fn test_tick_size_multiple_crossings() {
let tick = TickSize::Varying {
thresholds: vec![
(dec!(0), dec!(0.01)),
(dec!(100), dec!(0.05)),
(dec!(500), dec!(0.10)),
],
};
let result = tick.increment(dec!(50), 12000);
assert_eq!(result, Some(dec!(450)));
let result = tick.increment(dec!(600), -5100);
assert_eq!(result, Some(dec!(295.00)));
}
#[test]
fn test_round_up_simple() {
let tick = TickSize::Simple(dec!(0.01));
assert_eq!(tick.round_up(dec!(100.00)), Some(dec!(100.00)));
assert_eq!(tick.round_up(dec!(100.001)), Some(dec!(100.01)));
assert_eq!(tick.round_up(dec!(100.005)), Some(dec!(100.01)));
assert_eq!(tick.round_up(dec!(100.009)), Some(dec!(100.01)));
assert_eq!(tick.round_up(dec!(-99.995)), Some(dec!(-99.99)));
assert_eq!(tick.round_up(dec!(-100.00)), Some(dec!(-100.00)));
let zero_tick = TickSize::Simple(dec!(0));
assert_eq!(zero_tick.round_up(dec!(100.123)), None);
}
#[test]
fn test_round_down_simple() {
let tick = TickSize::Simple(dec!(0.01));
assert_eq!(tick.round_down(dec!(100.00)), Some(dec!(100.00)));
assert_eq!(tick.round_down(dec!(100.001)), Some(dec!(100.00)));
assert_eq!(tick.round_down(dec!(100.005)), Some(dec!(100.00)));
assert_eq!(tick.round_down(dec!(100.009)), Some(dec!(100.00)));
assert_eq!(tick.round_down(dec!(-99.995)), Some(dec!(-100.00)));
assert_eq!(tick.round_down(dec!(-100.00)), Some(dec!(-100.00)));
let zero_tick = TickSize::Simple(dec!(0));
assert_eq!(zero_tick.round_down(dec!(100.123)), None);
}
#[test]
fn test_round_up_varying() {
let tick = TickSize::Varying {
thresholds: vec![
(dec!(0), dec!(0.01)),
(dec!(100), dec!(0.05)),
(dec!(500), dec!(0.10)),
],
};
assert_eq!(tick.round_up(dec!(50.00)), Some(dec!(50.00)));
assert_eq!(tick.round_up(dec!(50.001)), Some(dec!(50.01)));
assert_eq!(tick.round_up(dec!(99.996)), Some(dec!(100.00)));
assert_eq!(tick.round_up(dec!(100.00)), Some(dec!(100.00)));
assert_eq!(tick.round_up(dec!(100.01)), Some(dec!(100.05)));
assert_eq!(tick.round_up(dec!(100.03)), Some(dec!(100.05)));
assert_eq!(tick.round_up(dec!(100.05)), Some(dec!(100.05)));
assert_eq!(tick.round_up(dec!(500.00)), Some(dec!(500.00)));
assert_eq!(tick.round_up(dec!(500.01)), Some(dec!(500.10)));
assert_eq!(tick.round_up(dec!(500.05)), Some(dec!(500.10)));
assert_eq!(tick.round_up(dec!(500.09)), Some(dec!(500.10)));
assert_eq!(tick.round_up(dec!(500.10)), Some(dec!(500.10)));
}
#[test]
fn test_round_down_varying() {
let tick = TickSize::Varying {
thresholds: vec![
(dec!(0), dec!(0.01)),
(dec!(100), dec!(0.05)),
(dec!(500), dec!(0.10)),
],
};
assert_eq!(tick.round_down(dec!(50.00)), Some(dec!(50.00)));
assert_eq!(tick.round_down(dec!(50.001)), Some(dec!(50.00)));
assert_eq!(tick.round_down(dec!(99.999)), Some(dec!(99.99)));
assert_eq!(tick.round_down(dec!(100.00)), Some(dec!(100.00)));
assert_eq!(tick.round_down(dec!(100.01)), Some(dec!(100.00)));
assert_eq!(tick.round_down(dec!(100.04)), Some(dec!(100.00)));
assert_eq!(tick.round_down(dec!(100.05)), Some(dec!(100.05)));
assert_eq!(tick.round_down(dec!(100.09)), Some(dec!(100.05)));
assert_eq!(tick.round_down(dec!(500.00)), Some(dec!(500.00)));
assert_eq!(tick.round_down(dec!(500.01)), Some(dec!(500.00)));
assert_eq!(tick.round_down(dec!(500.09)), Some(dec!(500.00)));
assert_eq!(tick.round_down(dec!(500.10)), Some(dec!(500.10)));
assert_eq!(tick.round_down(dec!(500.19)), Some(dec!(500.10)));
}
#[test]
fn test_round_aggressive() {
let tick = TickSize::Simple(dec!(0.01));
assert_eq!(tick.round_aggressive(dec!(100.001), Dir::Buy), Some(dec!(100.01)));
assert_eq!(tick.round_aggressive(dec!(100.00), Dir::Buy), Some(dec!(100.00)));
assert_eq!(tick.round_aggressive(dec!(100.009), Dir::Sell), Some(dec!(100.00)));
assert_eq!(tick.round_aggressive(dec!(100.00), Dir::Sell), Some(dec!(100.00)));
}
#[test]
fn test_round_passive() {
let tick = TickSize::Simple(dec!(0.01));
assert_eq!(tick.round_passive(dec!(100.009), Dir::Buy), Some(dec!(100.00)));
assert_eq!(tick.round_passive(dec!(100.00), Dir::Buy), Some(dec!(100.00)));
assert_eq!(tick.round_passive(dec!(100.001), Dir::Sell), Some(dec!(100.01)));
assert_eq!(tick.round_passive(dec!(100.00), Dir::Sell), Some(dec!(100.00)));
}
#[test]
fn test_rounding_with_large_tick_sizes() {
let tick = TickSize::Simple(dec!(0.25));
assert_eq!(tick.round_up(dec!(10.00)), Some(dec!(10.00)));
assert_eq!(tick.round_up(dec!(10.10)), Some(dec!(10.25)));
assert_eq!(tick.round_up(dec!(10.25)), Some(dec!(10.25)));
assert_eq!(tick.round_up(dec!(10.30)), Some(dec!(10.50)));
assert_eq!(tick.round_down(dec!(10.00)), Some(dec!(10.00)));
assert_eq!(tick.round_down(dec!(10.10)), Some(dec!(10.00)));
assert_eq!(tick.round_down(dec!(10.25)), Some(dec!(10.25)));
assert_eq!(tick.round_down(dec!(10.30)), Some(dec!(10.25)));
assert_eq!(tick.round_down(dec!(10.50)), Some(dec!(10.50)));
assert_eq!(tick.round_down(dec!(10.60)), Some(dec!(10.50)));
}
}