1use std::{collections::HashMap, ops::Neg};
2
3use either::Either;
4use exc_core::{types, Asset, ExchangeError, Str};
5use rust_decimal::Decimal;
6use serde::Deserialize;
7
8use crate::{
9 types::trading::{OrderSide, PositionSide, Status, TimeInForce},
10 websocket::error::WsError,
11};
12
13use super::{Name, Nameable, StreamFrame, StreamFrameKind};
14
15#[derive(Debug, Clone, Deserialize)]
17#[serde(tag = "e", rename_all = "camelCase")]
18pub enum AccountEvent {
19 ListenKeyExpired {
21 #[serde(rename = "E")]
23 ts: i64,
24 },
25 #[serde(rename = "ORDER_TRADE_UPDATE")]
27 OrderTradeUpdate {
28 #[serde(rename = "E")]
30 event_ts: i64,
31 #[serde(rename = "T")]
33 trade_ts: i64,
34 #[serde(rename = "o")]
36 order: OrderUpdate,
37 },
38 #[serde(rename = "executionReport")]
40 ExecutionReport(ExecutionReport),
41}
42
43#[derive(Debug, Clone)]
45#[non_exhaustive]
46pub enum OrderUpdateFrame {
47 Options(OptionsOrder),
49 UsdMarginFutures(OrderUpdate),
51 Spot(ExecutionReport),
53}
54
55#[derive(Debug, Clone, Copy, Deserialize)]
57#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
58pub enum OrderType {
59 Market,
61 Limit,
63 Stop,
65 TakeProfit,
67 Liquidation,
69 LimitMaker,
71}
72
73#[derive(Debug, Clone, Copy, Deserialize)]
75#[serde(rename_all = "UPPERCASE")]
76pub enum UpdateKind {
77 New,
79 Canceled,
81 Calculated,
83 Expired,
85 Trade,
87}
88
89#[derive(Debug, Clone, Deserialize)]
91pub struct OrderUpdate {
92 #[serde(rename = "s")]
94 pub symbol: Str,
95 #[serde(rename = "c")]
97 pub client_id: Str,
98 #[serde(rename = "S")]
100 pub side: OrderSide,
101 #[serde(rename = "o")]
103 pub order_type: OrderType,
104 #[serde(rename = "f")]
106 pub time_in_force: TimeInForce,
107 #[serde(rename = "q")]
109 pub size: Decimal,
110 #[serde(rename = "p")]
112 pub price: Decimal,
113 #[serde(rename = "ap")]
115 pub cost: Decimal,
116 #[serde(rename = "sp")]
118 pub trigger_price: Decimal,
119 #[serde(rename = "x")]
121 pub kind: UpdateKind,
122 #[serde(rename = "X")]
124 pub status: Status,
125 #[serde(rename = "i")]
127 pub order_id: i64,
128 #[serde(rename = "l")]
130 pub last_trade_size: Decimal,
131 #[serde(rename = "z")]
133 pub filled_size: Decimal,
134 #[serde(rename = "L")]
136 pub last_trade_price: Decimal,
137 #[serde(rename = "N")]
139 pub fee_asset: Option<Asset>,
140 #[serde(rename = "n", default)]
142 pub fee: Decimal,
143 #[serde(rename = "T")]
145 pub trade_ts: i64,
146 #[serde(rename = "t")]
148 pub trade_id: i64,
149 #[serde(rename = "b")]
151 pub bid_equity: Decimal,
152 #[serde(rename = "a")]
154 pub ask_equity: Decimal,
155 #[serde(rename = "m")]
157 pub marker: bool,
158 #[serde(rename = "R")]
160 pub reduce_only: bool,
161 #[serde(rename = "wt")]
163 pub trigger_type: String,
164 #[serde(rename = "ot")]
166 pub original_order_type: OrderType,
167 #[serde(rename = "ps")]
169 pub position_side: PositionSide,
170 #[serde(rename = "cp")]
172 pub is_triggered_close: Option<bool>,
173 #[serde(rename = "AP")]
175 pub active_price: Option<Decimal>,
176 #[serde(rename = "cr")]
178 pub cr: Option<Decimal>,
179 #[serde(rename = "rp")]
181 pub pnl: Decimal,
182}
183
184#[derive(Debug, Clone, Deserialize)]
186pub struct ExecutionReport {
187 #[serde(rename = "E")]
189 pub event_ts: i64,
190 #[serde(rename = "s")]
192 pub symbol: Str,
193 #[serde(rename = "c")]
195 pub client_id: Str,
196 #[serde(rename = "C")]
198 pub orignal_client_id: Str,
199 #[serde(rename = "S")]
201 pub side: OrderSide,
202 #[serde(rename = "o")]
204 pub order_type: OrderType,
205 #[serde(rename = "f")]
207 pub time_in_force: TimeInForce,
208 #[serde(rename = "Q")]
210 pub quote_size: Decimal,
211 #[serde(rename = "q")]
213 pub size: Decimal,
214 #[serde(rename = "p")]
216 pub price: Decimal,
217 #[serde(rename = "x")]
219 pub kind: UpdateKind,
220 #[serde(rename = "X")]
222 pub status: Status,
223 #[serde(rename = "i")]
225 pub order_id: i64,
226 #[serde(rename = "l")]
228 pub last_trade_size: Decimal,
229 #[serde(rename = "Y")]
231 pub last_trade_quote_size: Decimal,
232 #[serde(rename = "z")]
234 pub filled_size: Decimal,
235 #[serde(rename = "Z")]
237 pub filled_quote_size: Decimal,
238 #[serde(rename = "L")]
240 pub last_trade_price: Decimal,
241 #[serde(rename = "N")]
243 pub fee_asset: Option<Asset>,
244 #[serde(rename = "n", default)]
246 pub fee: Decimal,
247 #[serde(rename = "T")]
249 pub trade_ts: i64,
250 #[serde(rename = "t")]
252 pub trade_id: i64,
253 #[serde(rename = "m")]
255 pub marker: bool,
256 #[serde(rename = "O")]
258 pub create_ts: i64,
259}
260
261impl ExecutionReport {
262 pub fn client_id(&self) -> &str {
264 if self.orignal_client_id.is_empty() {
265 self.client_id.as_str()
266 } else {
267 self.orignal_client_id.as_str()
268 }
269 }
270}
271
272#[derive(Debug, Clone, Deserialize)]
274#[allow(unused)]
275pub struct OptionsOrderUpdate {
276 #[serde(rename = "E")]
278 pub(crate) event_ts: i64,
279 #[serde(rename = "o")]
280 pub(crate) order: Vec<OptionsOrder>,
281}
282
283#[serde_with::serde_as]
285#[derive(Debug, Clone, Deserialize)]
286#[allow(unused)]
287pub struct OptionsOrder {
288 #[serde(rename = "T")]
290 pub(crate) create_ts: i64,
291 #[serde(rename = "t")]
293 pub(crate) update_ts: i64,
294 #[serde(rename = "s")]
296 pub(crate) symbol: Str,
297 #[serde(rename = "c")]
299 #[serde_as(as = "serde_with::NoneAsEmptyString")]
300 pub(crate) client_id: Option<Str>,
301 #[serde(rename = "oid")]
303 pub(crate) order_id: Str,
304 #[serde(rename = "p")]
306 pub(crate) price: Decimal,
307 #[serde(rename = "q")]
309 pub(crate) size: Decimal,
310 #[serde(rename = "r")]
312 pub(crate) reduce_only: bool,
313 #[serde(rename = "po")]
315 pub(crate) post_only: bool,
316 #[serde(rename = "S")]
318 pub(crate) status: Status,
319 #[serde(rename = "e")]
321 pub(crate) filled_size: Decimal,
322 #[serde(rename = "ec")]
324 pub(crate) filled_amount: Decimal,
325 #[serde(rename = "f")]
327 pub(crate) fee: Decimal,
328 #[serde(rename = "tif")]
330 pub(crate) time_in_force: TimeInForce,
331 #[serde(rename = "oty")]
333 pub(crate) order_type: OrderType,
334 #[serde(rename = "fi")]
336 #[serde(default)]
337 pub(crate) trades: Vec<OptionsTrade>,
338}
339
340#[derive(Debug, Clone, Deserialize)]
342#[allow(unused)]
343pub struct OptionsTrade {
344 #[serde(rename = "T")]
346 pub(crate) trade_ts: i64,
347 #[serde(rename = "t")]
349 pub(crate) trade_id: Str,
350 #[serde(rename = "q")]
352 pub(crate) size: Decimal,
353 #[serde(rename = "p")]
355 pub(crate) price: Decimal,
356 #[serde(rename = "f")]
358 pub(crate) fee: Decimal,
359 #[serde(rename = "m")]
361 pub(crate) maker: Str,
362}
363
364impl Nameable for OptionsOrder {
365 fn to_name(&self) -> Name {
366 Name::order_trade_update(&self.symbol)
367 }
368}
369
370impl Nameable for AccountEvent {
371 fn to_name(&self) -> Name {
372 match self {
373 Self::ListenKeyExpired { .. } => Name::listen_key_expired(),
374 Self::OrderTradeUpdate { order, .. } => {
375 Name::order_trade_update(&order.symbol.to_lowercase())
376 }
377 Self::ExecutionReport(r) => Name::order_trade_update(&r.symbol.to_lowercase()),
378 }
379 }
380}
381
382impl TryFrom<StreamFrame> for AccountEvent {
383 type Error = WsError;
384
385 fn try_from(frame: StreamFrame) -> Result<Self, Self::Error> {
386 if let StreamFrameKind::AccountEvent(e) = frame.data {
387 Ok(e)
388 } else {
389 Err(WsError::UnexpectedFrame(anyhow::anyhow!("{frame:?}")))
390 }
391 }
392}
393
394impl TryFrom<StreamFrame> for Either<OrderUpdate, ExecutionReport> {
395 type Error = WsError;
396
397 fn try_from(frame: StreamFrame) -> Result<Self, Self::Error> {
398 if let StreamFrameKind::AccountEvent(e) = frame.data {
399 match e {
400 AccountEvent::OrderTradeUpdate { order, .. } => Ok(Either::Left(order)),
401 AccountEvent::ExecutionReport(r) => Ok(Either::Right(r)),
402 e => Err(WsError::UnexpectedFrame(anyhow::anyhow!("{e:?}"))),
403 }
404 } else {
405 Err(WsError::UnexpectedFrame(anyhow::anyhow!("{frame:?}")))
406 }
407 }
408}
409
410impl TryFrom<StreamFrame> for OrderUpdateFrame {
411 type Error = WsError;
412
413 fn try_from(frame: StreamFrame) -> Result<Self, Self::Error> {
414 match frame.data {
415 StreamFrameKind::AccountEvent(e) => match e {
416 AccountEvent::OrderTradeUpdate { order, .. } => Ok(Self::UsdMarginFutures(order)),
417 AccountEvent::ExecutionReport(r) => Ok(Self::Spot(r)),
418 e => Err(WsError::UnexpectedFrame(anyhow::anyhow!("{e:?}"))),
419 },
420 StreamFrameKind::OptionsOrder(order) => Ok(Self::Options(order)),
421 e => Err(WsError::UnexpectedFrame(anyhow::anyhow!("{e:?}"))),
422 }
423 }
424}
425
426impl TryFrom<OrderUpdateFrame> for types::OrderUpdate {
427 type Error = ExchangeError;
428
429 fn try_from(value: OrderUpdateFrame) -> Result<Self, Self::Error> {
430 match value {
431 OrderUpdateFrame::UsdMarginFutures(update) => {
432 let kind = match update.order_type {
433 OrderType::Limit => match update.time_in_force {
434 TimeInForce::Gtc => types::OrderKind::Limit(
435 update.price.normalize(),
436 types::TimeInForce::GoodTilCancelled,
437 ),
438 TimeInForce::Fok => types::OrderKind::Limit(
439 update.price.normalize(),
440 types::TimeInForce::FillOrKill,
441 ),
442 TimeInForce::Ioc => types::OrderKind::Limit(
443 update.price.normalize(),
444 types::TimeInForce::ImmediateOrCancel,
445 ),
446 TimeInForce::Gtx => types::OrderKind::PostOnly(update.price.normalize()),
447 },
448 OrderType::Market => types::OrderKind::Market,
449 other => {
450 return Err(ExchangeError::Other(anyhow!(
451 "unsupported order type: {other:?}"
452 )));
453 }
454 };
455 let mut filled = update.filled_size.abs().normalize();
456 let mut size = update.size.abs().normalize();
457 match update.side {
458 OrderSide::Buy => {
459 filled.set_sign_positive(true);
460 size.set_sign_positive(true)
461 }
462 OrderSide::Sell => {
463 filled.set_sign_positive(false);
464 size.set_sign_positive(false)
465 }
466 }
467 let status = update.status.try_into()?;
468 let trade_size = update.last_trade_size.abs().normalize();
469 let trade = if !trade_size.is_zero() {
470 let mut trade = types::OrderTrade {
471 price: update.last_trade_price.normalize(),
472 size: if matches!(update.side, OrderSide::Buy) {
473 trade_size
474 } else {
475 -trade_size
476 },
477 fee: Decimal::ZERO,
478 fee_asset: None,
479 };
480 if let Some(asset) = update.fee_asset {
481 trade.fee = -update.fee.normalize();
482 trade.fee_asset = Some(asset);
483 }
484 Some(trade)
485 } else {
486 None
487 };
488 Ok(types::OrderUpdate {
489 ts: crate::types::adaptations::from_timestamp(update.trade_ts)?,
490 order: types::Order {
491 id: types::OrderId::from(update.client_id),
492 target: types::Place { size, kind },
493 state: types::OrderState {
494 filled,
495 cost: if filled.is_zero() {
496 Decimal::ONE
497 } else {
498 update.cost
499 },
500 status,
501 fees: HashMap::default(),
502 },
503 trade,
504 },
505 })
506 }
507 OrderUpdateFrame::Spot(update) => {
508 let client_id = update.client_id().to_string();
509 let kind = match update.order_type {
510 OrderType::Limit => match update.time_in_force {
511 TimeInForce::Gtc => types::OrderKind::Limit(
512 update.price.normalize(),
513 types::TimeInForce::GoodTilCancelled,
514 ),
515 TimeInForce::Fok => types::OrderKind::Limit(
516 update.price.normalize(),
517 types::TimeInForce::FillOrKill,
518 ),
519 TimeInForce::Ioc => types::OrderKind::Limit(
520 update.price.normalize(),
521 types::TimeInForce::ImmediateOrCancel,
522 ),
523 TimeInForce::Gtx => types::OrderKind::PostOnly(update.price.normalize()),
524 },
525 OrderType::Market => types::OrderKind::Market,
526 OrderType::LimitMaker => types::OrderKind::PostOnly(update.price.normalize()),
527 other => {
528 return Err(ExchangeError::Other(anyhow!(
529 "unsupported order type: {other:?}"
530 )));
531 }
532 };
533 let mut filled = update.filled_size.abs().normalize();
534 let mut size = update.size.abs().normalize();
535 match update.side {
536 OrderSide::Buy => {
537 filled.set_sign_positive(true);
538 size.set_sign_positive(true)
539 }
540 OrderSide::Sell => {
541 filled.set_sign_positive(false);
542 size.set_sign_positive(false)
543 }
544 }
545 let status = match update.status {
546 Status::New | Status::PartiallyFilled => types::OrderStatus::Pending,
547 Status::Canceled | Status::Expired | Status::Filled => {
548 types::OrderStatus::Finished
549 }
550 Status::NewAdl | Status::NewInsurance => types::OrderStatus::Pending,
551 };
552 let trade_size = update.last_trade_size.abs().normalize();
553 let trade = if !trade_size.is_zero() {
554 let mut trade = types::OrderTrade {
555 price: update.last_trade_price,
556 size: if matches!(update.side, OrderSide::Buy) {
557 trade_size
558 } else {
559 -trade_size
560 },
561 fee: Decimal::ZERO,
562 fee_asset: None,
563 };
564 if let Some(asset) = update.fee_asset {
565 trade.fee = -update.fee.normalize();
566 trade.fee_asset = Some(asset);
567 }
568 Some(trade)
569 } else {
570 None
571 };
572 Ok(types::OrderUpdate {
573 ts: crate::types::adaptations::from_timestamp(update.trade_ts)?,
574 order: types::Order {
575 id: types::OrderId::from(client_id),
576 target: types::Place { size, kind },
577 state: types::OrderState {
578 filled,
579 cost: if update.filled_size.is_zero() {
580 Decimal::ONE
581 } else {
582 (update.filled_quote_size / update.filled_size).normalize()
583 },
584 status,
585 fees: HashMap::default(),
586 },
587 trade,
588 },
589 })
590 }
591 OrderUpdateFrame::Options(update) => {
592 let kind = match update.order_type {
593 OrderType::Limit => match (update.post_only, update.time_in_force) {
594 (false, TimeInForce::Gtc) => types::OrderKind::Limit(
595 update.price.normalize(),
596 types::TimeInForce::GoodTilCancelled,
597 ),
598 (false, TimeInForce::Fok) => types::OrderKind::Limit(
599 update.price.normalize(),
600 types::TimeInForce::FillOrKill,
601 ),
602 (false, TimeInForce::Ioc) => types::OrderKind::Limit(
603 update.price.normalize(),
604 types::TimeInForce::ImmediateOrCancel,
605 ),
606 (true, _) => types::OrderKind::PostOnly(update.price.normalize()),
607 other => {
608 return Err(ExchangeError::Other(anyhow!(
609 "unsupported time in force: {other:?}"
610 )));
611 }
612 },
613 OrderType::Market => types::OrderKind::Market,
614 OrderType::LimitMaker => types::OrderKind::PostOnly(update.price.normalize()),
615 other => {
616 return Err(ExchangeError::Other(anyhow!(
617 "unsupported order type: {other:?}"
618 )));
619 }
620 };
621 let filled = update.filled_size.normalize();
622 let cost = if filled.is_zero() {
623 Decimal::ONE
624 } else {
625 update
626 .filled_amount
627 .normalize()
628 .checked_div(filled)
629 .ok_or_else(|| {
630 ExchangeError::Other(anyhow::anyhow!(
631 "parse options order: failed to calculate cost"
632 ))
633 })?
634 };
635 let quote_fee = update.fee.normalize().neg();
636 const QUOTE: Asset = Asset::USDT;
638 let state = types::OrderState {
639 filled,
640 cost,
641 status: update.status.try_into()?,
642 fees: HashMap::from([(QUOTE, quote_fee)]),
643 };
644 let order = types::Order {
645 id: types::OrderId::from(update.client_id.unwrap_or(update.order_id)),
646 target: types::Place {
647 size: update.size.normalize(),
648 kind,
649 },
650 state,
651 trade: None,
653 };
654 Ok(types::OrderUpdate {
655 ts: crate::types::adaptations::from_timestamp(update.update_ts)?,
656 order,
657 })
658 }
659 }
660 }
661}