1use crate::MarginMode;
7use crate::WalletAddress;
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10use std::str::FromStr;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum LiquidationMode {
16 Partial,
17 Full,
18}
19
20impl LiquidationMode {
21 pub fn as_str(self) -> &'static str {
22 match self {
23 Self::Partial => "partial",
24 Self::Full => "full",
25 }
26 }
27}
28
29#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub struct PartialLiquidationMetadata {
33 pub entered_at: u64,
35 pub mm_shortfall: Decimal,
37 pub target_equity: Decimal,
39 pub escalation_deadline: u64,
41 pub last_reprice_at: Option<u64>,
43 pub active_order_request_ids: Vec<String>,
45 pub active_order_client_ids: Vec<String>,
47 pub bonus_bps: u32,
49 pub pending_full_auction_id: Option<String>,
51 pub pending_full_request_id: Option<String>,
53 pub pending_full_tx_hash: Option<String>,
55 pub pending_full_margin_needed: Option<Decimal>,
57}
58
59impl Default for PartialLiquidationMetadata {
60 fn default() -> Self {
61 Self {
62 entered_at: 0,
63 mm_shortfall: Decimal::ZERO,
64 target_equity: Decimal::ZERO,
65 escalation_deadline: 0,
66 last_reprice_at: None,
67 active_order_request_ids: Vec::new(),
68 active_order_client_ids: Vec::new(),
69 bonus_bps: 0,
70 pending_full_auction_id: None,
71 pending_full_request_id: None,
72 pending_full_tx_hash: None,
73 pending_full_margin_needed: None,
74 }
75 }
76}
77
78impl PartialLiquidationMetadata {
79 pub fn has_pending_full_liquidation(&self) -> bool {
80 self.pending_full_auction_id.is_some() || self.pending_full_request_id.is_some()
81 }
82}
83
84#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub struct FullLiquidationMetadata {
88 pub auction_id: String,
90 pub request_id: Option<String>,
92 pub tx_hash: Option<String>,
94 pub started_at: u64,
96 pub chain_start_time: Option<u64>,
98 pub margin_needed: Decimal,
100 pub stop_request_id: Option<String>,
102 pub stop_tx_hash: Option<String>,
104}
105
106impl Default for FullLiquidationMetadata {
107 fn default() -> Self {
108 Self {
109 auction_id: String::new(),
110 request_id: None,
111 tx_hash: None,
112 started_at: 0,
113 chain_start_time: None,
114 margin_needed: Decimal::ZERO,
115 stop_request_id: None,
116 stop_tx_hash: None,
117 }
118 }
119}
120
121#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub struct LiquidatedMetadata {
125 pub auction_id: String,
127 pub completed_at: u64,
129 pub winner: Option<WalletAddress>,
131 pub bonus: Decimal,
133 pub tx_hash: Option<String>,
135}
136
137impl Default for LiquidatedMetadata {
138 fn default() -> Self {
139 Self {
140 auction_id: String::new(),
141 completed_at: 0,
142 winner: None,
143 bonus: Decimal::ZERO,
144 tx_hash: None,
145 }
146 }
147}
148
149#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(tag = "state", rename_all = "snake_case")]
152#[derive(Default)]
153pub enum LiquidationState {
154 #[default]
155 Healthy,
156 PreLiquidation(PartialLiquidationMetadata),
157 InLiquidation(FullLiquidationMetadata),
158 Liquidated(LiquidatedMetadata),
159}
160
161pub mod state_str {
163 pub const HEALTHY: &str = "healthy";
164 pub const PRE_LIQUIDATION: &str = "pre_liquidation";
165 pub const IN_LIQUIDATION: &str = "in_liquidation";
166 pub const LIQUIDATED: &str = "liquidated";
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct ParseLiquidationStateError(pub String);
172
173impl std::fmt::Display for ParseLiquidationStateError {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 write!(f, "unknown liquidation state: {}", self.0)
176 }
177}
178
179impl std::error::Error for ParseLiquidationStateError {}
180
181impl LiquidationState {
182 pub fn is_pre_liquidation(&self) -> bool {
183 matches!(self, Self::PreLiquidation(..))
184 }
185
186 pub fn is_in_liquidation(&self) -> bool {
187 matches!(self, Self::InLiquidation(..))
188 }
189
190 pub fn is_liquidated(&self) -> bool {
191 matches!(self, Self::Liquidated(..))
192 }
193
194 pub fn is_healthy(&self) -> bool {
195 matches!(self, Self::Healthy)
196 }
197
198 pub fn should_block_risk_increasing(&self) -> bool {
199 matches!(self, Self::PreLiquidation(..) | Self::InLiquidation(..))
200 }
201
202 pub fn auction_id(&self) -> Option<&str> {
203 match self {
204 Self::PreLiquidation(metadata) => metadata.pending_full_auction_id.as_deref(),
205 Self::InLiquidation(metadata) => Some(metadata.auction_id.as_str()),
206 Self::Liquidated(metadata) => Some(metadata.auction_id.as_str()),
207 _ => None,
208 }
209 }
210
211 pub fn liquidation_mode(&self) -> Option<LiquidationMode> {
212 match self {
213 Self::Healthy => None,
214 Self::PreLiquidation(metadata) => Some(if metadata.has_pending_full_liquidation() {
215 LiquidationMode::Full
216 } else {
217 LiquidationMode::Partial
218 }),
219 Self::InLiquidation(..) | Self::Liquidated(..) => Some(LiquidationMode::Full),
220 }
221 }
222
223 pub fn as_str(&self) -> &'static str {
225 match self {
226 Self::Healthy => "Healthy",
227 Self::PreLiquidation(..) => "PreLiquidation",
228 Self::InLiquidation(..) => "InLiquidation",
229 Self::Liquidated(..) => "Liquidated",
230 }
231 }
232
233 pub fn db_str(&self) -> &'static str {
234 match self {
235 Self::Healthy => state_str::HEALTHY,
236 Self::PreLiquidation(..) => state_str::PRE_LIQUIDATION,
237 Self::InLiquidation(..) => state_str::IN_LIQUIDATION,
238 Self::Liquidated(..) => state_str::LIQUIDATED,
239 }
240 }
241}
242
243pub fn has_material_projection_change(
244 previous: &LiquidationState,
245 current: &LiquidationState,
246) -> bool {
247 match (previous, current) {
248 (LiquidationState::Healthy, LiquidationState::Healthy) => false,
249 (LiquidationState::PreLiquidation(previous), LiquidationState::PreLiquidation(current)) => {
250 previous.mm_shortfall != current.mm_shortfall
251 || previous.target_equity != current.target_equity
252 || previous.escalation_deadline != current.escalation_deadline
253 || previous.last_reprice_at != current.last_reprice_at
254 || previous.active_order_request_ids != current.active_order_request_ids
255 || previous.active_order_client_ids != current.active_order_client_ids
256 || previous.bonus_bps != current.bonus_bps
257 || previous.pending_full_auction_id != current.pending_full_auction_id
258 || previous.pending_full_request_id != current.pending_full_request_id
259 || previous.pending_full_tx_hash != current.pending_full_tx_hash
260 || previous.pending_full_margin_needed != current.pending_full_margin_needed
261 }
262 (LiquidationState::InLiquidation(previous), LiquidationState::InLiquidation(current)) => {
263 previous.auction_id != current.auction_id
264 || previous.request_id != current.request_id
265 || previous.tx_hash != current.tx_hash
266 || previous.chain_start_time != current.chain_start_time
267 || previous.margin_needed != current.margin_needed
268 || previous.stop_request_id != current.stop_request_id
269 || previous.stop_tx_hash != current.stop_tx_hash
270 }
271 (LiquidationState::Liquidated(previous), LiquidationState::Liquidated(current)) => {
272 previous.auction_id != current.auction_id
273 || previous.completed_at != current.completed_at
274 || previous.winner != current.winner
275 || previous.bonus != current.bonus
276 || previous.tx_hash != current.tx_hash
277 }
278 _ => true,
279 }
280}
281
282impl FromStr for LiquidationState {
283 type Err = ParseLiquidationStateError;
284
285 fn from_str(s: &str) -> Result<Self, Self::Err> {
286 match s {
287 state_str::HEALTHY => Ok(Self::Healthy),
288 state_str::PRE_LIQUIDATION => {
289 Ok(Self::PreLiquidation(PartialLiquidationMetadata::default()))
290 }
291 state_str::IN_LIQUIDATION => {
292 Ok(Self::InLiquidation(FullLiquidationMetadata::default()))
293 }
294 state_str::LIQUIDATED => Ok(Self::Liquidated(LiquidatedMetadata::default())),
295 _ => Err(ParseLiquidationStateError(s.to_string())),
296 }
297 }
298}
299
300impl std::fmt::Display for LiquidationState {
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 match self {
303 Self::Healthy => write!(f, "Healthy"),
304 Self::PreLiquidation(metadata) => write!(
305 f,
306 "PreLiquidation(shortfall={}, target_equity={}, orders={}, entered_at={})",
307 metadata.mm_shortfall,
308 metadata.target_equity,
309 metadata.active_order_client_ids.len(),
310 metadata.entered_at
311 ),
312 Self::InLiquidation(metadata) => write!(
313 f,
314 "InLiquidation(auction={}, margin_needed={}, chain_start_time={:?})",
315 metadata.auction_id, metadata.margin_needed, metadata.chain_start_time
316 ),
317 Self::Liquidated(metadata) => write!(
318 f,
319 "Liquidated(auction={}, completed_at={}, winner={:?}, bonus={})",
320 metadata.auction_id, metadata.completed_at, metadata.winner, metadata.bonus
321 ),
322 }
323 }
324}
325
326#[derive(Clone, Debug, Serialize, Deserialize)]
328pub struct AccountLiquidationStatus {
329 pub wallet: WalletAddress,
330 pub state: LiquidationState,
331 pub margin_mode: MarginMode,
332 pub equity: Decimal,
333 pub mm_required: Decimal,
334 pub maintenance_margin: Decimal,
335 pub updated_at: u64,
336}
337
338impl AccountLiquidationStatus {
339 pub fn healthy(
340 wallet: WalletAddress,
341 margin_mode: MarginMode,
342 equity: Decimal,
343 mm_required: Decimal,
344 timestamp: u64,
345 ) -> Self {
346 Self {
347 wallet,
348 state: LiquidationState::Healthy,
349 margin_mode,
350 equity,
351 mm_required,
352 maintenance_margin: equity - mm_required,
353 updated_at: timestamp,
354 }
355 }
356
357 pub fn needs_liquidation(&self) -> bool {
358 self.maintenance_margin < Decimal::ZERO
359 }
360
361 pub fn shortfall(&self) -> Decimal {
362 if self.maintenance_margin < Decimal::ZERO {
363 -self.maintenance_margin
364 } else {
365 Decimal::ZERO
366 }
367 }
368
369 pub fn liquidation_mode(&self) -> Option<LiquidationMode> {
370 self.state.liquidation_mode()
371 }
372
373 pub fn enter_pre_liquidation(&mut self, metadata: PartialLiquidationMetadata, timestamp: u64) {
374 self.state = LiquidationState::PreLiquidation(metadata);
375 self.updated_at = timestamp;
376 }
377
378 pub fn recover_to_healthy(&mut self, timestamp: u64) {
379 self.state = LiquidationState::Healthy;
380 self.updated_at = timestamp;
381 }
382
383 pub fn enter_liquidation(&mut self, metadata: FullLiquidationMetadata, timestamp: u64) {
384 self.state = LiquidationState::InLiquidation(metadata);
385 self.updated_at = timestamp;
386 }
387
388 pub fn complete_liquidation(&mut self, metadata: LiquidatedMetadata, timestamp: u64) {
389 self.state = LiquidationState::Liquidated(metadata);
390 self.updated_at = timestamp;
391 }
392
393 pub fn update_health(&mut self, equity: Decimal, mm_required: Decimal, timestamp: u64) {
394 self.equity = equity;
395 self.mm_required = mm_required;
396 self.maintenance_margin = equity - mm_required;
397 self.updated_at = timestamp;
398 let shortfall = self.shortfall();
399
400 if let LiquidationState::PreLiquidation(metadata) = &mut self.state {
401 metadata.mm_shortfall = shortfall;
402 }
403 }
404}