1use rust_decimal::Decimal;
24use ustr::Ustr;
25
26use super::user_data::{
27 BinanceSpotAccountPositionMsg, BinanceSpotBalanceEntry, BinanceSpotBalanceUpdateMsg,
28 BinanceSpotExecutionReport, BinanceSpotExecutionType,
29};
30use crate::{
31 common::enums::{BinanceOrderStatus, BinanceSide, BinanceTimeInForce},
32 spot::sbe::spot::{
33 ReadBuf, balance_update_event_codec, bool_enum, execution_report_event_codec,
34 execution_type, message_header_codec, order_side, order_status, order_type,
35 outbound_account_position_event_codec, time_in_force,
36 },
37};
38
39const HEADER_LEN: usize = message_header_codec::ENCODED_LENGTH;
40
41pub fn decode_execution_report(data: &[u8]) -> anyhow::Result<BinanceSpotExecutionReport> {
50 if data.len() < HEADER_LEN {
51 anyhow::bail!(
52 "Buffer too short for SBE header: expected {HEADER_LEN}, was {}",
53 data.len()
54 );
55 }
56
57 let buf = ReadBuf::new(data);
58 let block_length = buf.get_u16_at(0);
59 let template_id = buf.get_u16_at(2);
60 let schema_id = buf.get_u16_at(4);
61 let version = buf.get_u16_at(6);
62
63 if template_id != execution_report_event_codec::SBE_TEMPLATE_ID {
64 anyhow::bail!(
65 "Wrong template ID: expected {}, received {template_id}",
66 execution_report_event_codec::SBE_TEMPLATE_ID
67 );
68 }
69
70 if schema_id != crate::spot::sbe::spot::SBE_SCHEMA_ID {
71 anyhow::bail!(
72 "Wrong schema ID: expected {}, received {schema_id}",
73 crate::spot::sbe::spot::SBE_SCHEMA_ID
74 );
75 }
76
77 let min_len = HEADER_LEN + block_length as usize;
78 if data.len() < min_len {
79 anyhow::bail!(
80 "Buffer too short for fixed block: expected {min_len}, was {}",
81 data.len()
82 );
83 }
84
85 let mut dec = execution_report_event_codec::ExecutionReportEventDecoder::default().wrap(
86 buf,
87 HEADER_LEN,
88 block_length,
89 version,
90 );
91
92 let price_exp = dec.price_exponent();
93 let qty_exp = dec.qty_exponent();
94 let commission_exp = dec.commission_exponent();
95
96 let event_time_us = dec.event_time();
97 let transact_time_us = dec.transact_time();
98 let order_creation_time_us = dec.order_creation_time();
99 let order_id = dec.order_id();
100 let trade_id = dec.trade_id().unwrap_or(-1);
101
102 let execution_type = map_execution_type(dec.execution_type())?;
103 let order_status = map_order_status(dec.order_status());
104 let side = map_side(dec.side())?;
105 let time_in_force = map_time_in_force(dec.time_in_force());
106 let order_type_str = map_order_type(dec.order_type());
107 let is_working = dec.is_working() == bool_enum::BoolEnum::True;
108 let is_maker = dec.is_maker() == bool_enum::BoolEnum::True;
109
110 let price_mantissa = dec.price();
111 let orig_qty_mantissa = dec.orig_qty();
112 let stop_price_mantissa = dec.stop_price();
113 let last_qty_mantissa = dec.last_qty();
114 let last_price_mantissa = dec.last_price();
115 let executed_qty_mantissa = dec.executed_qty();
116 let cummulative_quote_qty_mantissa = dec.cummulative_quote_qty();
117 let commission_mantissa = dec.commission();
118
119 let symbol = {
122 let coords = dec.symbol_decoder();
123 String::from_utf8_lossy(dec.symbol_slice(coords)).into_owned()
124 };
125
126 let client_order_id = {
127 let coords = dec.client_order_id_decoder();
128 String::from_utf8_lossy(dec.client_order_id_slice(coords)).into_owned()
129 };
130
131 let orig_client_order_id = {
132 let coords = dec.orig_client_order_id_decoder();
133 let s = String::from_utf8_lossy(dec.orig_client_order_id_slice(coords)).into_owned();
134 if s.is_empty() { None } else { Some(s) }
135 };
136
137 let commission_asset = {
138 let coords = dec.commission_asset_decoder();
139 let bytes = dec.commission_asset_slice(coords);
140 if bytes.is_empty() {
141 None
142 } else {
143 Some(Ustr::from(&String::from_utf8_lossy(bytes)))
144 }
145 };
146
147 let reject_reason = {
148 let coords = dec.reject_reason_decoder();
149 String::from_utf8_lossy(dec.reject_reason_slice(coords)).into_owned()
150 };
151
152 let _counter_symbol_coords = dec.counter_symbol_decoder();
154
155 Ok(BinanceSpotExecutionReport {
156 event_type: "executionReport".to_string(),
157 event_time: us_to_ms(event_time_us),
158 symbol: Ustr::from(&symbol),
159 client_order_id,
160 side,
161 order_type: order_type_str.to_string(),
162 time_in_force,
163 original_qty: mantissa_to_decimal_string(orig_qty_mantissa, qty_exp),
164 price: mantissa_to_decimal_string(price_mantissa, price_exp),
165 stop_price: mantissa_to_decimal_string(stop_price_mantissa, price_exp),
166 execution_type,
167 order_status,
168 reject_reason,
169 order_id,
170 last_filled_qty: mantissa_to_decimal_string(last_qty_mantissa, qty_exp),
171 cumulative_filled_qty: mantissa_to_decimal_string(executed_qty_mantissa, qty_exp),
172 last_filled_price: mantissa_to_decimal_string(last_price_mantissa, price_exp),
173 commission: mantissa_to_decimal_string(commission_mantissa, commission_exp),
174 commission_asset,
175 transaction_time: us_to_ms(transact_time_us),
176 trade_id,
177 is_working,
178 is_maker,
179 order_creation_time: order_creation_time_us.map_or(0, us_to_ms),
180 cumulative_quote_qty: mantissa_to_decimal_string(
181 cummulative_quote_qty_mantissa,
182 price_exp + qty_exp,
183 ),
184 original_client_order_id: orig_client_order_id,
185 })
186}
187
188pub fn decode_account_position(data: &[u8]) -> anyhow::Result<BinanceSpotAccountPositionMsg> {
198 if data.len() < HEADER_LEN {
199 anyhow::bail!(
200 "Buffer too short for SBE header: expected {HEADER_LEN}, was {}",
201 data.len()
202 );
203 }
204
205 let buf = ReadBuf::new(data);
206 let block_length = buf.get_u16_at(0);
207 let template_id = buf.get_u16_at(2);
208 let schema_id = buf.get_u16_at(4);
209 let version = buf.get_u16_at(6);
210
211 if template_id != outbound_account_position_event_codec::SBE_TEMPLATE_ID {
212 anyhow::bail!(
213 "Wrong template ID: expected {}, received {template_id}",
214 outbound_account_position_event_codec::SBE_TEMPLATE_ID
215 );
216 }
217
218 if schema_id != crate::spot::sbe::spot::SBE_SCHEMA_ID {
219 anyhow::bail!(
220 "Wrong schema ID: expected {}, received {schema_id}",
221 crate::spot::sbe::spot::SBE_SCHEMA_ID
222 );
223 }
224
225 let min_len = HEADER_LEN + block_length as usize;
226 if data.len() < min_len {
227 anyhow::bail!(
228 "Buffer too short for fixed block: expected {min_len}, was {}",
229 data.len()
230 );
231 }
232
233 let dec = outbound_account_position_event_codec::OutboundAccountPositionEventDecoder::default()
234 .wrap(buf, HEADER_LEN, block_length, version);
235
236 let event_time_us = dec.event_time();
237 let update_time_us = dec.update_time();
238
239 let mut balances_dec = dec.balances_decoder();
240 let count = balances_dec.count() as usize;
241 let mut balances = Vec::with_capacity(count);
242
243 while let Some(_idx) = balances_dec
244 .advance()
245 .map_err(|e| anyhow::anyhow!("Failed to advance balances group: {e:?}"))?
246 {
247 let exponent = balances_dec.exponent();
248 let free_mantissa = balances_dec.free();
249 let locked_mantissa = balances_dec.locked();
250
251 let asset_coords = balances_dec.asset_decoder();
252 let asset_bytes = balances_dec.asset_slice(asset_coords);
253 let asset = Ustr::from(&String::from_utf8_lossy(asset_bytes));
254
255 balances.push(BinanceSpotBalanceEntry {
256 asset,
257 free: mantissa_to_decimal(free_mantissa, exponent),
258 locked: mantissa_to_decimal(locked_mantissa, exponent),
259 });
260 }
261
262 Ok(BinanceSpotAccountPositionMsg {
263 event_type: "outboundAccountPosition".to_string(),
264 event_time: us_to_ms(event_time_us),
265 last_update_time: us_to_ms(update_time_us),
266 balances,
267 })
268}
269
270pub fn decode_balance_update(data: &[u8]) -> anyhow::Result<BinanceSpotBalanceUpdateMsg> {
279 if data.len() < HEADER_LEN {
280 anyhow::bail!(
281 "Buffer too short for SBE header: expected {HEADER_LEN}, was {}",
282 data.len()
283 );
284 }
285
286 let buf = ReadBuf::new(data);
287 let block_length = buf.get_u16_at(0);
288 let template_id = buf.get_u16_at(2);
289 let schema_id = buf.get_u16_at(4);
290 let version = buf.get_u16_at(6);
291
292 if template_id != balance_update_event_codec::SBE_TEMPLATE_ID {
293 anyhow::bail!(
294 "Wrong template ID: expected {}, received {template_id}",
295 balance_update_event_codec::SBE_TEMPLATE_ID
296 );
297 }
298
299 if schema_id != crate::spot::sbe::spot::SBE_SCHEMA_ID {
300 anyhow::bail!(
301 "Wrong schema ID: expected {}, received {schema_id}",
302 crate::spot::sbe::spot::SBE_SCHEMA_ID
303 );
304 }
305
306 let min_len = HEADER_LEN + block_length as usize;
307 if data.len() < min_len {
308 anyhow::bail!(
309 "Buffer too short for fixed block: expected {min_len}, was {}",
310 data.len()
311 );
312 }
313
314 let mut dec = balance_update_event_codec::BalanceUpdateEventDecoder::default().wrap(
315 buf,
316 HEADER_LEN,
317 block_length,
318 version,
319 );
320
321 let event_time_us = dec.event_time();
322 let clear_time_us = dec.clear_time().unwrap_or(0);
323 let qty_exponent = dec.qty_exponent();
324 let free_qty_delta = dec.free_qty_delta();
325
326 let asset = {
327 let coords = dec.asset_decoder();
328 String::from_utf8_lossy(dec.asset_slice(coords)).into_owned()
329 };
330
331 Ok(BinanceSpotBalanceUpdateMsg {
332 event_type: "balanceUpdate".to_string(),
333 event_time: us_to_ms(event_time_us),
334 asset: Ustr::from(&asset),
335 delta: mantissa_to_decimal_string(free_qty_delta, qty_exponent),
336 clear_time: us_to_ms(clear_time_us),
337 })
338}
339
340fn map_execution_type(
341 et: execution_type::ExecutionType,
342) -> anyhow::Result<BinanceSpotExecutionType> {
343 match et {
344 execution_type::ExecutionType::New => Ok(BinanceSpotExecutionType::New),
345 execution_type::ExecutionType::Canceled => Ok(BinanceSpotExecutionType::Canceled),
346 execution_type::ExecutionType::Replaced => Ok(BinanceSpotExecutionType::Replaced),
347 execution_type::ExecutionType::Rejected => Ok(BinanceSpotExecutionType::Rejected),
348 execution_type::ExecutionType::Trade => Ok(BinanceSpotExecutionType::Trade),
349 execution_type::ExecutionType::Expired => Ok(BinanceSpotExecutionType::Expired),
350 execution_type::ExecutionType::TradePrevention => {
351 Ok(BinanceSpotExecutionType::TradePrevention)
352 }
353 _ => anyhow::bail!("Unsupported SBE execution type: {et}"),
354 }
355}
356
357fn map_order_status(os: order_status::OrderStatus) -> BinanceOrderStatus {
358 match os {
359 order_status::OrderStatus::New => BinanceOrderStatus::New,
360 order_status::OrderStatus::PartiallyFilled => BinanceOrderStatus::PartiallyFilled,
361 order_status::OrderStatus::Filled => BinanceOrderStatus::Filled,
362 order_status::OrderStatus::Canceled => BinanceOrderStatus::Canceled,
363 order_status::OrderStatus::PendingCancel => BinanceOrderStatus::PendingCancel,
364 order_status::OrderStatus::Rejected => BinanceOrderStatus::Rejected,
365 order_status::OrderStatus::Expired => BinanceOrderStatus::Expired,
366 order_status::OrderStatus::ExpiredInMatch => BinanceOrderStatus::ExpiredInMatch,
367 _ => BinanceOrderStatus::Unknown,
368 }
369}
370
371fn map_side(side: order_side::OrderSide) -> anyhow::Result<BinanceSide> {
372 match side {
373 order_side::OrderSide::Buy => Ok(BinanceSide::Buy),
374 order_side::OrderSide::Sell => Ok(BinanceSide::Sell),
375 _ => anyhow::bail!("Unsupported SBE order side: {side}"),
376 }
377}
378
379fn map_time_in_force(tif: time_in_force::TimeInForce) -> BinanceTimeInForce {
380 match tif {
381 time_in_force::TimeInForce::Gtc => BinanceTimeInForce::Gtc,
382 time_in_force::TimeInForce::Ioc => BinanceTimeInForce::Ioc,
383 time_in_force::TimeInForce::Fok => BinanceTimeInForce::Fok,
384 _ => BinanceTimeInForce::Unknown,
385 }
386}
387
388fn map_order_type(ot: order_type::OrderType) -> &'static str {
389 match ot {
390 order_type::OrderType::Market => "MARKET",
391 order_type::OrderType::Limit => "LIMIT",
392 order_type::OrderType::StopLoss => "STOP_LOSS",
393 order_type::OrderType::StopLossLimit => "STOP_LOSS_LIMIT",
394 order_type::OrderType::TakeProfit => "TAKE_PROFIT",
395 order_type::OrderType::TakeProfitLimit => "TAKE_PROFIT_LIMIT",
396 order_type::OrderType::LimitMaker => "LIMIT_MAKER",
397 _ => "UNKNOWN",
398 }
399}
400
401#[inline]
403fn us_to_ms(us: i64) -> i64 {
404 us / 1000
405}
406
407fn mantissa_to_decimal(mantissa: i64, exponent: i8) -> Decimal {
409 if exponent >= 0 {
410 Decimal::from(mantissa) * Decimal::from(10_i64.pow(exponent as u32))
411 } else {
412 Decimal::new(mantissa, (-exponent) as u32)
413 }
414}
415
416fn mantissa_to_decimal_string(mantissa: i64, exponent: i8) -> String {
421 if mantissa == 0 {
422 if exponent >= 0 {
423 return "0".to_string();
424 }
425 let mut s = "0.".to_string();
426 for _ in 0..(-exponent) {
427 s.push('0');
428 }
429 return s;
430 }
431
432 let negative = mantissa < 0;
433 let abs_mantissa = mantissa.unsigned_abs();
434 let digits = abs_mantissa.to_string();
435
436 let result = if exponent >= 0 {
437 let mut s = digits;
438 for _ in 0..exponent {
439 s.push('0');
440 }
441 s
442 } else {
443 let decimal_places = (-exponent) as usize;
444 if digits.len() <= decimal_places {
445 let padding = decimal_places - digits.len();
446 let mut s = "0.".to_string();
447 for _ in 0..padding {
448 s.push('0');
449 }
450 s.push_str(&digits);
451 s
452 } else {
453 let split_pos = digits.len() - decimal_places;
454 let mut s = digits[..split_pos].to_string();
455 s.push('.');
456 s.push_str(&digits[split_pos..]);
457 s
458 }
459 };
460
461 if negative {
462 format!("-{result}")
463 } else {
464 result
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use rstest::rstest;
471
472 use super::*;
473 use crate::spot::sbe::spot::{
474 WriteBuf, bool_enum::BoolEnum, execution_type::ExecutionType, floor, match_type,
475 order_capacity, order_side::OrderSide, order_status::OrderStatus,
476 order_type::OrderType as SbeOrderType, peg_offset_type, peg_price_type,
477 self_trade_prevention_mode::SelfTradePreventionMode, time_in_force::TimeInForce as SbeTif,
478 };
479
480 #[expect(clippy::too_many_arguments)]
481 fn encode_execution_report(
482 symbol: &str,
483 client_order_id: &str,
484 order_id: i64,
485 trade_id: Option<i64>,
486 side: OrderSide,
487 order_type: SbeOrderType,
488 tif: SbeTif,
489 exec_type: ExecutionType,
490 status: OrderStatus,
491 price_exp: i8,
492 qty_exp: i8,
493 commission_exp: i8,
494 price_mantissa: i64,
495 orig_qty_mantissa: i64,
496 stop_price_mantissa: i64,
497 last_qty_mantissa: i64,
498 last_price_mantissa: i64,
499 executed_qty_mantissa: i64,
500 cumm_quote_qty_mantissa: i64,
501 commission_mantissa: i64,
502 commission_asset: &str,
503 is_maker: bool,
504 is_working: bool,
505 event_time_us: i64,
506 transact_time_us: i64,
507 order_creation_time_us: Option<i64>,
508 ) -> Vec<u8> {
509 let var_data_len = 6 + symbol.len() + client_order_id.len() + commission_asset.len();
510 let total = 8 + execution_report_event_codec::SBE_BLOCK_LENGTH as usize + var_data_len;
511 let mut buf_vec = vec![0u8; total];
512
513 let buf = WriteBuf::new(buf_vec.as_mut_slice());
514 let enc = execution_report_event_codec::ExecutionReportEventEncoder::default()
515 .wrap(buf, HEADER_LEN);
516 let mut header = enc.header(0);
517 let mut enc = header.parent().unwrap();
518
519 enc.event_time(event_time_us);
520 enc.transact_time(transact_time_us);
521 enc.price_exponent(price_exp);
522 enc.qty_exponent(qty_exp);
523 enc.commission_exponent(commission_exp);
524 enc.order_creation_time(order_creation_time_us.unwrap_or(i64::MIN));
525 enc.working_time(i64::MIN); enc.order_id(order_id);
527 enc.order_list_id(i64::MIN); enc.orig_qty(orig_qty_mantissa);
529 enc.price(price_mantissa);
530 enc.orig_quote_order_qty(0);
531 enc.iceberg_qty(0);
532 enc.stop_price(stop_price_mantissa);
533 enc.order_type(order_type);
534 enc.side(side);
535 enc.time_in_force(tif);
536 enc.execution_type(exec_type);
537 enc.order_status(status);
538 enc.trade_id(trade_id.unwrap_or(i64::MIN));
539 enc.execution_id(0);
540 enc.executed_qty(executed_qty_mantissa);
541 enc.cummulative_quote_qty(cumm_quote_qty_mantissa);
542 enc.last_qty(last_qty_mantissa);
543 enc.last_price(last_price_mantissa);
544 enc.quote_qty(0);
545 enc.commission(commission_mantissa);
546 enc.is_working(if is_working {
547 BoolEnum::True
548 } else {
549 BoolEnum::False
550 });
551 enc.is_maker(if is_maker {
552 BoolEnum::True
553 } else {
554 BoolEnum::False
555 });
556 enc.is_best_match(BoolEnum::False);
557 enc.match_type(match_type::MatchType::default());
558 enc.self_trade_prevention_mode(SelfTradePreventionMode::default());
559 enc.order_capacity(order_capacity::OrderCapacity::default());
560 enc.working_floor(floor::Floor::default());
561 enc.used_sor(BoolEnum::False);
562 enc.alloc_id(i64::MIN);
563 enc.trailing_delta(u64::MAX);
564 enc.trailing_time(i64::MIN);
565 enc.trade_group_id(i64::MIN);
566 enc.prevented_qty(0);
567 enc.last_prevented_qty(i64::MIN);
568 enc.prevented_match_id(i64::MIN);
569 enc.prevented_execution_qty(i64::MIN);
570 enc.prevented_execution_price(i64::MIN);
571 enc.prevented_execution_quote_qty(i64::MIN);
572 enc.strategy_type(i32::MIN);
573 enc.strategy_id(i64::MIN);
574 enc.counter_order_id(i64::MIN);
575 enc.subscription_id(0xFFFF); enc.peg_price_type(peg_price_type::PegPriceType::default());
577 enc.peg_offset_type(peg_offset_type::PegOffsetType::default());
578 enc.peg_offset_value(0xFF); enc.pegged_price(i64::MIN);
580
581 enc.symbol(symbol);
583 enc.client_order_id(client_order_id);
584 enc.orig_client_order_id("");
585 enc.commission_asset(commission_asset);
586 enc.reject_reason("");
587 enc.counter_symbol("");
588
589 buf_vec
590 }
591
592 fn encode_account_position(
593 event_time_us: i64,
594 update_time_us: i64,
595 balances: &[(&str, i8, i64, i64)], ) -> Vec<u8> {
597 let var_data_len: usize = balances.iter().map(|(a, _, _, _)| 1 + a.len()).sum();
598 let total = 8 + 18 + 6 + (balances.len() * 17) + var_data_len;
599 let mut buf_vec = vec![0u8; total];
600
601 let buf = WriteBuf::new(buf_vec.as_mut_slice());
602 let enc =
603 outbound_account_position_event_codec::OutboundAccountPositionEventEncoder::default()
604 .wrap(buf, HEADER_LEN);
605 let mut header = enc.header(0);
606 let mut enc = header.parent().unwrap();
607
608 enc.event_time(event_time_us);
609 enc.update_time(update_time_us);
610 enc.subscription_id(0xFFFF); let balances_enc =
613 outbound_account_position_event_codec::encoder::BalancesEncoder::default();
614 let mut bal_enc = enc.balances_encoder(balances.len() as u32, balances_enc);
615
616 for (asset, exponent, free, locked) in balances {
617 bal_enc.advance().unwrap();
618 bal_enc.exponent(*exponent);
619 bal_enc.free(*free);
620 bal_enc.locked(*locked);
621 bal_enc.asset(asset);
622 }
623
624 buf_vec
625 }
626
627 #[rstest]
628 fn test_mantissa_to_decimal_string_basic() {
629 assert_eq!(mantissa_to_decimal_string(250000, -2), "2500.00");
630 assert_eq!(mantissa_to_decimal_string(100000000, -8), "1.00000000");
631 assert_eq!(mantissa_to_decimal_string(0, -8), "0.00000000");
632 assert_eq!(mantissa_to_decimal_string(0, 0), "0");
633 assert_eq!(mantissa_to_decimal_string(42, 0), "42");
634 assert_eq!(mantissa_to_decimal_string(42, 2), "4200");
635 assert_eq!(mantissa_to_decimal_string(5, -3), "0.005");
636 assert_eq!(mantissa_to_decimal_string(-250000, -2), "-2500.00");
637 }
638
639 #[rstest]
640 #[case::typical_price(250000_i64, -2_i8, "2500.00")]
641 #[case::btc_one(100000000_i64, -8_i8, "1.00000000")]
642 #[case::zero_negative_exp(0_i64, -8_i8, "0")]
643 #[case::zero_zero_exp(0_i64, 0_i8, "0")]
644 #[case::whole_no_scale(42_i64, 0_i8, "42")]
645 #[case::positive_exponent(42_i64, 2_i8, "4200")]
646 #[case::small_fractional(5_i64, -3_i8, "0.005")]
647 #[case::negative_mantissa(-250000_i64, -2_i8, "-2500.00")]
648 #[case::large_positive_exponent(1_i64, 9_i8, "1000000000")]
649 fn test_mantissa_to_decimal_parametrized(
650 #[case] mantissa: i64,
651 #[case] exponent: i8,
652 #[case] expected: &str,
653 ) {
654 let result = mantissa_to_decimal(mantissa, exponent);
655 assert_eq!(result, Decimal::from_str_exact(expected).unwrap());
656 }
657
658 #[rstest]
659 fn test_decode_execution_report_new_limit() {
660 let data = encode_execution_report(
661 "ETHUSDT",
662 "O-20200101-000000-000-000-0",
663 12345678,
664 None, OrderSide::Buy,
666 SbeOrderType::Limit,
667 SbeTif::Gtc,
668 ExecutionType::New,
669 OrderStatus::New,
670 -2, -5, -8, 250000, 100000, 0, 0, 0, 0, 0, 0, "", false,
683 true, 1709654400000000, 1709654400000000, Some(1709654400000000),
687 );
688
689 let report = decode_execution_report(&data).unwrap();
690
691 assert_eq!(report.symbol, "ETHUSDT");
692 assert_eq!(report.client_order_id, "O-20200101-000000-000-000-0");
693 assert_eq!(report.order_id, 12345678);
694 assert_eq!(report.side, BinanceSide::Buy);
695 assert_eq!(report.order_type, "LIMIT");
696 assert_eq!(report.time_in_force, BinanceTimeInForce::Gtc);
697 assert_eq!(report.execution_type, BinanceSpotExecutionType::New);
698 assert_eq!(report.order_status, BinanceOrderStatus::New);
699 assert_eq!(report.price, "2500.00");
700 assert_eq!(report.original_qty, "1.00000");
701 assert_eq!(report.trade_id, -1);
702 assert!(report.is_working);
703 assert!(!report.is_maker);
704 assert_eq!(report.event_time, 1709654400000);
705 assert_eq!(report.transaction_time, 1709654400000);
706 }
707
708 #[rstest]
709 fn test_decode_execution_report_trade_fill() {
710 let data = encode_execution_report(
711 "ETHUSDT",
712 "O-20200101-000000-000-000-0",
713 12345678,
714 Some(98765432),
715 OrderSide::Buy,
716 SbeOrderType::Limit,
717 SbeTif::Gtc,
718 ExecutionType::Trade,
719 OrderStatus::Filled,
720 -2, -8, -8, 250000, 100000000, 0, 100000000, 250000, 100000000, 250000000000, 250000, "USDT",
732 true, false,
734 1709654400000000,
735 1709654400000000,
736 Some(1709654400000000),
737 );
738
739 let report = decode_execution_report(&data).unwrap();
740
741 assert_eq!(report.execution_type, BinanceSpotExecutionType::Trade);
742 assert_eq!(report.order_status, BinanceOrderStatus::Filled);
743 assert_eq!(report.trade_id, 98765432);
744 assert_eq!(report.last_filled_qty, "1.00000000");
745 assert_eq!(report.last_filled_price, "2500.00");
746 assert_eq!(report.commission_asset, Some(Ustr::from("USDT")));
747 assert!(report.is_maker);
748 }
749
750 #[rstest]
751 fn test_decode_execution_report_canceled() {
752 let data = encode_execution_report(
753 "BTCUSDT",
754 "O-20200101-000000-000-000-1",
755 99999,
756 None,
757 OrderSide::Sell,
758 SbeOrderType::Limit,
759 SbeTif::Gtc,
760 ExecutionType::Canceled,
761 OrderStatus::Canceled,
762 -2,
763 -8,
764 -8,
765 5000000, 10000000, 0,
768 0,
769 0,
770 0,
771 0,
772 0,
773 "",
774 false,
775 false,
776 1709654400000000,
777 1709654400000000,
778 Some(1709654400000000),
779 );
780
781 let report = decode_execution_report(&data).unwrap();
782
783 assert_eq!(report.execution_type, BinanceSpotExecutionType::Canceled);
784 assert_eq!(report.order_status, BinanceOrderStatus::Canceled);
785 assert_eq!(report.symbol, "BTCUSDT");
786 assert_eq!(report.side, BinanceSide::Sell);
787 }
788
789 #[rstest]
790 fn test_decode_execution_report_stop_loss_limit() {
791 let data = encode_execution_report(
792 "ETHUSDT",
793 "O-20200101-000000-000-000-1",
794 12345679,
795 None,
796 OrderSide::Sell,
797 SbeOrderType::StopLossLimit,
798 SbeTif::Gtc,
799 ExecutionType::New,
800 OrderStatus::New,
801 -2,
802 -5,
803 -8,
804 240000, 100000, 245000, 0,
808 0,
809 0,
810 0,
811 0,
812 "",
813 false,
814 true,
815 1709654400000000,
816 1709654400000000,
817 Some(1709654400000000),
818 );
819
820 let report = decode_execution_report(&data).unwrap();
821
822 assert_eq!(report.order_type, "STOP_LOSS_LIMIT");
823 assert_eq!(report.price, "2400.00");
824 assert_eq!(report.stop_price, "2450.00");
825 }
826
827 #[rstest]
828 fn test_decode_execution_report_truncated_header() {
829 let data = vec![0u8; 5];
830 let err = decode_execution_report(&data).unwrap_err();
831 assert!(err.to_string().contains("too short for SBE header"));
832 }
833
834 #[rstest]
835 fn test_decode_execution_report_wrong_template() {
836 let mut data = encode_execution_report(
837 "TEST",
838 "test",
839 1,
840 None,
841 OrderSide::Buy,
842 SbeOrderType::Limit,
843 SbeTif::Gtc,
844 ExecutionType::New,
845 OrderStatus::New,
846 -2,
847 -8,
848 -8,
849 0,
850 0,
851 0,
852 0,
853 0,
854 0,
855 0,
856 0,
857 "",
858 false,
859 false,
860 0,
861 0,
862 None,
863 );
864 data[2..4].copy_from_slice(&50u16.to_le_bytes());
866
867 let err = decode_execution_report(&data).unwrap_err();
868 assert!(err.to_string().contains("Wrong template ID"));
869 }
870
871 #[rstest]
872 fn test_decode_account_position_single_balance() {
873 let data = encode_account_position(
874 1709654400000000, 1709654400000000, &[("USDT", -8, 1000000000000, 50000000000)], );
878
879 let msg = decode_account_position(&data).unwrap();
880
881 assert_eq!(msg.event_type, "outboundAccountPosition");
882 assert_eq!(msg.event_time, 1709654400000);
883 assert_eq!(msg.balances.len(), 1);
884 assert_eq!(msg.balances[0].asset, "USDT");
885 assert_eq!(
886 msg.balances[0].free,
887 Decimal::from_str_exact("10000.00000000").unwrap()
888 );
889 assert_eq!(
890 msg.balances[0].locked,
891 Decimal::from_str_exact("500.00000000").unwrap()
892 );
893 }
894
895 #[rstest]
896 fn test_decode_account_position_multiple_balances() {
897 let data = encode_account_position(
898 1709654400000000,
899 1709654400000000,
900 &[
901 ("BTC", -8, 100000000, 0), ("USDT", -8, 5000000000000, 0), ],
904 );
905
906 let msg = decode_account_position(&data).unwrap();
907
908 assert_eq!(msg.balances.len(), 2);
909 assert_eq!(msg.balances[0].asset, "BTC");
910 assert_eq!(
911 msg.balances[0].free,
912 Decimal::from_str_exact("1.00000000").unwrap()
913 );
914 assert_eq!(msg.balances[1].asset, "USDT");
915 assert_eq!(
916 msg.balances[1].free,
917 Decimal::from_str_exact("50000.00000000").unwrap()
918 );
919 }
920
921 #[rstest]
922 fn test_decode_account_position_zero_balances() {
923 let data = encode_account_position(1709654400000000, 1709654400000000, &[]);
924
925 let msg = decode_account_position(&data).unwrap();
926 assert!(msg.balances.is_empty());
927 }
928
929 #[rstest]
930 fn test_decode_account_position_truncated_header() {
931 let data = vec![0u8; 5];
932 let err = decode_account_position(&data).unwrap_err();
933 assert!(err.to_string().contains("too short for SBE header"));
934 }
935
936 #[rstest]
937 fn test_decode_account_position_wrong_template() {
938 let mut data = encode_account_position(0, 0, &[]);
939 data[2..4].copy_from_slice(&50u16.to_le_bytes());
940
941 let err = decode_account_position(&data).unwrap_err();
942 assert!(err.to_string().contains("Wrong template ID"));
943 }
944
945 fn encode_balance_update(
946 event_time_us: i64,
947 clear_time_us: i64,
948 qty_exponent: i8,
949 free_qty_delta: i64,
950 asset: &str,
951 ) -> Vec<u8> {
952 let total = 8 + 27 + 1 + asset.len();
953 let mut buf_vec = vec![0u8; total];
954
955 let buf = WriteBuf::new(buf_vec.as_mut_slice());
956 let enc =
957 balance_update_event_codec::BalanceUpdateEventEncoder::default().wrap(buf, HEADER_LEN);
958 let mut header = enc.header(0);
959 let mut enc = header.parent().unwrap();
960
961 enc.event_time(event_time_us);
962 enc.clear_time(clear_time_us);
963 enc.qty_exponent(qty_exponent);
964 enc.free_qty_delta(free_qty_delta);
965 enc.subscription_id(0xFFFF); enc.asset(asset);
967
968 buf_vec
969 }
970
971 #[rstest]
972 fn test_decode_balance_update() {
973 let data = encode_balance_update(
974 1709654400000000, 1709654400000000, -8,
977 10000000000, "BTC",
979 );
980
981 let msg = decode_balance_update(&data).unwrap();
982
983 assert_eq!(msg.event_type, "balanceUpdate");
984 assert_eq!(msg.event_time, 1709654400000);
985 assert_eq!(msg.asset, "BTC");
986 assert_eq!(msg.delta, "100.00000000");
987 assert_eq!(msg.clear_time, 1709654400000);
988 }
989
990 #[rstest]
991 fn test_decode_balance_update_truncated_header() {
992 let data = vec![0u8; 5];
993 let err = decode_balance_update(&data).unwrap_err();
994 assert!(err.to_string().contains("too short for SBE header"));
995 }
996
997 #[rstest]
998 fn test_decode_balance_update_wrong_template() {
999 let mut data = encode_balance_update(0, 0, -8, 0, "BTC");
1000 data[2..4].copy_from_slice(&50u16.to_le_bytes());
1001
1002 let err = decode_balance_update(&data).unwrap_err();
1003 assert!(err.to_string().contains("Wrong template ID"));
1004 }
1005
1006 #[rstest]
1007 fn test_us_to_ms() {
1008 assert_eq!(us_to_ms(1709654400000000), 1709654400000);
1009 assert_eq!(us_to_ms(1709654400123456), 1709654400123);
1010 }
1011
1012 #[rstest]
1013 fn test_decode_captured_execution_report_new() {
1014 let data = crate::common::testing::load_fixture_bytes(
1015 "spot/user_data_sbe/mainnet/execution_report_event_1.sbe",
1016 );
1017 let report = decode_execution_report(&data).unwrap();
1018
1019 assert_eq!(report.symbol, "BTCUSDT");
1020 assert_eq!(report.client_order_id, "O-20200101-000000-000-000-0");
1021 assert_eq!(report.execution_type, BinanceSpotExecutionType::New);
1022 assert_eq!(report.order_status, BinanceOrderStatus::New);
1023 assert_eq!(report.side, BinanceSide::Buy);
1024 assert_eq!(report.order_type, "LIMIT");
1025 assert_eq!(report.time_in_force, BinanceTimeInForce::Gtc);
1026 assert_eq!(report.order_id, 12345678);
1027 assert!(report.is_working);
1028 assert!(!report.is_maker);
1029 assert_eq!(report.trade_id, -1);
1030 }
1031
1032 #[rstest]
1033 fn test_decode_captured_execution_report_canceled() {
1034 let data = crate::common::testing::load_fixture_bytes(
1035 "spot/user_data_sbe/mainnet/execution_report_event_2.sbe",
1036 );
1037 let report = decode_execution_report(&data).unwrap();
1038
1039 assert_eq!(report.symbol, "BTCUSDT");
1040 assert_eq!(report.execution_type, BinanceSpotExecutionType::Canceled);
1041 assert_eq!(report.order_status, BinanceOrderStatus::Canceled);
1042 assert_eq!(report.order_id, 12345678);
1043 assert!(!report.is_working);
1044 }
1045
1046 #[rstest]
1047 fn test_decode_captured_account_position() {
1048 let data = crate::common::testing::load_fixture_bytes(
1049 "spot/user_data_sbe/mainnet/outbound_account_position_event_1.sbe",
1050 );
1051 let msg = decode_account_position(&data).unwrap();
1052
1053 assert_eq!(msg.event_type, "outboundAccountPosition");
1054 assert_eq!(msg.balances.len(), 3);
1055 assert_eq!(msg.balances[0].asset, "BTC");
1056 assert_eq!(
1057 msg.balances[0].free,
1058 Decimal::from_str_exact("1.00000000").unwrap()
1059 );
1060 assert_eq!(msg.balances[1].asset, "BNB");
1061 assert_eq!(msg.balances[2].asset, "USDT");
1062 assert_eq!(
1063 msg.balances[2].free,
1064 Decimal::from_str_exact("50000.00000000").unwrap()
1065 );
1066 }
1067}