coinbase_v3/
orders.rs

1//! Structures, Enums & helper functions for Coinbase's order related structures
2
3use anyhow::anyhow;
4use anyhow::Result;
5use bigdecimal::{BigDecimal, FromPrimitive};
6use serde_derive::{Deserialize, Serialize};
7use serde_enum_str::{Deserialize_enum_str, Serialize_enum_str};
8
9use crate::products::ProductType;
10use crate::products::Side; // Move to order? might make more sense...
11use crate::DateTime;
12
13/// Structure representing Coinbase's order configuration structure
14///
15/// `Option` can nornally be not `None` for one of the item.
16/// It is not an enum to be able to deserialize the response
17#[derive(Deserialize, Serialize, Debug, Eq, PartialEq)]
18pub struct OrderConfiguration {
19    pub market_market_ioc: Option<Market>,
20    pub limit_limit_gtc: Option<Limit>,
21    pub limit_limit_gtd: Option<Limit>,
22    pub stop_limit_stop_limit_gtc: Option<StopLimit>,
23    pub stop_limit_stop_limit_gtd: Option<StopLimit>,
24}
25
26/// Structure representing Coinbase's Market order structure
27#[derive(Deserialize, Serialize, Debug, Eq, PartialEq)]
28pub struct Market {
29    /// Amount of quote currency to spend on order. Required for BUY orders.
30    pub quote_size: Option<BigDecimal>,
31    /// Amount of base currency to spend on order. Required for SELL orders
32    pub base_size: Option<BigDecimal>,
33}
34
35/// Structure representing Coinbase's limit order structure
36///
37/// end_time is only used for gtd orders, not gtc
38#[derive(Deserialize, Serialize, Debug, Eq, PartialEq)]
39pub struct Limit {
40    /// Amount of base currency to spend on order
41    pub base_size: BigDecimal,
42    /// Ceiling price for which the order should get filled
43    pub limit_price: BigDecimal,
44    /// Time at which the order should be cancelled if it's not filled.
45    pub end_time: Option<DateTime>,
46    /// Post only limit order
47    pub post_only: Option<bool>,
48}
49
50/// Enum representing the possible direction of the stop order.
51#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, PartialEq, Eq)]
52#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
53pub enum StopDirection {
54    UnknownStopDirection,
55    StopDirectionStopUp,
56    StopDirectionStopDown,
57}
58
59/// Structure representing Coinbase's stop-limit order structure
60///
61/// end_time is only used for gtd orders, not gtc
62#[derive(Deserialize, Serialize, Debug, Eq, PartialEq)]
63pub struct StopLimit {
64    /// Amount of base currency to spend on order
65    pub base_size: BigDecimal,
66    /// Ceiling price for which the order should get filled
67    pub limit_price: BigDecimal,
68    /// Price at which the order should trigger - if stop direction is Up,
69    /// then the order will trigger when the last trade price goes above this,
70    /// otherwise order will trigger when last trade price goes below this price.
71    pub stop_price: BigDecimal,
72    pub stop_direction: StopDirection,
73    pub end_time: Option<DateTime>,
74}
75
76/// Enum representing the possible status values of an order
77#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, PartialEq, Eq)]
78#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
79pub enum Status {
80    Open,
81    Filled,
82    Cancelled,
83    Expired,
84    Failed,
85    UnknownOrderStatus,
86}
87
88/// Enum representing the possible values for the time in force of an order
89#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, PartialEq, Eq)]
90#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
91pub enum TimeInForce {
92    UnknownTimeInForce,
93    GoodUntilDateTime,
94    GoodUntilCancelled,
95    ImmediateOrCancel,
96    FillOrKill,
97}
98
99/// Enum representing the possible values for the trigger status of an order
100#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, PartialEq, Eq)]
101#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
102pub enum TriggerStatus {
103    UnknownTriggerStatus,
104    InvalidOrderType,
105    StopPending,
106    StopTriggered,
107}
108
109/// Enum representing the possible values for type of order
110#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, PartialEq, Eq)]
111#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
112pub enum OrderType {
113    UnknownOrderType,
114    Market,
115    Limit,
116    Stop,
117    #[serde(rename = "STOP_LIMIT")]
118    StopLimitOrderType,
119}
120
121/// Enum representing the possible values for the reject reason
122#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, PartialEq, Eq)]
123#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
124pub enum RejectReason {
125    RejectReasonUnspecified,
126}
127
128/// Enum representing the possible values for the source of the order placed
129#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, PartialEq, Eq)]
130#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
131pub enum OrderPlacementSource {
132    RetailSimple,
133    RetailAdvanced,
134}
135
136/// Structure representing an order response
137#[derive(Deserialize, Debug, Eq, PartialEq)]
138pub struct Order {
139    /// The unique id for this order
140    pub order_id: String,
141    /// The product this order was created for e.g. 'BTC-USD'
142    pub product_id: String,
143    /// The id of the User owning this Order
144    pub user_id: String,
145    pub order_configuration: OrderConfiguration,
146    /// Possible values: [UNKNOWN_ORDER_SIDE, BUY, SELL]
147    pub side: Side,
148    /// Client specified ID of order.
149    pub client_order_id: String,
150    /// Possible values: [OPEN, FILLED, CANCELLED, EXPIRED, FAILED, UNKNOWN_ORDER_STATUS]
151    pub status: Status,
152    /// Possible values: [UNKNOWN_TIME_IN_FORCE, GOOD_UNTIL_DATE_TIME, GOOD_UNTIL_CANCELLED, IMMEDIATE_OR_CANCEL, FILL_OR_KILL]
153    pub time_in_force: TimeInForce,
154    /// Timestamp for when the order was created
155    pub created_time: DateTime,
156    /// The percent of total order amount that has been filled
157    pub completion_percentage: String,
158    /// The portion (in base currency) of total order amount that has been filled
159    pub filled_size: String,
160    /// The average of all prices of fills for this order
161    pub average_filled_price: String,
162    /// Commission amount
163    pub fee: String,
164    /// Number of fills that have been posted for this order
165    pub number_of_fills: String,
166    /// The portion (in quote current) of total order amount that has been filled
167    pub filled_value: String,
168    /// Whether a cancel request has been initiated for the order, and not yet completed
169    pub pending_cancel: bool,
170    /// Whether the order was placed with quote currency
171    pub size_in_quote: bool,
172    /// The total fees for the order
173    pub total_fees: String,
174    /// Whether the order size includes fees
175    pub size_inclusive_of_fees: bool,
176    /// derived field: filled_value + total_fees for buy orders and filled_value - total_fees for sell orders.
177    pub total_value_after_fees: String,
178    /// Possible values: [UNKNOWN_TRIGGER_STATUS, INVALID_ORDER_TYPE, STOP_PENDING, STOP_TRIGGERED]
179    pub trigger_status: TriggerStatus,
180    /// Possible values: [UNKNOWN_ORDER_TYPE, MARKET, LIMIT, STOP, STOP_LIMIT]
181    pub order_type: OrderType,
182    /// Possible values: REJECT_REASON_UNSPECIFIED
183    pub reject_reason: RejectReason,
184    // True if the order is fully filled, false otherwise.
185    pub settled: bool,
186    /// Possible values: [SPOT, FUTURE]
187    pub product_type: ProductType,
188    /// Message stating why the order was rejected.
189    pub reject_message: Option<String>,
190    /// Message stating why the order was canceled.
191    pub cancel_message: Option<String>,
192    /// Possible values: [RETAIL_SIMPLE, RETAIL_ADVANCED]
193    pub order_placement_source: OrderPlacementSource,
194    // The remaining hold amount (holdAmount - holdAmountReleased). [value is 0 if holdReleased is true]
195    pub outstanding_hold_amount: String,
196    /// True if order is of liquidation type.
197    pub is_liquidation: bool,
198}
199
200#[doc(hidden)]
201/// Structure representing Coinbase's wrapped response for a single order
202#[derive(Deserialize, Debug)]
203pub struct OrderResponse {
204    pub order: Order,
205}
206
207pub type OrderSide = crate::products::Side;
208pub type TradeType = crate::products::TradeType;
209
210#[doc(hidden)]
211/// Structure representing Coinbase's wrapper response for multiple orders
212#[derive(Deserialize, Debug)]
213pub struct OrdersResponse {
214    pub orders: Vec<Order>,
215    pub sequence: String,
216    pub has_next: bool,
217    pub cursor: String,
218}
219
220/// Structure representing CB's response to a fill request
221#[derive(Deserialize, Debug)]
222pub struct Fill {
223    /// Unique identifier for the fill.
224    pub entry_id: String,
225    /// ID of the fill -- unique for all `FILL` trade_types but not unique for adjusted fills.
226    pub trade_id: String,
227    /// ID of the order the fill belongs to.
228    pub order_id: String,
229    /// Time at which this fill was completed.
230    pub trade_time: DateTime,
231    /// String denoting what type of fill this is. Regular fills have the value `FILL`. Adjusted fills have possible values `REVERSAL`, `CORRECTION`, `SYNTHETIC`.
232    pub trade_type: TradeType,
233    /// Price the fill was posted at.
234    pub price: String,
235    /// Amount of order that was transacted at this fill.
236    pub size: String,
237    /// Fee amount for fill.
238    pub commission: String,
239    /// The product this order was created for.
240    pub product_id: String,
241    /// Time at which this fill was posted.
242    pub sequence_timestamp: DateTime,
243    /// Possible values: [UNKNOWN_LIQUIDITY_INDICATOR, MAKER, TAKER]
244    pub liquidity_indicator: LiquidityIndicator,
245    /// Whether the order was placed with quote currency.
246    pub size_in_quote: bool,
247    /// User that placed the order the fill belongs to.
248    pub user_id: String,
249    /// Possible values: [UNKNOWN_ORDER_SIDE, BUY, SELL]
250    pub side: OrderSide,
251}
252
253/// Enum representing the possible values for the liquidity indicator
254#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, PartialEq, Eq)]
255#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
256pub enum LiquidityIndicator {
257    UnknownLiquidityIndicator,
258    Maker,
259    Taker,
260}
261
262#[doc(hidden)]
263#[derive(Deserialize, Debug)]
264pub struct FillsResponse {
265    pub fills: Vec<Fill>,
266    pub cursor: String,
267    // CB Bug? why no `has_next`?
268}
269
270/// Structure to fill to create a new request to be sent to CB
271#[derive(Serialize, Debug)]
272pub struct OrderToSend {
273    /// Client set unique uuid for this order
274    client_order_id: String,
275    /// The product this order was created for e.g. 'BTC-USD'
276    product_id: String,
277    /// Possible values: [UNKNOWN_ORDER_SIDE, BUY, SELL]
278    side: OrderSide,
279    order_configuration: OrderConfiguration,
280}
281
282/// Enum representing the possible values for failure to create an order
283#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, PartialEq, Eq)]
284#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
285pub enum CreateOrderFailureReason {
286    UnknownFailureReason,
287    UnsupportedOrderConfiguration,
288    InvalidSide,
289    InvalidProductId,
290    InvalidSizePrecision,
291    InvalidPricePrecision,
292    InsufficientFund,
293    InvalidLedgerBalance,
294    OrderEntryDisabled,
295    IneligiblePair,
296    InvalidLimitPricePostOnly,
297    InvalidLimitPrice,
298    InvalidNoLiquidity,
299    InvalidRequest,
300    CommanderRejectedNewOrder,
301    InsufficientFunds,
302}
303
304/// Enum representing the possible values for failure to preview create an order
305#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, PartialEq, Eq)]
306#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
307pub enum PreviewCreateOrderFailureReason {
308    UnknownPreviewFailureReason,
309    PreviewMissingCommissionRate,
310    PreviewInvalidSide,
311    PreviewInvalidOrderConfig,
312    PreviewInvalidProductId,
313    PreviewInvalidSizePrecision,
314    PreviewInvalidPricePrecision,
315    PreviewMissingProductPriceBook,
316    PreviewInvalidLedgerBalance,
317    PreviewInsufficientLedgerBalance,
318    PreviewInvalidLimitPricePostOnly,
319    PreviewInvalidLimitPrice,
320    PreviewInvalidNoLiquidity,
321    PreviewInsufficientFund,
322    PreviewInvalidCommissionConfiguration,
323    PreviewInvalidStopPrice,
324    PreviewInvalidBaseSizeTooLarge,
325    PreviewInvalidBaseSizeTooSmall,
326    PreviewInvalidQuoteSizePrecision,
327    PreviewInvalidQuoteSizeTooLarge,
328    PreviewInvalidPriceTooLarge,
329    PreviewInvalidQuoteSizeTooSmall,
330    PreviewInsufficientFundsForFutures,
331    PreviewBreachedPriceLimit,
332    PreviewBreachedAccountPositionLimit,
333    PreviewBreachedCompanyPositionLimit,
334    PreviewInvalidMarginHealth,
335    PreviewRiskProxyFailure,
336    PreviewUntradableFcmAccountStatus,
337}
338
339#[doc(hidden)]
340#[derive(Deserialize, Debug)]
341pub struct OrderSuccessResponse {
342    pub order_id: String,
343    pub product_id: String,
344    pub side: OrderSide,
345    pub client_order_id: String,
346}
347
348#[doc(hidden)]
349#[derive(Deserialize, Debug)]
350pub struct OrderErrorResponse {
351    pub error: CreateOrderFailureReason,
352    pub message: String,
353    pub error_details: String,
354    pub preview_failure_reason: PreviewCreateOrderFailureReason,
355    pub new_order_failure_reason: CreateOrderFailureReason,
356}
357
358/// Structure representing CB's response to a create order request
359#[derive(Deserialize, Debug)]
360pub struct CreateOrderResponse {
361    /// Whether the order was created.
362    pub success: bool,
363    pub failure_reason: CreateOrderFailureReason,
364    /// The ID of the order created
365    pub order_id: String,
366    pub success_response: Option<OrderSuccessResponse>,
367    pub error_response: Option<OrderErrorResponse>,
368    pub order_configuration: OrderConfiguration,
369}
370
371/// Enum representating the possible values for CB failing to cancel an order
372#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, PartialEq, Eq)]
373#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
374pub enum CancelOrderFailureReason {
375    UnknownCancelFailureReason,
376    InvalidCancelRequest,
377    UnknownCancelOrder,
378    CommanderRejectedCancelOrder,
379    DuplicateCancelRequest,
380}
381
382/// Structure representing CB's response to a cancel order request
383#[derive(Deserialize, Debug)]
384pub struct CancelOrderResponse {
385    /// Whether the order was cancelled
386    pub success: bool,
387    pub failure_reason: Option<CancelOrderFailureReason>,
388    /// The ID of the order cancelled
389    pub order_id: String,
390}
391
392#[doc(hidden)]
393#[derive(Deserialize, Debug)]
394pub struct CancelOrdersResponse {
395    pub results: Vec<CancelOrderResponse>,
396}
397
398/// Create a MARKET order
399///
400/// `side` (Buy or Sell) `product_id` for an amount of `order_size`
401///
402/// `order_size is quote size for Buy and base_size for SELL
403///
404/// returns an [`OrderToSend`] struct filled with relevant values. Does not make the actual order.
405pub fn create_market_order(
406    product_id: &str,
407    side: OrderSide,
408    order_size: f64,
409) -> Result<OrderToSend> {
410    let client_order_id = uuid::Uuid::new_v4().to_string();
411
412    let mut base_size = None;
413    let mut quote_size = None;
414
415    let order_size = f64_to_valid_bigdecimal(order_size)?;
416
417    match side {
418        OrderSide::Buy => quote_size = Some(order_size),
419        OrderSide::Sell => base_size = Some(order_size),
420        _ => {
421            return Err(anyhow!(
422                "Orders' side should be Buy or Sell . Got: {:?}",
423                side
424            ));
425        }
426    }
427
428    let order = OrderToSend {
429        client_order_id,
430        product_id: product_id.to_string(),
431        side,
432        order_configuration: OrderConfiguration {
433            market_market_ioc: Some(Market {
434                base_size,
435                quote_size,
436            }),
437            limit_limit_gtc: None,
438            limit_limit_gtd: None,
439            stop_limit_stop_limit_gtc: None,
440            stop_limit_stop_limit_gtd: None,
441        },
442    };
443    Ok(order)
444}
445
446/// Create a LIMIT Good-Til-Canceled order
447///
448/// `side` (Buy or Sell) `product_id` for an amount of `base_size` at a price of `limit_price`
449///
450/// returns an [`OrderToSend`] struct filled with relevant values. Does not make the actual order.
451pub fn create_limit_order_good_til_canceled(
452    product_id: &str,
453    side: OrderSide,
454    base_size: f64,
455    limit_price: f64,
456    post_only: bool,
457) -> Result<OrderToSend> {
458    let client_order_id = uuid::Uuid::new_v4().to_string();
459    anyhow::ensure!(
460        side == OrderSide::Buy || side == OrderSide::Sell,
461        "Orders' side should be Buy or Sell . Got: {:?}",
462        side
463    );
464    let base_size = f64_to_valid_bigdecimal(base_size)?;
465    let limit_price = f64_to_valid_bigdecimal(limit_price)?;
466
467    let order = OrderToSend {
468        client_order_id,
469        product_id: product_id.to_string(),
470        side,
471        order_configuration: OrderConfiguration {
472            market_market_ioc: None,
473            limit_limit_gtc: Some(Limit {
474                base_size,
475                limit_price,
476                end_time: None,
477                post_only: Some(post_only),
478            }),
479            limit_limit_gtd: None,
480            stop_limit_stop_limit_gtc: None,
481            stop_limit_stop_limit_gtd: None,
482        },
483    };
484    Ok(order)
485}
486
487/// Create a LIMIT Good-Til-Date order
488///
489/// `side` (Buy or Sell) `product_id` for an amount of `base_size` at a price of `limit_price`
490///
491/// returns an [`OrderToSend`] struct filled with relevant values. Does not make the actual order.
492pub fn create_limit_order_good_til_date(
493    product_id: &str,
494    side: OrderSide,
495    base_size: f64,
496    limit_price: f64,
497    end_time: DateTime,
498    post_only: bool,
499) -> Result<OrderToSend> {
500    let client_order_id = uuid::Uuid::new_v4().to_string();
501    anyhow::ensure!(
502        side == OrderSide::Buy || side == OrderSide::Sell,
503        "Orders' side should be Buy or Sell . Got: {:?}",
504        side
505    );
506    let size = f64_to_valid_bigdecimal(base_size)?;
507    let price = f64_to_valid_bigdecimal(limit_price)?;
508
509    let order = OrderToSend {
510        client_order_id,
511        product_id: product_id.to_string(),
512        side,
513        order_configuration: OrderConfiguration {
514            market_market_ioc: None,
515            limit_limit_gtc: None,
516            limit_limit_gtd: Some(Limit {
517                base_size: size,
518                limit_price: price,
519                end_time: Some(end_time),
520                post_only: Some(post_only),
521            }),
522            stop_limit_stop_limit_gtc: None,
523            stop_limit_stop_limit_gtd: None,
524        },
525    };
526    Ok(order)
527}
528
529/// Create a STOP-LIMIT Good-Til-Canceled order
530///
531/// `side` (Buy or Sell) `product_id` for an amount of `base_size` at a price of `limit_price`
532///
533/// returns an [`OrderToSend`] struct filled with relevant values. Does not make the actual order.
534pub fn create_stop_limit_order_good_til_canceled(
535    product_id: &str,
536    side: OrderSide,
537    base_size: f64,
538    limit_price: f64,
539    stop_price: f64,
540    stop_direction: StopDirection,
541) -> Result<OrderToSend> {
542    let client_order_id = uuid::Uuid::new_v4().to_string();
543    anyhow::ensure!(
544        side == OrderSide::Buy || side == OrderSide::Sell,
545        "Orders' side should be Buy or Sell . Got: {:?}",
546        side
547    );
548    let base_size = f64_to_valid_bigdecimal(base_size)?;
549    let limit_price = f64_to_valid_bigdecimal(limit_price)?;
550    let stop_price = f64_to_valid_bigdecimal(stop_price)?;
551
552    let order = OrderToSend {
553        client_order_id,
554        product_id: product_id.to_string(),
555        side,
556        order_configuration: OrderConfiguration {
557            market_market_ioc: None,
558            limit_limit_gtc: None,
559            limit_limit_gtd: None,
560            stop_limit_stop_limit_gtc: Some(StopLimit {
561                base_size,
562                limit_price,
563                stop_price,
564                end_time: None,
565                stop_direction,
566            }),
567            stop_limit_stop_limit_gtd: None,
568        },
569    };
570    Ok(order)
571}
572
573/// Create a STOP-LIMIT Good-Til-Date order
574///
575/// `side` (Buy or Sell) `product_id` for an amount of `base_size` at a price of `limit_price`
576///
577/// returns an [`OrderToSend`] struct filled with relevant values. Does not make the actual order.
578pub fn create_stop_limit_order_good_til_date(
579    product_id: &str,
580    side: OrderSide,
581    base_size: f64,
582    limit_price: f64,
583    stop_price: f64,
584    end_time: DateTime,
585    stop_direction: StopDirection,
586) -> Result<OrderToSend> {
587    let client_order_id = uuid::Uuid::new_v4().to_string();
588    anyhow::ensure!(
589        side == OrderSide::Buy || side == OrderSide::Sell,
590        "Orders' side should be Buy or Sell . Got: {:?}",
591        side
592    );
593    let base_size = f64_to_valid_bigdecimal(base_size)?;
594    let limit_price = f64_to_valid_bigdecimal(limit_price)?;
595    let stop_price = f64_to_valid_bigdecimal(stop_price)?;
596
597    let order = OrderToSend {
598        client_order_id,
599        product_id: product_id.to_string(),
600        side,
601        order_configuration: OrderConfiguration {
602            market_market_ioc: None,
603            limit_limit_gtc: None,
604            limit_limit_gtd: None,
605            stop_limit_stop_limit_gtc: None,
606            stop_limit_stop_limit_gtd: Some(StopLimit {
607                base_size,
608                limit_price,
609                stop_price,
610                end_time: Some(end_time),
611                stop_direction,
612            }),
613        },
614    };
615    Ok(order)
616}
617
618/// Converting a f64 to a Result<BigDecimal> instead of an Option<BigDecimal>
619///
620/// Useful for instance when creating an order and failure is preferred to a non-relevant value.
621fn f64_to_valid_bigdecimal(x: f64) -> Result<BigDecimal> {
622    FromPrimitive::from_f64(x).ok_or(anyhow!("Could not convert {} to BigDecimal", x))
623}
624
625//=========== TESTS ===========================================================
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630    #[test]
631    fn test_order_deserialize() {
632        let input = r##"{
633            "order": {
634                "order_id": "0000-000000-000000",
635                "product_id": "BTC-USD",
636                "user_id": "2222-000000-000000",
637                "order_configuration": {
638                    "market_market_ioc": {
639                        "quote_size": "10.00",
640                        "base_size": "0.001"
641                    },
642                    "limit_limit_gtc": {
643                        "base_size": "0.001",
644                        "limit_price": "10000.00",
645                        "post_only": false
646                    },
647                    "limit_limit_gtd": {
648                        "base_size": "0.001",
649                        "limit_price": "10000.00",
650                        "end_time": "2021-05-31T09:59:59Z",
651                        "post_only": false
652                    },
653                    "stop_limit_stop_limit_gtc": {
654                        "base_size": "0.001",
655                        "limit_price": "10000.00",
656                        "stop_price": "20000.00",
657                        "stop_direction": "UNKNOWN_STOP_DIRECTION"
658                    },
659                    "stop_limit_stop_limit_gtd": {
660                        "base_size": "0.001",
661                        "limit_price": "10000.00",
662                        "stop_price": "20000.00",
663                        "end_time": "2021-05-31T09:59:59Z",
664                        "stop_direction": "UNKNOWN_STOP_DIRECTION"
665                    }
666                },
667                "side": "UNKNOWN_ORDER_SIDE",
668                "client_order_id": "11111-000000-000000",
669                "status": "OPEN",
670                "time_in_force": "UNKNOWN_TIME_IN_FORCE",
671                "created_time": "2021-05-31T09:59:59Z",
672                "completion_percentage": "50",
673                "filled_size": "0.001",
674                "average_filled_price": "50",
675                "fee": "string",
676                "number_of_fills": "2",
677                "filled_value": "10000",
678                "pending_cancel": true,
679                "size_in_quote": false,
680                "total_fees": "5.00",
681                "size_inclusive_of_fees": false,
682                "total_value_after_fees": "string",
683                "trigger_status": "UNKNOWN_TRIGGER_STATUS",
684                "order_type": "UNKNOWN_ORDER_TYPE",
685                "reject_reason": "REJECT_REASON_UNSPECIFIED",
686                "settled": false,
687                "product_type": "SPOT",
688                "reject_message": "string",
689                "cancel_message": "string",
690                "order_placement_source": "RETAIL_ADVANCED",
691                "outstanding_hold_amount": "string",
692                "is_liquidation": false 
693            }
694        }"##;
695        let result: OrderResponse = serde_json::from_slice(input.as_bytes()).unwrap();
696        let order = result.order;
697        assert_eq!(order.product_id, "BTC-USD".to_string());
698        assert!(!order
699            .order_configuration
700            .limit_limit_gtc
701            .unwrap()
702            .post_only
703            .unwrap());
704    }
705
706    #[test]
707    fn test_stop_direction_deserialize() {
708        let input = r##""UNKNOWN_STOP_DIRECTION""##;
709        let result: StopDirection = serde_json::from_slice(input.as_bytes()).unwrap();
710        assert_eq!(result, StopDirection::UnknownStopDirection);
711
712        let input = r##""STOP_DIRECTION_STOP_UP""##;
713        let result: StopDirection = serde_json::from_slice(input.as_bytes()).unwrap();
714        assert_eq!(result, StopDirection::StopDirectionStopUp);
715
716        let input = r##""STOP_DIRECTION_STOP_DOWN""##;
717        let result: StopDirection = serde_json::from_slice(input.as_bytes()).unwrap();
718        assert_eq!(result, StopDirection::StopDirectionStopDown);
719    }
720
721    #[test]
722    fn test_stop_direction_serialize() {
723        let expected = r##""STOP_DIRECTION_STOP_DOWN""##;
724        assert_eq!(
725            expected,
726            serde_json::to_string(&StopDirection::StopDirectionStopDown).unwrap()
727        );
728    }
729
730    #[test]
731    fn test_status_deserialize() {
732        let input = r##""OPEN""##;
733        let result: Status = serde_json::from_slice(input.as_bytes()).unwrap();
734        assert_eq!(result, Status::Open);
735
736        let input = r##""FILLED""##;
737        let result: Status = serde_json::from_slice(input.as_bytes()).unwrap();
738        assert_eq!(result, Status::Filled);
739
740        let input = r##""CANCELLED""##;
741        let result: Status = serde_json::from_slice(input.as_bytes()).unwrap();
742        assert_eq!(result, Status::Cancelled);
743
744        let input = r##""EXPIRED""##;
745        let result: Status = serde_json::from_slice(input.as_bytes()).unwrap();
746        assert_eq!(result, Status::Expired);
747
748        let input = r##""FAILED""##;
749        let result: Status = serde_json::from_slice(input.as_bytes()).unwrap();
750        assert_eq!(result, Status::Failed);
751
752        let input = r##""UNKNOWN_ORDER_STATUS""##;
753        let result: Status = serde_json::from_slice(input.as_bytes()).unwrap();
754        assert_eq!(result, Status::UnknownOrderStatus);
755    }
756
757    #[test]
758    fn test_status_serialize() {
759        let expected = r##""FILLED""##;
760        assert_eq!(expected, serde_json::to_string(&Status::Filled).unwrap());
761    }
762
763    #[test]
764    fn test_time_in_force_deserialize() {
765        let input = r##""UNKNOWN_TIME_IN_FORCE""##;
766        let result: TimeInForce = serde_json::from_slice(input.as_bytes()).unwrap();
767        assert_eq!(result, TimeInForce::UnknownTimeInForce);
768
769        let input = r##""GOOD_UNTIL_DATE_TIME""##;
770        let result: TimeInForce = serde_json::from_slice(input.as_bytes()).unwrap();
771        assert_eq!(result, TimeInForce::GoodUntilDateTime);
772
773        let input = r##""GOOD_UNTIL_CANCELLED""##;
774        let result: TimeInForce = serde_json::from_slice(input.as_bytes()).unwrap();
775        assert_eq!(result, TimeInForce::GoodUntilCancelled);
776
777        let input = r##""IMMEDIATE_OR_CANCEL""##;
778        let result: TimeInForce = serde_json::from_slice(input.as_bytes()).unwrap();
779        assert_eq!(result, TimeInForce::ImmediateOrCancel);
780
781        let input = r##""FILL_OR_KILL""##;
782        let result: TimeInForce = serde_json::from_slice(input.as_bytes()).unwrap();
783        assert_eq!(result, TimeInForce::FillOrKill);
784    }
785
786    #[test]
787    fn test_time_in_force_serialize() {
788        let expected = r##""GOOD_UNTIL_CANCELLED""##;
789        assert_eq!(
790            expected,
791            serde_json::to_string(&TimeInForce::GoodUntilCancelled).unwrap()
792        );
793    }
794
795    #[test]
796    fn test_trigger_status_deserialize() {
797        let input = r##""UNKNOWN_TRIGGER_STATUS""##;
798        let result: TriggerStatus = serde_json::from_slice(input.as_bytes()).unwrap();
799        assert_eq!(result, TriggerStatus::UnknownTriggerStatus);
800
801        let input = r##""INVALID_ORDER_TYPE""##;
802        let result: TriggerStatus = serde_json::from_slice(input.as_bytes()).unwrap();
803        assert_eq!(result, TriggerStatus::InvalidOrderType);
804
805        let input = r##""STOP_PENDING""##;
806        let result: TriggerStatus = serde_json::from_slice(input.as_bytes()).unwrap();
807        assert_eq!(result, TriggerStatus::StopPending);
808
809        let input = r##""STOP_TRIGGERED""##;
810        let result: TriggerStatus = serde_json::from_slice(input.as_bytes()).unwrap();
811        assert_eq!(result, TriggerStatus::StopTriggered);
812    }
813
814    #[test]
815    fn test_trigger_status_serialize() {
816        let expected = r##""INVALID_ORDER_TYPE""##;
817        assert_eq!(
818            expected,
819            serde_json::to_string(&TriggerStatus::InvalidOrderType).unwrap()
820        );
821    }
822
823    #[test]
824    fn test_order_type_deserialize() {
825        let input = r##""UNKNOWN_ORDER_TYPE""##;
826        let result: OrderType = serde_json::from_slice(input.as_bytes()).unwrap();
827        assert_eq!(result, OrderType::UnknownOrderType);
828
829        let input = r##""MARKET""##;
830        let result: OrderType = serde_json::from_slice(input.as_bytes()).unwrap();
831        assert_eq!(result, OrderType::Market);
832
833        let input = r##""LIMIT""##;
834        let result: OrderType = serde_json::from_slice(input.as_bytes()).unwrap();
835        assert_eq!(result, OrderType::Limit);
836
837        let input = r##""STOP""##;
838        let result: OrderType = serde_json::from_slice(input.as_bytes()).unwrap();
839        assert_eq!(result, OrderType::Stop);
840
841        let input = r##""STOP_LIMIT""##;
842        let result: OrderType = serde_json::from_slice(input.as_bytes()).unwrap();
843        assert_eq!(result, OrderType::StopLimitOrderType);
844    }
845
846    #[test]
847    fn test_order_type_serialize() {
848        let expected = r##""MARKET""##;
849        assert_eq!(expected, serde_json::to_string(&OrderType::Market).unwrap());
850    }
851
852    #[test]
853    fn test_reject_reason_deserialize() {
854        let input = r##""REJECT_REASON_UNSPECIFIED""##;
855        let result: RejectReason = serde_json::from_slice(input.as_bytes()).unwrap();
856        assert_eq!(result, RejectReason::RejectReasonUnspecified);
857    }
858
859    #[test]
860    fn test_reject_reason_serialize() {
861        let expected = r##""REJECT_REASON_UNSPECIFIED""##;
862        assert_eq!(
863            expected,
864            serde_json::to_string(&RejectReason::RejectReasonUnspecified).unwrap()
865        );
866    }
867
868    #[test]
869    fn test_order_placement_source_deserialize() {
870        let input = r##""RETAIL_SIMPLE""##;
871        let result: OrderPlacementSource = serde_json::from_slice(input.as_bytes()).unwrap();
872        assert_eq!(result, OrderPlacementSource::RetailSimple);
873
874        let input = r##""RETAIL_ADVANCED""##;
875        let result: OrderPlacementSource = serde_json::from_slice(input.as_bytes()).unwrap();
876        assert_eq!(result, OrderPlacementSource::RetailAdvanced);
877    }
878
879    #[test]
880    fn test_order_placement_source_serialize() {
881        let expected = r##""RETAIL_SIMPLE""##;
882        assert_eq!(
883            expected,
884            serde_json::to_string(&OrderPlacementSource::RetailSimple).unwrap()
885        );
886    }
887
888    #[test]
889    fn test_create_market_order_serialize() {
890        let product_id = "BTC-USD";
891        let side = OrderSide::Buy;
892        let order_size = 0.00001;
893        let order = create_market_order(product_id, side, order_size).unwrap();
894        let json = serde_json::to_string(&order);
895        assert!(json.is_ok());
896    }
897
898    #[test]
899    fn test_create_limit_order_good_til_canceled_serialize() {
900        let product_id = "BTC-USD";
901        let side = OrderSide::Buy;
902        let base_size = 0.00001;
903        let limit_price = 5000.0;
904        let post_only = false;
905        let order = create_limit_order_good_til_canceled(
906            product_id,
907            side,
908            base_size,
909            limit_price,
910            post_only,
911        )
912        .unwrap();
913        let json = serde_json::to_string(&order);
914        assert!(json.is_ok());
915    }
916
917    #[test]
918    fn test_create_limit_order_good_til_date_serialize() {
919        let product_id = "BTC-USD";
920        let side = OrderSide::Buy;
921        let base_size = 0.00001;
922        let limit_price = 5000.0;
923        let end_time = chrono::offset::Utc::now(); // good enough for serde test
924        let post_only = false;
925        let order = create_limit_order_good_til_date(
926            product_id,
927            side,
928            base_size,
929            limit_price,
930            end_time,
931            post_only,
932        )
933        .unwrap();
934        let json = serde_json::to_string(&order);
935        assert!(json.is_ok());
936    }
937
938    #[test]
939    fn test_create_stop_limit_order_good_til_canceled_serialize() {
940        let product_id = "BTC-USD";
941        let side = OrderSide::Buy;
942        let base_size = 0.00001;
943        let limit_price = 5000.0;
944        let stop_price = 4000.0;
945        let stop_direction = StopDirection::StopDirectionStopUp;
946        let order = create_stop_limit_order_good_til_canceled(
947            product_id,
948            side,
949            base_size,
950            limit_price,
951            stop_price,
952            stop_direction,
953        )
954        .unwrap();
955        let json = serde_json::to_string(&order);
956        assert!(json.is_ok());
957    }
958
959    #[test]
960    fn test_create_stop_limit_order_good_til_date_serialize() {
961        let product_id = "BTC-USD";
962        let side = OrderSide::Buy;
963        let base_size = 0.00001;
964        let limit_price = 5000.0;
965        let stop_price = 4000.0;
966        let end_time = chrono::offset::Utc::now(); // good enough for serde test
967        let stop_direction = StopDirection::StopDirectionStopUp;
968        let order = create_stop_limit_order_good_til_date(
969            product_id,
970            side,
971            base_size,
972            limit_price,
973            stop_price,
974            end_time,
975            stop_direction,
976        )
977        .unwrap();
978        let json = serde_json::to_string(&order);
979        assert!(json.is_ok());
980    }
981
982    #[test]
983    fn test_order_response_serde() {
984        let input = r##"{
985          "success": true,
986          "failure_reason": "INVALID_SIDE",
987          "order_id": "string",
988          "success_response": {
989            "order_id": "11111-00000-000000",
990            "product_id": "BTC-USD",
991            "side": "UNKNOWN_ORDER_SIDE",
992            "client_order_id": "0000-00000-000000"
993          },
994          "error_response": {
995            "error": "UNKNOWN_FAILURE_REASON",
996            "message": "The order configuration was invalid",
997            "error_details": "Market orders cannot be placed with empty order sizes",
998            "preview_failure_reason": "UNKNOWN_PREVIEW_FAILURE_REASON",
999            "new_order_failure_reason": "UNKNOWN_FAILURE_REASON"
1000          },
1001          "order_configuration": {
1002            "market_market_ioc": {
1003              "quote_size": "10.00",
1004              "base_size": "0.001"
1005            },
1006            "limit_limit_gtc": {
1007              "base_size": "0.001",
1008              "limit_price": "10000.00",
1009              "post_only": false
1010            },
1011            "limit_limit_gtd": {
1012              "base_size": "0.001",
1013              "limit_price": "10000.00",
1014              "end_time": "2021-05-31T09:59:59Z",
1015              "post_only": false
1016            },
1017            "stop_limit_stop_limit_gtc": {
1018              "base_size": "0.001",
1019              "limit_price": "10000.00",
1020              "stop_price": "20000.00",
1021              "stop_direction": "UNKNOWN_STOP_DIRECTION"
1022            },
1023            "stop_limit_stop_limit_gtd": {
1024              "base_size": 0.001,
1025              "limit_price": "10000.00",
1026              "stop_price": "20000.00",
1027              "end_time": "2021-05-31T09:59:59Z",
1028              "stop_direction": "UNKNOWN_STOP_DIRECTION"
1029            }
1030          }
1031        }"##;
1032        let result: CreateOrderResponse = serde_json::from_slice(input.as_bytes()).unwrap();
1033        assert!(result.success);
1034        assert!(!result
1035            .order_configuration
1036            .limit_limit_gtc
1037            .unwrap()
1038            .post_only
1039            .unwrap());
1040    }
1041
1042    #[test]
1043    fn test_order_response_success_serde() {
1044        let input = r##"{
1045            "success": true,
1046            "failure_reason": "UNKNOWN_FAILURE_REASON",
1047            "order_id": "11111111-4c82-40e2-980a-222222222222",
1048            "success_response": {
1049                "order_id": "11111111-4c82-40e2-980a-222222222222",
1050                "product_id": "BTC-USDT",
1051                "side": "BUY",
1052                "client_order_id": "33333333-74f5-4508-8b9a-222222222222"
1053            },
1054            "order_configuration": {
1055                "limit_limit_gtd": {
1056                    "base_size": "1.000000000000000",
1057                    "limit_price": "0.01000000000000000",
1058                    "end_time": "2023-08-17T04:59:45.166512756Z",
1059                    "post_only": false
1060                }
1061            }
1062        }"##;
1063        let result: CreateOrderResponse = serde_json::from_slice(input.as_bytes()).unwrap();
1064        assert!(result.success);
1065        assert!(!result
1066            .order_configuration
1067            .limit_limit_gtd
1068            .unwrap()
1069            .post_only
1070            .unwrap());
1071    }
1072
1073    #[test]
1074    fn test_cancel_orders_response_serde() {
1075        let input = r##"{
1076            "results": [
1077                {
1078                    "success":false, 
1079                    "failure_reason": "UNKNOWN_CANCEL_ORDER", 
1080                    "order_id": "foo"
1081                }
1082            ]
1083        }"##;
1084        let results: CancelOrdersResponse = serde_json::from_slice(input.as_bytes()).unwrap();
1085        let result = &results.results[0];
1086        assert!(!result.success);
1087    }
1088}