1use std::str::FromStr;
16
17use serde::{Deserialize, Serialize};
18
19use crate::core::trade::PutOrCall;
20use crate::core::utils::{dN, N};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum FuturesSettlement {
26 Discounted,
28 Margined,
30}
31
32impl FromStr for FuturesSettlement {
33 type Err = String;
34 fn from_str(s: &str) -> Result<Self, Self::Err> {
35 match s.trim().to_lowercase().as_str() {
36 "discounted" | "black76" | "premium" | "premium_settled" => {
37 Ok(FuturesSettlement::Discounted)
38 }
39 "margined" | "futures_style" | "future_style" | "futures-style" => {
40 Ok(FuturesSettlement::Margined)
41 }
42 other => Err(format!(
43 "Invalid futures settlement '{other}' (use 'discounted' or 'margined')"
44 )),
45 }
46 }
47}
48
49impl FuturesSettlement {
50 pub fn discount_factor(&self, r: f64, t: f64) -> f64 {
53 match self {
54 FuturesSettlement::Discounted => (-r * t).exp(),
55 FuturesSettlement::Margined => 1.0,
56 }
57 }
58}
59
60fn d1_d2(f: f64, k: f64, sigma: f64, t: f64) -> (f64, f64) {
61 let st = sigma * t.sqrt();
62 let d1 = ((f / k).ln() + 0.5 * sigma * sigma * t) / st;
63 (d1, d1 - st)
64}
65
66pub fn price(
68 f: f64,
69 k: f64,
70 r: f64,
71 sigma: f64,
72 t: f64,
73 put_or_call: PutOrCall,
74 settlement: FuturesSettlement,
75) -> f64 {
76 assert!(f > 0.0 && k > 0.0, "futures price and strike must be positive");
77 let df = settlement.discount_factor(r, t);
78 if t <= 0.0 || sigma <= 0.0 {
79 let intrinsic = match put_or_call {
80 PutOrCall::Call => (f - k).max(0.0),
81 PutOrCall::Put => (k - f).max(0.0),
82 };
83 return df * intrinsic;
84 }
85 let (d1, d2) = d1_d2(f, k, sigma, t);
86 match put_or_call {
87 PutOrCall::Call => df * (f * N(d1) - k * N(d2)),
88 PutOrCall::Put => df * (k * N(-d2) - f * N(-d1)),
89 }
90}
91
92pub fn delta(
94 f: f64,
95 k: f64,
96 r: f64,
97 sigma: f64,
98 t: f64,
99 put_or_call: PutOrCall,
100 settlement: FuturesSettlement,
101) -> f64 {
102 let df = settlement.discount_factor(r, t);
103 let (d1, _) = d1_d2(f, k, sigma, t);
104 match put_or_call {
105 PutOrCall::Call => df * N(d1),
106 PutOrCall::Put => -df * N(-d1),
107 }
108}
109
110pub fn gamma(
112 f: f64,
113 k: f64,
114 r: f64,
115 sigma: f64,
116 t: f64,
117 settlement: FuturesSettlement,
118) -> f64 {
119 let df = settlement.discount_factor(r, t);
120 let (d1, _) = d1_d2(f, k, sigma, t);
121 df * dN(d1) / (f * sigma * t.sqrt())
122}
123
124pub fn vega(
126 f: f64,
127 k: f64,
128 r: f64,
129 sigma: f64,
130 t: f64,
131 settlement: FuturesSettlement,
132) -> f64 {
133 let df = settlement.discount_factor(r, t);
134 let (d1, _) = d1_d2(f, k, sigma, t);
135 df * f * dN(d1) * t.sqrt()
136}
137
138pub fn rho(
142 f: f64,
143 k: f64,
144 r: f64,
145 sigma: f64,
146 t: f64,
147 put_or_call: PutOrCall,
148 settlement: FuturesSettlement,
149) -> f64 {
150 match settlement {
151 FuturesSettlement::Margined => 0.0,
152 FuturesSettlement::Discounted => -t * price(f, k, r, sigma, t, put_or_call, settlement),
153 }
154}
155
156pub fn theta(
158 f: f64,
159 k: f64,
160 r: f64,
161 sigma: f64,
162 t: f64,
163 put_or_call: PutOrCall,
164 settlement: FuturesSettlement,
165) -> f64 {
166 let df = settlement.discount_factor(r, t);
167 let (d1, _) = d1_d2(f, k, sigma, t);
168 let bleed = df * f * dN(d1) * sigma / (2.0 * t.sqrt());
171 match settlement {
172 FuturesSettlement::Margined => -bleed,
173 FuturesSettlement::Discounted => {
174 r * price(f, k, r, sigma, t, put_or_call, settlement) - bleed
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::equity::blackscholes::bs_price;
183
184 const F: f64 = 100.0;
185 const K: f64 = 100.0;
186 const R: f64 = 0.05;
187 const SIG: f64 = 0.30;
188 const T: f64 = 1.0;
189
190 #[test]
192 fn discounted_golden_values() {
193 use FuturesSettlement::Discounted as D;
194 assert!((price(F, K, R, SIG, T, PutOrCall::Call, D) - 11.34202064).abs() < 1e-7);
195 assert!((delta(F, K, R, SIG, T, PutOrCall::Call, D) - 0.53232482).abs() < 1e-7);
196 assert!((delta(F, K, R, SIG, T, PutOrCall::Put, D) + 0.41890461).abs() < 1e-7);
197 assert!((gamma(F, K, R, SIG, T, D) - 0.01250801).abs() < 1e-7);
198 assert!((vega(F, K, R, SIG, T, D) - 37.52403469).abs() < 1e-6);
199 assert!((rho(F, K, R, SIG, T, PutOrCall::Call, D) + 11.34202064).abs() < 1e-6);
200 assert!((theta(F, K, R, SIG, T, PutOrCall::Call, D) + 5.06150417).abs() < 1e-6);
201 }
202
203 #[test]
204 fn margined_golden_values() {
205 use FuturesSettlement::Margined as M;
206 assert!((price(F, K, R, SIG, T, PutOrCall::Call, M) - 11.92353847).abs() < 1e-7);
207 assert!((delta(F, K, R, SIG, T, PutOrCall::Call, M) - 0.55961769).abs() < 1e-7);
208 assert!((gamma(F, K, R, SIG, T, M) - 0.01314931).abs() < 1e-7);
209 assert!((vega(F, K, R, SIG, T, M) - 39.44793309).abs() < 1e-6);
210 assert!((theta(F, K, R, SIG, T, PutOrCall::Call, M) + 5.91718996).abs() < 1e-6);
211 }
212
213 #[test]
214 fn margined_rho_is_zero() {
215 for pc in [PutOrCall::Call, PutOrCall::Put] {
217 assert_eq!(rho(F, K, R, SIG, T, pc, FuturesSettlement::Margined), 0.0);
218 }
219 }
220
221 #[test]
222 fn margined_exceeds_discounted() {
223 let disc = price(F, K, R, SIG, T, PutOrCall::Call, FuturesSettlement::Discounted);
225 let marg = price(F, K, R, SIG, T, PutOrCall::Call, FuturesSettlement::Margined);
226 assert!(marg > disc);
227 assert!((marg - disc * (R * T).exp()).abs() < 1e-10);
229 }
230
231 #[test]
232 fn put_call_parity_both_styles() {
233 for (s, factor) in [
234 (FuturesSettlement::Discounted, (-R * T).exp()),
235 (FuturesSettlement::Margined, 1.0),
236 ] {
237 let c = price(F, 95.0, R, SIG, T, PutOrCall::Call, s);
238 let p = price(F, 95.0, R, SIG, T, PutOrCall::Put, s);
239 assert!((c - p - factor * (F - 95.0)).abs() < 1e-10, "{s:?}");
240 }
241 }
242
243 #[test]
244 fn discounted_black76_equals_black_scholes_at_the_forward() {
245 let (s, q) = (100.0, 0.02);
247 let fwd = s * ((R - q) * T).exp();
248 let b76 = price(fwd, K, R, SIG, T, PutOrCall::Call, FuturesSettlement::Discounted);
249 let bsm = bs_price(s, K, R, q, SIG, T, PutOrCall::Call);
250 assert!((b76 - bsm).abs() < 1e-10, "b76 {b76} vs bsm {bsm}");
251 }
252
253 #[test]
254 fn settlement_parses_from_strings() {
255 assert_eq!("discounted".parse::<FuturesSettlement>().unwrap(), FuturesSettlement::Discounted);
256 assert_eq!("black76".parse::<FuturesSettlement>().unwrap(), FuturesSettlement::Discounted);
257 assert_eq!("margined".parse::<FuturesSettlement>().unwrap(), FuturesSettlement::Margined);
258 assert_eq!("futures_style".parse::<FuturesSettlement>().unwrap(), FuturesSettlement::Margined);
259 assert!("bad".parse::<FuturesSettlement>().is_err());
260 }
261}