1use crate::crypto::secrets::Token;
2use crate::request_types::{TimeInForce, TriggerType};
3use crate::response_types::{BuySell, OrderStatusV2, OrderType, PositionStatusV2};
4use crate::wss::{
5 BookSubscriptionResponse, OhlcSubscriptionResponse, TickerSubscriptionResponse,
6 TradeSubscriptionResponse,
7};
8use crate::wss::{ConditionalParams, FeePreference, PriceType};
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11use serde_with::skip_serializing_none;
12
13#[derive(Debug, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum ExecutionResponseType {
16 Snapshot,
17 Update,
18}
19
20#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
21pub enum MakerTaker {
22 #[serde(rename = "m")]
23 Maker,
24 #[serde(rename = "t")]
25 Taker,
26}
27
28#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Copy)]
30#[serde(rename_all = "snake_case")]
31pub enum LedgerEntryTypeV2 {
32 Trade,
33 Credit,
34 Deposit,
35 Withdrawal,
36 Transfer,
37 Margin,
38 Rollover,
39 Settled,
40 Adjustment,
41 Staking,
42 Sale,
43 Reserve,
44 Conversion,
45 Dividend,
46 Reward,
47 CreatorFee,
48}
49
50#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Copy)]
51#[serde(rename_all = "lowercase")]
52pub enum LedgerEntrySubType {
53 SpotFromFutures,
54 SpotToFutures,
55 StakingFromSpot,
56 SpotFromStaking,
57 StakingToSpot,
58 SpotToStaking,
59}
60
61#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Copy)]
62#[serde(rename_all = "kebab-case")]
63pub enum LedgerCategory {
64 Deposit,
65 Withdrawal,
66 Trade,
67 MarginTrade,
68 MarginSettled,
69 MarginConversion,
70 Conversion,
71 Credit,
72 MarginRollover,
73 StakingRewards,
74 Instant,
75 EquityTrade,
76 Airdrop,
77 EquityDividend,
78 RewardBonus,
79 Nft,
80 BlockTrade,
81}
82
83#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
84#[serde(rename_all = "lowercase")]
85pub enum WalletType {
86 Spot,
87 Earn,
88}
89
90#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
91#[serde(rename_all = "lowercase")]
92pub enum WalletId {
93 Main,
94 Flex,
95 Bonded,
96}
97
98#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
99#[serde(rename_all = "snake_case")]
100pub enum ExecutionType {
101 PendingNew,
102 New,
103 Trade,
104 Filled,
105 Canceled,
106 Expired,
107 Amended,
108 Restated,
109 Status,
110}
111
112#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
113#[serde(rename_all = "lowercase")]
114pub enum TriggerStatus {
115 Triggered,
116 Untriggered,
117}
118
119#[derive(Debug, Serialize, Clone)]
120pub struct SubscriptionRequest<T> {
121 pub method: String,
122 pub params: T,
123 pub req_id: Option<i64>,
124}
125
126#[skip_serializing_none]
127#[derive(Debug, Serialize, Clone)]
128pub struct ExecutionSubscription {
129 pub channel: String,
130 pub token: Token,
131 #[serde(rename = "snap_trades")]
132 pub snapshot_trades: Option<bool>,
133 #[serde(rename = "snap_orders")]
134 pub snapshot_orders: Option<bool>,
135 pub rate_counter: Option<bool>,
136}
137
138impl ExecutionSubscription {
139 pub fn new(token: Token) -> Self {
140 ExecutionSubscription {
141 channel: "executions".to_string(),
142 token,
143 snapshot_trades: None,
144 snapshot_orders: None,
145 rate_counter: None,
146 }
147 }
148}
149
150#[derive(Debug, Deserialize, PartialEq)]
151#[serde(deny_unknown_fields)]
152pub struct InstrumentSubscriptionResult {
153 pub snapshot: Option<bool>,
154 pub warnings: Option<Vec<String>>,
155}
156
157#[derive(Debug, Deserialize, PartialEq)]
158pub struct ExecutionsSubscriptionResult {
159 #[serde(rename = "maxratecount")]
160 pub max_rate_count: Option<i64>,
161 pub snapshot: Option<bool>,
162 pub warnings: Option<Vec<String>>,
163}
164
165#[derive(Debug, Deserialize, PartialEq)]
166pub struct BalanceSubscriptionResult {
167 pub snapshot: Option<bool>,
168 pub warnings: Option<Vec<String>>,
169}
170
171#[derive(Debug, Deserialize, PartialEq)]
172#[serde(tag = "channel")]
173pub enum SubscriptionResult {
174 #[serde(rename = "level3")]
175 L3(BookSubscriptionResponse),
176 #[serde(rename = "book")]
177 Book(BookSubscriptionResponse),
178 #[serde(rename = "ticker")]
179 Ticker(TickerSubscriptionResponse),
180 #[serde(rename = "ohlc")]
181 Ohlc(OhlcSubscriptionResponse),
182 #[serde(rename = "trade")]
183 Trade(TradeSubscriptionResponse),
184 #[serde(rename = "executions")]
185 Execution(ExecutionsSubscriptionResult),
186 #[serde(rename = "balances")]
187 Balance(BalanceSubscriptionResult),
188 #[serde(rename = "instrument")]
189 Instrument(InstrumentSubscriptionResult),
190}
191
192#[derive(Debug, Deserialize)]
193pub struct ExecutionResponse {
194 pub channel: String,
195 #[serde(rename = "type")]
196 pub execution_response_type: ExecutionResponseType,
197}
198
199#[derive(Debug, Deserialize, PartialEq)]
200pub struct Fee {
201 pub asset: String,
202 #[serde(rename = "qty")]
203 pub quantity: Decimal,
204}
205
206#[derive(Debug, Deserialize, PartialEq)]
207pub struct TriggerDescription {
208 pub reference: TriggerType,
209 pub price: Decimal,
210 pub price_type: PriceType,
211 pub actual_price: Option<Decimal>,
212 pub peak_price: Option<Decimal>,
213 pub last_price: Option<Decimal>,
214 pub status: TriggerStatus,
215 pub timestamp: Option<String>,
216}
217
218#[derive(Debug, Deserialize, PartialEq)]
219pub struct ExecutionResult {
220 pub amended: Option<bool>,
221 #[serde(rename = "exec_type")]
222 pub execution_type: ExecutionType,
223 #[serde(rename = "cash_order_qty")]
224 pub cash_order_quantity: Option<Decimal>,
225 #[serde(rename = "cl_ord_id")]
226 pub client_order_id: Option<String>,
227 pub contingent: Option<ConditionalParams>,
228 pub cost: Option<Decimal>,
229 #[serde(rename = "exec_id")]
230 pub execution_id: Option<String>,
231 pub fees: Option<Vec<Fee>>,
232 #[serde(rename = "liquidity_ind")]
233 pub liquidity_indicator: Option<MakerTaker>,
234 pub last_price: Option<Decimal>,
235 #[serde(rename = "last_qty")]
236 pub last_quantity: Option<Decimal>,
237 #[serde(rename = "avg_price")]
238 pub average_price: Option<Decimal>,
239 pub reason: Option<String>,
240 #[serde(rename = "cum_cost")]
241 pub cumulative_cost: Option<Decimal>,
242 #[serde(rename = "cum_qty")]
243 pub cumulative_quantity: Option<Decimal>,
244 #[serde(rename = "display_qty")]
245 pub display_quantity: Option<Decimal>,
246 pub effective_time: Option<String>,
247 pub expire_time: Option<String>,
248 pub ext_ord_id: Option<String>,
249 pub ext_exec_id: Option<String>,
250 #[serde(rename = "fee_ccy_pref")]
251 pub fee_preference: Option<FeePreference>,
252 #[serde(rename = "fee_usd_equiv")]
253 pub fee_usd_equivalent: Option<Decimal>,
254 pub limit_price: Option<Decimal>,
255 pub limit_price_type: Option<PriceType>,
256 pub liquidated: Option<bool>,
257 pub margin: Option<bool>,
258 pub margin_borrow: Option<bool>,
259 #[serde(rename = "no_mpp")]
260 pub no_market_price_protection: Option<bool>,
261 #[serde(rename = "ord_ref_id")]
262 pub order_ref_id: Option<i64>,
263 pub order_id: String,
264 #[serde(rename = "order_qty")]
265 pub order_quantity: Option<Decimal>,
266 pub order_type: Option<OrderType>,
267 pub order_status: OrderStatusV2,
268 #[serde(rename = "order_userref")]
269 pub order_user_ref: Option<i64>,
270 pub post_only: Option<bool>,
271 pub position_status: Option<PositionStatusV2>,
272 pub reduce_only: Option<bool>,
273 pub sender_sub_id: Option<String>,
274 pub side: Option<BuySell>,
275 pub symbol: Option<String>,
276 pub time_in_force: Option<TimeInForce>,
277 pub timestamp: String,
278 pub trade_id: Option<i64>,
279 pub triggers: Option<TriggerDescription>,
280}
281
282#[skip_serializing_none]
283#[derive(Debug, Serialize, Clone)]
284pub struct BalancesSubscription {
285 pub channel: String,
286 pub token: Token,
287 pub snapshot: Option<bool>,
288}
289
290impl BalancesSubscription {
291 pub fn new(token: Token) -> Self {
292 BalancesSubscription {
293 channel: "balances".to_string(),
294 token,
295 snapshot: None,
296 }
297 }
298}
299
300#[derive(Debug, Deserialize, PartialEq)]
301pub struct Wallet {
302 pub balance: Decimal,
303 #[serde(rename = "type")]
304 pub wallet_type: WalletType,
305 pub id: WalletId,
306}
307
308#[derive(Debug, Deserialize, PartialEq)]
309#[serde(untagged)]
310pub enum BalanceResponse {
311 Update(Vec<LedgerUpdate>),
312 Snapshot(Vec<Balance>),
313}
314
315#[derive(Debug, Deserialize, PartialEq)]
316pub struct Balance {
317 pub asset: String,
318 pub balance: Decimal,
319 pub wallets: Vec<Wallet>,
320}
321
322#[derive(Debug, Deserialize, PartialEq)]
323pub struct LedgerUpdate {
324 pub asset: String,
325 pub amount: Decimal,
326 pub balance: Decimal,
327 pub fee: Decimal,
328 pub ledger_id: String,
329 pub ref_id: String,
330 pub timestamp: String,
331 pub asset_class: String,
332 #[serde(rename = "type")]
333 pub ledger_type: LedgerEntryTypeV2,
334 pub sub_type: Option<LedgerEntrySubType>,
335 pub category: LedgerCategory,
336 pub wallet_type: WalletType,
337 pub wallet_id: WalletId,
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use rust_decimal_macros::dec;
344
345 #[test]
346 fn test_deserializing_execution_trade() {
347 let message = r#"{"order_id":"O7IBL5-O2V6X-EEXY4U","exec_id":"TJE7HC-DKBTI-5BFVKE","exec_type":"trade","ext_ord_id":"some-uuid","ext_exec_id":"another-uuid","trade_id":365573,"symbol":"KAR/USD","side":"buy","last_qty":105.02014889,"last_price":0.121,"liquidity_ind":"t","cost":12.70744,"order_status":"filled","order_type":"limit","timestamp":"2024-05-18T05:41:33.480251Z","fee_usd_equiv":0.05083,"fees":[{"asset":"USD","qty":0.05083}]}"#;
348 let expected = ExecutionResult {
349 amended: None,
350 execution_type: ExecutionType::Trade,
351 cash_order_quantity: None,
352 contingent: None,
353 cost: Some(dec!(12.70744)),
354 execution_id: Some("TJE7HC-DKBTI-5BFVKE".to_string()),
355 fees: Some(vec![Fee {
356 asset: "USD".to_string(),
357 quantity: dec!(0.05083),
358 }]),
359 liquidity_indicator: Some(MakerTaker::Taker),
360 last_price: Some(dec!(0.121)),
361 last_quantity: Some(dec!(105.02014889)),
362 average_price: None,
363 reason: None,
364 cumulative_cost: None,
365 cumulative_quantity: None,
366 display_quantity: None,
367 effective_time: None,
368 expire_time: None,
369 ext_ord_id: Some("some-uuid".to_string()),
370 ext_exec_id: Some("another-uuid".to_string()),
371 fee_preference: None,
372 fee_usd_equivalent: Some(dec!(0.05083)),
373 limit_price: None,
374 limit_price_type: None,
375 liquidated: None,
376 margin: None,
377 margin_borrow: None,
378 no_market_price_protection: None,
379 order_ref_id: None,
380 order_id: "O7IBL5-O2V6X-EEXY4U".to_string(),
381 order_quantity: None,
382 order_type: Some(OrderType::Limit),
383 order_status: OrderStatusV2::Filled,
384 order_user_ref: None,
385 post_only: None,
386 position_status: None,
387 reduce_only: None,
388 sender_sub_id: None,
389 side: Some(BuySell::Buy),
390 symbol: Some("KAR/USD".to_string()),
391 time_in_force: None,
392 timestamp: "2024-05-18T05:41:33.480251Z".to_string(),
393 trade_id: Some(365573),
394 triggers: None,
395 client_order_id: None,
396 };
397 let parsed: ExecutionResult = serde_json::from_str(message).unwrap();
398
399 assert_eq!(expected, parsed);
400 }
401
402 #[test]
403 fn test_deserializing_execution_new_update() {
404 let message = r#"{"timestamp":"2024-05-18T11:00:37.240691Z","order_status":"new","exec_type":"new","order_userref":0,"order_id":"OLADEP-E5D5S-IKEHMF"}"#;
405 let expected = ExecutionResult {
406 amended: None,
407 execution_type: ExecutionType::New,
408 cash_order_quantity: None,
409 contingent: None,
410 cost: None,
411 execution_id: None,
412 fees: None,
413 liquidity_indicator: None,
414 last_price: None,
415 last_quantity: None,
416 average_price: None,
417 reason: None,
418 cumulative_cost: None,
419 cumulative_quantity: None,
420 display_quantity: None,
421 effective_time: None,
422 expire_time: None,
423 ext_ord_id: None,
424 ext_exec_id: None,
425 fee_preference: None,
426 fee_usd_equivalent: None,
427 limit_price: None,
428 limit_price_type: None,
429 liquidated: None,
430 margin: None,
431 margin_borrow: None,
432 no_market_price_protection: None,
433 order_ref_id: None,
434 order_id: "OLADEP-E5D5S-IKEHMF".to_string(),
435 order_quantity: None,
436 order_type: None,
437 order_status: OrderStatusV2::New,
438 order_user_ref: Some(0),
439 post_only: None,
440 position_status: None,
441 reduce_only: None,
442 sender_sub_id: None,
443 side: None,
444 symbol: None,
445 time_in_force: None,
446 timestamp: "2024-05-18T11:00:37.240691Z".to_string(),
447 trade_id: None,
448 triggers: None,
449 client_order_id: None,
450 };
451 let parsed: ExecutionResult = serde_json::from_str(message).unwrap();
452
453 assert_eq!(expected, parsed);
454 }
455}