1#[derive(Clone, Copy, Debug)]
9pub struct CostModel {
10 pub fee_bps: f64,
11 pub slippage_bps: f64,
12 pub impact_bps: f64,
16 pub financing_bps: f64,
20 pub max_participation: f64,
24}
25
26impl Default for CostModel {
27 fn default() -> Self {
28 Self {
29 fee_bps: 2.0,
30 slippage_bps: 3.0,
31 impact_bps: 50.0,
32 financing_bps: 5.0,
33 max_participation: f64::INFINITY,
34 }
35 }
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum CostProfile {
44 None,
46 Typical,
48 WorstCase,
50}
51
52#[derive(Clone, Copy, Debug)]
55pub struct ExecutionProfile {
56 pub costs: CostModel,
57 pub decision_delay_bars: usize,
59}
60
61impl CostProfile {
62 pub fn resolve(self) -> ExecutionProfile {
64 match self {
65 CostProfile::None => ExecutionProfile {
66 costs: CostModel {
67 fee_bps: 0.0,
68 slippage_bps: 0.0,
69 impact_bps: 0.0,
70 financing_bps: 0.0,
71 max_participation: f64::INFINITY,
72 },
73 decision_delay_bars: 0,
74 },
75 CostProfile::Typical => ExecutionProfile {
76 costs: CostModel::default(),
77 decision_delay_bars: 0,
78 },
79 CostProfile::WorstCase => ExecutionProfile {
80 costs: CostModel {
81 fee_bps: 10.0,
82 slippage_bps: 15.0,
83 impact_bps: 150.0,
84 financing_bps: 20.0,
85 max_participation: 0.1,
86 },
87 decision_delay_bars: 2,
88 },
89 }
90 }
91}
92
93pub fn financing_cost_frac(financing_bps: f64, gross_exposure: f64) -> f64 {
97 financing_bps / 10_000.0 * (gross_exposure - 1.0).max(0.0)
98}
99
100pub fn liquidity_capped_delta(delta_value: f64, max_participation: f64, nav: f64) -> f64 {
103 if !max_participation.is_finite() {
104 return delta_value;
105 }
106 let cap = max_participation * nav.max(0.0);
107 delta_value.clamp(-cap, cap)
108}
109
110pub fn market_impact_frac(impact_bps: f64, participation: f64) -> f64 {
114 impact_bps / 10_000.0 * participation.max(0.0).sqrt()
115}
116
117pub struct Rng(u64);
119
120impl Rng {
121 pub fn new(seed: u64) -> Self {
122 Rng(seed ^ 0xA5A5_5A5A_C3C3_3C3C)
123 }
124 fn next_u64(&mut self) -> u64 {
125 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
126 let mut z = self.0;
127 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
128 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
129 z ^ (z >> 31)
130 }
131 pub fn signed_unit(&mut self) -> f64 {
133 (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0
134 }
135
136 pub fn unit(&mut self) -> f64 {
138 (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn impact_grows_with_participation() {
148 let small = market_impact_frac(50.0, 0.01);
149 let big = market_impact_frac(50.0, 0.5);
150 assert!(big > small, "bigger trade should cost more");
151 assert!(market_impact_frac(50.0, 0.0).abs() < 1e-12);
152 }
153
154 #[test]
155 fn impact_is_concave() {
156 let a = market_impact_frac(50.0, 0.1);
158 let b = market_impact_frac(50.0, 0.2);
159 assert!(b < 2.0 * a, "impact must be concave in size");
160 }
161
162 #[test]
163 fn financing_only_bites_above_full_investment() {
164 assert_eq!(financing_cost_frac(50.0, 1.0), 0.0);
165 assert_eq!(financing_cost_frac(50.0, 0.5), 0.0);
166 assert!(financing_cost_frac(50.0, 2.0) > 0.0);
167 }
168
169 #[test]
170 fn profile_none_is_frictionless() {
171 let p = CostProfile::None.resolve();
172 assert_eq!(p.costs.fee_bps, 0.0);
173 assert_eq!(p.costs.slippage_bps, 0.0);
174 assert_eq!(p.costs.impact_bps, 0.0);
175 assert_eq!(p.costs.financing_bps, 0.0);
176 assert!(!p.costs.max_participation.is_finite());
177 assert_eq!(p.decision_delay_bars, 0);
178 }
179
180 #[test]
181 fn profile_typical_matches_default_costs_no_delay() {
182 let p = CostProfile::Typical.resolve();
183 let d = CostModel::default();
184 assert_eq!(p.costs.fee_bps, d.fee_bps);
185 assert_eq!(p.costs.slippage_bps, d.slippage_bps);
186 assert_eq!(p.decision_delay_bars, 0);
187 }
188
189 #[test]
190 fn worst_case_is_strictly_harsher_with_delay() {
191 let none = CostProfile::None.resolve();
192 let typ = CostProfile::Typical.resolve();
193 let worst = CostProfile::WorstCase.resolve();
194 assert!(none.costs.fee_bps <= typ.costs.fee_bps);
196 assert!(typ.costs.fee_bps < worst.costs.fee_bps);
197 assert!(typ.costs.slippage_bps < worst.costs.slippage_bps);
198 assert!(typ.costs.impact_bps < worst.costs.impact_bps);
199 assert!(worst.costs.max_participation.is_finite());
201 assert!(worst.decision_delay_bars > 0);
202 assert_eq!(typ.decision_delay_bars, 0);
203 }
204
205 #[test]
206 fn liquidity_cap_clamps_large_trades() {
207 assert_eq!(liquidity_capped_delta(200.0, 0.05, 1000.0), 50.0);
209 assert_eq!(liquidity_capped_delta(-200.0, 0.05, 1000.0), -50.0);
210 assert_eq!(liquidity_capped_delta(30.0, 0.05, 1000.0), 30.0);
212 assert_eq!(liquidity_capped_delta(1e9, f64::INFINITY, 1000.0), 1e9);
213 }
214}