#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CostModel {
pub commission_bps: u32,
pub slippage_bps: u32,
pub min_trade_fee: i64,
}
impl CostModel {
pub fn zero() -> Self {
Self {
commission_bps: 0,
slippage_bps: 0,
min_trade_fee: 0,
}
}
pub fn compute_cost(&self, notional: i64) -> i64 {
let notional = notional.unsigned_abs() as u128;
let total_bps = self.commission_bps as u128 + self.slippage_bps as u128;
let raw = notional * total_bps / 10_000;
let bps_cost = i64::try_from(raw).unwrap_or(i64::MAX);
bps_cost.max(self.min_trade_fee)
}
}
impl Default for CostModel {
fn default() -> Self {
Self::zero()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_cost() {
let model = CostModel::zero();
assert_eq!(model.compute_cost(1_000_000), 0);
}
#[test]
fn bps_cost() {
let model = CostModel {
commission_bps: 10,
slippage_bps: 5,
min_trade_fee: 0,
};
assert_eq!(model.compute_cost(1_000_000), 1500);
}
#[test]
fn min_fee_applied() {
let model = CostModel {
commission_bps: 1,
slippage_bps: 0,
min_trade_fee: 1_00, };
assert_eq!(model.compute_cost(10_000), 1_00);
}
#[test]
fn negative_notional_uses_abs() {
let model = CostModel {
commission_bps: 10,
slippage_bps: 0,
min_trade_fee: 0,
};
assert_eq!(
model.compute_cost(-1_000_000),
model.compute_cost(1_000_000)
);
}
#[test]
fn cost_always_non_negative() {
let model = CostModel::zero();
assert!(model.compute_cost(0) >= 0);
assert!(model.compute_cost(-100) >= 0);
}
}