exc_binance/websocket/protocol/frame/
account.rs

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/// Account events.
16#[derive(Debug, Clone, Deserialize)]
17#[serde(tag = "e", rename_all = "camelCase")]
18pub enum AccountEvent {
19    /// Listen key expired.
20    ListenKeyExpired {
21        /// Event timestamp.
22        #[serde(rename = "E")]
23        ts: i64,
24    },
25    /// Order trade update.
26    #[serde(rename = "ORDER_TRADE_UPDATE")]
27    OrderTradeUpdate {
28        /// Event timestamp.
29        #[serde(rename = "E")]
30        event_ts: i64,
31        /// Trade timestamp.
32        #[serde(rename = "T")]
33        trade_ts: i64,
34        /// Order.
35        #[serde(rename = "o")]
36        order: OrderUpdate,
37    },
38    /// Order update (for spot).
39    #[serde(rename = "executionReport")]
40    ExecutionReport(ExecutionReport),
41}
42
43/// Order Update Frame.
44#[derive(Debug, Clone)]
45#[non_exhaustive]
46pub enum OrderUpdateFrame {
47    /// Options.
48    Options(OptionsOrder),
49    /// USD-M Futures.
50    UsdMarginFutures(OrderUpdate),
51    /// Execution report.
52    Spot(ExecutionReport),
53}
54
55/// Order type.
56#[derive(Debug, Clone, Copy, Deserialize)]
57#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
58pub enum OrderType {
59    /// Market.
60    Market,
61    /// Limit.
62    Limit,
63    /// Stop.
64    Stop,
65    /// Take profit.
66    TakeProfit,
67    /// Liquidation.
68    Liquidation,
69    /// Limit maker.
70    LimitMaker,
71}
72
73/// Update kind.
74#[derive(Debug, Clone, Copy, Deserialize)]
75#[serde(rename_all = "UPPERCASE")]
76pub enum UpdateKind {
77    /// New.
78    New,
79    /// Cancelled.
80    Canceled,
81    /// Calculated.
82    Calculated,
83    /// Expired.
84    Expired,
85    /// Trade.
86    Trade,
87}
88
89/// Order update.
90#[derive(Debug, Clone, Deserialize)]
91pub struct OrderUpdate {
92    /// Symbol.
93    #[serde(rename = "s")]
94    pub symbol: Str,
95    /// Client id.
96    #[serde(rename = "c")]
97    pub client_id: Str,
98    /// Order side.
99    #[serde(rename = "S")]
100    pub side: OrderSide,
101    /// Order type.
102    #[serde(rename = "o")]
103    pub order_type: OrderType,
104    /// Time-in-force.
105    #[serde(rename = "f")]
106    pub time_in_force: TimeInForce,
107    /// Size.
108    #[serde(rename = "q")]
109    pub size: Decimal,
110    /// Price. (FIXME: should this to be optional?)
111    #[serde(rename = "p")]
112    pub price: Decimal,
113    /// Cost.
114    #[serde(rename = "ap")]
115    pub cost: Decimal,
116    /// Trigger price.
117    #[serde(rename = "sp")]
118    pub trigger_price: Decimal,
119    /// Update kind.
120    #[serde(rename = "x")]
121    pub kind: UpdateKind,
122    /// Status.
123    #[serde(rename = "X")]
124    pub status: Status,
125    /// Order id.
126    #[serde(rename = "i")]
127    pub order_id: i64,
128    /// Last trade size.
129    #[serde(rename = "l")]
130    pub last_trade_size: Decimal,
131    /// Filled size.
132    #[serde(rename = "z")]
133    pub filled_size: Decimal,
134    /// Last trade price.
135    #[serde(rename = "L")]
136    pub last_trade_price: Decimal,
137    /// Fee asset.
138    #[serde(rename = "N")]
139    pub fee_asset: Option<Asset>,
140    /// Fee.
141    #[serde(rename = "n", default)]
142    pub fee: Decimal,
143    /// Trade timestamp.
144    #[serde(rename = "T")]
145    pub trade_ts: i64,
146    /// Trade id.
147    #[serde(rename = "t")]
148    pub trade_id: i64,
149    /// Bid equity.
150    #[serde(rename = "b")]
151    pub bid_equity: Decimal,
152    /// Ask equity.
153    #[serde(rename = "a")]
154    pub ask_equity: Decimal,
155    /// Maker.
156    #[serde(rename = "m")]
157    pub marker: bool,
158    /// Reduce-Only.
159    #[serde(rename = "R")]
160    pub reduce_only: bool,
161    /// Trigger type.
162    #[serde(rename = "wt")]
163    pub trigger_type: String,
164    /// Original order type.
165    #[serde(rename = "ot")]
166    pub original_order_type: OrderType,
167    /// Position side.
168    #[serde(rename = "ps")]
169    pub position_side: PositionSide,
170    /// Is triggered close.
171    #[serde(rename = "cp")]
172    pub is_triggered_close: Option<bool>,
173    /// Active price.
174    #[serde(rename = "AP")]
175    pub active_price: Option<Decimal>,
176    /// Cr.
177    #[serde(rename = "cr")]
178    pub cr: Option<Decimal>,
179    /// Profit and loss.
180    #[serde(rename = "rp")]
181    pub pnl: Decimal,
182}
183
184/// Order update for spot.
185#[derive(Debug, Clone, Deserialize)]
186pub struct ExecutionReport {
187    /// Event timestamp.
188    #[serde(rename = "E")]
189    pub event_ts: i64,
190    /// Symbol.
191    #[serde(rename = "s")]
192    pub symbol: Str,
193    /// Client id.
194    #[serde(rename = "c")]
195    pub client_id: Str,
196    /// Orignal Client id.
197    #[serde(rename = "C")]
198    pub orignal_client_id: Str,
199    /// Order side.
200    #[serde(rename = "S")]
201    pub side: OrderSide,
202    /// Order type.
203    #[serde(rename = "o")]
204    pub order_type: OrderType,
205    /// Time-in-force.
206    #[serde(rename = "f")]
207    pub time_in_force: TimeInForce,
208    /// Quote size.
209    #[serde(rename = "Q")]
210    pub quote_size: Decimal,
211    /// Size.
212    #[serde(rename = "q")]
213    pub size: Decimal,
214    /// Price. (FIXME: should this to be optional?)
215    #[serde(rename = "p")]
216    pub price: Decimal,
217    /// Update kind.
218    #[serde(rename = "x")]
219    pub kind: UpdateKind,
220    /// Status.
221    #[serde(rename = "X")]
222    pub status: Status,
223    /// Order id.
224    #[serde(rename = "i")]
225    pub order_id: i64,
226    /// Last trade size.
227    #[serde(rename = "l")]
228    pub last_trade_size: Decimal,
229    /// Last trade money.
230    #[serde(rename = "Y")]
231    pub last_trade_quote_size: Decimal,
232    /// Filled size.
233    #[serde(rename = "z")]
234    pub filled_size: Decimal,
235    /// Filled money.
236    #[serde(rename = "Z")]
237    pub filled_quote_size: Decimal,
238    /// Last trade price.
239    #[serde(rename = "L")]
240    pub last_trade_price: Decimal,
241    /// Fee asset.
242    #[serde(rename = "N")]
243    pub fee_asset: Option<Asset>,
244    /// Fee.
245    #[serde(rename = "n", default)]
246    pub fee: Decimal,
247    /// Trade timestamp.
248    #[serde(rename = "T")]
249    pub trade_ts: i64,
250    /// Trade id.
251    #[serde(rename = "t")]
252    pub trade_id: i64,
253    /// Maker.
254    #[serde(rename = "m")]
255    pub marker: bool,
256    /// Created timestamp.
257    #[serde(rename = "O")]
258    pub create_ts: i64,
259}
260
261impl ExecutionReport {
262    /// Get client id (original).
263    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/// Order update for options.
273#[derive(Debug, Clone, Deserialize)]
274#[allow(unused)]
275pub struct OptionsOrderUpdate {
276    /// Event timestamp.
277    #[serde(rename = "E")]
278    pub(crate) event_ts: i64,
279    #[serde(rename = "o")]
280    pub(crate) order: Vec<OptionsOrder>,
281}
282
283/// Options order.
284#[serde_with::serde_as]
285#[derive(Debug, Clone, Deserialize)]
286#[allow(unused)]
287pub struct OptionsOrder {
288    /// Created timestamp.
289    #[serde(rename = "T")]
290    pub(crate) create_ts: i64,
291    /// Updated timestamp.
292    #[serde(rename = "t")]
293    pub(crate) update_ts: i64,
294    /// Symbol.
295    #[serde(rename = "s")]
296    pub(crate) symbol: Str,
297    /// Client id.
298    #[serde(rename = "c")]
299    #[serde_as(as = "serde_with::NoneAsEmptyString")]
300    pub(crate) client_id: Option<Str>,
301    /// Order id.
302    #[serde(rename = "oid")]
303    pub(crate) order_id: Str,
304    /// Price.
305    #[serde(rename = "p")]
306    pub(crate) price: Decimal,
307    /// Size.
308    #[serde(rename = "q")]
309    pub(crate) size: Decimal,
310    /// Reduce-only.
311    #[serde(rename = "r")]
312    pub(crate) reduce_only: bool,
313    /// Post-only.
314    #[serde(rename = "po")]
315    pub(crate) post_only: bool,
316    /// Status.
317    #[serde(rename = "S")]
318    pub(crate) status: Status,
319    /// Filled size.
320    #[serde(rename = "e")]
321    pub(crate) filled_size: Decimal,
322    /// Filled amount.
323    #[serde(rename = "ec")]
324    pub(crate) filled_amount: Decimal,
325    /// Fee.
326    #[serde(rename = "f")]
327    pub(crate) fee: Decimal,
328    /// Time-in-force.
329    #[serde(rename = "tif")]
330    pub(crate) time_in_force: TimeInForce,
331    /// Order type.
332    #[serde(rename = "oty")]
333    pub(crate) order_type: OrderType,
334    /// Trades.
335    #[serde(rename = "fi")]
336    #[serde(default)]
337    pub(crate) trades: Vec<OptionsTrade>,
338}
339
340/// Options trade.
341#[derive(Debug, Clone, Deserialize)]
342#[allow(unused)]
343pub struct OptionsTrade {
344    /// Trade ts.
345    #[serde(rename = "T")]
346    pub(crate) trade_ts: i64,
347    /// Trade id.
348    #[serde(rename = "t")]
349    pub(crate) trade_id: Str,
350    /// Size.
351    #[serde(rename = "q")]
352    pub(crate) size: Decimal,
353    /// Price.
354    #[serde(rename = "p")]
355    pub(crate) price: Decimal,
356    /// Fee.
357    #[serde(rename = "f")]
358    pub(crate) fee: Decimal,
359    /// Maker or taker.
360    #[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                // FIXME: we are assuming that the quote is in USDT.
637                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                    // FIXME: we are not parsing the trades.
652                    trade: None,
653                };
654                Ok(types::OrderUpdate {
655                    ts: crate::types::adaptations::from_timestamp(update.update_ts)?,
656                    order,
657                })
658            }
659        }
660    }
661}