hypercall/liquidator/
health_check.rs1use crate::rsm::margin_service::MarginService;
7use crate::rsm::MarginMode;
8use crate::standard_margin::StandardMarginService;
9use crate::types::Account;
10use hypercall_margin::standard::StandardAccount;
11use rust_decimal::prelude::ToPrimitive;
12
13#[derive(Debug, Clone)]
15pub struct LiquidationHealthResult {
16 pub wallet: String,
18 pub margin_mode: MarginMode,
20 pub is_liquidatable: bool,
22 pub maintenance_margin: f64,
24 pub equity: f64,
26 pub mm_required: f64,
28}
29
30impl LiquidationHealthResult {
31 pub fn needs_liquidation(&self) -> bool {
33 self.is_liquidatable
34 }
35
36 pub fn shortfall(&self) -> f64 {
38 if self.maintenance_margin < 0.0 {
39 -self.maintenance_margin
40 } else {
41 0.0
42 }
43 }
44}
45
46pub async fn check_portfolio_health<M: MarginService>(
51 wallet: &str,
52 account: &Account,
53 margin_service: &M,
54) -> LiquidationHealthResult {
55 let details = margin_service.compute_margin_for_account(account).await;
56
57 match details {
58 Ok(d) => {
59 let maintenance_margin = d.equity - d.maintenance_margin_required;
60 let maintenance_margin_f64 = maintenance_margin.to_f64().unwrap_or(0.0);
61 LiquidationHealthResult {
62 wallet: wallet.to_string(),
63 margin_mode: MarginMode::Portfolio,
64 is_liquidatable: maintenance_margin_f64 < 0.0,
65 maintenance_margin: maintenance_margin_f64,
66 equity: d.equity.to_f64().unwrap_or(0.0),
67 mm_required: d.maintenance_margin_required.to_f64().unwrap_or(0.0),
68 }
69 }
70 Err(_) => {
71 LiquidationHealthResult {
75 wallet: wallet.to_string(),
76 margin_mode: MarginMode::Portfolio,
77 is_liquidatable: true,
78 maintenance_margin: -1.0,
79 equity: account.cash,
80 mm_required: f64::MAX,
81 }
82 }
83 }
84}
85
86pub fn check_standard_health(
91 wallet: &str,
92 account: &StandardAccount,
93 margin_service: &StandardMarginService,
94) -> LiquidationHealthResult {
95 let result = margin_service.compute_margin(account);
96
97 LiquidationHealthResult {
98 wallet: wallet.to_string(),
99 margin_mode: MarginMode::Standard,
100 is_liquidatable: result.is_liquidatable(),
101 maintenance_margin: result.maintenance_margin.to_f64().unwrap_or(0.0),
102 equity: result.equity.to_f64().unwrap_or(0.0),
103 mm_required: result.position_mm.to_f64().unwrap_or(0.0),
104 }
105}
106
107#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::standard_margin::StandardMarginService;
134 use hypercall_margin::standard::{OptionPosition, StandardAccount};
135 use rust_decimal_macros::dec;
136
137 #[test]
138 fn test_standard_health_check_healthy() {
139 let service = StandardMarginService::new();
140 let account = StandardAccount::new("test".to_string(), dec!(10000));
141
142 let result = check_standard_health("test", &account, &service);
143
144 assert!(!result.needs_liquidation());
145 assert_eq!(result.shortfall(), 0.0);
146 assert_eq!(result.margin_mode, MarginMode::Standard);
147 }
148
149 #[test]
150 fn test_standard_health_check_liquidatable() {
151 let service = StandardMarginService::new();
152 let mut account = StandardAccount::new("test".to_string(), dec!(100)); account.option_positions.push(OptionPosition {
156 symbol: "ETH-20251231-3500-C".to_string(),
157 underlying: "ETH".to_string(),
158 expiry_ts: 0,
159 strike: dec!(3500),
160 is_call: true,
161 size: dec!(-10),
162 mark_price: dec!(500),
163 entry_price: dec!(200), spot_price: dec!(3500),
165 });
166
167 let result = check_standard_health("test", &account, &service);
168
169 assert!(result.needs_liquidation());
170 assert!(result.shortfall() > 0.0);
171 }
172}