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 };
89 let x_abs = x.abs() / std::f64::consts::SQRT_2;
90 let t = 1.0 / (1.0 + p * x_abs);
91 let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x_abs * x_abs).exp();
92
93 0.5 * (1.0 + sign * y)
94}
95
96pub fn black_scholes_call(s: f64, k: f64, r: f64, sigma: f64, t: f64) -> f64 {
104 if t <= 0.0 {
105 return (s - k).max(0.0);
106 }
107 let d1 = ((s / k).ln() + (r + 0.5 * sigma * sigma) * t) / (sigma * t.sqrt());
108 let d2 = d1 - sigma * t.sqrt();
109 s * normal_cdf(d1) - k * (-r * t).exp() * normal_cdf(d2)
110}
111
112pub fn black_scholes_put(s: f64, k: f64, r: f64, sigma: f64, t: f64) -> f64 {
120 if t <= 0.0 {
121 return (k - s).max(0.0);
122 }
123 let d1 = ((s / k).ln() + (r + 0.5 * sigma * sigma) * t) / (sigma * t.sqrt());
124 let d2 = d1 - sigma * t.sqrt();
125 k * (-r * t).exp() * normal_cdf(-d2) - s * normal_cdf(-d1)
126}
127
128pub fn implied_volatility_call(s: f64, k: f64, r: f64, t: f64, market_price: f64) -> f64 {
130 let mut lo = 0.001;
131 let mut hi = 5.0;
132 for _ in 0..100 {
133 let mid = (lo + hi) / 2.0;
134 let price = black_scholes_call(s, k, r, mid, t);
135 if price < market_price {
136 lo = mid;
137 } else {
138 hi = mid;
139 }
140 }
141 (lo + hi) / 2.0
142}
143
144pub struct Greeks {
146 pub delta: f64,
147 pub gamma: f64,
148 pub theta: f64,
149 pub vega: f64,
150 pub rho: f64,
151}
152
153pub fn call_greeks(s: f64, k: f64, r: f64, sigma: f64, t: f64) -> Greeks {
155 let sqrt_t = t.sqrt();
156 let d1 = ((s / k).ln() + (r + 0.5 * sigma * sigma) * t) / (sigma * sqrt_t);
157 let d2 = d1 - sigma * sqrt_t;
158 let pdf_d1 = (-0.5 * d1 * d1).exp() / (2.0 * std::f64::consts::PI).sqrt();
159
160 let delta = normal_cdf(d1);
161 let gamma = pdf_d1 / (s * sigma * sqrt_t);
162 let theta = -(s * pdf_d1 * sigma) / (2.0 * sqrt_t) - r * k * (-r * t).exp() * normal_cdf(d2);
163 let vega = s * pdf_d1 * sqrt_t;
164 let rho = k * t * (-r * t).exp() * normal_cdf(d2);
165
166 Greeks { delta, gamma, theta, vega, rho }
167}
168
169pub struct GBMRenderer {
175 pub character: char,
176 pub color: [f32; 4],
177 pub x_scale: f32,
178 pub y_scale: f32,
179}
180
181impl GBMRenderer {
182 pub fn new() -> Self {
183 Self {
184 character: '█',
185 color: [0.2, 1.0, 0.3, 1.0],
186 x_scale: 0.05,
187 y_scale: 0.01,
188 }
189 }
190
191 pub fn with_scales(mut self, x_scale: f32, y_scale: f32) -> Self {
192 self.x_scale = x_scale;
193 self.y_scale = y_scale;
194 self
195 }
196
197 pub fn render_path(&self, path: &[f64]) -> Vec<(Vec2, char, [f32; 4])> {
199 path.iter()
200 .enumerate()
201 .map(|(i, &price)| {
202 let pos = Vec2::new(i as f32 * self.x_scale, price as f32 * self.y_scale);
203 (pos, self.character, self.color)
204 })
205 .collect()
206 }
207
208 pub fn render_fan(&self, paths: &[Vec<f64>]) -> Vec<(Vec2, char, [f32; 4])> {
210 let n = paths.len().max(1);
211 let mut glyphs = Vec::new();
212 for (pi, path) in paths.iter().enumerate() {
213 let alpha = 0.1 + 0.3 * (pi as f32 / n as f32);
214 let color = [self.color[0], self.color[1], self.color[2], alpha];
215 for (i, &price) in path.iter().enumerate() {
216 let pos = Vec2::new(i as f32 * self.x_scale, price as f32 * self.y_scale);
217 glyphs.push((pos, self.character, color));
218 }
219 }
220 glyphs
221 }
222}
223
224impl Default for GBMRenderer {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn test_gbm_always_positive() {
240 let gbm = GeometricBM::new(0.05, 0.2, 100.0, 0.01);
241 let mut rng = Rng::new(42);
242 let path = gbm.path(&mut rng, 1000);
243 assert!(path.iter().all(|&p| p > 0.0), "GBM should always be positive");
244 }
245
246 #[test]
247 fn test_gbm_expected_value() {
248 let mu = 0.05;
250 let sigma = 0.3;
251 let s0 = 100.0;
252 let dt = 0.001;
253 let steps = 1000; let trials = 5000;
255 let gbm = GeometricBM::new(mu, sigma, s0, dt);
256 let mut rng = Rng::new(12345);
257
258 let sum: f64 = (0..trials)
259 .map(|_| {
260 let path = gbm.path(&mut rng, steps);
261 *path.last().unwrap()
262 })
263 .sum();
264 let empirical_mean = sum / trials as f64;
265 let expected = s0 * (mu * 1.0).exp(); assert!(
268 (empirical_mean - expected).abs() / expected < 0.1,
269 "empirical mean {} should be close to expected {}",
270 empirical_mean,
271 expected
272 );
273 }
274
275 #[test]
276 fn test_gbm_log_normal() {
277 let mu = 0.1;
280 let sigma = 0.2;
281 let s0 = 100.0;
282 let dt = 0.01;
283 let steps = 100; let trials = 10_000;
285 let gbm = GeometricBM::new(mu, sigma, s0, dt);
286 let mut rng = Rng::new(777);
287
288 let log_returns: Vec<f64> = (0..trials)
289 .map(|_| {
290 let path = gbm.path(&mut rng, steps);
291 (path.last().unwrap() / s0).ln()
292 })
293 .collect();
294
295 let mean = log_returns.iter().sum::<f64>() / trials as f64;
296 let var = log_returns.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / trials as f64;
297 let expected_mean = (mu - 0.5 * sigma * sigma) * 1.0;
298 let expected_var = sigma * sigma * 1.0;
299
300 assert!(
301 (mean - expected_mean).abs() < 0.05,
302 "log-return mean {} should be ~{}",
303 mean,
304 expected_mean
305 );
306 assert!(
307 (var - expected_var).abs() < 0.02,
308 "log-return variance {} should be ~{}",
309 var,
310 expected_var
311 );
312 }
313
314 #[test]
315 fn test_black_scholes_put_call_parity() {
316 let s = 100.0;
318 let k = 100.0;
319 let r = 0.05;
320 let sigma = 0.2;
321 let t = 1.0;
322
323 let c = black_scholes_call(s, k, r, sigma, t);
324 let p = black_scholes_put(s, k, r, sigma, t);
325 let parity = s - k * (-r * t).exp();
326
327 assert!(
328 (c - p - parity).abs() < 1e-10,
329 "Put-call parity violated: C={}, P={}, S-Ke^-rT={}",
330 c,
331 p,
332 parity
333 );
334 }
335
336 #[test]
337 fn test_black_scholes_call_value() {
338 let c = black_scholes_call(100.0, 100.0, 0.05, 0.2, 1.0);
340 assert!(
341 (c - 10.4506).abs() < 0.01,
342 "BS call should be ~10.45, got {}",
343 c
344 );
345 }
346
347 #[test]
348 fn test_black_scholes_at_expiry() {
349 assert!((black_scholes_call(110.0, 100.0, 0.05, 0.2, 0.0) - 10.0).abs() < 1e-10);
350 assert!((black_scholes_call(90.0, 100.0, 0.05, 0.2, 0.0) - 0.0).abs() < 1e-10);
351 assert!((black_scholes_put(90.0, 100.0, 0.05, 0.2, 0.0) - 10.0).abs() < 1e-10);
352 }
353
354 #[test]
355 fn test_implied_volatility() {
356 let sigma = 0.25;
357 let price = black_scholes_call(100.0, 100.0, 0.05, sigma, 1.0);
358 let iv = implied_volatility_call(100.0, 100.0, 0.05, 1.0, price);
359 assert!(
360 (iv - sigma).abs() < 0.001,
361 "implied vol {} should be ~{}",
362 iv,
363 sigma
364 );
365 }
366
367 #[test]
368 fn test_greeks_delta_range() {
369 let g = call_greeks(100.0, 100.0, 0.05, 0.2, 1.0);
370 assert!(g.delta > 0.0 && g.delta < 1.0, "delta should be in (0,1)");
371 assert!(g.gamma > 0.0, "gamma should be positive");
372 assert!(g.vega > 0.0, "vega should be positive");
373 }
374
375 #[test]
376 fn test_gbm_renderer() {
377 let renderer = GBMRenderer::new();
378 let gbm = GeometricBM::new(0.05, 0.2, 100.0, 0.01);
379 let mut rng = Rng::new(42);
380 let path = gbm.path(&mut rng, 50);
381 let glyphs = renderer.render_path(&path);
382 assert_eq!(glyphs.len(), 51);
383 }
384}