rustyqlib/equity/
autocallable.rs1use crate::core::trade::PutOrCall;
20use crate::core::utils::ContractStyle;
21use crate::equity::utils::{Payoff, PayoffType};
22
23#[derive(Debug)]
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,
34 pub notional: f64,
35 pub initial_fixing: f64,
37}
38
39impl AutocallablePayoff {
40 pub fn path_value(&self, path: &[f64], obs_idx: &[usize], dfs: &[f64]) -> f64 {
44 for (m, &idx) in obs_idx.iter().enumerate() {
45 if path[idx] >= self.autocall_barrier {
46 return (self.notional + self.coupon * (m + 1) as f64) * dfs[m];
47 }
48 }
49 let df_final = *dfs.last().unwrap();
51 let knocked_in = path.iter().any(|&s| s <= self.protection_barrier);
52 if knocked_in {
53 let terminal = *path.last().unwrap();
54 self.notional * (terminal / self.initial_fixing) * df_final
55 } else {
56 self.notional * df_final
57 }
58 }
59}
60
61impl Payoff for AutocallablePayoff {
62 fn payoff(&self, _spot: f64, _strike: f64) -> f64 {
65 0.0
66 }
67 fn path_payoff(&self, _path: &[f64], _strike: f64) -> f64 {
68 panic!(
69 "Autocallables pay at multiple dates and cannot be valued through \
70 path_payoff; the Monte Carlo engine prices them via path_value"
71 );
72 }
73 fn is_path_dependent(&self) -> bool {
74 true
75 }
76 fn payoff_kind(&self) -> PayoffType {
77 PayoffType::Autocallable
78 }
79 fn put_or_call(&self) -> &PutOrCall {
80 &PutOrCall::Call
83 }
84 fn exercise_style(&self) -> &ContractStyle {
85 &self.exercise_style
86 }
87 fn as_any(&self) -> &dyn std::any::Any {
88 self
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 fn note() -> AutocallablePayoff {
97 AutocallablePayoff {
98 exercise_style: ContractStyle::European,
99 autocall_barrier: 100.0,
100 protection_barrier: 70.0,
101 coupon: 5.0,
102 observations: 4,
103 notional: 100.0,
104 initial_fixing: 100.0,
105 }
106 }
107
108 #[test]
109 fn calls_at_first_breach_with_accrued_coupon() {
110 let payoff = note();
111 let obs_idx = [1, 3, 5, 7];
112 let dfs = [0.99, 0.98, 0.97, 0.96];
113 let path = [90.0, 95.0, 99.0, 101.0, 50.0, 50.0, 50.0, 50.0];
115 let value = payoff.path_value(&path, &obs_idx, &dfs);
116 assert!((value - (100.0 + 2.0 * 5.0) * 0.98).abs() < 1e-12);
117 }
118
119 #[test]
120 fn protected_redemption_when_never_called_nor_knocked() {
121 let payoff = note();
122 let path = [90.0, 92.0, 91.0, 95.0, 93.0, 92.0, 94.0, 96.0];
123 let value = payoff.path_value(&path, &[1, 3, 5, 7], &[0.99, 0.98, 0.97, 0.96]);
124 assert!((value - 100.0 * 0.96).abs() < 1e-12);
125 }
126
127 #[test]
128 fn downside_participation_after_knock_in() {
129 let payoff = note();
130 let path = [90.0, 65.0, 75.0, 80.0, 78.0, 82.0, 79.0, 80.0];
132 let value = payoff.path_value(&path, &[1, 3, 5, 7], &[0.99, 0.98, 0.97, 0.96]);
133 assert!((value - 100.0 * (80.0 / 100.0) * 0.96).abs() < 1e-12);
134 }
135}