catalog_manager/
collateral_registry.rs1use 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}