sandbox_quant/ev/
price_model.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2pub enum PositionSide {
3 Long,
4 Short,
5}
6
7#[derive(Debug, Clone, Copy)]
8pub struct YNormal {
9 pub mu: f64,
10 pub sigma: f64,
11}
12
13#[derive(Debug, Clone, Copy)]
14pub struct SpotEvInputs {
15 pub p0: f64,
16 pub qty: f64,
17 pub side: PositionSide,
18 pub fee: f64,
19 pub slippage: f64,
20 pub borrow: f64,
21}
22
23#[derive(Debug, Clone, Copy)]
24pub struct FuturesEvInputs {
25 pub p0: f64,
26 pub qty: f64,
27 pub multiplier: f64,
28 pub side: PositionSide,
29 pub fee: f64,
30 pub slippage: f64,
31 pub funding: f64,
32 pub liq_risk: f64,
33}
34
35#[derive(Debug, Clone, Copy, Default)]
36pub struct EvStats {
37 pub ev: f64,
38 pub ev_std: f64,
39 pub p_win: f64,
40}
41
42pub fn spot_ev_from_y_normal(y: YNormal, i: SpotEvInputs) -> EvStats {
43 let p0 = i.p0.max(0.0);
44 let qty = i.qty.abs();
45 if p0 <= f64::EPSILON || qty <= f64::EPSILON {
46 return EvStats::default();
47 }
48 let sigma = y.sigma.max(0.0);
49 let cost = i.fee + i.slippage + i.borrow;
50 let signed = side_sign(i.side);
51 let (e_pt, var_pt) = lognormal_moments(p0, y.mu, sigma);
52 let pnl_mean = signed * qty * (e_pt - p0) - cost;
53 let pnl_std = (qty * var_pt.sqrt()).abs();
54 let p_win = p_win_lognormal(y.mu, sigma, p0, qty, signed, cost);
55 EvStats {
56 ev: pnl_mean,
57 ev_std: pnl_std,
58 p_win,
59 }
60}
61
62pub fn futures_ev_from_y_normal(y: YNormal, i: FuturesEvInputs) -> EvStats {
63 let p0 = i.p0.max(0.0);
64 let qty = i.qty.abs();
65 let m = i.multiplier.abs();
66 if p0 <= f64::EPSILON || qty <= f64::EPSILON || m <= f64::EPSILON {
67 return EvStats::default();
68 }
69 let sigma = y.sigma.max(0.0);
70 let cost = i.fee + i.slippage + i.funding + i.liq_risk;
71 let signed = side_sign(i.side);
72 let scale = m * qty;
73 let (e_pt, var_pt) = lognormal_moments(p0, y.mu, sigma);
74 let pnl_mean = signed * scale * (e_pt - p0) - cost;
75 let pnl_std = (scale * var_pt.sqrt()).abs();
76 let p_win = p_win_lognormal(y.mu, sigma, p0, scale, signed, cost);
77 EvStats {
78 ev: pnl_mean,
79 ev_std: pnl_std,
80 p_win,
81 }
82}
83
84fn side_sign(side: PositionSide) -> f64 {
85 match side {
86 PositionSide::Long => 1.0,
87 PositionSide::Short => -1.0,
88 }
89}
90
91fn lognormal_moments(p0: f64, mu: f64, sigma: f64) -> (f64, f64) {
92 let sigma2 = sigma * sigma;
93 let e_pt = p0 * (mu + 0.5 * sigma2).exp();
94 let var_pt = e_pt * e_pt * (sigma2.exp() - 1.0).max(0.0);
95 (e_pt, var_pt)
96}
97
98fn p_win_lognormal(mu: f64, sigma: f64, p0: f64, scale: f64, signed: f64, cost: f64) -> f64 {
99 if scale <= f64::EPSILON || p0 <= f64::EPSILON {
100 return 0.0;
101 }
102 let thresh = if signed > 0.0 {
103 p0 + cost / scale
104 } else {
105 p0 - cost / scale
106 };
107 if thresh <= 0.0 {
108 return if signed > 0.0 { 1.0 } else { 0.0 };
109 }
110 if sigma <= f64::EPSILON {
111 let pt = p0 * mu.exp();
112 return if signed > 0.0 {
113 (pt > thresh) as i32 as f64
114 } else {
115 (pt < thresh) as i32 as f64
116 };
117 }
118 let z = ((thresh / p0).ln() - mu) / sigma;
119 if signed > 0.0 {
120 1.0 - normal_cdf(z)
121 } else {
122 normal_cdf(z)
123 }
124}
125
126fn normal_cdf(x: f64) -> f64 {
127 0.5 * (1.0 + erf_approx(x / 2f64.sqrt()))
128}
129
130fn erf_approx(x: f64) -> f64 {
132 let sign = if x < 0.0 { -1.0 } else { 1.0 };
133 let x = x.abs();
134 let t = 1.0 / (1.0 + 0.3275911 * x);
135 let a1 = 0.254829592;
136 let a2 = -0.284496736;
137 let a3 = 1.421413741;
138 let a4 = -1.453152027;
139 let a5 = 1.061405429;
140 let y = 1.0 - (((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * (-x * x).exp());
141 sign * y
142}