#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CostModel {
pub commission_bps: f64,
pub slippage_bps: f64,
pub min_commission: i64,
}
impl CostModel {
pub fn zero() -> Self {
Self {
commission_bps: 0.0,
slippage_bps: 0.0,
min_commission: 0,
}
}
pub fn compute_cost(&self, notional: i64) -> i64 {
let notional = notional.unsigned_abs() as f64;
let bps_cost = (notional * self.commission_bps / 10_000.0).round() as i64;
bps_cost.max(self.min_commission)
}
}
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.0,
slippage_bps: 5.0,
min_commission: 0,
};
assert_eq!(model.compute_cost(1_000_000), 1000);
}
#[test]
fn min_fee_applied() {
let model = CostModel {
commission_bps: 1.0,
slippage_bps: 0.0,
min_commission: 1_00, };
assert_eq!(model.compute_cost(10_000), 1_00);
}
#[test]
fn negative_notional_uses_abs() {
let model = CostModel {
commission_bps: 10.0,
slippage_bps: 0.0,
min_commission: 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);
}
#[test]
fn parity_no_slippage_matches_old_formula() {
let model = CostModel {
commission_bps: 12.5,
slippage_bps: 0.0,
min_commission: 50,
};
assert_eq!(model.compute_cost(1_000_000), 1250); assert_eq!(model.compute_cost(10_000), 50); }
}