Skip to main content

hypercall_engine/
greeks.rs

1//! Pure greek calculations used by engine admission and MMP checks.
2
3use crate::instrument::{perp_underlying, ParsedInstrument};
4use hypercall_types::{to_human_readable_decimal, Side};
5use rust_decimal::prelude::ToPrimitive;
6use rust_decimal::Decimal;
7use std::collections::HashMap;
8
9/// Compute fill delta and vega from engine-owned spot state and a pure IV lookup.
10///
11/// Returns `None` when critical option market data is missing or invalid. Callers
12/// must fail closed when `None` is returned.
13pub fn compute_fill_greeks_sync<F>(
14    spot_prices: &HashMap<String, Decimal>,
15    iv_lookup: F,
16    symbol: &str,
17    size: Decimal,
18    taker_side: Side,
19    timestamp_ms: u64,
20) -> Option<(f64, f64)>
21where
22    F: Fn(&str, f64, i64, f64) -> Option<f64>,
23{
24    if perp_underlying(symbol).is_some() {
25        return Some((0.0, 0.0));
26    }
27
28    let parsed = ParsedInstrument::parse(symbol).ok()?;
29
30    let spot = spot_prices.get(&parsed.underlying)?.to_f64()?;
31    if spot <= 0.0 {
32        return None;
33    }
34
35    let strike = parsed.strike.to_f64()?;
36    let expiry_ts = parsed.expiry_timestamp().ok()?;
37    let now_s = (timestamp_ms / 1000) as i64;
38    let tte = ((expiry_ts - now_s) as f64) / (365.25 * 86400.0);
39    if tte <= 0.0 {
40        return None;
41    }
42
43    let iv = iv_lookup(&parsed.underlying, strike, expiry_ts, spot)?;
44    if iv <= 0.0 {
45        return None;
46    }
47
48    let option_type = match parsed.option_type {
49        hypercall_types::OptionType::Call => hypercall_margin::OptionType::Call,
50        hypercall_types::OptionType::Put => hypercall_margin::OptionType::Put,
51    };
52    let (_, delta, _, vega, _) =
53        hypercall_margin::black_scholes::calculate_greeks(&option_type, spot, strike, tte, 0.0, iv);
54
55    let qty_human = to_human_readable_decimal(symbol, size).to_f64()?;
56    let side_multiplier = match taker_side {
57        Side::Buy => 1.0,
58        Side::Sell => -1.0,
59    };
60
61    Some((
62        delta * qty_human * side_multiplier,
63        vega * qty_human * side_multiplier,
64    ))
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use rust_decimal_macros::dec;
71
72    #[test]
73    fn missing_spot_fails_closed() {
74        assert_eq!(
75            compute_fill_greeks_sync(
76                &HashMap::new(),
77                |_, _, _, _| Some(0.5),
78                "ETH-20260131-4000-C",
79                hypercall_types::CONTRACT_UNIT_MULTIPLIER_DECIMAL,
80                Side::Buy,
81                1_700_000_000_000,
82            ),
83            None
84        );
85    }
86
87    #[test]
88    fn perp_has_zero_greeks() {
89        let mut spots = HashMap::new();
90        spots.insert("ETH".to_string(), dec!(3000));
91        assert_eq!(
92            compute_fill_greeks_sync(
93                &spots,
94                |_, _, _, _| None,
95                "ETH-PERP",
96                dec!(1),
97                Side::Buy,
98                1_700_000_000_000,
99            ),
100            Some((0.0, 0.0))
101        );
102    }
103
104    #[test]
105    fn invalid_option_symbol_fails_closed() {
106        let spots = HashMap::from([("ETH".to_string(), dec!(3000))]);
107        assert_eq!(
108            compute_fill_greeks_sync(
109                &spots,
110                |_, _, _, _| Some(0.5),
111                "ETH-31JAN2026-4000-C",
112                hypercall_types::CONTRACT_UNIT_MULTIPLIER_DECIMAL,
113                Side::Buy,
114                1_700_000_000_000,
115            ),
116            None
117        );
118    }
119
120    #[test]
121    fn zero_spot_fails_closed() {
122        let spots = HashMap::from([("ETH".to_string(), dec!(0))]);
123        assert_eq!(
124            compute_fill_greeks_sync(
125                &spots,
126                |_, _, _, _| Some(0.5),
127                "ETH-20260131-4000-C",
128                hypercall_types::CONTRACT_UNIT_MULTIPLIER_DECIMAL,
129                Side::Buy,
130                1_700_000_000_000,
131            ),
132            None
133        );
134    }
135
136    #[test]
137    fn zero_iv_fails_closed() {
138        let spots = HashMap::from([("ETH".to_string(), dec!(3000))]);
139        assert_eq!(
140            compute_fill_greeks_sync(
141                &spots,
142                |_, _, _, _| Some(0.0),
143                "ETH-20260131-4000-C",
144                hypercall_types::CONTRACT_UNIT_MULTIPLIER_DECIMAL,
145                Side::Buy,
146                1_700_000_000_000,
147            ),
148            None
149        );
150    }
151
152    #[test]
153    fn missing_iv_fails_closed() {
154        let spots = HashMap::from([("ETH".to_string(), dec!(3000))]);
155        assert_eq!(
156            compute_fill_greeks_sync(
157                &spots,
158                |_, _, _, _| None,
159                "ETH-20260131-4000-C",
160                hypercall_types::CONTRACT_UNIT_MULTIPLIER_DECIMAL,
161                Side::Buy,
162                1_700_000_000_000,
163            ),
164            None
165        );
166    }
167
168    #[test]
169    fn buy_call_has_positive_delta() {
170        let spots = HashMap::from([("ETH".to_string(), dec!(3000))]);
171        let result = compute_fill_greeks_sync(
172            &spots,
173            |_, _, _, _| Some(0.8),
174            "ETH-20260131-4000-C",
175            hypercall_types::CONTRACT_UNIT_MULTIPLIER_DECIMAL,
176            Side::Buy,
177            1_700_000_000_000,
178        );
179        let (delta, _vega) = result.expect("should compute greeks");
180        assert!(
181            delta > 0.0,
182            "buy call delta should be positive, got {}",
183            delta
184        );
185    }
186
187    #[test]
188    fn sell_call_has_negative_delta() {
189        let spots = HashMap::from([("ETH".to_string(), dec!(3000))]);
190        let result = compute_fill_greeks_sync(
191            &spots,
192            |_, _, _, _| Some(0.8),
193            "ETH-20260131-4000-C",
194            hypercall_types::CONTRACT_UNIT_MULTIPLIER_DECIMAL,
195            Side::Sell,
196            1_700_000_000_000,
197        );
198        let (delta, _vega) = result.expect("should compute greeks");
199        assert!(
200            delta < 0.0,
201            "sell call delta should be negative, got {}",
202            delta
203        );
204    }
205
206    #[test]
207    fn buy_and_sell_delta_are_opposite() {
208        let spots = HashMap::from([("ETH".to_string(), dec!(3000))]);
209        let iv_fn = |_: &str, _: f64, _: i64, _: f64| Some(0.8);
210        let symbol = "ETH-20260131-4000-C";
211        let size = hypercall_types::CONTRACT_UNIT_MULTIPLIER_DECIMAL;
212        let ts = 1_700_000_000_000u64;
213
214        let (buy_delta, buy_vega) =
215            compute_fill_greeks_sync(&spots, iv_fn, symbol, size, Side::Buy, ts).unwrap();
216        let (sell_delta, sell_vega) =
217            compute_fill_greeks_sync(&spots, iv_fn, symbol, size, Side::Sell, ts).unwrap();
218
219        assert!(
220            (buy_delta + sell_delta).abs() < 1e-10,
221            "buy + sell delta should net to zero"
222        );
223        assert!(
224            (buy_vega + sell_vega).abs() < 1e-10,
225            "buy + sell vega should net to zero"
226        );
227    }
228
229    #[test]
230    fn expired_option_fails_closed() {
231        let spots = HashMap::from([("ETH".to_string(), dec!(3000))]);
232        assert_eq!(
233            compute_fill_greeks_sync(
234                &spots,
235                |_, _, _, _| Some(0.5),
236                "ETH-20200131-4000-C",
237                hypercall_types::CONTRACT_UNIT_MULTIPLIER_DECIMAL,
238                Side::Buy,
239                1_700_000_000_000,
240            ),
241            None
242        );
243    }
244}