1use 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 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 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 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 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 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 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 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 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}