1use super::brownian::Rng;
8use glam::Vec2;
9
10pub struct GeometricBM {
16 pub mu: f64,
18 pub sigma: f64,
20 pub s0: f64,
22 pub dt: f64,
24}
25
26impl GeometricBM {
27 pub fn new(mu: f64, sigma: f64, s0: f64, dt: f64) -> Self {
28 Self { mu, sigma, s0, dt }
29 }
30
31 pub fn step(&self, rng: &mut Rng, current: f64) -> f64 {
34 let z = rng.normal();
35 let drift = (self.mu - 0.5 * self.sigma * self.sigma) * self.dt;
36 let diffusion = self.sigma * self.dt.sqrt() * z;
37 current * (drift + diffusion).exp()
38 }
39
40 pub fn path(&self, rng: &mut Rng, steps: usize) -> Vec<f64> {
42 let mut prices = Vec::with_capacity(steps + 1);
43 prices.push(self.s0);
44 let mut current = self.s0;
45 for _ in 0..steps {
46 current = self.step(rng, current);
47 prices.push(current);
48 }
49 prices
50 }
51
52 pub fn expected_value(&self, t: f64) -> f64 {
54 self.s0 * (self.mu * t).exp()
55 }
56
57 pub fn variance(&self, t: f64) -> f64 {
59 let s0_sq = self.s0 * self.s0;
60 s0_sq * (2.0 * self.mu * t).exp() * ((self.sigma * self.sigma * t).exp() - 1.0)
61 }
62
63 pub fn paths(&self, rng: &mut Rng, steps: usize, count: usize) -> Vec<Vec<f64>> {
65 (0..count).map(|_| self.path(rng, steps)).collect()
66 }
67}
68
69fn normal_cdf(x: f64) -> f64 {
75 let a1 = 0.254829592;
77 let a2 = -0.284496736;
78 let a3 = 1.421413741;
79 let a4 = -1.453152027;
80 let a5 = 1.061405429;
81 let p = 0.3275911;
82
83 let sign = if x < 0.0 { -1.0 } else { 1.0 };
84 let x_abs = x.abs();
85 let t = 1.0 / (1.0 + p * x_abs);
86 let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x_abs * x_abs / 2.0).exp();
87
88 0.5 * (1.0 + sign * y)
89}
90
91pub fn black_scholes_call(s: f64, k: f64, r: f64, sigma: f64, t: f64) -> f64 {
99 if t <= 0.0 {
100 return (s - k).max(0.0);
101 }
102 let d1 = ((s / k).ln() + (r + 0.5 * sigma * sigma) * t) / (sigma * t.sqrt());
103 let d2 = d1 - sigma * t.sqrt();
104 s * normal_cdf(d1) - k * (-r * t).exp() * normal_cdf(d2)
105}
106
107pub fn black_scholes_put(s: f64, k: f64, r: f64, sigma: f64, t: f64) -> f64 {
115 if t <= 0.0 {
116 return (k - s).max(0.0);
117 }
118 let d1 = ((s / k).ln() + (r + 0.5 * sigma * sigma) * t) / (sigma * t.sqrt());
119 let d2 = d1 - sigma * t.sqrt();
120 k * (-r * t).exp() * normal_cdf(-d2) - s * normal_cdf(-d1)
121}
122
123pub fn implied_volatility_call(s: f64, k: f64, r: f64, t: f64, market_price: f64) -> f64 {
125 let mut lo = 0.001;
126 let mut hi = 5.0;
127 for _ in 0..100 {
128 let mid = (lo + hi) / 2.0;
129 let price = black_scholes_call(s, k, r, mid, t);
130 if price < market_price {
131 lo = mid;
132 } else {
133 hi = mid;
134 }
135 }
136 (lo + hi) / 2.0
137}
138
139pub struct Greeks {
141 pub delta: f64,
142 pub gamma: f64,
143 pub theta: f64,
144 pub vega: f64,
145 pub rho: f64,
146}
147
148pub fn call_greeks(s: f64, k: f64, r: f64, sigma: f64, t: f64) -> Greeks {
150 let sqrt_t = t.sqrt();
151 let d1 = ((s / k).ln() + (r + 0.5 * sigma * sigma) * t) / (sigma * sqrt_t);
152 let d2 = d1 - sigma * sqrt_t;
153 let pdf_d1 = (-0.5 * d1 * d1).exp() / (2.0 * std::f64::consts::PI).sqrt();
154
155 let delta = normal_cdf(d1);
156 let gamma = pdf_d1 / (s * sigma * sqrt_t);
157 let theta = -(s * pdf_d1 * sigma) / (2.0 * sqrt_t) - r * k * (-r * t).exp() * normal_cdf(d2);
158 let vega = s * pdf_d1 * sqrt_t;
159 let rho = k * t * (-r * t).exp() * normal_cdf(d2);
160
161 Greeks { delta, gamma, theta, vega, rho }
162}
163
164pub struct GBMRenderer {
170 pub character: char,
171 pub color: [f32; 4],
172 pub x_scale: f32,
173 pub y_scale: f32,
174}
175
176impl GBMRenderer {
177 pub fn new() -> Self {
178 Self {
179 character: '█',
180 color: [0.2, 1.0, 0.3, 1.0],
181 x_scale: 0.05,
182 y_scale: 0.01,
183 }
184 }
185
186 pub fn with_scales(mut self, x_scale: f32, y_scale: f32) -> Self {
187 self.x_scale = x_scale;
188 self.y_scale = y_scale;
189 self
190 }
191
192 pub fn render_path(&self, path: &[f64]) -> Vec<(Vec2, char, [f32; 4])> {
194 path.iter()
195 .enumerate()
196 .map(|(i, &price)| {
197 let pos = Vec2::new(i as f32 * self.x_scale, price as f32 * self.y_scale);
198 (pos, self.character, self.color)
199 })
200 .collect()
201 }
202
203 pub fn render_fan(&self, paths: &[Vec<f64>]) -> Vec<(Vec2, char, [f32; 4])> {
205 let n = paths.len().max(1);
206 let mut glyphs = Vec::new();
207 for (pi, path) in paths.iter().enumerate() {
208 let alpha = 0.1 + 0.3 * (pi as f32 / n as f32);
209 let color = [self.color[0], self.color[1], self.color[2], alpha];
210 for (i, &price) in path.iter().enumerate() {
211 let pos = Vec2::new(i as f32 * self.x_scale, price as f32 * self.y_scale);
212 glyphs.push((pos, self.character, color));
213 }
214 }
215 glyphs
216 }
217}
218
219impl Default for GBMRenderer {
220 fn default() -> Self {
221 Self::new()
222 }
223}
224
225#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_gbm_always_positive() {
235 let gbm = GeometricBM::new(0.05, 0.2, 100.0, 0.01);
236 let mut rng = Rng::new(42);
237 let path = gbm.path(&mut rng, 1000);
238 assert!(path.iter().all(|&p| p > 0.0), "GBM should always be positive");
239 }
240
241 #[test]
242 fn test_gbm_expected_value() {
243 let mu = 0.05;
245 let sigma = 0.3;
246 let s0 = 100.0;
247 let dt = 0.001;
248 let steps = 1000; let trials = 5000;
250 let gbm = GeometricBM::new(mu, sigma, s0, dt);
251 let mut rng = Rng::new(12345);
252
253 let sum: f64 = (0..trials)
254 .map(|_| {
255 let path = gbm.path(&mut rng, steps);
256 *path.last().unwrap()
257 })
258 .sum();
259 let empirical_mean = sum / trials as f64;
260 let expected = s0 * (mu * 1.0).exp(); assert!(
263 (empirical_mean - expected).abs() / expected < 0.1,
264 "empirical mean {} should be close to expected {}",
265 empirical_mean,
266 expected
267 );
268 }
269
270 #[test]
271 fn test_gbm_log_normal() {
272 let mu = 0.1;
275 let sigma = 0.2;
276 let s0 = 100.0;
277 let dt = 0.01;
278 let steps = 100; let trials = 10_000;
280 let gbm = GeometricBM::new(mu, sigma, s0, dt);
281 let mut rng = Rng::new(777);
282
283 let log_returns: Vec<f64> = (0..trials)
284 .map(|_| {
285 let path = gbm.path(&mut rng, steps);
286 (path.last().unwrap() / s0).ln()
287 })
288 .collect();
289
290 let mean = log_returns.iter().sum::<f64>() / trials as f64;
291 let var = log_returns.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / trials as f64;
292 let expected_mean = (mu - 0.5 * sigma * sigma) * 1.0;
293 let expected_var = sigma * sigma * 1.0;
294
295 assert!(
296 (mean - expected_mean).abs() < 0.05,
297 "log-return mean {} should be ~{}",
298 mean,
299 expected_mean
300 );
301 assert!(
302 (var - expected_var).abs() < 0.02,
303 "log-return variance {} should be ~{}",
304 var,
305 expected_var
306 );
307 }
308
309 #[test]
310 fn test_black_scholes_put_call_parity() {
311 let s = 100.0;
313 let k = 100.0;
314 let r = 0.05;
315 let sigma = 0.2;
316 let t = 1.0;
317
318 let c = black_scholes_call(s, k, r, sigma, t);
319 let p = black_scholes_put(s, k, r, sigma, t);
320 let parity = s - k * (-r * t).exp();
321
322 assert!(
323 (c - p - parity).abs() < 1e-10,
324 "Put-call parity violated: C={}, P={}, S-Ke^-rT={}",
325 c,
326 p,
327 parity
328 );
329 }
330
331 #[test]
332 fn test_black_scholes_call_value() {
333 let c = black_scholes_call(100.0, 100.0, 0.05, 0.2, 1.0);
335 assert!(
336 (c - 10.45).abs() < 0.5,
337 "BS call should be ~10.45, got {}",
338 c
339 );
340 }
341
342 #[test]
343 fn test_black_scholes_at_expiry() {
344 assert!((black_scholes_call(110.0, 100.0, 0.05, 0.2, 0.0) - 10.0).abs() < 1e-10);
345 assert!((black_scholes_call(90.0, 100.0, 0.05, 0.2, 0.0) - 0.0).abs() < 1e-10);
346 assert!((black_scholes_put(90.0, 100.0, 0.05, 0.2, 0.0) - 10.0).abs() < 1e-10);
347 }
348
349 #[test]
350 fn test_implied_volatility() {
351 let sigma = 0.25;
352 let price = black_scholes_call(100.0, 100.0, 0.05, sigma, 1.0);
353 let iv = implied_volatility_call(100.0, 100.0, 0.05, 1.0, price);
354 assert!(
355 (iv - sigma).abs() < 0.001,
356 "implied vol {} should be ~{}",
357 iv,
358 sigma
359 );
360 }
361
362 #[test]
363 fn test_greeks_delta_range() {
364 let g = call_greeks(100.0, 100.0, 0.05, 0.2, 1.0);
365 assert!(g.delta > 0.0 && g.delta < 1.0, "delta should be in (0,1)");
366 assert!(g.gamma > 0.0, "gamma should be positive");
367 assert!(g.vega > 0.0, "vega should be positive");
368 }
369
370 #[test]
371 fn test_gbm_renderer() {
372 let renderer = GBMRenderer::new();
373 let gbm = GeometricBM::new(0.05, 0.2, 100.0, 0.01);
374 let mut rng = Rng::new(42);
375 let path = gbm.path(&mut rng, 50);
376 let glyphs = renderer.render_path(&path);
377 assert_eq!(glyphs.len(), 51);
378 }
379}