1use std::str::FromStr;
16
17use serde::{Deserialize, Serialize};
18
19use crate::core::trade::PutOrCall;
20use crate::core::utils::{norm_pdf, norm_cdf};
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 * norm_cdf(d1) - k * norm_cdf(d2)),
88 PutOrCall::Put => df * (k * norm_cdf(-d2) - f * norm_cdf(-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 * norm_cdf(d1),
106 PutOrCall::Put => -df * norm_cdf(-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 * norm_pdf(d1) / (f * sigma * t.sqrt())
122}
123
124pub fn gamma_p(
127 f: f64,
128 k: f64,
129 r: f64,
130 sigma: f64,
131 t: f64,
132 put_or_call: PutOrCall,
133 settlement: FuturesSettlement,
134) -> f64 {
135 let d = delta(f, k, r, sigma, t, put_or_call, settlement);
136 if d == 0.0 {
137 f64::NAN
138 } else {
139 f * gamma(f, k, r, sigma, t, settlement) / d
140 }
141}
142
143pub fn zomma(
145 f: f64,
146 k: f64,
147 r: f64,
148 sigma: f64,
149 t: f64,
150 settlement: FuturesSettlement,
151) -> f64 {
152 let (d1, d2) = d1_d2(f, k, sigma, t);
153 gamma(f, k, r, sigma, t, settlement) * (d1 * d2 - 1.0) / sigma
154}
155
156pub fn vega(
158 f: f64,
159 k: f64,
160 r: f64,
161 sigma: f64,
162 t: f64,
163 settlement: FuturesSettlement,
164) -> f64 {
165 let df = settlement.discount_factor(r, t);
166 let (d1, _) = d1_d2(f, k, sigma, t);
167 df * f * norm_pdf(d1) * t.sqrt()
168}
169
170pub fn volga(
175 f: f64,
176 k: f64,
177 r: f64,
178 sigma: f64,
179 t: f64,
180 settlement: FuturesSettlement,
181) -> f64 {
182 let (d1, d2) = d1_d2(f, k, sigma, t);
183 vega(f, k, r, sigma, t, settlement) * d1 * d2 / sigma
184}
185
186pub fn vanna(
188 f: f64,
189 k: f64,
190 r: f64,
191 sigma: f64,
192 t: f64,
193 settlement: FuturesSettlement,
194) -> f64 {
195 let df = settlement.discount_factor(r, t);
196 let (d1, _) = d1_d2(f, k, sigma, t);
197 df * norm_pdf(d1) * (t.sqrt() - d1 / sigma)
198}
199
200pub fn charm(
202 f: f64,
203 k: f64,
204 r: f64,
205 sigma: f64,
206 t: f64,
207 put_or_call: PutOrCall,
208 settlement: FuturesSettlement,
209) -> f64 {
210 let df = settlement.discount_factor(r, t);
211 let (d1, _) = d1_d2(f, k, sigma, t);
212 let d1_dt = sigma / (2.0 * t.sqrt()) - d1 / (2.0 * t);
213 let delta_component = match put_or_call {
214 PutOrCall::Call => norm_cdf(d1),
215 PutOrCall::Put => norm_cdf(d1) - 1.0,
216 };
217 let discount_decay = match settlement {
218 FuturesSettlement::Discounted => r * df * delta_component,
219 FuturesSettlement::Margined => 0.0,
220 };
221 discount_decay - df * norm_pdf(d1) * d1_dt
222}
223
224pub fn rho(
228 f: f64,
229 k: f64,
230 r: f64,
231 sigma: f64,
232 t: f64,
233 put_or_call: PutOrCall,
234 settlement: FuturesSettlement,
235) -> f64 {
236 match settlement {
237 FuturesSettlement::Margined => 0.0,
238 FuturesSettlement::Discounted => -t * price(f, k, r, sigma, t, put_or_call, settlement),
239 }
240}
241
242pub fn theta(
244 f: f64,
245 k: f64,
246 r: f64,
247 sigma: f64,
248 t: f64,
249 put_or_call: PutOrCall,
250 settlement: FuturesSettlement,
251) -> f64 {
252 let df = settlement.discount_factor(r, t);
253 let (d1, _) = d1_d2(f, k, sigma, t);
254 let bleed = df * f * norm_pdf(d1) * sigma / (2.0 * t.sqrt());
257 match settlement {
258 FuturesSettlement::Margined => -bleed,
259 FuturesSettlement::Discounted => {
260 r * price(f, k, r, sigma, t, put_or_call, settlement) - bleed
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::equity::blackscholes::bs_price;
269
270 const F: f64 = 100.0;
271 const K: f64 = 100.0;
272 const R: f64 = 0.05;
273 const SIG: f64 = 0.30;
274 const T: f64 = 1.0;
275
276 #[test]
278 fn discounted_golden_values() {
279 use FuturesSettlement::Discounted as D;
280 assert!((price(F, K, R, SIG, T, PutOrCall::Call, D) - 11.34202064).abs() < 1e-7);
281 assert!((delta(F, K, R, SIG, T, PutOrCall::Call, D) - 0.53232482).abs() < 1e-7);
282 assert!((delta(F, K, R, SIG, T, PutOrCall::Put, D) + 0.41890461).abs() < 1e-7);
283 assert!((gamma(F, K, R, SIG, T, D) - 0.01250801).abs() < 1e-7);
284 assert!((vega(F, K, R, SIG, T, D) - 37.52403469).abs() < 1e-6);
285 let h_vol = 1e-5;
286 let bumped_vanna = (delta(F, K, R, SIG + h_vol, T, PutOrCall::Call, D)
287 - delta(F, K, R, SIG - h_vol, T, PutOrCall::Call, D))
288 / (2.0 * h_vol);
289 assert!((vanna(F, K, R, SIG, T, D) - bumped_vanna).abs() < 1e-9);
290 let h_time = 1e-5;
291 let bumped_charm = -(delta(F, K, R, SIG, T + h_time, PutOrCall::Call, D)
292 - delta(F, K, R, SIG, T - h_time, PutOrCall::Call, D))
293 / (2.0 * h_time);
294 assert!((charm(F, K, R, SIG, T, PutOrCall::Call, D) - bumped_charm).abs() < 1e-9);
295 assert!((rho(F, K, R, SIG, T, PutOrCall::Call, D) + 11.34202064).abs() < 1e-6);
296 assert!((theta(F, K, R, SIG, T, PutOrCall::Call, D) + 5.06150417).abs() < 1e-6);
297 }
298
299 #[test]
300 fn margined_golden_values() {
301 use FuturesSettlement::Margined as M;
302 assert!((price(F, K, R, SIG, T, PutOrCall::Call, M) - 11.92353847).abs() < 1e-7);
303 assert!((delta(F, K, R, SIG, T, PutOrCall::Call, M) - 0.55961769).abs() < 1e-7);
304 assert!((gamma(F, K, R, SIG, T, M) - 0.01314931).abs() < 1e-7);
305 assert!((vega(F, K, R, SIG, T, M) - 39.44793309).abs() < 1e-6);
306 assert!((theta(F, K, R, SIG, T, PutOrCall::Call, M) + 5.91718996).abs() < 1e-6);
307 }
308
309 #[test]
310 fn volga_matches_vega_bump_and_smile_sign() {
311 for s in [FuturesSettlement::Discounted, FuturesSettlement::Margined] {
312 let h = 1e-5;
314 let bump =
315 (vega(F, K, R, SIG + h, T, s) - vega(F, K, R, SIG - h, T, s)) / (2.0 * h);
316 assert!((volga(F, K, R, SIG, T, s) - bump).abs() < 1e-6, "{s:?}");
317 assert!(volga(F, 100.0, R, SIG, T, s) < 0.0);
319 assert!(volga(F, 70.0, R, SIG, T, s) > 0.0);
320 assert!(volga(F, 130.0, R, SIG, T, s) > 0.0);
321 }
322 assert!(
324 (volga(F, K, R, SIG, T, FuturesSettlement::Discounted) + 2.81430260).abs() < 1e-6
325 );
326 assert!(
327 (volga(F, K, R, SIG, T, FuturesSettlement::Margined) + 2.95859498).abs() < 1e-6
328 );
329 }
330
331 #[test]
332 fn margined_rho_is_zero() {
333 for pc in [PutOrCall::Call, PutOrCall::Put] {
335 assert_eq!(rho(F, K, R, SIG, T, pc, FuturesSettlement::Margined), 0.0);
336 }
337 }
338
339 #[test]
340 fn margined_exceeds_discounted() {
341 let disc = price(F, K, R, SIG, T, PutOrCall::Call, FuturesSettlement::Discounted);
343 let marg = price(F, K, R, SIG, T, PutOrCall::Call, FuturesSettlement::Margined);
344 assert!(marg > disc);
345 assert!((marg - disc * (R * T).exp()).abs() < 1e-10);
347 }
348
349 #[test]
350 fn put_call_parity_both_styles() {
351 for (s, factor) in [
352 (FuturesSettlement::Discounted, (-R * T).exp()),
353 (FuturesSettlement::Margined, 1.0),
354 ] {
355 let c = price(F, 95.0, R, SIG, T, PutOrCall::Call, s);
356 let p = price(F, 95.0, R, SIG, T, PutOrCall::Put, s);
357 assert!((c - p - factor * (F - 95.0)).abs() < 1e-10, "{s:?}");
358 }
359 }
360
361 #[test]
362 fn discounted_black76_equals_black_scholes_at_the_forward() {
363 let (s, q) = (100.0, 0.02);
365 let fwd = s * ((R - q) * T).exp();
366 let b76 = price(fwd, K, R, SIG, T, PutOrCall::Call, FuturesSettlement::Discounted);
367 let bsm = bs_price(s, K, R, q, SIG, T, PutOrCall::Call);
368 assert!((b76 - bsm).abs() < 1e-10, "b76 {b76} vs bsm {bsm}");
369 }
370
371 #[test]
372 fn settlement_parses_from_strings() {
373 assert_eq!("discounted".parse::<FuturesSettlement>().unwrap(), FuturesSettlement::Discounted);
374 assert_eq!("black76".parse::<FuturesSettlement>().unwrap(), FuturesSettlement::Discounted);
375 assert_eq!("margined".parse::<FuturesSettlement>().unwrap(), FuturesSettlement::Margined);
376 assert_eq!("futures_style".parse::<FuturesSettlement>().unwrap(), FuturesSettlement::Margined);
377 assert!("bad".parse::<FuturesSettlement>().is_err());
378 }
379}