schwab_cli/agent/backtest/
bs.rs1pub fn norm_cdf(x: f64) -> f64 {
5 if x.is_nan() {
6 return f64::NAN;
7 }
8 let a1 = 0.254829592;
9 let a2 = -0.284496736;
10 let a3 = 1.421413741;
11 let a4 = -1.453152027;
12 let a5 = 1.061405429;
13 let p = 0.3275911;
14 let sign = if x < 0.0 { -1.0 } else { 1.0 };
15 let x = x.abs() / (2.0_f64).sqrt();
16 let t = 1.0 / (1.0 + p * x);
17 let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
18 0.5 * (1.0 + sign * y)
19}
20
21fn d1_d2(spot: f64, strike: f64, t: f64, r: f64, sigma: f64) -> Option<(f64, f64)> {
22 if spot <= 0.0 || strike <= 0.0 || t <= 0.0 || sigma <= 0.0 {
23 return None;
24 }
25 let vol_sqrt_t = sigma * t.sqrt();
26 if vol_sqrt_t <= f64::EPSILON {
27 return None;
28 }
29 let d1 = ((spot / strike).ln() + (r + 0.5 * sigma * sigma) * t) / vol_sqrt_t;
30 let d2 = d1 - vol_sqrt_t;
31 Some((d1, d2))
32}
33
34pub fn bs_price(is_put: bool, spot: f64, strike: f64, t_years: f64, r: f64, sigma: f64) -> f64 {
36 if t_years <= 0.0 {
37 return intrinsic(is_put, spot, strike);
38 }
39 let Some((d1, d2)) = d1_d2(spot, strike, t_years, r, sigma) else {
40 return intrinsic(is_put, spot, strike);
41 };
42 let disc = (-r * t_years).exp();
43 if is_put {
44 (strike * disc * norm_cdf(-d2) - spot * norm_cdf(-d1)).max(0.0)
45 } else {
46 (spot * norm_cdf(d1) - strike * disc * norm_cdf(d2)).max(0.0)
47 }
48}
49
50pub fn bs_delta(is_put: bool, spot: f64, strike: f64, t_years: f64, r: f64, sigma: f64) -> f64 {
51 if t_years <= 0.0 {
52 return if is_put {
53 if spot < strike {
54 -1.0
55 } else {
56 0.0
57 }
58 } else if spot > strike {
59 1.0
60 } else {
61 0.0
62 };
63 }
64 let Some((d1, _)) = d1_d2(spot, strike, t_years, r, sigma) else {
65 return 0.0;
66 };
67 if is_put {
68 norm_cdf(d1) - 1.0
69 } else {
70 norm_cdf(d1)
71 }
72}
73
74fn intrinsic(is_put: bool, spot: f64, strike: f64) -> f64 {
75 if is_put {
76 (strike - spot).max(0.0)
77 } else {
78 (spot - strike).max(0.0)
79 }
80}
81
82pub fn years_from_dte(dte: i64) -> f64 {
83 (dte.max(0) as f64) / 365.0
84}
85
86pub fn vertical_credit(
88 is_put: bool,
89 spot: f64,
90 short_strike: f64,
91 long_strike: f64,
92 dte: i64,
93 iv_pct: f64,
94) -> f64 {
95 let t = years_from_dte(dte);
96 let sigma = (iv_pct / 100.0).max(0.01);
97 let short = bs_price(is_put, spot, short_strike, t, 0.0, sigma);
98 let long = bs_price(is_put, spot, long_strike, t, 0.0, sigma);
99 (short - long).max(0.01)
100}
101
102pub fn vertical_debit_to_close(
104 is_put: bool,
105 spot: f64,
106 short_strike: f64,
107 long_strike: f64,
108 dte: i64,
109 iv_pct: f64,
110) -> f64 {
111 vertical_credit(is_put, spot, short_strike, long_strike, dte, iv_pct)
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn atm_call_has_near_half_delta() {
120 let d = bs_delta(false, 100.0, 100.0, 30.0 / 365.0, 0.0, 0.20);
121 assert!((d - 0.5).abs() < 0.05, "delta={d}");
122 }
123
124 #[test]
125 fn put_credit_positive() {
126 let c = vertical_credit(true, 500.0, 480.0, 475.0, 35, 18.0);
127 assert!(c > 0.05, "credit={c}");
128 }
129}