1use 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
9pub 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}