Skip to main content

qualia_core_db/specialized_libs/financial_modeling/
pricing.rs

1use super::*;
2
3/// Pricing engine
4pub struct PricingEngine {
5    pricing_models: HashMap<String, PricingModel>,
6    market_data: MarketData,
7    valuation_engine: ValuationEngine,
8}
9
10/// Pricing models
11#[derive(Debug, Clone)]
12pub struct PricingModel {
13    pub model_id: String,
14    pub model_type: PricingModelType,
15    pub parameters: PricingModelParameters,
16}
17
18/// Pricing model types
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub enum PricingModelType {
21    BlackScholes,
22    Binomial,
23    MonteCarlo,
24    FiniteDifference,
25    Analytical,
26}
27
28/// Pricing model parameters
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct PricingModelParameters {
31    pub risk_free_rate: f64,
32    pub volatility: f64,
33    pub dividend_yield: f64,
34    pub time_to_maturity: f64,
35}
36
37/// Valuation engine
38pub struct ValuationEngine {
39    valuation_methods: HashMap<String, ValuationMethod>,
40    discount_rates: HashMap<String, f64>,
41    cash_flow_projections: HashMap<String, CashFlowProjection>,
42}
43
44/// Valuation methods
45#[derive(Debug, Clone)]
46pub struct ValuationMethod {
47    pub method_id: String,
48    pub method_type: ValuationMethodType,
49    pub parameters: ValuationMethodParameters,
50}
51
52/// Valuation method types
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54pub enum ValuationMethodType {
55    DCF,
56    DDM,
57    Multiples,
58    AssetBased,
59    OptionPricing,
60}
61
62/// Valuation method parameters
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct ValuationMethodParameters {
65    pub discount_rate: f64,
66    pub growth_rate: f64,
67    pub terminal_growth: f64,
68    pub multiples: HashMap<String, f64>,
69}
70
71/// Cash flow projections
72#[derive(Debug, Clone)]
73pub struct CashFlowProjection {
74    pub projection_id: String,
75    pub cash_flows: Vec<CashFlow>,
76    pub assumptions: Vec<Assumption>,
77}
78
79/// Cash flows
80#[derive(Debug, Clone)]
81pub struct CashFlow {
82    pub period: u32,
83    pub amount: f64,
84    pub cash_flow_type: CashFlowType,
85}
86
87/// Cash flow types
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89pub enum CashFlowType {
90    Operating,
91    Investing,
92    Financing,
93    Free,
94}
95
96/// Assumptions
97#[derive(Debug, Clone)]
98pub struct Assumption {
99    pub assumption_id: String,
100    pub assumption_name: String,
101    pub assumption_value: f64,
102    pub justification: String,
103}
104
105impl PricingEngine {
106    pub fn new() -> Self {
107        Self {
108            pricing_models: HashMap::new(),
109            market_data: MarketData::new(),
110            valuation_engine: ValuationEngine::new(),
111        }
112    }
113
114    pub fn initialize(&mut self) -> Result<(), FinancialError> {
115        self.valuation_engine.initialize()?;
116        Ok(())
117    }
118
119    pub fn validate_option_parameters(
120        &self,
121        params: &OptionParameters,
122    ) -> Result<(), FinancialError> {
123        if params.underlying_price <= 0.0 {
124            return Err(FinancialError::ValidationError(
125                "Underlying price must be positive".to_string(),
126            ));
127        }
128        if params.strike <= 0.0 {
129            return Err(FinancialError::ValidationError(
130                "Strike price must be positive".to_string(),
131            ));
132        }
133        if params.time_to_maturity < 0.0 {
134            return Err(FinancialError::ValidationError(
135                "Time to maturity must be non-negative".to_string(),
136            ));
137        }
138        if params.volatility < 0.0 {
139            return Err(FinancialError::ValidationError(
140                "Volatility must be non-negative".to_string(),
141            ));
142        }
143        Ok(())
144    }
145
146    pub fn price_option(&self, params: &OptionParameters) -> Result<OptionPrice, FinancialError> {
147        // Price option using Black-Scholes
148        let option_price = self.black_scholes_price(params)?;
149        Ok(option_price)
150    }
151
152    fn black_scholes_price(
153        &self,
154        params: &OptionParameters,
155    ) -> Result<OptionPrice, FinancialError> {
156        let s = params.underlying_price;
157        let k = params.strike;
158        let r = params.risk_free_rate;
159        let sigma = params.volatility;
160        let t = params.time_to_maturity;
161
162        // Edge case: zero time to expiry -> option is worth its intrinsic value
163        // (no time value remains). Greeks collapse to their intrinsic boundary.
164        if t <= 0.0 {
165            return Ok(self.intrinsic_price(params));
166        }
167
168        // Edge case: zero volatility -> payoff is deterministic. The terminal
169        // price is S*exp(rT), so the discounted call payoff is max(S - K*exp(-rT), 0)
170        // and the put payoff is max(K*exp(-rT) - S, 0). Greeks are zero except
171        // delta, which is the step function at the strike.
172        if sigma <= 0.0 {
173            let disc = (-r * t).exp();
174            let fwd = s - k * disc;
175            let (price, delta) = match params.option_type {
176                OptionType::Call => (fwd.max(0.0), if fwd > 0.0 { 1.0 } else { 0.0 }),
177                OptionType::Put => ((-fwd).max(0.0), if fwd < 0.0 { -1.0 } else { 0.0 }),
178            };
179            return Ok(OptionPrice {
180                price,
181                delta,
182                gamma: 0.0,
183                theta: 0.0,
184                vega: 0.0,
185                rho: 0.0,
186            });
187        }
188
189        // Edge case: zero underlying price -> call is worthless, put is the
190        // discounted strike.
191        if s <= 0.0 {
192            let disc = (-r * t).exp();
193            return Ok(match params.option_type {
194                OptionType::Call => OptionPrice {
195                    price: 0.0,
196                    delta: 0.0,
197                    gamma: 0.0,
198                    theta: 0.0,
199                    vega: 0.0,
200                    rho: 0.0,
201                },
202                OptionType::Put => OptionPrice {
203                    price: k * disc,
204                    delta: -1.0,
205                    gamma: 0.0,
206                    theta: r * k * disc,
207                    vega: 0.0,
208                    rho: -t * k * disc,
209                },
210            });
211        }
212
213        // Standard Black-Scholes formula.
214        let sqrt_t = t.sqrt();
215        let d1 = ((s / k).ln() + (r + 0.5 * sigma * sigma) * t) / (sigma * sqrt_t);
216        let d2 = d1 - sigma * sqrt_t;
217        let disc = (-r * t).exp();
218        let pdf_d1 = self.normal_pdf(d1);
219
220        let (price, delta) = match params.option_type {
221            OptionType::Call => {
222                let p = s * self.normal_cdf(d1) - k * disc * self.normal_cdf(d2);
223                (p, self.normal_cdf(d1))
224            }
225            OptionType::Put => {
226                let p = k * disc * self.normal_cdf(-d2) - s * self.normal_cdf(-d1);
227                (p, self.normal_cdf(d1) - 1.0)
228            }
229        };
230
231        // Gamma and Vega are identical for calls and puts.
232        let gamma = pdf_d1 / (s * sigma * sqrt_t);
233        let vega = s * pdf_d1 * sqrt_t;
234
235        let theta = self.calculate_theta(params, d1, d2, pdf_d1);
236        let rho = self.calculate_rho(params, d2, disc);
237
238        Ok(OptionPrice {
239            price,
240            delta,
241            gamma,
242            theta,
243            vega,
244            rho,
245        })
246    }
247
248    /// Intrinsic value at expiry (T=0): call = max(S-K, 0), put = max(K-S, 0).
249    /// Delta is the step at the strike; other Greeks are zero.
250    fn intrinsic_price(&self, params: &OptionParameters) -> OptionPrice {
251        let intrinsic = match params.option_type {
252            OptionType::Call => (params.underlying_price - params.strike).max(0.0),
253            OptionType::Put => (params.strike - params.underlying_price).max(0.0),
254        };
255        let delta = match params.option_type {
256            OptionType::Call => {
257                if params.underlying_price > params.strike {
258                    1.0
259                } else {
260                    0.0
261                }
262            }
263            OptionType::Put => {
264                if params.underlying_price < params.strike {
265                    -1.0
266                } else {
267                    0.0
268                }
269            }
270        };
271        OptionPrice {
272            price: intrinsic,
273            delta,
274            gamma: 0.0,
275            theta: 0.0,
276            vega: 0.0,
277            rho: 0.0,
278        }
279    }
280
281    fn normal_cdf(&self, x: f64) -> f64 {
282        // Abramowitz and Stegun approximation for normal CDF (max error 7.5e-8)
283        let t = 1.0 / (1.0 + 0.2316419 * x.abs());
284        let d = 0.3989422819 * (-x * x / 2.0).exp();
285        let p = d
286            * t
287            * (0.3193815306
288                + t * (-0.3565637813
289                    + t * (1.7814779372 + t * (-1.8212559978 + t * 1.3302744929))));
290        if x >= 0.0 {
291            1.0 - p
292        } else {
293            p
294        }
295    }
296
297    fn normal_pdf(&self, x: f64) -> f64 {
298        (-0.5 * x * x).exp() / (2.0 * std::f64::consts::PI).sqrt()
299    }
300
301    fn calculate_theta(&self, params: &OptionParameters, _d1: f64, d2: f64, pdf_d1: f64) -> f64 {
302        // Theta per calendar day (divided by 365). The annualized theta is the
303        // standard Black-Scholes expression; reporting per-day matches how the
304        // Greek is conventionally quoted.
305        let sqrt_t = params.time_to_maturity.sqrt();
306        let disc = (-params.risk_free_rate * params.time_to_maturity).exp();
307        let annualized = match params.option_type {
308            OptionType::Call => {
309                -(params.underlying_price * pdf_d1 * params.volatility) / (2.0 * sqrt_t)
310                    - params.risk_free_rate * params.strike * disc * self.normal_cdf(d2)
311            }
312            OptionType::Put => {
313                -(params.underlying_price * pdf_d1 * params.volatility) / (2.0 * sqrt_t)
314                    + params.risk_free_rate * params.strike * disc * self.normal_cdf(-d2)
315            }
316        };
317        annualized / 365.0
318    }
319
320    fn calculate_rho(&self, params: &OptionParameters, d2: f64, disc: f64) -> f64 {
321        match params.option_type {
322            OptionType::Call => {
323                params.strike * params.time_to_maturity * disc * self.normal_cdf(d2)
324            }
325            OptionType::Put => {
326                -params.strike * params.time_to_maturity * disc * self.normal_cdf(-d2)
327            }
328        }
329    }
330
331    pub fn add_pricing_model(&mut self, model: PricingModel) {
332        self.pricing_models.insert(model.model_id.clone(), model);
333    }
334
335    pub fn get_pricing_model(&self, model_id: &str) -> Option<&PricingModel> {
336        self.pricing_models.get(model_id)
337    }
338
339    pub fn list_pricing_models(&self) -> Vec<String> {
340        self.pricing_models.keys().cloned().collect()
341    }
342
343    pub fn market_data(&self) -> &MarketData {
344        &self.market_data
345    }
346
347    pub fn market_data_mut(&mut self) -> &mut MarketData {
348        &mut self.market_data
349    }
350}
351
352impl ValuationEngine {
353    pub fn new() -> Self {
354        Self {
355            valuation_methods: HashMap::new(),
356            discount_rates: HashMap::new(),
357            cash_flow_projections: HashMap::new(),
358        }
359    }
360
361    pub fn initialize(&mut self) -> Result<(), FinancialError> {
362        Ok(())
363    }
364
365    pub fn add_valuation_method(&mut self, method: ValuationMethod) {
366        self.valuation_methods
367            .insert(method.method_id.clone(), method);
368    }
369
370    pub fn get_valuation_method(&self, method_id: &str) -> Option<&ValuationMethod> {
371        self.valuation_methods.get(method_id)
372    }
373
374    pub fn list_valuation_methods(&self) -> Vec<String> {
375        self.valuation_methods.keys().cloned().collect()
376    }
377
378    pub fn set_discount_rate(&mut self, name: &str, rate: f64) {
379        self.discount_rates.insert(name.to_string(), rate);
380    }
381
382    pub fn get_discount_rate(&self, name: &str) -> Option<&f64> {
383        self.discount_rates.get(name)
384    }
385
386    pub fn add_cash_flow_projection(&mut self, projection: CashFlowProjection) {
387        self.cash_flow_projections
388            .insert(projection.projection_id.clone(), projection);
389    }
390
391    pub fn get_cash_flow_projection(&self, projection_id: &str) -> Option<&CashFlowProjection> {
392        self.cash_flow_projections.get(projection_id)
393    }
394}