#[derive(Debug, Clone, PartialEq)]
pub enum ExecutionCostModel {
Fixed(f64),
Proportional(f64),
Tiered(Vec<(f64, f64)>),
ZeroCommission,
}
impl ExecutionCostModel {
pub fn commission_usd(&self, notional: f64) -> f64 {
match self {
ExecutionCostModel::Fixed(c) => *c,
ExecutionCostModel::Proportional(bps) => notional * bps / 10_000.0,
ExecutionCostModel::Tiered(tiers) => {
let rate_bps = tiers
.iter()
.filter(|(threshold, _)| notional >= *threshold)
.last()
.map(|(_, rate)| *rate)
.unwrap_or_else(|| tiers.first().map(|(_, r)| *r).unwrap_or(0.0));
notional * rate_bps / 10_000.0
}
ExecutionCostModel::ZeroCommission => 0.0,
}
}
}
pub struct SpreadCost;
impl SpreadCost {
pub fn half_spread_bps(bid: f64, ask: f64) -> f64 {
let mid = (bid + ask) / 2.0;
if mid == 0.0 {
return 0.0;
}
(ask - bid) / (2.0 * mid) * 10_000.0
}
pub fn spread_cost(size: f64, bid: f64, ask: f64) -> f64 {
let mid = (bid + ask) / 2.0;
let hs_bps = Self::half_spread_bps(bid, ask);
size * hs_bps / 10_000.0 * mid
}
}
pub struct MarketImpact;
impl MarketImpact {
pub fn linear_impact(size: f64, volume: f64, impact_bps_per_pct: f64) -> f64 {
if volume == 0.0 {
return 0.0;
}
let participation = size / volume;
participation * impact_bps_per_pct / 10_000.0 * size
}
pub fn sqrt_impact(size: f64, volume: f64, sigma: f64, eta: f64) -> f64 {
if volume == 0.0 {
return 0.0;
}
eta * sigma * (size / volume).sqrt() * size
}
pub fn almgren_chriss(
size: f64,
volume: f64,
sigma: f64,
gamma: f64,
eta: f64,
) -> (f64, f64) {
let permanent = gamma * size;
let temporary = Self::sqrt_impact(size, volume, sigma, eta);
(permanent, temporary)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExecutionCostBreakdown {
pub commission_usd: f64,
pub spread_cost_usd: f64,
pub market_impact_usd: f64,
pub total_usd: f64,
pub total_bps: f64,
}
pub struct TotalExecutionCost;
impl TotalExecutionCost {
pub fn compute(
size: f64,
price: f64,
commission_model: &ExecutionCostModel,
bid: f64,
ask: f64,
volume: f64,
sigma: f64,
) -> ExecutionCostBreakdown {
let notional = size * price;
let commission_usd = commission_model.commission_usd(notional);
let spread_cost_usd = SpreadCost::spread_cost(size, bid, ask);
let market_impact_usd = MarketImpact::sqrt_impact(size, volume, sigma, 0.1);
let total_usd = commission_usd + spread_cost_usd + market_impact_usd;
let total_bps = if notional > 0.0 {
total_usd / notional * 10_000.0
} else {
0.0
};
ExecutionCostBreakdown {
commission_usd,
spread_cost_usd,
market_impact_usd,
total_usd,
total_bps,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fixed_commission() {
let m = ExecutionCostModel::Fixed(7.50);
assert!((m.commission_usd(10_000.0) - 7.50).abs() < 1e-9);
assert!((m.commission_usd(1.0) - 7.50).abs() < 1e-9);
}
#[test]
fn test_proportional_commission() {
let m = ExecutionCostModel::Proportional(10.0);
assert!((m.commission_usd(10_000.0) - 10.0).abs() < 1e-9);
}
#[test]
fn test_zero_commission() {
let m = ExecutionCostModel::ZeroCommission;
assert_eq!(m.commission_usd(100_000.0), 0.0);
}
#[test]
fn test_tiered_commission() {
let m = ExecutionCostModel::Tiered(vec![
(0.0, 20.0),
(10_000.0, 10.0),
(100_000.0, 5.0),
]);
assert!((m.commission_usd(5_000.0) - 10.0).abs() < 1e-9);
assert!((m.commission_usd(50_000.0) - 50.0).abs() < 1e-9);
assert!((m.commission_usd(200_000.0) - 100.0).abs() < 1e-9);
}
#[test]
fn test_half_spread_bps() {
let hs = SpreadCost::half_spread_bps(99.90, 100.10);
assert!((hs - 10.0).abs() < 1e-6);
}
#[test]
fn test_half_spread_bps_zero_mid() {
assert_eq!(SpreadCost::half_spread_bps(0.0, 0.0), 0.0);
}
#[test]
fn test_spread_cost() {
let cost = SpreadCost::spread_cost(1000.0, 99.90, 100.10);
assert!((cost - 100.0).abs() < 1e-6);
}
#[test]
fn test_linear_impact() {
let imp = MarketImpact::linear_impact(1000.0, 100_000.0, 50.0);
assert!((imp - 0.05).abs() < 1e-9);
}
#[test]
fn test_linear_impact_zero_volume() {
assert_eq!(MarketImpact::linear_impact(1000.0, 0.0, 50.0), 0.0);
}
#[test]
fn test_sqrt_impact() {
let imp = MarketImpact::sqrt_impact(10_000.0, 1_000_000.0, 0.02, 0.1);
assert!((imp - 2.0).abs() < 1e-9);
}
#[test]
fn test_sqrt_impact_zero_volume() {
assert_eq!(MarketImpact::sqrt_impact(1000.0, 0.0, 0.02, 0.1), 0.0);
}
#[test]
fn test_almgren_chriss() {
let (perm, temp) = MarketImpact::almgren_chriss(10_000.0, 1_000_000.0, 0.02, 0.001, 0.1);
assert!((perm - 10.0).abs() < 1e-9);
assert!((temp - 2.0).abs() < 1e-9);
}
#[test]
fn test_total_execution_cost_components() {
let breakdown = TotalExecutionCost::compute(
1000.0,
100.0,
&ExecutionCostModel::Fixed(5.0),
99.90,
100.10,
1_000_000.0,
0.02,
);
assert!((breakdown.commission_usd - 5.0).abs() < 1e-9);
assert!((breakdown.spread_cost_usd - 100.0).abs() < 1e-6);
assert!(breakdown.market_impact_usd > 0.0);
assert!((breakdown.total_usd - (breakdown.commission_usd + breakdown.spread_cost_usd + breakdown.market_impact_usd)).abs() < 1e-9);
}
#[test]
fn test_total_execution_cost_bps() {
let breakdown = TotalExecutionCost::compute(
1000.0,
100.0,
&ExecutionCostModel::ZeroCommission,
99.90,
100.10,
1_000_000.0,
0.02,
);
let expected_bps = breakdown.total_usd / 100_000.0 * 10_000.0;
assert!((breakdown.total_bps - expected_bps).abs() < 1e-9);
}
#[test]
fn test_total_execution_cost_zero_commission() {
let breakdown = TotalExecutionCost::compute(
100.0,
50.0,
&ExecutionCostModel::ZeroCommission,
49.95,
50.05,
100_000.0,
0.015,
);
assert_eq!(breakdown.commission_usd, 0.0);
assert!(breakdown.total_usd > 0.0);
}
}