Skip to main content

finance_solution/derivatives/
state.rs

1//! # Live BSM state — engineering for streaming underliers
2//!
3//! Holds one European contract’s BSM inputs and exposes `set_*` mutators so your
4//! market-data loop does not rebuild parameter graphs on every tick.
5//!
6//! ---
7//!
8//! ## Trading perspective
9//!
10//! On each **underlier** print, risk wants fresh **Δ/Γ** for every open option.
11//! On each **option quote**, vol traders update **IV** from mid and recompute vega/theta.
12//! This type is the per-contract scratchpad for that loop — not the book itself.
13//!
14//! ---
15//!
16//! ## Engineering perspective
17//!
18//! ```text
19//! HashMap<OptionKey, BsmState>   // in YOUR engine
20//!
21//! on_underlier_tick(s):
22//!   for state in map.values_mut() {
23//!       state.set_spot(s)?;
24//!       let g = state.greeks()?;   // or throttle / rayon
25//!       aggregate_risk(g);
26//!   }
27//!
28//! on_option_quote(key, mid):
29//!   map[key].set_vol_from_price(mid)?;
30//! ```
31//!
32//! Combine with TA on the same symbol:
33//!
34//! ```text
35//! on_1m_bar → equity.ta.push(...)
36//! on_spot   → options[*].set_spot(s)
37//! ```
38//!
39//! Both pipelines are **sync** math. Concurrency is optional and **outside** this crate
40//! (`rayon` over keys, async tasks that only deliver messages).
41//!
42//! ---
43//!
44//! ## Example
45//!
46//! ```
47//! use finance_solution::derivatives::{BsmParams, BsmState, OptionType};
48//!
49//! let p = BsmParams::atm_one_year(100.0, 0.05, 0.20);
50//! let mut opt = BsmState::new(p, OptionType::Call).unwrap();
51//! // underlier tick:
52//! opt.set_spot(101.5).unwrap();
53//! let g = opt.greeks().unwrap();
54//! assert!(g.delta > 0.0);
55//! // mark IV from mid:
56//! opt.set_vol_from_price(11.0).unwrap();
57//! assert!(opt.params().vol > 0.0);
58//! ```
59
60use crate::derivatives::black_scholes::{
61    bsm_cross_greeks, bsm_greeks, bsm_price, BsmCrossGreeks, BsmGreeks,
62};
63use crate::derivatives::implied_vol::bsm_implied_vol;
64use crate::derivatives::types::{validate_bsm_params, BsmParams, OptionType};
65use crate::util::error::{require_finite, FinanceResult};
66
67/// Mutable European option under BSM (spot / vol / time / strike updates).
68///
69/// **Fallible constructor** is [`BsmState::new`] → [`FinanceResult`] (Result-only naming).
70#[derive(Clone, Debug, PartialEq)]
71pub struct BsmState {
72    params: BsmParams,
73    option_type: OptionType,
74}
75
76impl BsmState {
77    /// Validate and store contract + market snapshot.
78    pub fn new(params: BsmParams, option_type: OptionType) -> FinanceResult<Self> {
79        validate_bsm_params(params)?;
80        Ok(Self {
81            params,
82            option_type,
83        })
84    }
85
86    pub fn params(&self) -> BsmParams {
87        self.params
88    }
89
90    pub fn option_type(&self) -> OptionType {
91        self.option_type
92    }
93
94    /// Underlier mark moved (most common live update).
95    pub fn set_spot(&mut self, spot: f64) -> FinanceResult<()> {
96        require_finite("spot", spot)?;
97        let mut p = self.params;
98        p.spot = spot;
99        validate_bsm_params(p)?;
100        self.params = p;
101        Ok(())
102    }
103
104    /// Set model / implied vol directly (absolute annualized).
105    pub fn set_vol(&mut self, vol: f64) -> FinanceResult<()> {
106        require_finite("vol", vol)?;
107        let mut p = self.params;
108        p.vol = vol;
109        validate_bsm_params(p)?;
110        self.params = p;
111        Ok(())
112    }
113
114    /// Clock / expiry decay — pass remaining time in **years**.
115    pub fn set_time_years(&mut self, time_years: f64) -> FinanceResult<()> {
116        require_finite("time_years", time_years)?;
117        let mut p = self.params;
118        p.time_years = time_years;
119        validate_bsm_params(p)?;
120        self.params = p;
121        Ok(())
122    }
123
124    /// Rarely changes live; included for completeness (e.g. corporate action resstrike).
125    pub fn set_strike(&mut self, strike: f64) -> FinanceResult<()> {
126        require_finite("strike", strike)?;
127        let mut p = self.params;
128        p.strike = strike;
129        validate_bsm_params(p)?;
130        self.params = p;
131        Ok(())
132    }
133
134    pub fn set_rate(&mut self, rate: f64) -> FinanceResult<()> {
135        require_finite("rate", rate)?;
136        let mut p = self.params;
137        p.rate = rate;
138        validate_bsm_params(p)?;
139        self.params = p;
140        Ok(())
141    }
142
143    pub fn set_dividend_yield(&mut self, dividend_yield: f64) -> FinanceResult<()> {
144        require_finite("dividend_yield", dividend_yield)?;
145        let mut p = self.params;
146        p.dividend_yield = dividend_yield;
147        validate_bsm_params(p)?;
148        self.params = p;
149        Ok(())
150    }
151
152    /// Invert market premium into IV and store it (**trading:** mark to mid).
153    pub fn set_vol_from_price(&mut self, market_price: f64) -> FinanceResult<f64> {
154        let iv = bsm_implied_vol(self.params, self.option_type, market_price)?;
155        self.set_vol(iv)?;
156        Ok(iv)
157    }
158
159    /// Model price at current params.
160    pub fn price(&self) -> FinanceResult<f64> {
161        bsm_price(self.params, self.option_type)
162    }
163
164    /// Model Greeks at current params.
165    pub fn greeks(&self) -> FinanceResult<BsmGreeks> {
166        bsm_greeks(self.params, self.option_type)
167    }
168
169    /// Cross Greeks (vanna, volga, charm) at current params.
170    pub fn cross_greeks(&self) -> FinanceResult<BsmCrossGreeks> {
171        bsm_cross_greeks(self.params, self.option_type)
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn set_spot_moves_delta() {
181        let p = BsmParams::atm_one_year(100.0, 0.05, 0.2);
182        let mut s = BsmState::new(p, OptionType::Call).unwrap();
183        let d0 = s.greeks().unwrap().delta;
184        s.set_spot(110.0).unwrap();
185        let d1 = s.greeks().unwrap().delta;
186        assert!(d1 > d0);
187    }
188
189    #[test]
190    fn set_vol_from_price_round_trip() {
191        let p = BsmParams::atm_one_year(100.0, 0.05, 0.22);
192        let mut s = BsmState::new(p, OptionType::Call).unwrap();
193        let px = s.price().unwrap();
194        s.set_vol(0.10).unwrap();
195        let iv = s.set_vol_from_price(px).unwrap();
196        assert!((iv - 0.22).abs() < 1e-5);
197    }
198}