hypercall_settlement/
persistence.rs1use rust_decimal::Decimal;
2
3use crate::{PositionExpiredMessage, SettlementEconomics, SettlementError, SettlementInstrument};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct SettlementPersistenceIntent {
8 pub wallet: hypercall_types::WalletAddress,
9 pub margin_mode: hypercall_types::MarginMode,
10 pub symbol: String,
11 pub expiry_ts: i64,
12 pub position_size: Decimal,
13 pub settlement_price: Decimal,
14 pub settlement_value: Decimal,
15 pub economics: Option<SettlementEconomics>,
16}
17
18impl SettlementPersistenceIntent {
19 pub fn from_position_expired(
20 message: &PositionExpiredMessage,
21 ) -> Result<Self, SettlementError> {
22 let instrument = SettlementInstrument::from_symbol(&message.symbol)?;
23 validate_settlement_value(
24 message.position_size,
25 message.settlement_price,
26 message.settlement_value,
27 &format!("{}/{}", message.wallet_address, message.symbol),
28 )?;
29 let economics = validate_settlement_economics(
30 message.settlement_entry_price,
31 message.cost_basis,
32 message.net_pnl,
33 &format!("{}/{}", message.wallet_address, message.symbol),
34 )?;
35
36 Ok(Self {
37 wallet: message.wallet_address,
38 margin_mode: message.margin_mode,
39 symbol: message.symbol.clone(),
40 expiry_ts: instrument.expiry_ts,
41 position_size: message.position_size,
42 settlement_price: message.settlement_price,
43 settlement_value: message.settlement_value,
44 economics,
45 })
46 }
47}
48
49pub fn settlement_cash_delta_for_margin_mode(
51 margin_mode: hypercall_types::MarginMode,
52 settlement_value: Decimal,
53 net_pnl: Option<Decimal>,
54 context: &str,
55) -> Result<Decimal, SettlementError> {
56 match margin_mode {
57 hypercall_types::MarginMode::Standard => Ok(settlement_value),
58 hypercall_types::MarginMode::Portfolio => net_pnl.ok_or_else(|| {
59 SettlementError::new(format!(
60 "Missing settlement net_pnl for portfolio margin expiry {} - cannot calculate ledger delta",
61 context
62 ))
63 }),
64 }
65}
66
67pub fn validate_settlement_value(
69 position_size: Decimal,
70 settlement_price: Decimal,
71 settlement_value: Decimal,
72 context: &str,
73) -> Result<(), SettlementError> {
74 let expected_settlement_value =
75 position_size.checked_mul(settlement_price).ok_or_else(|| {
76 SettlementError::new(format!(
77 "Settlement value overflow for {}: position_size={}, settlement_price={}",
78 context, position_size, settlement_price
79 ))
80 })?;
81 if settlement_value != expected_settlement_value {
82 return Err(SettlementError::new(format!(
83 "Settlement value mismatch for {}: expected {}, got {}",
84 context, expected_settlement_value, settlement_value
85 )));
86 }
87
88 Ok(())
89}
90
91pub fn validate_settlement_economics(
93 settlement_entry_price: Option<Decimal>,
94 cost_basis: Option<Decimal>,
95 net_pnl: Option<Decimal>,
96 context: &str,
97) -> Result<Option<SettlementEconomics>, SettlementError> {
98 match (settlement_entry_price, cost_basis, net_pnl) {
99 (Some(settlement_entry_price), Some(cost_basis), Some(net_pnl)) => {
100 Ok(Some(SettlementEconomics {
101 settlement_entry_price,
102 cost_basis,
103 net_pnl,
104 }))
105 }
106 (None, None, None) => Ok(None),
107 _ => Err(SettlementError::new(format!(
108 "Partial settlement economics tuple for {}: settlement_entry_price={:?}, cost_basis={:?}, net_pnl={:?}",
109 context, settlement_entry_price, cost_basis, net_pnl
110 ))),
111 }
112}