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::{bsm_greeks, bsm_price, BsmGreeks};
61use crate::derivatives::implied_vol::bsm_implied_vol;
62use crate::derivatives::types::{validate_bsm_params, BsmParams, OptionType};
63use crate::util::error::{require_finite, FinanceResult};
64
65/// Mutable European option under BSM (spot / vol / time / strike updates).
66///
67/// **Fallible constructor** is [`BsmState::new`] → [`FinanceResult`] (Result-only naming).
68#[derive(Clone, Debug, PartialEq)]
69pub struct BsmState {
70    params: BsmParams,
71    option_type: OptionType,
72}
73
74impl BsmState {
75    /// Validate and store contract + market snapshot.
76    pub fn new(params: BsmParams, option_type: OptionType) -> FinanceResult<Self> {
77        validate_bsm_params(params)?;
78        Ok(Self {
79            params,
80            option_type,
81        })
82    }
83
84    pub fn params(&self) -> BsmParams {
85        self.params
86    }
87
88    pub fn option_type(&self) -> OptionType {
89        self.option_type
90    }
91
92    /// Underlier mark moved (most common live update).
93    pub fn set_spot(&mut self, spot: f64) -> FinanceResult<()> {
94        require_finite("spot", spot)?;
95        let mut p = self.params;
96        p.spot = spot;
97        validate_bsm_params(p)?;
98        self.params = p;
99        Ok(())
100    }
101
102    /// Set model / implied vol directly (absolute annualized).
103    pub fn set_vol(&mut self, vol: f64) -> FinanceResult<()> {
104        require_finite("vol", vol)?;
105        let mut p = self.params;
106        p.vol = vol;
107        validate_bsm_params(p)?;
108        self.params = p;
109        Ok(())
110    }
111
112    /// Clock / expiry decay — pass remaining time in **years**.
113    pub fn set_time_years(&mut self, time_years: f64) -> FinanceResult<()> {
114        require_finite("time_years", time_years)?;
115        let mut p = self.params;
116        p.time_years = time_years;
117        validate_bsm_params(p)?;
118        self.params = p;
119        Ok(())
120    }
121
122    /// Rarely changes live; included for completeness (e.g. corporate action resstrike).
123    pub fn set_strike(&mut self, strike: f64) -> FinanceResult<()> {
124        require_finite("strike", strike)?;
125        let mut p = self.params;
126        p.strike = strike;
127        validate_bsm_params(p)?;
128        self.params = p;
129        Ok(())
130    }
131
132    pub fn set_rate(&mut self, rate: f64) -> FinanceResult<()> {
133        require_finite("rate", rate)?;
134        let mut p = self.params;
135        p.rate = rate;
136        validate_bsm_params(p)?;
137        self.params = p;
138        Ok(())
139    }
140
141    pub fn set_dividend_yield(&mut self, dividend_yield: f64) -> FinanceResult<()> {
142        require_finite("dividend_yield", dividend_yield)?;
143        let mut p = self.params;
144        p.dividend_yield = dividend_yield;
145        validate_bsm_params(p)?;
146        self.params = p;
147        Ok(())
148    }
149
150    /// Invert market premium into IV and store it (**trading:** mark to mid).
151    pub fn set_vol_from_price(&mut self, market_price: f64) -> FinanceResult<f64> {
152        let iv = bsm_implied_vol(self.params, self.option_type, market_price)?;
153        self.set_vol(iv)?;
154        Ok(iv)
155    }
156
157    /// Model price at current params.
158    pub fn price(&self) -> FinanceResult<f64> {
159        bsm_price(self.params, self.option_type)
160    }
161
162    /// Model Greeks at current params.
163    pub fn greeks(&self) -> FinanceResult<BsmGreeks> {
164        bsm_greeks(self.params, self.option_type)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn set_spot_moves_delta() {
174        let p = BsmParams::atm_one_year(100.0, 0.05, 0.2);
175        let mut s = BsmState::new(p, OptionType::Call).unwrap();
176        let d0 = s.greeks().unwrap().delta;
177        s.set_spot(110.0).unwrap();
178        let d1 = s.greeks().unwrap().delta;
179        assert!(d1 > d0);
180    }
181
182    #[test]
183    fn set_vol_from_price_round_trip() {
184        let p = BsmParams::atm_one_year(100.0, 0.05, 0.22);
185        let mut s = BsmState::new(p, OptionType::Call).unwrap();
186        let px = s.price().unwrap();
187        s.set_vol(0.10).unwrap();
188        let iv = s.set_vol_from_price(px).unwrap();
189        assert!((iv - 0.22).abs() < 1e-5);
190    }
191}