Skip to main content

hypercall_margin/portfolio/
config.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone)]
4pub struct PortfolioMarginConfig {
5    pub base_grid: PortfolioMarginGridConfig,
6    pub symbol_overrides: Vec<PortfolioMarginSymbolOverride>,
7    pub contingency: PortfolioMarginContingencyConfig,
8    pub risk_free_rate: f64,
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct PortfolioMarginScenario {
13    pub id: String,
14    pub spot_shock_pct: f64,
15    pub vol_shock_pct: f64,
16    pub pnl_weight: f64,
17    pub is_tail: bool,
18}
19
20#[derive(Debug, Clone)]
21pub struct PortfolioMarginGridConfig {
22    pub scenarios: Vec<PortfolioMarginScenario>,
23    pub base_volatility: f64,
24    pub base_skew: f64,
25    pub base_excess_kurtosis: f64,
26    pub delta_threshold: f64,
27    pub strike_match_tolerance: f64,
28    pub expiry_match_tolerance_years: f64,
29}
30
31#[derive(Debug, Clone)]
32pub struct PortfolioMarginSymbolOverride {
33    pub underlying: String,
34    pub grid: PortfolioMarginGridConfig,
35    pub contingency: Option<PortfolioMarginContingencyConfig>,
36}
37
38#[derive(Debug, Clone)]
39pub struct PortfolioMarginContingencyConfig {
40    pub option_floor_factor: f64,
41    pub gamma_kicker_factor: f64,
42    pub dte_threshold_hours: u64,
43    pub apply_floor_to_open_orders: bool,
44    pub apply_gamma_to_open_orders: bool,
45}
46
47impl PortfolioMarginConfig {
48    /// Construct a default PortfolioMarginConfig from legacy scalar parameters.
49    pub fn from_legacy_config(
50        risk_free_rate: f64,
51        base_volatility: f64,
52        base_skew: f64,
53        base_excess_kurtosis: f64,
54        delta_threshold: f64,
55        strike_match_tolerance: f64,
56        expiry_match_tolerance_years: f64,
57    ) -> Self {
58        for (name, value) in [
59            ("risk_free_rate", risk_free_rate),
60            ("base_volatility", base_volatility),
61            ("base_skew", base_skew),
62            ("base_excess_kurtosis", base_excess_kurtosis),
63            ("delta_threshold", delta_threshold),
64            ("strike_match_tolerance", strike_match_tolerance),
65            ("expiry_match_tolerance_years", expiry_match_tolerance_years),
66        ] {
67            assert!(
68                value.is_finite(),
69                "STATE_CORRUPTION: portfolio margin config {name} must be finite, got {value}"
70            );
71        }
72
73        Self {
74            base_grid: PortfolioMarginGridConfig {
75                scenarios: PortfolioMarginScenario::finalized_default_grid(),
76                base_volatility,
77                base_skew,
78                base_excess_kurtosis,
79                delta_threshold,
80                strike_match_tolerance,
81                expiry_match_tolerance_years,
82            },
83            symbol_overrides: Vec::new(),
84            contingency: PortfolioMarginContingencyConfig::finalized_default(),
85            risk_free_rate,
86        }
87    }
88
89    pub fn grid_for_underlying(&self, underlying: &str) -> &PortfolioMarginGridConfig {
90        self.symbol_overrides
91            .iter()
92            .find(|override_config| override_config.underlying == underlying)
93            .map(|override_config| &override_config.grid)
94            .unwrap_or(&self.base_grid)
95    }
96
97    pub fn contingency_for_underlying(
98        &self,
99        underlying: &str,
100    ) -> &PortfolioMarginContingencyConfig {
101        self.symbol_overrides
102            .iter()
103            .find(|override_config| override_config.underlying == underlying)
104            .and_then(|override_config| override_config.contingency.as_ref())
105            .unwrap_or(&self.contingency)
106    }
107}
108
109impl PortfolioMarginScenario {
110    fn new(
111        id: &str,
112        spot_shock_pct: f64,
113        vol_shock_pct: f64,
114        pnl_weight: f64,
115        is_tail: bool,
116    ) -> Self {
117        Self {
118            id: id.to_string(),
119            spot_shock_pct,
120            vol_shock_pct,
121            pnl_weight,
122            is_tail,
123        }
124    }
125
126    pub fn finalized_default_grid() -> Vec<Self> {
127        vec![
128            Self::new("1", 0.12, 0.35, 1.0, false),
129            Self::new("2", 0.08, 0.0, 1.0, false),
130            Self::new("3", 0.04, -0.15, 1.0, false),
131            Self::new("4", 0.0, 0.35, 1.0, false),
132            Self::new("5", 0.0, 0.0, 1.0, false),
133            Self::new("6", 0.0, -0.15, 1.0, false),
134            Self::new("7", -0.04, -0.15, 1.0, false),
135            Self::new("8", -0.08, 0.0, 1.0, false),
136            Self::new("9", -0.12, 0.45, 1.0, false),
137            Self::new("10", 0.12, 0.0, 1.0, false),
138            Self::new("11", -0.12, 0.0, 1.0, false),
139            Self::new("12", 0.08, 0.25, 1.0, false),
140            Self::new("13", -0.08, 0.35, 1.0, false),
141            Self::new("T1", -0.25, 0.70, 0.60, true),
142            Self::new("T2", 0.25, 0.55, 0.60, true),
143            Self::new("T3", -0.40, 0.90, 0.35, true),
144            Self::new("T4", 0.40, 0.70, 0.35, true),
145        ]
146    }
147}
148
149impl PortfolioMarginContingencyConfig {
150    pub fn finalized_default() -> Self {
151        Self {
152            option_floor_factor: 0.015,
153            gamma_kicker_factor: 0.01,
154            dte_threshold_hours: 48,
155            apply_floor_to_open_orders: true,
156            apply_gamma_to_open_orders: true,
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn finalized_default_grid_matches_docs() {
167        let scenarios = PortfolioMarginScenario::finalized_default_grid();
168        assert_eq!(scenarios.len(), 17);
169
170        assert_eq!(scenarios[0].id, "1");
171        assert_eq!(scenarios[0].spot_shock_pct, 0.12);
172        assert_eq!(scenarios[0].vol_shock_pct, 0.35);
173        assert_eq!(scenarios[0].pnl_weight, 1.0);
174        assert!(!scenarios[0].is_tail);
175
176        assert_eq!(scenarios[13].id, "T1");
177        assert_eq!(scenarios[13].spot_shock_pct, -0.25);
178        assert_eq!(scenarios[13].vol_shock_pct, 0.70);
179        assert_eq!(scenarios[13].pnl_weight, 0.60);
180        assert!(scenarios[13].is_tail);
181
182        assert_eq!(scenarios[16].id, "T4");
183        assert_eq!(scenarios[16].spot_shock_pct, 0.40);
184        assert_eq!(scenarios[16].vol_shock_pct, 0.70);
185        assert_eq!(scenarios[16].pnl_weight, 0.35);
186        assert!(scenarios[16].is_tail);
187    }
188}