rustyqlib/equity/
forward_start_option.rs1use crate::core::trade::PutOrCall;
13use crate::core::utils::ContractStyle;
14use crate::equity::blackscholes::bs_price;
15use crate::equity::utils::{Payoff, PayoffType};
16
17#[derive(Debug, Clone)]
18pub struct ForwardStartPayoff {
19 pub put_or_call: PutOrCall,
20 pub exercise_style: ContractStyle,
21 pub strike_fraction: f64,
23 pub start_fraction: f64,
25}
26
27impl Payoff for ForwardStartPayoff {
28 fn payoff(&self, _spot: f64, _strike: f64) -> f64 {
30 0.0
31 }
32 fn path_payoff(&self, path: &[f64], _strike: f64) -> f64 {
33 let n = path.len();
34 let idx = ((self.start_fraction * n as f64).round() as usize).clamp(1, n - 1) - 1;
36 let strike = self.strike_fraction * path[idx];
37 let terminal = path[n - 1];
38 match self.put_or_call {
39 PutOrCall::Call => (terminal - strike).max(0.0),
40 PutOrCall::Put => (strike - terminal).max(0.0),
41 }
42 }
43 fn path_payoff_var<'t>(
44 &self,
45 path: &[crate::core::aad::Var<'t>],
46 _strike: f64,
47 ) -> Option<crate::core::aad::Var<'t>> {
48 let n = path.len();
49 let idx = ((self.start_fraction * n as f64).round() as usize).clamp(1, n - 1) - 1;
50 let strike = path[idx] * self.strike_fraction;
51 let terminal = path[n - 1];
52 Some(match self.put_or_call {
53 PutOrCall::Call => (terminal - strike).maxf(0.0),
54 PutOrCall::Put => (strike - terminal).maxf(0.0),
55 })
56 }
57 fn is_path_dependent(&self) -> bool {
58 true
59 }
60 fn payoff_kind(&self) -> PayoffType {
61 PayoffType::ForwardStart
62 }
63 fn put_or_call(&self) -> &PutOrCall {
64 &self.put_or_call
65 }
66 fn exercise_style(&self) -> &ContractStyle {
67 &self.exercise_style
68 }
69 fn as_any(&self) -> &dyn std::any::Any {
70 self
71 }
72 fn clone_box(&self) -> Box<dyn Payoff> {
73 Box::new(self.clone())
74 }
75}
76
77#[allow(clippy::too_many_arguments)]
81pub fn forward_start_price(
82 s: f64,
83 strike_fraction: f64,
84 r: f64,
85 q: f64,
86 sigma: f64,
87 start_t: f64,
88 t: f64,
89 put_or_call: PutOrCall,
90) -> f64 {
91 assert!(start_t > 0.0 && start_t < t, "fixing must lie inside the option life");
92 s * (-q * start_t).exp() * bs_price(1.0, strike_fraction, r, q, sigma, t - start_t, put_or_call)
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn reduces_to_vanilla_when_fixing_is_immediate() {
101 let fs = forward_start_price(100.0, 1.0, 0.05, 0.02, 0.3, 1e-6, 1.0, PutOrCall::Call);
104 let vanilla = bs_price(100.0, 100.0, 0.05, 0.02, 0.3, 1.0, PutOrCall::Call);
105 assert!((fs - vanilla).abs() < 1e-3, "{fs} vs {vanilla}");
106 }
107
108 #[test]
109 fn price_is_homogeneous_in_spot() {
110 let p1 = forward_start_price(100.0, 1.0, 0.05, 0.02, 0.3, 0.5, 1.0, PutOrCall::Call);
111 let p2 = forward_start_price(200.0, 1.0, 0.05, 0.02, 0.3, 0.5, 1.0, PutOrCall::Call);
112 assert!((p2 - 2.0 * p1).abs() < 1e-12);
113 }
114
115 #[test]
116 fn path_payoff_reads_fixing_off_the_path() {
117 let payoff = ForwardStartPayoff {
118 put_or_call: PutOrCall::Call,
119 exercise_style: ContractStyle::European,
120 strike_fraction: 1.0,
121 start_fraction: 0.5,
122 };
123 let path = [100.0, 90.0, 110.0, 120.0];
125 assert!((payoff.path_payoff(&path, 0.0) - 30.0).abs() < 1e-12); }
127}