exc_binance/types/adaptations/
trading.rs1use std::{collections::HashMap, ops::Neg};
2
3use exc_core::{types, Adaptor, ExchangeError, Str};
4use futures::{FutureExt, StreamExt, TryStreamExt};
5use rust_decimal::Decimal;
6use time::OffsetDateTime;
7
8use crate::{
9 http::{
10 request::trading::{CancelOrder, GetOrder, GetOrderInner, PlaceOrder},
11 response::trading::Order,
12 },
13 types::{
14 trading::{self, OrderSide, Status, TimeInForce},
15 Name,
16 },
17 websocket::protocol::frame::account::OrderUpdateFrame,
18 Request,
19};
20
21impl Adaptor<types::SubscribeOrders> for Request {
22 fn from_request(req: types::SubscribeOrders) -> Result<Self, ExchangeError> {
23 Ok(Self::subscribe(Name::order_trade_update(&req.instrument)))
24 }
25
26 fn into_response(resp: Self::Response) -> Result<types::OrderStream, ExchangeError> {
27 let stream = resp.into_stream::<OrderUpdateFrame>()?;
28 Ok(stream
29 .map_err(ExchangeError::from)
30 .and_then(|update| async move { update.try_into() })
31 .boxed())
32 }
33}
34
35impl TryFrom<Order> for types::Order {
36 type Error = ExchangeError;
37
38 fn try_from(order: Order) -> Result<Self, Self::Error> {
39 match order {
40 Order::UsdMarginFutures(order) => {
41 let mut filled = order.executed_qty.abs();
42 let mut size = order.orig_qty.abs();
43 match order.side {
44 OrderSide::Buy => {
45 filled.set_sign_positive(true);
46 size.set_sign_positive(true);
47 }
48 OrderSide::Sell => {
49 filled.set_sign_positive(false);
50 size.set_sign_positive(false);
51 }
52 }
53 let kind = match order.order_type {
54 trading::OrderType::Limit => match order.time_in_force {
55 TimeInForce::Gtc => types::OrderKind::Limit(
56 order.price,
57 types::TimeInForce::GoodTilCancelled,
58 ),
59 TimeInForce::Fok => {
60 types::OrderKind::Limit(order.price, types::TimeInForce::FillOrKill)
61 }
62 TimeInForce::Ioc => types::OrderKind::Limit(
63 order.price,
64 types::TimeInForce::ImmediateOrCancel,
65 ),
66 TimeInForce::Gtx => types::OrderKind::PostOnly(order.price),
67 },
68 trading::OrderType::Market => types::OrderKind::Market,
69 other => {
70 return Err(ExchangeError::Other(anyhow!(
71 "unsupported order type: {other:?}"
72 )));
73 }
74 };
75 let status = match order.status {
76 Status::New | Status::PartiallyFilled => types::OrderStatus::Pending,
77 Status::Canceled | Status::Expired | Status::Filled => {
78 types::OrderStatus::Finished
79 }
80 Status::NewAdl | Status::NewInsurance => types::OrderStatus::Pending,
81 };
82 Ok(types::Order {
83 id: types::OrderId::from(order.client_order_id),
84 target: types::Place { size, kind },
85 state: types::OrderState {
86 filled,
87 cost: if filled.is_zero() {
88 Decimal::ONE
89 } else {
90 order.avg_price
91 },
92 status,
93 fees: HashMap::default(),
94 },
95 trade: None,
96 })
97 }
98 Order::Spot(order) => {
99 let ack = order.ack;
100 if let Some(result) = order.result {
101 let mut filled = result.executed_qty.abs().normalize();
102 let mut size = result.orig_qty.abs().normalize();
103 match result.side {
104 OrderSide::Buy => {
105 filled.set_sign_positive(true);
106 size.set_sign_positive(true);
107 }
108 OrderSide::Sell => {
109 filled.set_sign_positive(false);
110 size.set_sign_positive(false);
111 }
112 }
113 let kind = match result.order_type {
114 trading::OrderType::Limit => match result.time_in_force {
115 TimeInForce::Gtc | TimeInForce::Gtx => types::OrderKind::Limit(
116 result.price.normalize(),
117 types::TimeInForce::GoodTilCancelled,
118 ),
119 TimeInForce::Fok => types::OrderKind::Limit(
120 result.price.normalize(),
121 types::TimeInForce::FillOrKill,
122 ),
123 TimeInForce::Ioc => types::OrderKind::Limit(
124 result.price.normalize(),
125 types::TimeInForce::ImmediateOrCancel,
126 ),
127 },
128 trading::OrderType::Market => types::OrderKind::Market,
129 trading::OrderType::LimitMaker => {
130 types::OrderKind::PostOnly(result.price.normalize())
131 }
132 other => {
133 return Err(ExchangeError::Other(anyhow!(
134 "unsupported order type: {other:?}"
135 )));
136 }
137 };
138 let status = match result.status {
139 Status::New | Status::PartiallyFilled => types::OrderStatus::Pending,
140 Status::Canceled | Status::Expired | Status::Filled => {
141 types::OrderStatus::Finished
142 }
143 Status::NewAdl | Status::NewInsurance => types::OrderStatus::Pending,
144 };
145 let mut fees = HashMap::default();
146 let mut last_trade = None;
147 for fill in order.fills {
148 let fee = fees.entry(fill.commission_asset.clone()).or_default();
149 *fee -= fill.commission.normalize();
150 last_trade = Some(types::OrderTrade {
151 price: fill.price.normalize(),
152 size: fill.qty.normalize(),
153 fee: -fill.commission.normalize(),
154 fee_asset: Some(fill.commission_asset),
155 });
156 }
157 Ok(types::Order {
158 id: types::OrderId::from(ack.client_order_id().to_string()),
159 target: types::Place { size, kind },
160 state: types::OrderState {
161 filled,
162 cost: if result.executed_qty.is_zero() {
163 Decimal::ONE
164 } else {
165 (result.cummulative_quote_qty / result.executed_qty).normalize()
166 },
167 status,
168 fees,
169 },
170 trade: last_trade,
171 })
172 } else {
173 Err(ExchangeError::Other(anyhow::anyhow!(
174 "order result is missing"
175 )))
176 }
177 }
178 Order::EuropeanOptions(order) => {
179 let id = order
180 .client_order_id
181 .unwrap_or_else(|| Str::new(order.order_id.to_string()));
182 let mut size = order.quantity.normalize().abs();
183 match order.side {
184 OrderSide::Buy => size.set_sign_positive(true),
185 OrderSide::Sell => size.set_sign_positive(false),
186 }
187 let state = order
188 .state
189 .ok_or_else(|| ExchangeError::Other(anyhow::anyhow!("order is not ready")))?;
190 let kind = match order.order_type {
191 trading::OrderType::Limit => match (order.post_only, state.time_in_force) {
192 (false, TimeInForce::Gtc) => types::OrderKind::Limit(
193 order.price.normalize(),
194 types::TimeInForce::GoodTilCancelled,
195 ),
196 (false, TimeInForce::Fok) => types::OrderKind::Limit(
197 order.price.normalize(),
198 types::TimeInForce::FillOrKill,
199 ),
200 (false, TimeInForce::Ioc) => types::OrderKind::Limit(
201 order.price.normalize(),
202 types::TimeInForce::ImmediateOrCancel,
203 ),
204 (true, _) => types::OrderKind::PostOnly(order.price.normalize()),
205 kind => {
206 return Err(ExchangeError::Other(anyhow::anyhow!(
207 "unsupported order kind: {kind:?}"
208 )));
209 }
210 },
211 trading::OrderType::Market => types::OrderKind::Market,
212 other => {
213 return Err(ExchangeError::Other(anyhow!(
214 "unsupported order type: {other:?}"
215 )));
216 }
217 };
218 let mut filled = state.executed_qty.normalize().abs();
219 match order.side {
220 OrderSide::Buy => filled.set_sign_positive(true),
221 OrderSide::Sell => filled.set_sign_positive(false),
222 }
223 let state = types::OrderState {
224 filled,
225 cost: state.avg_price.normalize(),
226 status: state.status.try_into()?,
227 fees: HashMap::from([(state.quote_asset, state.fee.normalize().neg())]),
228 };
229 Ok(types::Order {
230 id: types::OrderId::from(id),
231 target: types::Place { size, kind },
232 state,
233 trade: None,
234 })
235 }
236 }
237 }
238}
239
240impl Adaptor<types::PlaceOrder> for Request {
241 fn from_request(req: types::PlaceOrder) -> Result<Self, ExchangeError> {
242 Ok(Self::with_rest_payload(PlaceOrder { inner: req }))
243 }
244
245 fn into_response(
246 resp: Self::Response,
247 ) -> Result<<types::PlaceOrder as exc_core::Request>::Response, ExchangeError> {
248 Ok(async move {
249 let order = resp.into_response::<Order>()?;
250 let id = types::OrderId::from(order.client_id().to_string());
251 Ok(types::Placed {
252 ts: order
253 .updated()
254 .map(super::from_timestamp)
255 .unwrap_or_else(|| Ok(OffsetDateTime::now_utc()))?,
256 id,
257 order: order
258 .try_into()
259 .map_err(|err| {
260 tracing::warn!(%err, "failed to convert order");
261 })
262 .ok(),
263 })
264 }
265 .boxed())
266 }
267}
268
269impl Adaptor<types::CancelOrder> for Request {
270 fn from_request(req: types::CancelOrder) -> Result<Self, ExchangeError> {
271 Ok(Self::with_rest_payload(CancelOrder {
272 inner: GetOrderInner {
273 symbol: req.instrument.to_uppercase(),
274 order_id: None,
275 orig_client_order_id: Some(req.id.as_str().to_string()),
276 client_order_id: None,
277 },
278 }))
279 }
280
281 fn into_response(
282 resp: Self::Response,
283 ) -> Result<<types::CancelOrder as exc_core::Request>::Response, ExchangeError> {
284 Ok(async move {
285 let order = resp.into_response::<Order>()?;
286 Ok(types::Canceled {
287 ts: order
288 .updated()
289 .map(super::from_timestamp)
290 .unwrap_or_else(|| Ok(OffsetDateTime::now_utc()))?,
291 order: Some(order.try_into()?),
292 })
293 }
294 .boxed())
295 }
296}
297
298impl Adaptor<types::GetOrder> for Request {
299 fn from_request(req: types::GetOrder) -> Result<Self, ExchangeError> {
300 Ok(Self::with_rest_payload(GetOrder {
301 inner: GetOrderInner {
302 symbol: req.instrument.to_uppercase(),
303 order_id: None,
304 orig_client_order_id: Some(req.id.as_str().to_string()),
305 client_order_id: None,
306 },
307 }))
308 }
309
310 fn into_response(
311 resp: Self::Response,
312 ) -> Result<<types::GetOrder as exc_core::Request>::Response, ExchangeError> {
313 Ok(async move {
314 let order = resp.into_response::<Order>()?;
315 Ok(types::OrderUpdate {
316 ts: order
317 .updated()
318 .map(super::from_timestamp)
319 .unwrap_or_else(|| Ok(OffsetDateTime::now_utc()))?,
320 order: order.try_into()?,
321 })
322 }
323 .boxed())
324 }
325}