1use crate::core::trade::PutOrCall;
20use crate::core::utils::ContractStyle;
21use crate::equity::utils::{Payoff, PayoffType};
22
23#[derive(Debug, Clone)]
24pub struct AutocallablePayoff {
25 pub exercise_style: ContractStyle,
26 pub autocall_barrier: f64,
28 pub protection_barrier: f64,
30 pub coupon: f64,
32 pub observations: usize,
35 pub observation_times: Option<Vec<f64>>,
40 pub notional: f64,
41 pub initial_fixing: f64,
43 pub coupon_barrier: Option<f64>,
47 pub memory: bool,
50}
51
52impl AutocallablePayoff {
53 pub fn path_value(&self, path: &[f64], obs_idx: &[usize], dfs: &[f64]) -> f64 {
57 match self.coupon_barrier {
58 None => self.athena_path_value(path, obs_idx, dfs),
59 Some(cb) => self.phoenix_path_value(path, obs_idx, dfs, cb),
60 }
61 }
62
63 fn athena_path_value(&self, path: &[f64], obs_idx: &[usize], dfs: &[f64]) -> f64 {
65 for (m, &idx) in obs_idx.iter().enumerate() {
66 if path[idx] >= self.autocall_barrier {
67 return (self.notional + self.coupon * (m + 1) as f64) * dfs[m];
68 }
69 }
70 self.redemption_at_maturity(path) * dfs.last().unwrap()
71 }
72
73 fn phoenix_path_value(&self, path: &[f64], obs_idx: &[usize], dfs: &[f64], cb: f64) -> f64 {
76 let mut value = 0.0;
77 let mut missed = 0usize;
78 for (m, &idx) in obs_idx.iter().enumerate() {
79 let s = path[idx];
80 if s >= cb {
81 let units = if self.memory { 1 + missed } else { 1 };
82 value += self.coupon * units as f64 * dfs[m];
83 missed = 0;
84 } else {
85 missed += 1;
86 }
87 if s >= self.autocall_barrier {
88 return value + self.notional * dfs[m];
89 }
90 }
91 value + self.redemption_at_maturity(path) * dfs.last().unwrap()
92 }
93
94 fn redemption_at_maturity(&self, path: &[f64]) -> f64 {
96 let knocked_in = path.iter().any(|&s| s <= self.protection_barrier);
97 if knocked_in {
98 self.notional * (path.last().unwrap() / self.initial_fixing)
99 } else {
100 self.notional
101 }
102 }
103}
104
105impl Payoff for AutocallablePayoff {
106 fn payoff(&self, _spot: f64, _strike: f64) -> f64 {
109 0.0
110 }
111 fn path_payoff(&self, _path: &[f64], _strike: f64) -> f64 {
112 panic!(
113 "Autocallables pay at multiple dates and cannot be valued through \
114 path_payoff; the Monte Carlo engine prices them via path_value"
115 );
116 }
117 fn is_path_dependent(&self) -> bool {
118 true
119 }
120 fn payoff_kind(&self) -> PayoffType {
121 PayoffType::Autocallable
122 }
123 fn put_or_call(&self) -> &PutOrCall {
124 &PutOrCall::Call
127 }
128 fn exercise_style(&self) -> &ContractStyle {
129 &self.exercise_style
130 }
131 fn as_any(&self) -> &dyn std::any::Any {
132 self
133 }
134 fn clone_box(&self) -> Box<dyn Payoff> {
135 Box::new(self.clone())
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn note() -> AutocallablePayoff {
144 AutocallablePayoff {
145 exercise_style: ContractStyle::European,
146 autocall_barrier: 100.0,
147 protection_barrier: 70.0,
148 coupon: 5.0,
149 observations: 4,
150 observation_times: None,
151 notional: 100.0,
152 initial_fixing: 100.0,
153 coupon_barrier: None,
154 memory: false,
155 }
156 }
157
158 #[test]
159 fn calls_at_first_breach_with_accrued_coupon() {
160 let payoff = note();
161 let obs_idx = [1, 3, 5, 7];
162 let dfs = [0.99, 0.98, 0.97, 0.96];
163 let path = [90.0, 95.0, 99.0, 101.0, 50.0, 50.0, 50.0, 50.0];
165 let value = payoff.path_value(&path, &obs_idx, &dfs);
166 assert!((value - (100.0 + 2.0 * 5.0) * 0.98).abs() < 1e-12);
167 }
168
169 #[test]
170 fn protected_redemption_when_never_called_nor_knocked() {
171 let payoff = note();
172 let path = [90.0, 92.0, 91.0, 95.0, 93.0, 92.0, 94.0, 96.0];
173 let value = payoff.path_value(&path, &[1, 3, 5, 7], &[0.99, 0.98, 0.97, 0.96]);
174 assert!((value - 100.0 * 0.96).abs() < 1e-12);
175 }
176
177 fn phoenix(memory: bool) -> AutocallablePayoff {
178 let mut p = note();
179 p.coupon_barrier = Some(80.0);
180 p.memory = memory;
181 p
182 }
183
184 #[test]
185 fn phoenix_pays_conditional_coupons_and_redeems_at_call() {
186 let payoff = phoenix(false);
187 let obs_idx = [1, 3, 5, 7];
188 let dfs = [0.99, 0.98, 0.97, 0.96];
189 let path = [90.0, 85.0, 99.0, 101.0, 50.0, 50.0, 50.0, 50.0];
191 let value = payoff.path_value(&path, &obs_idx, &dfs);
192 let expect = 5.0 * 0.99 + 5.0 * 0.98 + 100.0 * 0.98;
193 assert!((value - expect).abs() < 1e-12, "{value} vs {expect}");
194 }
195
196 #[test]
197 fn phoenix_memory_recovers_missed_coupons() {
198 let no_memory = phoenix(false);
199 let with_memory = phoenix(true);
200 let obs_idx = [1, 3, 5, 7];
201 let dfs = [0.99, 0.98, 0.97, 0.96];
202 let path = [90.0, 75.0, 78.0, 85.0, 90.0, 88.0, 90.0, 95.0];
205 let v_plain = no_memory.path_value(&path, &obs_idx, &dfs);
206 let v_memory = with_memory.path_value(&path, &obs_idx, &dfs);
207 let plain = 5.0 * (0.98 + 0.97 + 0.96) + 100.0 * 0.96;
209 assert!((v_plain - plain).abs() < 1e-12, "{v_plain} vs {plain}");
211 assert!((v_memory - (plain + 5.0 * 0.98)).abs() < 1e-12, "{v_memory}");
212 }
213
214 #[test]
215 fn phoenix_with_zero_coupon_equals_athena_with_zero_coupon() {
216 let mut athena = note();
218 athena.coupon = 0.0;
219 let mut phx = phoenix(true);
220 phx.coupon = 0.0;
221 let obs_idx = [1, 3, 5, 7];
222 let dfs = [0.99, 0.98, 0.97, 0.96];
223 for path in [
224 [90.0, 85.0, 99.0, 101.0, 50.0, 50.0, 50.0, 50.0],
225 [90.0, 65.0, 75.0, 80.0, 78.0, 82.0, 79.0, 80.0],
226 [90.0, 92.0, 91.0, 95.0, 93.0, 92.0, 94.0, 96.0],
227 ] {
228 let a = athena.path_value(&path, &obs_idx, &dfs);
229 let p = phx.path_value(&path, &obs_idx, &dfs);
230 assert!((a - p).abs() < 1e-12);
231 }
232 }
233
234 #[test]
235 fn downside_participation_after_knock_in() {
236 let payoff = note();
237 let path = [90.0, 65.0, 75.0, 80.0, 78.0, 82.0, 79.0, 80.0];
239 let value = payoff.path_value(&path, &[1, 3, 5, 7], &[0.99, 0.98, 0.97, 0.96]);
240 assert!((value - 100.0 * (80.0 / 100.0) * 0.96).abs() < 1e-12);
241 }
242}