Skip to main content

hypercall_settlement/
persistence.rs

1use rust_decimal::Decimal;
2
3use crate::{PositionExpiredMessage, SettlementEconomics, SettlementError, SettlementInstrument};
4
5/// Database-neutral settlement persistence intent.
6#[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
49/// Calculate the cash delta that settlement should apply for a margin mode.
50pub 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
67/// Validate that settlement cashflow matches size times intrinsic settlement price.
68pub 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
91/// Validate that optional settlement economics are either complete or absent.
92pub 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}