Skip to main content

catalog_manager/
collateral_registry.rs

1use anyhow::{bail, Result};
2use std::collections::{HashMap, HashSet};
3
4use crate::config::{CatalogConfig, HyperliquidAssetConfig};
5
6#[derive(Debug, Clone)]
7pub struct CollateralRegistry {
8    perp_underlyings_by_asset_id: HashMap<u32, String>,
9    stablecoin_token_ids: HashSet<u32>,
10}
11
12impl CollateralRegistry {
13    pub fn from_catalog(config: &CatalogConfig) -> Result<Self> {
14        let mut perp_underlyings_by_asset_id = HashMap::new();
15        let mut stablecoin_token_ids = HashSet::new();
16
17        for (symbol, asset) in &config.collateral {
18            match asset {
19                HyperliquidAssetConfig::Perp(perp) => {
20                    if !config.underlyings.contains_key(&perp.underlying) {
21                        bail!(
22                            "Collateral perp {} references unknown underlying {}",
23                            symbol,
24                            perp.underlying
25                        );
26                    }
27
28                    if perp_underlyings_by_asset_id
29                        .insert(perp.asset_id, perp.underlying.clone())
30                        .is_some()
31                    {
32                        bail!(
33                            "Collateral registry contains duplicate perp asset_id {} for {}",
34                            perp.asset_id,
35                            symbol
36                        );
37                    }
38                }
39                HyperliquidAssetConfig::Stablecoin(stablecoin) => {
40                    if !stablecoin_token_ids.insert(stablecoin.token_id) {
41                        bail!(
42                            "Collateral registry contains duplicate stablecoin token_id {} for {}",
43                            stablecoin.token_id,
44                            symbol
45                        );
46                    }
47                }
48            }
49        }
50
51        if stablecoin_token_ids.is_empty() {
52            bail!("Collateral registry must configure at least one stablecoin");
53        }
54
55        Ok(Self {
56            perp_underlyings_by_asset_id,
57            stablecoin_token_ids,
58        })
59    }
60
61    pub fn is_known_perp_collateral_asset(&self, asset_id: u32) -> bool {
62        self.perp_underlyings_by_asset_id.contains_key(&asset_id)
63    }
64
65    pub fn resolve_perp_collateral_underlying(&self, asset_id: u32) -> Option<String> {
66        self.perp_underlyings_by_asset_id.get(&asset_id).cloned()
67    }
68
69    pub fn is_known_stablecoin_token(&self, token_id: u32) -> bool {
70        self.stablecoin_token_ids.contains(&token_id)
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::CollateralRegistry;
77    use crate::config::CatalogConfig;
78
79    #[test]
80    fn from_catalog_requires_at_least_one_stablecoin() {
81        let config: CatalogConfig = serde_yaml_ng::from_str(
82            r#"
83version: 1
84expiry:
85  expiry_time_utc: "08:00"
86  schedule:
87    daily_count: 1
88    weekly_count: 1
89    monthly_count: 1
90underlyings:
91  BTC:
92    trading_mode: orderbook
93collateral:
94  BTC_PERP:
95    kind: perp
96    asset_id: 3
97    underlying: BTC
98strike_selection:
99  deribit_table_assets: [BTC, ETH, SOL, AVAX, XRP, TRX]
100  deribit_region_steps:
101    atm: 3
102    outer: 4
103    wings: 3
104  occ_fallback_side_count: 8
105extension_policy:
106  enabled: false
107  ensure_min_strikes_per_side: 2
108  ensure_atm_within_pct: 0.01
109  cooldown_secs: 60
110  max_total_strikes_per_expiry: 10
111  min_spot_move_pct: 0.01
112vol_oracles:
113  providers:
114    fixed_test:
115      kind: fixed
116      iv: 0.5
117  routes:
118    BTC: fixed_test
119"#,
120        )
121        .unwrap();
122
123        let err = CollateralRegistry::from_catalog(&config)
124            .unwrap_err()
125            .to_string();
126        assert!(err.contains("at least one stablecoin"));
127    }
128
129    #[test]
130    fn from_catalog_rejects_unknown_perp_underlying() {
131        let config: CatalogConfig = serde_yaml_ng::from_str(
132            r#"
133version: 1
134expiry:
135  expiry_time_utc: "08:00"
136  schedule:
137    daily_count: 1
138    weekly_count: 1
139    monthly_count: 1
140underlyings:
141  BTC:
142    trading_mode: orderbook
143collateral:
144  BTC_PERP:
145    kind: perp
146    asset_id: 3
147    underlying: ETH
148  USDC:
149    kind: stablecoin
150    token_id: 0
151strike_selection:
152  deribit_table_assets: [BTC, ETH, SOL, AVAX, XRP, TRX]
153  deribit_region_steps:
154    atm: 3
155    outer: 4
156    wings: 3
157  occ_fallback_side_count: 8
158extension_policy:
159  enabled: false
160  ensure_min_strikes_per_side: 2
161  ensure_atm_within_pct: 0.01
162  cooldown_secs: 60
163  max_total_strikes_per_expiry: 10
164  min_spot_move_pct: 0.01
165vol_oracles:
166  providers:
167    fixed_test:
168      kind: fixed
169      iv: 0.5
170  routes:
171    BTC: fixed_test
172"#,
173        )
174        .unwrap();
175
176        let err = CollateralRegistry::from_catalog(&config)
177            .unwrap_err()
178            .to_string();
179        assert!(err.contains("unknown underlying ETH"));
180    }
181}