1use chrono::{DateTime, Utc};
17use nautilus_core::{nanos::UnixNanos, uuid::UUID4};
18use nautilus_model::{
19 enums::{AccountType, AggressorSide, LiquiditySide, PositionSide},
20 events::AccountState,
21 identifiers::{AccountId, InstrumentId, Symbol},
22 types::{AccountBalance, Currency, Money, QUANTITY_MAX, Quantity},
23};
24use ustr::Ustr;
25
26use crate::{
27 common::{
28 consts::BITMEX_VENUE,
29 enums::{BitmexLiquidityIndicator, BitmexSide},
30 },
31 websocket::messages::BitmexMarginMsg,
32};
33
34#[must_use]
36pub fn parse_instrument_id(symbol: Ustr) -> InstrumentId {
37 InstrumentId::new(Symbol::from_ustr_unchecked(symbol), *BITMEX_VENUE)
38}
39
40#[must_use]
44pub fn quantity_to_u32(quantity: &Quantity) -> u32 {
45 let value = quantity.as_f64();
46 if value > u32::MAX as f64 {
47 tracing::warn!(
48 "Quantity {value} exceeds u32::MAX, clamping to {}",
49 u32::MAX
50 );
51 u32::MAX
52 } else if value < 0.0 {
53 tracing::warn!("Quantity {value} is negative, using 0");
54 0
55 } else {
56 value as u32
57 }
58}
59
60#[must_use]
61pub fn parse_contracts_quantity(value: u64) -> Quantity {
62 let size_workaround = std::cmp::min(QUANTITY_MAX as u64, value);
63 if value > QUANTITY_MAX as u64 {
65 tracing::warn!(
66 "Quantity value {value} exceeds QUANTITY_MAX {QUANTITY_MAX}, clamping to maximum",
67 );
68 }
69 Quantity::new(size_workaround as f64, 0)
70}
71
72#[must_use]
73pub fn parse_frac_quantity(value: f64, size_precision: u8) -> Quantity {
74 let value_u64 = value as u64;
75 let size_workaround = std::cmp::min(QUANTITY_MAX as u64, value as u64);
76 if value_u64 > QUANTITY_MAX as u64 {
78 tracing::warn!(
79 "Quantity value {value} exceeds QUANTITY_MAX {QUANTITY_MAX}, clamping to maximum",
80 );
81 }
82 Quantity::new(size_workaround as f64, size_precision)
83}
84
85#[must_use]
90pub fn parse_optional_datetime_to_unix_nanos(
91 value: &Option<DateTime<Utc>>,
92 field: &str,
93) -> UnixNanos {
94 value
95 .map(|dt| {
96 UnixNanos::from(dt.timestamp_nanos_opt().unwrap_or_else(|| {
97 tracing::error!(field = field, timestamp = ?dt, "Invalid timestamp - out of range");
98 0
99 }) as u64)
100 })
101 .unwrap_or_default()
102}
103
104#[must_use]
105pub const fn parse_aggressor_side(side: &Option<BitmexSide>) -> AggressorSide {
106 match side {
107 Some(BitmexSide::Buy) => AggressorSide::Buyer,
108 Some(BitmexSide::Sell) => AggressorSide::Seller,
109 None => AggressorSide::NoAggressor,
110 }
111}
112
113#[must_use]
114pub fn parse_liquidity_side(liquidity: &Option<BitmexLiquidityIndicator>) -> LiquiditySide {
115 liquidity
116 .map(std::convert::Into::into)
117 .unwrap_or(LiquiditySide::NoLiquiditySide)
118}
119
120#[must_use]
121pub const fn parse_position_side(current_qty: Option<i64>) -> PositionSide {
122 match current_qty {
123 Some(qty) if qty > 0 => PositionSide::Long,
124 Some(qty) if qty < 0 => PositionSide::Short,
125 _ => PositionSide::Flat,
126 }
127}
128
129#[must_use]
138pub fn map_bitmex_currency(bitmex_currency: &str) -> String {
139 match bitmex_currency {
140 "XBt" => "XBT".to_string(),
141 "USDt" => "USDT".to_string(),
142 "LAMp" => "USDT".to_string(), other => other.to_uppercase(),
144 }
145}
146
147pub fn parse_account_state(
153 margin: &BitmexMarginMsg,
154 account_id: AccountId,
155 ts_init: UnixNanos,
156) -> anyhow::Result<AccountState> {
157 let currency_str = map_bitmex_currency(&margin.currency);
159 let currency = Currency::from(currency_str.as_str());
160
161 let divisor = if margin.currency == "XBt" {
164 100_000_000.0 } else if margin.currency == "USDt" || margin.currency == "LAMp" {
166 1_000_000.0 } else {
168 1.0
169 };
170
171 let total = if let Some(wallet_balance) = margin.wallet_balance {
173 Money::new(wallet_balance as f64 / divisor, currency)
174 } else {
175 Money::new(0.0, currency)
176 };
177
178 let free = if let Some(available_margin) = margin.available_margin {
180 Money::new(available_margin as f64 / divisor, currency)
181 } else {
182 Money::new(0.0, currency)
183 };
184
185 let locked = total - free;
187
188 let balance = AccountBalance::new(total, locked, free);
189 let balances = vec![balance];
190 let margins = vec![]; let account_type = AccountType::Margin;
193 let is_reported = true;
194 let event_id = UUID4::new();
195 let ts_event =
196 UnixNanos::from(margin.timestamp.timestamp_nanos_opt().unwrap_or_default() as u64);
197
198 Ok(AccountState::new(
199 account_id,
200 account_type,
201 balances,
202 margins,
203 is_reported,
204 event_id,
205 ts_event,
206 ts_init,
207 None,
208 ))
209}
210
211#[cfg(test)]
216mod tests {
217 use chrono::TimeZone;
218 use nautilus_model::enums::AccountType;
219 use rstest::rstest;
220 use ustr::Ustr;
221
222 use super::*;
223
224 #[rstest]
225 fn test_parse_account_state() {
226 let margin_msg = BitmexMarginMsg {
227 account: 123456,
228 currency: Ustr::from("XBt"),
229 risk_limit: Some(1000000000),
230 amount: Some(5000000),
231 prev_realised_pnl: Some(100000),
232 gross_comm: Some(1000),
233 gross_open_cost: Some(200000),
234 gross_open_premium: None,
235 gross_exec_cost: None,
236 gross_mark_value: Some(210000),
237 risk_value: Some(50000),
238 init_margin: Some(20000),
239 maint_margin: Some(10000),
240 target_excess_margin: Some(5000),
241 realised_pnl: Some(100000),
242 unrealised_pnl: Some(10000),
243 wallet_balance: Some(5000000),
244 margin_balance: Some(5010000),
245 margin_leverage: Some(2.5),
246 margin_used_pcnt: Some(0.25),
247 excess_margin: Some(4990000),
248 available_margin: Some(4980000),
249 withdrawable_margin: Some(4900000),
250 maker_fee_discount: Some(0.1),
251 taker_fee_discount: Some(0.05),
252 timestamp: chrono::Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap(),
253 foreign_margin_balance: None,
254 foreign_requirement: None,
255 };
256
257 let account_id = AccountId::new("BITMEX-001");
258 let ts_init = UnixNanos::from(1_000_000_000);
259
260 let account_state = parse_account_state(&margin_msg, account_id, ts_init).unwrap();
261
262 assert_eq!(account_state.account_id, account_id);
263 assert_eq!(account_state.account_type, AccountType::Margin);
264 assert_eq!(account_state.balances.len(), 1);
265 assert_eq!(account_state.margins.len(), 0);
266 assert!(account_state.is_reported);
267
268 let xbt_balance = &account_state.balances[0];
270 assert_eq!(xbt_balance.currency, Currency::from("XBT"));
271 assert_eq!(xbt_balance.total.as_f64(), 0.05); assert_eq!(xbt_balance.free.as_f64(), 0.0498); assert_eq!(xbt_balance.locked.as_f64(), 0.0002); }
275
276 #[rstest]
277 fn test_parse_account_state_usdt() {
278 let margin_msg = BitmexMarginMsg {
279 account: 123456,
280 currency: Ustr::from("USDt"),
281 risk_limit: Some(1000000000),
282 amount: Some(10000000000), prev_realised_pnl: None,
284 gross_comm: None,
285 gross_open_cost: None,
286 gross_open_premium: None,
287 gross_exec_cost: None,
288 gross_mark_value: None,
289 risk_value: None,
290 init_margin: None,
291 maint_margin: None,
292 target_excess_margin: None,
293 realised_pnl: None,
294 unrealised_pnl: None,
295 wallet_balance: Some(10000000000),
296 margin_balance: Some(10000000000),
297 margin_leverage: None,
298 margin_used_pcnt: None,
299 excess_margin: None,
300 available_margin: Some(9500000000), withdrawable_margin: None,
302 maker_fee_discount: None,
303 taker_fee_discount: None,
304 timestamp: chrono::Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap(),
305 foreign_margin_balance: None,
306 foreign_requirement: None,
307 };
308
309 let account_id = AccountId::new("BITMEX-001");
310 let ts_init = UnixNanos::from(1_000_000_000);
311
312 let account_state = parse_account_state(&margin_msg, account_id, ts_init).unwrap();
313
314 let usdt_balance = &account_state.balances[0];
316 assert_eq!(usdt_balance.currency, Currency::USDT());
317 assert_eq!(usdt_balance.total.as_f64(), 10000.0);
318 assert_eq!(usdt_balance.free.as_f64(), 9500.0);
319 assert_eq!(usdt_balance.locked.as_f64(), 500.0);
320 }
321}