Skip to main content

rustyqlib/equity/
builder.rs

1//! Ergonomic construction of [`EquityOption`] from Rust code.
2//!
3//! The JSON path ([`EquityOption::from_json`]) is the primary interface for
4//! the CLI; this builder is the equivalent for library users and for the
5//! runnable examples in `examples/`.
6//!
7//! ```no_run
8//! use rustyqlib::equity::builder::EquityOptionBuilder;
9//! use rustyqlib::equity::utils::Engine;
10//! use rustyqlib::core::trade::PutOrCall;
11//! use rustyqlib::Instrument;
12//!
13//! let option = EquityOptionBuilder::new()
14//!     .spot(100.0)
15//!     .strike(100.0)
16//!     .flat_vol(0.30)
17//!     .flat_rate(0.05)
18//!     .years_to_maturity(1.0)
19//!     .vanilla(PutOrCall::Call)
20//!     .engine(Engine::BlackScholes)
21//!     .build();
22//! println!("{}", option.npv());
23//! ```
24
25use chrono::{Duration, Local, NaiveDate};
26
27use crate::core::curves::{Compounding, YieldCurve};
28use crate::core::daycount::DayCountConvention;
29use crate::core::quotes::Quote;
30use crate::core::trade::PutOrCall;
31use crate::core::utils::ContractStyle;
32use crate::core::vols::VolSurface;
33use crate::equity::asian::{AsianStrikeType, AveragingType};
34use crate::equity::autocallable::AutocallablePayoff;
35use crate::equity::barrier::{BarrierDirection, KnockType};
36use crate::equity::finite_difference::FdConfig;
37use crate::equity::forward_start_option::ForwardStartPayoff;
38use crate::equity::heston::HestonParams;
39use crate::equity::montecarlo::{McModel, MonteCarloConfig};
40use crate::equity::utils::{Engine, LongShort, Payoff};
41use crate::equity::vanila_option::{
42    AsianPayoff, BarrierPayoff, BinaryPayoff, BinaryType, EquityOption, EquityOptionBase,
43    VanillaPayoff,
44};
45
46pub struct EquityOptionBuilder {
47    symbol: String,
48    spot: f64,
49    strike: f64,
50    vol_surface: Option<VolSurface>,
51    flat_vol: f64,
52    discount_curve: Option<YieldCurve>,
53    flat_rate: f64,
54    dividend_yield: f64,
55    borrow_cost: f64,
56    cash_dividends: Vec<(NaiveDate, f64)>,
57    futures_settlement: Option<crate::equity::black76::FuturesSettlement>,
58    valuation_date: NaiveDate,
59    maturity_date: Option<NaiveDate>,
60    exercise_style: ContractStyle,
61    payoff: Option<Box<dyn Payoff>>,
62    engine: Engine,
63    mc: MonteCarloConfig,
64    fd: FdConfig,
65    heston: Option<HestonParams>,
66}
67
68impl Default for EquityOptionBuilder {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74impl EquityOptionBuilder {
75    pub fn new() -> Self {
76        EquityOptionBuilder {
77            symbol: "TEST".to_string(),
78            spot: 100.0,
79            strike: 100.0,
80            vol_surface: None,
81            flat_vol: 0.2,
82            discount_curve: None,
83            flat_rate: 0.0,
84            dividend_yield: 0.0,
85            borrow_cost: 0.0,
86            cash_dividends: Vec::new(),
87            futures_settlement: None,
88            valuation_date: Local::now().date_naive(),
89            maturity_date: None,
90            exercise_style: ContractStyle::European,
91            payoff: None,
92            engine: Engine::BlackScholes,
93            mc: MonteCarloConfig::default(),
94            fd: FdConfig::default(),
95            heston: None,
96        }
97    }
98
99    // ── Market data ─────────────────────────────────────────────────────
100
101    pub fn symbol(mut self, symbol: &str) -> Self {
102        self.symbol = symbol.to_string();
103        self
104    }
105    pub fn spot(mut self, spot: f64) -> Self {
106        self.spot = spot;
107        self
108    }
109    pub fn strike(mut self, strike: f64) -> Self {
110        self.strike = strike;
111        self
112    }
113    pub fn flat_vol(mut self, vol: f64) -> Self {
114        self.flat_vol = vol;
115        self.vol_surface = None;
116        self
117    }
118    pub fn vol_surface(mut self, surface: VolSurface) -> Self {
119        self.vol_surface = Some(surface);
120        self
121    }
122    pub fn flat_rate(mut self, rate: f64) -> Self {
123        self.flat_rate = rate;
124        self.discount_curve = None;
125        self
126    }
127    pub fn discount_curve(mut self, curve: YieldCurve) -> Self {
128        self.discount_curve = Some(curve);
129        self
130    }
131    pub fn dividend_yield(mut self, q: f64) -> Self {
132        self.dividend_yield = q;
133        self
134    }
135    /// Continuous stock borrow (repo) cost; part of the carry.
136    pub fn borrow_cost(mut self, b: f64) -> Self {
137        self.borrow_cost = b;
138        self
139    }
140    pub fn cash_dividend(mut self, date: NaiveDate, amount: f64) -> Self {
141        self.cash_dividends.push((date, amount));
142        self
143    }
144    /// Price the option on a future with Black-76: `spot` is then the
145    /// futures price `F`. European vanilla, Analytical engine only.
146    pub fn on_future(
147        mut self,
148        settlement: crate::equity::black76::FuturesSettlement,
149    ) -> Self {
150        self.futures_settlement = Some(settlement);
151        self
152    }
153
154    // ── Dates ───────────────────────────────────────────────────────────
155
156    pub fn valuation_date(mut self, date: NaiveDate) -> Self {
157        self.valuation_date = date;
158        self
159    }
160    pub fn maturity_date(mut self, date: NaiveDate) -> Self {
161        self.maturity_date = Some(date);
162        self
163    }
164    /// Convenience for examples: maturity = valuation + `years * 365` days.
165    pub fn years_to_maturity(mut self, years: f64) -> Self {
166        self.maturity_date =
167            Some(self.valuation_date + Duration::days((years * 365.0).round() as i64));
168        self
169    }
170
171    // ── Payoffs ─────────────────────────────────────────────────────────
172
173    pub fn american(mut self) -> Self {
174        self.exercise_style = ContractStyle::American;
175        self
176    }
177    pub fn exercise_style(mut self, style: ContractStyle) -> Self {
178        self.exercise_style = style;
179        self
180    }
181    pub fn payoff(mut self, payoff: Box<dyn Payoff>) -> Self {
182        self.payoff = Some(payoff);
183        self
184    }
185    pub fn vanilla(mut self, put_or_call: PutOrCall) -> Self {
186        let style = self.exercise_style.clone();
187        self.payoff = Some(Box::new(VanillaPayoff { put_or_call, exercise_style: style }));
188        self
189    }
190    pub fn binary(mut self, put_or_call: PutOrCall, binary_type: BinaryType, cash: f64) -> Self {
191        let style = self.exercise_style.clone();
192        self.payoff = Some(Box::new(BinaryPayoff {
193            put_or_call,
194            exercise_style: style,
195            binary_type,
196            cash,
197        }));
198        self
199    }
200    pub fn barrier(
201        mut self,
202        put_or_call: PutOrCall,
203        direction: BarrierDirection,
204        knock: KnockType,
205        barrier: f64,
206    ) -> Self {
207        let style = self.exercise_style.clone();
208        self.payoff = Some(Box::new(BarrierPayoff {
209            put_or_call,
210            exercise_style: style,
211            direction,
212            knock,
213            barrier,
214        }));
215        self
216    }
217    pub fn asian(
218        mut self,
219        put_or_call: PutOrCall,
220        averaging: AveragingType,
221        strike_type: AsianStrikeType,
222    ) -> Self {
223        let style = self.exercise_style.clone();
224        self.payoff = Some(Box::new(AsianPayoff {
225            put_or_call,
226            exercise_style: style,
227            averaging,
228            strike_type,
229        }));
230        self
231    }
232    /// `start_fraction` is the strike-fixing time as a fraction of the
233    /// option's life, in (0, 1).
234    pub fn forward_start(
235        mut self,
236        put_or_call: PutOrCall,
237        strike_fraction: f64,
238        start_fraction: f64,
239    ) -> Self {
240        let style = self.exercise_style.clone();
241        self.payoff = Some(Box::new(ForwardStartPayoff {
242            put_or_call,
243            exercise_style: style,
244            strike_fraction,
245            start_fraction,
246        }));
247        self
248    }
249    pub fn autocallable(
250        mut self,
251        autocall_barrier: f64,
252        protection_barrier: f64,
253        coupon: f64,
254        observations: usize,
255        notional: f64,
256    ) -> Self {
257        let style = self.exercise_style.clone();
258        self.payoff = Some(Box::new(AutocallablePayoff {
259            exercise_style: style,
260            autocall_barrier,
261            protection_barrier,
262            coupon,
263            observations,
264            notional,
265            initial_fixing: self.spot,
266        }));
267        self
268    }
269
270    // ── Engine and model ────────────────────────────────────────────────
271
272    pub fn engine(mut self, engine: Engine) -> Self {
273        self.engine = engine;
274        self
275    }
276    pub fn model(mut self, model: McModel) -> Self {
277        self.mc.model = model;
278        self
279    }
280    pub fn heston(mut self, params: HestonParams) -> Self {
281        self.heston = Some(params);
282        self.mc.model = McModel::Heston;
283        self
284    }
285    pub fn mc_config(mut self, cfg: MonteCarloConfig) -> Self {
286        self.mc = cfg;
287        self
288    }
289    pub fn paths(mut self, paths: usize) -> Self {
290        self.mc.paths = paths;
291        self
292    }
293    pub fn mc_time_steps(mut self, steps: usize) -> Self {
294        self.mc.time_steps = steps;
295        self
296    }
297    pub fn seed(mut self, seed: u64) -> Self {
298        self.mc.seed = seed;
299        self
300    }
301    pub fn fd_config(mut self, cfg: FdConfig) -> Self {
302        self.fd = cfg;
303        self
304    }
305    pub fn fd_grid(mut self, spot_steps: usize, time_steps: usize) -> Self {
306        self.fd.spot_steps = spot_steps;
307        self.fd.time_steps = time_steps;
308        self
309    }
310
311    pub fn build(self) -> EquityOption {
312        let maturity_date = self
313            .maturity_date
314            .expect("set maturity_date() or years_to_maturity() before build()");
315        let discount_curve = self.discount_curve.unwrap_or_else(|| {
316            YieldCurve::flat(
317                self.flat_rate,
318                self.valuation_date,
319                DayCountConvention::Act365,
320                Compounding::Continuous,
321            )
322            .expect("invalid flat rate")
323        });
324        let vol_surface = self.vol_surface.unwrap_or_else(|| {
325            VolSurface::flat(self.flat_vol, self.valuation_date, DayCountConvention::Act365)
326                .expect("invalid flat vol")
327        });
328        let base = EquityOptionBase {
329            symbol: self.symbol,
330            currency: None,
331            exchange: None,
332            name: None,
333            cusip: None,
334            isin: None,
335            settlement_type: None,
336            underlying_price: Quote::new(self.spot),
337            current_price: Quote::new(0.0),
338            strike_price: self.strike,
339            dividend_yield: self.dividend_yield,
340            borrow_cost: self.borrow_cost,
341            cash_dividends: self.cash_dividends,
342            futures_settlement: self.futures_settlement,
343            vol_surface,
344            maturity_date,
345            valuation_date: self.valuation_date,
346            discount_curve,
347            entry_price: 0.0,
348            long_short: LongShort::LONG,
349            multiplier: 1.0,
350        };
351        EquityOption {
352            base,
353            payoff: self.payoff.expect("set a payoff (vanilla(), barrier(), ...) before build()"),
354            engine: self.engine,
355            mc: self.mc,
356            fd: self.fd,
357            heston: self.heston,
358        }
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use crate::core::traits::Instrument;
366
367    #[test]
368    fn builder_reproduces_black_scholes_golden() {
369        let option = EquityOptionBuilder::new()
370            .spot(100.0)
371            .strike(100.0)
372            .flat_vol(0.3)
373            .flat_rate(0.05)
374            .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
375            .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
376            .vanilla(PutOrCall::Call)
377            .engine(Engine::BlackScholes)
378            .build();
379        assert!((option.npv() - 14.2312547860).abs() < 1e-8);
380        assert!((option.delta() - 0.6242517279).abs() < 1e-8);
381    }
382
383    #[test]
384    fn builder_carries_dividends_and_borrow() {
385        let option = EquityOptionBuilder::new()
386            .spot(100.0)
387            .dividend_yield(0.01)
388            .borrow_cost(0.02)
389            .years_to_maturity(1.0)
390            .vanilla(PutOrCall::Call)
391            .build();
392        assert!((option.base.carry_yield() - 0.03).abs() < 1e-12);
393    }
394
395    #[test]
396    fn american_flag_applies_to_the_payoff() {
397        let option = EquityOptionBuilder::new()
398            .spot(100.0)
399            .years_to_maturity(1.0)
400            .american()
401            .vanilla(PutOrCall::Put)
402            .build();
403        assert!(matches!(option.payoff.exercise_style(), ContractStyle::American));
404    }
405}