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 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}