Skip to main content

perpl_sdk/state/
account.rs

1use alloy::primitives::{Address, U256};
2use fastnum::{D256, UD128};
3
4use super::*;
5use crate::{
6    abi::dex::Exchange::{AccountInfo, PositionBitMap},
7    types,
8};
9
10/// Exchange account.
11#[derive(Clone, derive_more::Debug)]
12pub struct Account {
13    instant: types::StateInstant,
14    id: types::AccountId,
15    address: Address,
16    #[debug("{balance}")]
17    balance: UD128, // SC allocates 80 bits
18    #[debug("{locked_balance}")]
19    locked_balance: UD128, // SC allocates 80 bits
20    frozen: bool,
21    fee_tier: Option<types::FeeTier>,
22    positions: HashMap<types::PerpetualId, Position>,
23}
24
25impl Account {
26    pub(crate) fn new(
27        instant: types::StateInstant,
28        id: types::AccountId,
29        info: &AccountInfo,
30        fee_tier: Option<types::FeeTier>,
31        positions: HashMap<types::PerpetualId, Position>,
32        collateral_converter: num::Converter,
33    ) -> Self {
34        Self {
35            instant,
36            id,
37            address: info.accountAddr,
38            balance: collateral_converter.from_unsigned(info.balanceCNS),
39            locked_balance: collateral_converter.from_unsigned(info.lockedBalanceCNS),
40            frozen: info.frozen != 0,
41            fee_tier,
42            positions,
43        }
44    }
45
46    /// Account observed being created by the event stream, so its whole state
47    /// is known from the start - including the base fee tier every account
48    /// is created at.
49    pub(crate) fn created(
50        instant: types::StateInstant,
51        id: types::AccountId,
52        address: Address,
53    ) -> Self {
54        Self {
55            instant,
56            id,
57            address,
58            balance: UD128::ZERO,
59            locked_balance: UD128::ZERO,
60            frozen: false,
61            fee_tier: Some(0),
62            positions: HashMap::new(),
63        }
64    }
65
66    /// Placeholder for an account that predates the snapshot and was not
67    /// snapshotted, tracked because the event stream mutates it. Only the
68    /// updates observed since are known.
69    pub(crate) fn untracked(instant: types::StateInstant, id: types::AccountId) -> Self {
70        Self {
71            instant,
72            id,
73            address: Address::ZERO,
74            balance: UD128::ZERO,
75            locked_balance: UD128::ZERO,
76            frozen: false,
77            fee_tier: None,
78            positions: HashMap::new(),
79        }
80    }
81
82    pub(crate) fn from_position(instant: types::StateInstant, position: Position) -> Self {
83        let account_id = position.account_id();
84        let mut positions = HashMap::new();
85        positions.insert(position.perpetual_id(), position);
86        Self {
87            instant,
88            id: account_id,
89            address: Address::ZERO,
90            balance: UD128::ZERO,
91            locked_balance: UD128::ZERO,
92            frozen: false,
93            // Not snapshotted for position-only accounts, see
94            // [`crate::state::SnapshotBuilder::with_all_positions`]
95            fee_tier: None,
96            positions,
97        }
98    }
99
100    /// Instant the account state is consistent with or was last updated at.
101    pub fn instant(&self) -> types::StateInstant { self.instant }
102
103    /// ID of the account.
104    pub fn id(&self) -> types::AccountId { self.id }
105
106    /// Account address.
107    pub fn address(&self) -> Address { self.address }
108
109    /// The current balance of collateral tokens in this account,
110    /// not including any open positions.
111    pub fn balance(&self) -> UD128 { self.balance }
112
113    /// The balance of collateral tokens locked by existing orders for this
114    /// account.
115    /// If this value exceeds [`Self::balance`], new Open* orders cannot be
116    /// placed.
117    pub fn locked_balance(&self) -> UD128 { self.locked_balance }
118
119    /// The balance of collateral tokens available for trading.
120    pub fn available_balance(&self) -> UD128 {
121        if self.locked_balance > self.balance {
122            // Valid scenario from the smart contract perspective
123            return UD128::ZERO;
124        }
125        self.balance - self.locked_balance
126    }
127
128    /// Total unrealized PnL of all positions of the account.
129    pub fn unrealized_pnl(&self) -> D256 { self.positions.values().map(|p| p.pnl()).sum() }
130
131    /// Indicator of the account being frozen.
132    pub fn frozen(&self) -> bool { self.frozen }
133
134    /// Fee tier of the account, indexing the
135    /// [`Perpetual::fee_schedule`] of every contract it trades. Tier 0 is the
136    /// base rate.
137    ///
138    /// `None` when the tier was never observed: contracts before v1.1.7.4 have
139    /// no per-account tiers, and accounts tracked via
140    /// [`SnapshotBuilder::with_all_positions`] are not snapshotted - such an
141    /// account reports a tier only once `AccountFeeTierSet` is observed for it.
142    pub fn fee_tier(&self) -> Option<types::FeeTier> { self.fee_tier }
143
144    /// Positions the account has, up to one per each perpetual contract.
145    pub fn positions(&self) -> &HashMap<types::PerpetualId, position::Position> { &self.positions }
146
147    pub(crate) fn update_frozen(&mut self, instant: types::StateInstant, frozen: bool) {
148        self.frozen = frozen;
149        self.instant = instant;
150    }
151
152    pub(crate) fn update_fee_tier(&mut self, instant: types::StateInstant, tier: types::FeeTier) {
153        self.fee_tier = Some(tier);
154        self.instant = instant;
155    }
156
157    pub(crate) fn update_balance(&mut self, instant: types::StateInstant, balance: UD128) {
158        self.balance = balance;
159        self.instant = instant;
160    }
161
162    pub(crate) fn update_locked_balance(
163        &mut self,
164        instant: types::StateInstant,
165        locked_balance: UD128,
166    ) {
167        self.locked_balance = locked_balance;
168        self.instant = instant;
169    }
170
171    pub(crate) fn positions_mut(&mut self) -> &mut HashMap<types::PerpetualId, position::Position> {
172        &mut self.positions
173    }
174}
175
176#[cfg(feature = "display")]
177impl std::fmt::Display for Account {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        use colored::Colorize;
180        use tabled::{Table, settings::Style};
181
182        if !self.address.is_zero() {
183            // Full account state is known
184            let pnl = self.unrealized_pnl();
185            writeln!(
186                f,
187                "{} ({}) {}\n    Balance: {} | Available: {} | Locked: {} | Unrealized PnL: {} | \
188                 Fee Tier: {}",
189                format!("Account #{}", self.id).blue(),
190                self.address,
191                if self.frozen { "FROZEN ".bright_red() } else { Default::default() },
192                self.balance,
193                self.available_balance().to_string().green(),
194                self.locked_balance,
195                if pnl.is_negative() { pnl.to_string().red() } else { pnl.to_string().green() },
196                self.fee_tier
197                    .map(|tier| tier.to_string())
198                    .unwrap_or("?".to_string()),
199            )?;
200        } else {
201            // Only ID is known
202            writeln!(f, "{}", format!("Account #{}", self.id).blue())?;
203        }
204
205        // Render positions in alternate mode
206        if f.alternate() {
207            let mut positions: Vec<_> = self.positions().values().collect();
208            positions.sort_by_key(|p| p.perpetual_id());
209            let mut positions_table = Table::new(positions);
210            positions_table.with(Style::sharp());
211            positions_table.fmt(f)
212        } else {
213            Ok(())
214        }
215    }
216}
217
218/// Returns IDs of perpetuals with positions according to [`PositionBitMap`].
219pub(crate) fn perpetuals_with_position(bitmap: &PositionBitMap) -> Vec<types::PerpetualId> {
220    let banks = vec![
221        (0, (0..U256::BITS - 3), bitmap.bank1, bitmap.bank1.count_ones()),
222        (253, (0..U256::BITS), bitmap.bank2, bitmap.bank2.count_ones()),
223        (509, (0..U256::BITS), bitmap.bank3, bitmap.bank3.count_ones()),
224        (765, (0..U256::BITS), bitmap.bank4, bitmap.bank4.count_ones()),
225    ];
226    banks
227        .into_iter()
228        .filter(|(_, _, _, count)| *count > 0)
229        .flat_map(|(offs, range, bank, _)| {
230            range.filter_map(move |i| bank.bit(i).then_some((offs + i) as types::PerpetualId))
231        })
232        .collect()
233}