1use std::collections::BTreeMap;
7
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10use serde_json::value::RawValue;
11use serde_repr::{Deserialize_repr, Serialize_repr};
12use thiserror::Error;
13
14use crate::{
15 AccountId, ContractId, OrderId, PositionId, ProviderDate, SymbolId, Timestamp, TradeId,
16};
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20#[non_exhaustive]
21pub enum Side {
22 Bid,
24 Ask,
26 Unknown(i32),
28}
29
30impl Side {
31 #[must_use]
33 pub const fn code(self) -> i32 {
34 match self {
35 Self::Bid => 0,
36 Self::Ask => 1,
37 Self::Unknown(code) => code,
38 }
39 }
40}
41
42impl Serialize for Side {
43 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
44 where
45 S: serde::Serializer,
46 {
47 serializer.serialize_i32(self.code())
48 }
49}
50
51impl<'de> Deserialize<'de> for Side {
52 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
53 where
54 D: serde::Deserializer<'de>,
55 {
56 Ok(match i32::deserialize(deserializer)? {
57 0 => Self::Bid,
58 1 => Self::Ask,
59 code => Self::Unknown(code),
60 })
61 }
62}
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66#[non_exhaustive]
67pub enum OrderType {
68 Limit,
70 Market,
72 StopLimit,
78 Stop,
80 TrailingStop,
82 JoinBid,
84 JoinAsk,
86 Unknown(i32),
88}
89
90impl OrderType {
91 #[must_use]
93 pub const fn code(self) -> i32 {
94 match self {
95 Self::Limit => 1,
96 Self::Market => 2,
97 Self::StopLimit => 3,
98 Self::Stop => 4,
99 Self::TrailingStop => 5,
100 Self::JoinBid => 6,
101 Self::JoinAsk => 7,
102 Self::Unknown(code) => code,
103 }
104 }
105}
106
107impl Serialize for OrderType {
108 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
109 where
110 S: serde::Serializer,
111 {
112 serializer.serialize_i32(self.code())
113 }
114}
115
116impl<'de> Deserialize<'de> for OrderType {
117 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
118 where
119 D: serde::Deserializer<'de>,
120 {
121 Ok(match i32::deserialize(deserializer)? {
122 1 => Self::Limit,
123 2 => Self::Market,
124 3 => Self::StopLimit,
125 4 => Self::Stop,
126 5 => Self::TrailingStop,
127 6 => Self::JoinBid,
128 7 => Self::JoinAsk,
129 code => Self::Unknown(code),
130 })
131 }
132}
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136#[non_exhaustive]
137pub enum OrderStatus {
138 None,
140 Open,
142 Filled,
144 Cancelled,
146 Expired,
148 Rejected,
150 Pending,
152 PendingCancellation,
154 Suspended,
156 Unknown(i32),
158}
159
160#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize_repr)]
165#[non_exhaustive]
166#[repr(i32)]
167pub enum OrderSortBy {
168 CreatedAt = 0,
170 Id = 1,
172}
173
174#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize_repr)]
176#[non_exhaustive]
177#[repr(i32)]
178pub enum OrderSortDirection {
179 Ascending = 0,
181 Descending = 1,
183}
184
185impl OrderStatus {
186 #[must_use]
188 pub const fn code(self) -> i32 {
189 match self {
190 Self::None => 0,
191 Self::Open => 1,
192 Self::Filled => 2,
193 Self::Cancelled => 3,
194 Self::Expired => 4,
195 Self::Rejected => 5,
196 Self::Pending => 6,
197 Self::PendingCancellation => 7,
198 Self::Suspended => 8,
199 Self::Unknown(code) => code,
200 }
201 }
202}
203
204impl Serialize for OrderStatus {
205 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
206 where
207 S: serde::Serializer,
208 {
209 serializer.serialize_i32(self.code())
210 }
211}
212
213impl<'de> Deserialize<'de> for OrderStatus {
214 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
215 where
216 D: serde::Deserializer<'de>,
217 {
218 Ok(match i32::deserialize(deserializer)? {
219 0 => Self::None,
220 1 => Self::Open,
221 2 => Self::Filled,
222 3 => Self::Cancelled,
223 4 => Self::Expired,
224 5 => Self::Rejected,
225 6 => Self::Pending,
226 7 => Self::PendingCancellation,
227 8 => Self::Suspended,
228 code => Self::Unknown(code),
229 })
230 }
231}
232
233#[derive(Clone, Copy, Debug, Eq, PartialEq)]
235#[non_exhaustive]
236pub enum TradeLogType {
237 Buy,
239 Sell,
241 Unknown(i32),
243}
244
245impl TradeLogType {
246 #[must_use]
248 pub const fn code(self) -> i32 {
249 match self {
250 Self::Buy => 0,
251 Self::Sell => 1,
252 Self::Unknown(code) => code,
253 }
254 }
255}
256
257impl Serialize for TradeLogType {
258 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
259 where
260 S: serde::Serializer,
261 {
262 serializer.serialize_i32(self.code())
263 }
264}
265
266impl<'de> Deserialize<'de> for TradeLogType {
267 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
268 where
269 D: serde::Deserializer<'de>,
270 {
271 Ok(match i32::deserialize(deserializer)? {
272 0 => Self::Buy,
273 1 => Self::Sell,
274 code => Self::Unknown(code),
275 })
276 }
277}
278
279#[derive(Clone, Copy, Debug, Eq, PartialEq)]
281#[non_exhaustive]
282pub enum PositionType {
283 Undefined,
285 Long,
287 Short,
289 Unknown(i32),
291}
292
293impl PositionType {
294 #[must_use]
296 pub const fn code(self) -> i32 {
297 match self {
298 Self::Undefined => 0,
299 Self::Long => 1,
300 Self::Short => 2,
301 Self::Unknown(code) => code,
302 }
303 }
304}
305
306impl Serialize for PositionType {
307 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
308 where
309 S: serde::Serializer,
310 {
311 serializer.serialize_i32(self.code())
312 }
313}
314
315impl<'de> Deserialize<'de> for PositionType {
316 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
317 where
318 D: serde::Deserializer<'de>,
319 {
320 Ok(match i32::deserialize(deserializer)? {
321 0 => Self::Undefined,
322 1 => Self::Long,
323 2 => Self::Short,
324 code => Self::Unknown(code),
325 })
326 }
327}
328
329#[derive(Clone, Copy, Debug, Eq, PartialEq)]
331#[non_exhaustive]
332pub enum DepthType {
333 Unknown,
335 Ask,
337 Bid,
339 BestAsk,
341 BestBid,
343 Trade,
345 Reset,
347 Low,
349 High,
351 NewBestBid,
353 NewBestAsk,
355 Fill,
357 UnknownCode(i32),
359}
360
361impl DepthType {
362 #[must_use]
364 pub const fn code(self) -> i32 {
365 match self {
366 Self::Unknown => 0,
367 Self::Ask => 1,
368 Self::Bid => 2,
369 Self::BestAsk => 3,
370 Self::BestBid => 4,
371 Self::Trade => 5,
372 Self::Reset => 6,
373 Self::Low => 7,
374 Self::High => 8,
375 Self::NewBestBid => 9,
376 Self::NewBestAsk => 10,
377 Self::Fill => 11,
378 Self::UnknownCode(code) => code,
379 }
380 }
381}
382
383impl Serialize for DepthType {
384 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
385 where
386 S: serde::Serializer,
387 {
388 serializer.serialize_i32(self.code())
389 }
390}
391
392impl<'de> Deserialize<'de> for DepthType {
393 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
394 where
395 D: serde::Deserializer<'de>,
396 {
397 Ok(match i32::deserialize(deserializer)? {
398 0 => Self::Unknown,
399 1 => Self::Ask,
400 2 => Self::Bid,
401 3 => Self::BestAsk,
402 4 => Self::BestBid,
403 5 => Self::Trade,
404 6 => Self::Reset,
405 7 => Self::Low,
406 8 => Self::High,
407 9 => Self::NewBestBid,
408 10 => Self::NewBestAsk,
409 11 => Self::Fill,
410 code => Self::UnknownCode(code),
411 })
412 }
413}
414
415#[derive(Clone, Copy, Debug, Deserialize_repr, Eq, PartialEq, Serialize_repr)]
420#[non_exhaustive]
421#[repr(i32)]
422pub enum BarUnit {
423 Second = 1,
425 Minute = 2,
427 Hour = 3,
429 Day = 4,
431 Week = 5,
433 Month = 6,
435 Tick = 7,
437}
438
439#[derive(Clone, Debug, Deserialize, PartialEq)]
441#[non_exhaustive]
442#[serde(rename_all = "camelCase")]
443pub struct Account {
444 pub id: AccountId,
446 pub name: String,
448 #[serde(default, with = "crate::decimal_serde::option")]
450 pub balance: Option<Decimal>,
451 pub can_trade: bool,
453 pub is_visible: bool,
455 #[serde(default)]
457 pub simulated: Option<bool>,
458}
459
460#[derive(Clone, Debug, Deserialize, PartialEq)]
462#[non_exhaustive]
463#[serde(rename_all = "camelCase")]
464pub struct Contract {
465 pub id: ContractId,
467 pub name: String,
469 pub description: String,
471 #[serde(with = "crate::decimal_serde")]
473 pub tick_size: Decimal,
474 #[serde(with = "crate::decimal_serde")]
476 pub tick_value: Decimal,
477 pub active_contract: bool,
479 pub symbol_id: SymbolId,
481}
482
483#[derive(Clone, Debug, Serialize)]
485#[serde(rename_all = "camelCase")]
486pub struct SearchContracts {
487 pub live: bool,
489 pub search_text: String,
491}
492
493#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
499#[serde(rename_all = "camelCase")]
500pub struct HistoryRequest {
501 contract_id: ContractId,
503 live: bool,
505 start_time: Timestamp,
507 end_time: Timestamp,
509 unit: BarUnit,
511 unit_number: i32,
513 limit: i32,
515 include_partial_bar: bool,
517}
518
519impl HistoryRequest {
520 pub fn builder(
522 contract_id: ContractId,
523 live: bool,
524 start_time: Timestamp,
525 end_time: Timestamp,
526 unit: BarUnit,
527 ) -> HistoryRequestBuilder {
528 HistoryRequestBuilder {
529 contract_id,
530 live,
531 start_time,
532 end_time,
533 unit,
534 unit_number: 1,
535 limit: 20_000,
536 include_partial_bar: false,
537 }
538 }
539
540 #[must_use]
542 pub const fn contract_id(&self) -> &ContractId {
543 &self.contract_id
544 }
545
546 #[must_use]
548 pub const fn is_live(&self) -> bool {
549 self.live
550 }
551
552 #[must_use]
554 pub const fn start_time(&self) -> Timestamp {
555 self.start_time
556 }
557
558 #[must_use]
560 pub const fn end_time(&self) -> Timestamp {
561 self.end_time
562 }
563
564 #[must_use]
566 pub const fn unit(&self) -> BarUnit {
567 self.unit
568 }
569
570 #[must_use]
572 pub const fn unit_number(&self) -> i32 {
573 self.unit_number
574 }
575
576 #[must_use]
578 pub const fn limit(&self) -> i32 {
579 self.limit
580 }
581
582 #[must_use]
584 pub const fn includes_partial_bar(&self) -> bool {
585 self.include_partial_bar
586 }
587}
588
589#[derive(Clone, Debug)]
591#[must_use = "a HistoryRequestBuilder does nothing until build is called"]
592pub struct HistoryRequestBuilder {
593 contract_id: ContractId,
594 live: bool,
595 start_time: Timestamp,
596 end_time: Timestamp,
597 unit: BarUnit,
598 unit_number: i32,
599 limit: i32,
600 include_partial_bar: bool,
601}
602
603impl HistoryRequestBuilder {
604 pub const fn unit_number(mut self, unit_number: i32) -> Self {
606 self.unit_number = unit_number;
607 self
608 }
609
610 pub const fn limit(mut self, limit: i32) -> Self {
612 self.limit = limit;
613 self
614 }
615
616 pub const fn include_partial_bar(mut self, include: bool) -> Self {
618 self.include_partial_bar = include;
619 self
620 }
621
622 pub fn build(self) -> Result<HistoryRequest, RequestValidationError> {
629 if self.start_time >= self.end_time {
630 return Err(RequestValidationError::HistoryRangeNotIncreasing);
631 }
632 if self.unit_number <= 0 {
633 return Err(RequestValidationError::NonPositiveHistoryUnitNumber);
634 }
635 if !(1..=20_000).contains(&self.limit) {
636 return Err(RequestValidationError::HistoryLimitOutOfRange);
637 }
638 Ok(HistoryRequest {
639 contract_id: self.contract_id,
640 live: self.live,
641 start_time: self.start_time,
642 end_time: self.end_time,
643 unit: self.unit,
644 unit_number: self.unit_number,
645 limit: self.limit,
646 include_partial_bar: self.include_partial_bar,
647 })
648 }
649}
650
651#[derive(Clone, Debug, Deserialize, PartialEq)]
653#[non_exhaustive]
654pub struct Bar {
655 pub t: Timestamp,
657 #[serde(with = "crate::decimal_serde")]
659 pub o: Decimal,
660 #[serde(with = "crate::decimal_serde")]
662 pub h: Decimal,
663 #[serde(with = "crate::decimal_serde")]
665 pub l: Decimal,
666 #[serde(with = "crate::decimal_serde")]
668 pub c: Decimal,
669 pub v: i64,
671 #[serde(default)]
673 pub d: Option<ProviderDate>,
674 #[serde(default)]
676 pub k: Option<i64>,
677}
678
679#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
681#[serde(rename_all = "camelCase")]
682pub struct OrderSearch {
683 account_id: AccountId,
685 start_timestamp: Timestamp,
687 #[serde(skip_serializing_if = "Option::is_none")]
689 end_timestamp: Option<Timestamp>,
690}
691
692impl OrderSearch {
693 pub fn new(
700 account_id: AccountId,
701 start_timestamp: Timestamp,
702 end_timestamp: Option<Timestamp>,
703 ) -> Result<Self, RequestValidationError> {
704 validate_search_range(Some(start_timestamp), end_timestamp)?;
705 Ok(Self {
706 account_id,
707 start_timestamp,
708 end_timestamp,
709 })
710 }
711
712 #[must_use]
714 pub const fn account_id(&self) -> AccountId {
715 self.account_id
716 }
717
718 #[must_use]
720 pub const fn start_timestamp(&self) -> Timestamp {
721 self.start_timestamp
722 }
723
724 #[must_use]
726 pub const fn end_timestamp(&self) -> Option<Timestamp> {
727 self.end_timestamp
728 }
729}
730
731#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
737#[serde(rename_all = "camelCase")]
738pub struct OrderQuery {
739 filter: OrderFilter,
740 #[serde(skip_serializing_if = "Option::is_none")]
741 page_size: Option<i32>,
742 #[serde(skip_serializing_if = "Option::is_none")]
743 page_offset: Option<i32>,
744 #[serde(skip_serializing_if = "Option::is_none")]
745 sort_by: Option<OrderSortBy>,
746 #[serde(skip_serializing_if = "Option::is_none")]
747 sort_direction: Option<OrderSortDirection>,
748 #[serde(skip_serializing_if = "Option::is_none")]
749 include_total_count: Option<bool>,
750}
751
752#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
753#[serde(rename_all = "camelCase")]
754struct OrderFilter {
755 account_id: AccountId,
756 #[serde(skip_serializing_if = "Vec::is_empty")]
757 statuses: Vec<OrderStatus>,
758 #[serde(skip_serializing_if = "Option::is_none")]
759 contract_id: Option<ContractId>,
760 #[serde(skip_serializing_if = "Option::is_none")]
761 created_after: Option<Timestamp>,
762 #[serde(skip_serializing_if = "Option::is_none")]
763 created_before: Option<Timestamp>,
764}
765
766impl OrderQuery {
767 pub fn builder(account_id: AccountId) -> OrderQueryBuilder {
769 OrderQueryBuilder {
770 account_id,
771 statuses: Vec::new(),
772 contract_id: None,
773 created_after: None,
774 created_before: None,
775 page_size: None,
776 page_offset: None,
777 sort_by: None,
778 sort_direction: None,
779 include_total_count: None,
780 }
781 }
782
783 #[must_use]
785 pub const fn account_id(&self) -> AccountId {
786 self.filter.account_id
787 }
788
789 #[must_use]
791 pub fn statuses(&self) -> &[OrderStatus] {
792 &self.filter.statuses
793 }
794
795 #[must_use]
797 pub const fn contract_id(&self) -> Option<&ContractId> {
798 self.filter.contract_id.as_ref()
799 }
800
801 #[must_use]
803 pub const fn created_after(&self) -> Option<Timestamp> {
804 self.filter.created_after
805 }
806
807 #[must_use]
809 pub const fn created_before(&self) -> Option<Timestamp> {
810 self.filter.created_before
811 }
812
813 #[must_use]
815 pub const fn page_size(&self) -> Option<i32> {
816 self.page_size
817 }
818
819 #[must_use]
821 pub const fn page_offset(&self) -> Option<i32> {
822 self.page_offset
823 }
824
825 #[must_use]
827 pub const fn sort_by(&self) -> Option<OrderSortBy> {
828 self.sort_by
829 }
830
831 #[must_use]
833 pub const fn sort_direction(&self) -> Option<OrderSortDirection> {
834 self.sort_direction
835 }
836
837 #[must_use]
839 pub const fn include_total_count(&self) -> Option<bool> {
840 self.include_total_count
841 }
842}
843
844#[derive(Clone, Debug)]
846#[must_use = "an OrderQueryBuilder does nothing until build is called"]
847pub struct OrderQueryBuilder {
848 account_id: AccountId,
849 statuses: Vec<OrderStatus>,
850 contract_id: Option<ContractId>,
851 created_after: Option<Timestamp>,
852 created_before: Option<Timestamp>,
853 page_size: Option<i32>,
854 page_offset: Option<i32>,
855 sort_by: Option<OrderSortBy>,
856 sort_direction: Option<OrderSortDirection>,
857 include_total_count: Option<bool>,
858}
859
860impl OrderQueryBuilder {
861 pub fn statuses(mut self, statuses: impl IntoIterator<Item = OrderStatus>) -> Self {
863 self.statuses = statuses.into_iter().collect();
864 self
865 }
866
867 pub fn contract_id(mut self, contract_id: ContractId) -> Self {
869 self.contract_id = Some(contract_id);
870 self
871 }
872
873 pub const fn created_after(mut self, created_after: Timestamp) -> Self {
875 self.created_after = Some(created_after);
876 self
877 }
878
879 pub const fn created_before(mut self, created_before: Timestamp) -> Self {
881 self.created_before = Some(created_before);
882 self
883 }
884
885 pub const fn page_size(mut self, page_size: i32) -> Self {
887 self.page_size = Some(page_size);
888 self
889 }
890
891 pub const fn page_offset(mut self, page_offset: i32) -> Self {
893 self.page_offset = Some(page_offset);
894 self
895 }
896
897 pub const fn sort_by(mut self, sort_by: OrderSortBy) -> Self {
899 self.sort_by = Some(sort_by);
900 self
901 }
902
903 pub const fn sort_direction(mut self, sort_direction: OrderSortDirection) -> Self {
905 self.sort_direction = Some(sort_direction);
906 self
907 }
908
909 pub const fn include_total_count(mut self, include: bool) -> Self {
911 self.include_total_count = Some(include);
912 self
913 }
914
915 pub fn build(self) -> Result<OrderQuery, RequestValidationError> {
923 if let Some(code) = self.statuses.iter().find_map(|status| match status {
924 OrderStatus::Unknown(code) => Some(*code),
925 _ => None,
926 }) {
927 return Err(RequestValidationError::UnsupportedOrderStatus { code });
928 }
929 if self
930 .created_after
931 .zip(self.created_before)
932 .is_some_and(|(after, before)| after >= before)
933 {
934 return Err(RequestValidationError::SearchRangeNotIncreasing);
935 }
936 if self.page_size.is_some_and(|size| size <= 0) {
937 return Err(RequestValidationError::NonPositiveOrderPageSize);
938 }
939 if self.page_offset.is_some_and(|offset| offset < 0) {
940 return Err(RequestValidationError::NegativeOrderPageOffset);
941 }
942 Ok(OrderQuery {
943 filter: OrderFilter {
944 account_id: self.account_id,
945 statuses: self.statuses,
946 contract_id: self.contract_id,
947 created_after: self.created_after,
948 created_before: self.created_before,
949 },
950 page_size: self.page_size,
951 page_offset: self.page_offset,
952 sort_by: self.sort_by,
953 sort_direction: self.sort_direction,
954 include_total_count: self.include_total_count,
955 })
956 }
957}
958
959#[derive(Clone, Debug, Deserialize, PartialEq)]
961#[non_exhaustive]
962#[serde(rename_all = "camelCase")]
963pub struct OrderPage {
964 #[serde(default, deserialize_with = "null_to_empty")]
966 pub orders: Vec<Order>,
967 #[serde(default)]
969 pub total_count: Option<i32>,
970}
971
972#[derive(Clone, Debug, Deserialize, PartialEq)]
974#[non_exhaustive]
975#[serde(rename_all = "camelCase")]
976pub struct Order {
977 pub id: OrderId,
979 pub account_id: AccountId,
981 pub contract_id: ContractId,
983 #[serde(default)]
985 pub symbol_id: Option<SymbolId>,
986 pub creation_timestamp: Timestamp,
988 pub update_timestamp: Timestamp,
990 pub status: OrderStatus,
992 #[serde(rename = "type")]
994 pub order_type: OrderType,
995 pub side: Side,
997 pub size: i32,
999 #[serde(default, with = "crate::decimal_serde::option")]
1001 pub limit_price: Option<Decimal>,
1002 #[serde(default, with = "crate::decimal_serde::option")]
1004 pub stop_price: Option<Decimal>,
1005 #[serde(default)]
1007 pub fill_volume: Option<i32>,
1008 #[serde(default, with = "crate::decimal_serde::option")]
1010 pub filled_price: Option<Decimal>,
1011 #[serde(default)]
1013 pub custom_tag: Option<String>,
1014 #[serde(default)]
1016 pub trail_distance: Option<i32>,
1017 #[serde(default, with = "crate::decimal_serde::option")]
1023 pub trail_price: Option<Decimal>,
1024 #[serde(default)]
1026 pub parent_order_id: Option<OrderId>,
1027 #[serde(default)]
1029 pub linked_order_id: Option<OrderId>,
1030}
1031
1032#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
1034#[non_exhaustive]
1035pub enum RequestValidationError {
1036 #[error("order size must be positive")]
1038 NonPositiveOrderSize,
1039 #[error("trailing-stop placement requires an absolute trail price")]
1041 MissingTrailPrice,
1042 #[error("replacement order size must be positive")]
1044 NonPositiveReplacementSize,
1045 #[error("order modification requires at least one replacement value")]
1047 EmptyModification,
1048 #[error("bracket ticks must be positive")]
1050 NonPositiveBracketTicks,
1051 #[error("unsupported order type code {code}")]
1053 UnsupportedOrderType {
1054 code: i32,
1056 },
1057 #[error("unsupported order side code {code}")]
1059 UnsupportedOrderSide {
1060 code: i32,
1062 },
1063 #[error("unsupported order status code {code}")]
1065 UnsupportedOrderStatus {
1066 code: i32,
1068 },
1069 #[error("order-query page size must be positive")]
1071 NonPositiveOrderPageSize,
1072 #[error("order-query page offset must not be negative")]
1074 NegativeOrderPageOffset,
1075 #[error("historical-bar unit number must be positive")]
1077 NonPositiveHistoryUnitNumber,
1078 #[error("historical-bar limit must be between 1 and 20,000")]
1080 HistoryLimitOutOfRange,
1081 #[error("historical-bar end time must be later than its start time")]
1083 HistoryRangeNotIncreasing,
1084 #[error("search end time must be later than its start time")]
1086 SearchRangeNotIncreasing,
1087 #[error("partial-close size must be positive")]
1089 NonPositivePartialCloseSize,
1090}
1091
1092#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1098#[serde(rename_all = "camelCase")]
1099pub struct Bracket {
1100 ticks: i32,
1102 #[serde(rename = "type")]
1104 order_type: OrderType,
1105}
1106
1107impl Bracket {
1108 pub fn new(ticks: i32, order_type: OrderType) -> Result<Self, RequestValidationError> {
1115 if ticks <= 0 {
1116 return Err(RequestValidationError::NonPositiveBracketTicks);
1117 }
1118 validate_request_order_type(order_type)?;
1119 Ok(Self { ticks, order_type })
1120 }
1121
1122 #[must_use]
1124 pub const fn ticks(&self) -> i32 {
1125 self.ticks
1126 }
1127
1128 #[must_use]
1130 pub const fn order_type(&self) -> OrderType {
1131 self.order_type
1132 }
1133}
1134
1135#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1141#[serde(rename_all = "camelCase")]
1142pub struct PlaceOrder {
1143 account_id: AccountId,
1145 contract_id: ContractId,
1147 #[serde(rename = "type")]
1149 order_type: OrderType,
1150 side: Side,
1152 size: i32,
1154 #[serde(
1156 skip_serializing_if = "Option::is_none",
1157 with = "crate::decimal_serde::option"
1158 )]
1159 limit_price: Option<Decimal>,
1160 #[serde(
1162 skip_serializing_if = "Option::is_none",
1163 with = "crate::decimal_serde::option"
1164 )]
1165 stop_price: Option<Decimal>,
1166 #[serde(
1168 skip_serializing_if = "Option::is_none",
1169 with = "crate::decimal_serde::option"
1170 )]
1171 trail_price: Option<Decimal>,
1172 #[serde(skip_serializing_if = "Option::is_none")]
1174 custom_tag: Option<String>,
1175 #[serde(skip_serializing_if = "Option::is_none")]
1177 stop_loss_bracket: Option<Bracket>,
1178 #[serde(skip_serializing_if = "Option::is_none")]
1180 take_profit_bracket: Option<Bracket>,
1181}
1182
1183impl PlaceOrder {
1184 pub fn builder(
1186 account_id: AccountId,
1187 contract_id: ContractId,
1188 order_type: OrderType,
1189 side: Side,
1190 quantity: i32,
1191 ) -> PlaceOrderBuilder {
1192 PlaceOrderBuilder {
1193 account_id,
1194 contract_id,
1195 order_type,
1196 side,
1197 size: quantity,
1198 limit_price: None,
1199 stop_price: None,
1200 trail_price: None,
1201 custom_tag: None,
1202 stop_loss_bracket: None,
1203 take_profit_bracket: None,
1204 }
1205 }
1206
1207 #[must_use]
1209 pub const fn account_id(&self) -> AccountId {
1210 self.account_id
1211 }
1212
1213 #[must_use]
1215 pub const fn contract_id(&self) -> &ContractId {
1216 &self.contract_id
1217 }
1218
1219 #[must_use]
1221 pub const fn order_type(&self) -> OrderType {
1222 self.order_type
1223 }
1224
1225 #[must_use]
1227 pub const fn side(&self) -> Side {
1228 self.side
1229 }
1230
1231 #[must_use]
1233 pub const fn size(&self) -> i32 {
1234 self.size
1235 }
1236
1237 #[must_use]
1239 pub const fn limit_price(&self) -> Option<Decimal> {
1240 self.limit_price
1241 }
1242
1243 #[must_use]
1245 pub const fn stop_price(&self) -> Option<Decimal> {
1246 self.stop_price
1247 }
1248
1249 #[must_use]
1253 pub const fn trail_price(&self) -> Option<Decimal> {
1254 self.trail_price
1255 }
1256
1257 #[must_use]
1259 pub fn custom_tag(&self) -> Option<&str> {
1260 self.custom_tag.as_deref()
1261 }
1262
1263 #[must_use]
1265 pub const fn stop_loss_bracket(&self) -> Option<&Bracket> {
1266 self.stop_loss_bracket.as_ref()
1267 }
1268
1269 #[must_use]
1271 pub const fn take_profit_bracket(&self) -> Option<&Bracket> {
1272 self.take_profit_bracket.as_ref()
1273 }
1274}
1275
1276#[derive(Clone, Debug)]
1278#[must_use = "a PlaceOrderBuilder does nothing until build is called"]
1279pub struct PlaceOrderBuilder {
1280 account_id: AccountId,
1281 contract_id: ContractId,
1282 order_type: OrderType,
1283 side: Side,
1284 size: i32,
1285 limit_price: Option<Decimal>,
1286 stop_price: Option<Decimal>,
1287 trail_price: Option<Decimal>,
1288 custom_tag: Option<String>,
1289 stop_loss_bracket: Option<Bracket>,
1290 take_profit_bracket: Option<Bracket>,
1291}
1292
1293impl PlaceOrderBuilder {
1294 pub const fn limit_price(mut self, limit_price: Decimal) -> Self {
1296 self.limit_price = Some(limit_price);
1297 self
1298 }
1299
1300 pub const fn stop_price(mut self, stop_price: Decimal) -> Self {
1302 self.stop_price = Some(stop_price);
1303 self
1304 }
1305
1306 pub const fn trail_price(mut self, trail_price: Decimal) -> Self {
1314 self.trail_price = Some(trail_price);
1315 self
1316 }
1317
1318 pub fn custom_tag(mut self, custom_tag: impl Into<String>) -> Self {
1320 self.custom_tag = Some(custom_tag.into());
1321 self
1322 }
1323
1324 pub fn stop_loss_bracket(mut self, stop_loss_bracket: Bracket) -> Self {
1328 self.stop_loss_bracket = Some(stop_loss_bracket);
1329 self
1330 }
1331
1332 pub fn take_profit_bracket(mut self, take_profit_bracket: Bracket) -> Self {
1336 self.take_profit_bracket = Some(take_profit_bracket);
1337 self
1338 }
1339
1340 pub fn build(self) -> Result<PlaceOrder, RequestValidationError> {
1350 if self.size <= 0 {
1351 return Err(RequestValidationError::NonPositiveOrderSize);
1352 }
1353 validate_request_order_type(self.order_type)?;
1354 if let Side::Unknown(code) = self.side {
1355 return Err(RequestValidationError::UnsupportedOrderSide { code });
1356 }
1357 if self.order_type == OrderType::TrailingStop && self.trail_price.is_none() {
1358 return Err(RequestValidationError::MissingTrailPrice);
1359 }
1360 Ok(PlaceOrder {
1361 account_id: self.account_id,
1362 contract_id: self.contract_id,
1363 order_type: self.order_type,
1364 side: self.side,
1365 size: self.size,
1366 limit_price: self.limit_price,
1367 stop_price: self.stop_price,
1368 trail_price: self.trail_price,
1369 custom_tag: self.custom_tag,
1370 stop_loss_bracket: self.stop_loss_bracket,
1371 take_profit_bracket: self.take_profit_bracket,
1372 })
1373 }
1374}
1375
1376fn validate_request_order_type(order_type: OrderType) -> Result<(), RequestValidationError> {
1377 match order_type {
1378 OrderType::Limit
1379 | OrderType::Market
1380 | OrderType::Stop
1381 | OrderType::TrailingStop
1382 | OrderType::JoinBid
1383 | OrderType::JoinAsk => Ok(()),
1384 OrderType::StopLimit => Err(RequestValidationError::UnsupportedOrderType { code: 3 }),
1385 OrderType::Unknown(code) => Err(RequestValidationError::UnsupportedOrderType { code }),
1386 }
1387}
1388
1389#[derive(Clone, Debug, Eq, PartialEq)]
1391#[non_exhaustive]
1392pub struct OrderResponse {
1393 pub order_id: OrderId,
1395}
1396
1397#[derive(Clone, Debug, Serialize)]
1399#[serde(rename_all = "camelCase")]
1400pub struct CancelOrder {
1401 pub account_id: AccountId,
1403 pub order_id: OrderId,
1405}
1406
1407#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1412#[serde(rename_all = "camelCase")]
1413pub struct ModifyOrder {
1414 account_id: AccountId,
1416 order_id: OrderId,
1418 #[serde(skip_serializing_if = "Option::is_none")]
1420 size: Option<i32>,
1421 #[serde(
1423 skip_serializing_if = "Option::is_none",
1424 with = "crate::decimal_serde::option"
1425 )]
1426 limit_price: Option<Decimal>,
1427 #[serde(
1429 skip_serializing_if = "Option::is_none",
1430 with = "crate::decimal_serde::option"
1431 )]
1432 stop_price: Option<Decimal>,
1433 #[serde(
1435 skip_serializing_if = "Option::is_none",
1436 with = "crate::decimal_serde::option"
1437 )]
1438 trail_price: Option<Decimal>,
1439}
1440
1441impl ModifyOrder {
1442 pub const fn builder(account_id: AccountId, order_id: OrderId) -> ModifyOrderBuilder {
1444 ModifyOrderBuilder {
1445 account_id,
1446 order_id,
1447 size: None,
1448 limit_price: None,
1449 stop_price: None,
1450 trail_price: None,
1451 }
1452 }
1453
1454 #[must_use]
1456 pub const fn account_id(&self) -> AccountId {
1457 self.account_id
1458 }
1459
1460 #[must_use]
1462 pub const fn order_id(&self) -> OrderId {
1463 self.order_id
1464 }
1465
1466 #[must_use]
1468 pub const fn size(&self) -> Option<i32> {
1469 self.size
1470 }
1471
1472 #[must_use]
1474 pub const fn limit_price(&self) -> Option<Decimal> {
1475 self.limit_price
1476 }
1477
1478 #[must_use]
1480 pub const fn stop_price(&self) -> Option<Decimal> {
1481 self.stop_price
1482 }
1483
1484 #[must_use]
1488 pub const fn trail_price(&self) -> Option<Decimal> {
1489 self.trail_price
1490 }
1491}
1492
1493#[derive(Clone, Copy, Debug)]
1495#[must_use = "a ModifyOrderBuilder does nothing until build is called"]
1496pub struct ModifyOrderBuilder {
1497 account_id: AccountId,
1498 order_id: OrderId,
1499 size: Option<i32>,
1500 limit_price: Option<Decimal>,
1501 stop_price: Option<Decimal>,
1502 trail_price: Option<Decimal>,
1503}
1504
1505impl ModifyOrderBuilder {
1506 pub const fn size(mut self, size: i32) -> Self {
1508 self.size = Some(size);
1509 self
1510 }
1511
1512 pub const fn limit_price(mut self, limit_price: Decimal) -> Self {
1514 self.limit_price = Some(limit_price);
1515 self
1516 }
1517
1518 pub const fn stop_price(mut self, stop_price: Decimal) -> Self {
1520 self.stop_price = Some(stop_price);
1521 self
1522 }
1523
1524 pub const fn trail_price(mut self, trail_price: Decimal) -> Self {
1533 self.trail_price = Some(trail_price);
1534 self
1535 }
1536
1537 pub fn build(self) -> Result<ModifyOrder, RequestValidationError> {
1546 if self.size.is_some_and(|size| size <= 0) {
1547 return Err(RequestValidationError::NonPositiveReplacementSize);
1548 }
1549 if self.size.is_none()
1550 && self.limit_price.is_none()
1551 && self.stop_price.is_none()
1552 && self.trail_price.is_none()
1553 {
1554 return Err(RequestValidationError::EmptyModification);
1555 }
1556 Ok(ModifyOrder {
1557 account_id: self.account_id,
1558 order_id: self.order_id,
1559 size: self.size,
1560 limit_price: self.limit_price,
1561 stop_price: self.stop_price,
1562 trail_price: self.trail_price,
1563 })
1564 }
1565}
1566
1567#[derive(Clone, Debug, Serialize)]
1569#[serde(rename_all = "camelCase")]
1570pub struct CloseContract {
1571 pub account_id: AccountId,
1573 pub contract_id: ContractId,
1575}
1576
1577#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1579#[serde(rename_all = "camelCase")]
1580pub struct PartialCloseContract {
1581 account_id: AccountId,
1583 contract_id: ContractId,
1585 size: i32,
1587}
1588
1589impl PartialCloseContract {
1590 pub fn new(
1597 account_id: AccountId,
1598 contract_id: ContractId,
1599 size: i32,
1600 ) -> Result<Self, RequestValidationError> {
1601 if size <= 0 {
1602 return Err(RequestValidationError::NonPositivePartialCloseSize);
1603 }
1604 Ok(Self {
1605 account_id,
1606 contract_id,
1607 size,
1608 })
1609 }
1610
1611 #[must_use]
1613 pub const fn account_id(&self) -> AccountId {
1614 self.account_id
1615 }
1616
1617 #[must_use]
1619 pub const fn contract_id(&self) -> &ContractId {
1620 &self.contract_id
1621 }
1622
1623 #[must_use]
1625 pub const fn size(&self) -> i32 {
1626 self.size
1627 }
1628}
1629
1630#[derive(Clone, Debug, Deserialize, PartialEq)]
1632#[non_exhaustive]
1633#[serde(rename_all = "camelCase")]
1634pub struct Position {
1635 pub id: PositionId,
1637 pub account_id: AccountId,
1639 pub contract_id: ContractId,
1641 #[serde(default)]
1643 pub contract_display_name: Option<String>,
1644 pub creation_timestamp: Timestamp,
1646 #[serde(rename = "type")]
1648 pub position_type: PositionType,
1649 pub size: i32,
1651 #[serde(with = "crate::decimal_serde")]
1653 pub average_price: Decimal,
1654}
1655
1656#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1658#[serde(rename_all = "camelCase")]
1659pub struct TradeSearch {
1660 account_id: AccountId,
1662 start_timestamp: Timestamp,
1664 #[serde(skip_serializing_if = "Option::is_none")]
1666 end_timestamp: Option<Timestamp>,
1667}
1668
1669#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1674#[serde(rename_all = "camelCase")]
1675pub struct TradeQuery {
1676 account_id: AccountId,
1678 #[serde(skip_serializing_if = "Option::is_none")]
1680 start_timestamp: Option<Timestamp>,
1681 #[serde(skip_serializing_if = "Option::is_none")]
1683 end_timestamp: Option<Timestamp>,
1684}
1685
1686impl TradeQuery {
1687 pub const fn builder(account_id: AccountId) -> TradeQueryBuilder {
1689 TradeQueryBuilder {
1690 account_id,
1691 start_timestamp: None,
1692 end_timestamp: None,
1693 }
1694 }
1695
1696 #[must_use]
1698 pub const fn account_id(&self) -> AccountId {
1699 self.account_id
1700 }
1701
1702 #[must_use]
1704 pub const fn start_timestamp(&self) -> Option<Timestamp> {
1705 self.start_timestamp
1706 }
1707
1708 #[must_use]
1710 pub const fn end_timestamp(&self) -> Option<Timestamp> {
1711 self.end_timestamp
1712 }
1713}
1714
1715#[derive(Clone, Copy, Debug)]
1717#[must_use = "a TradeQueryBuilder does nothing until build is called"]
1718pub struct TradeQueryBuilder {
1719 account_id: AccountId,
1720 start_timestamp: Option<Timestamp>,
1721 end_timestamp: Option<Timestamp>,
1722}
1723
1724impl TradeQueryBuilder {
1725 pub const fn start_timestamp(mut self, start_timestamp: Timestamp) -> Self {
1727 self.start_timestamp = Some(start_timestamp);
1728 self
1729 }
1730
1731 pub const fn end_timestamp(mut self, end_timestamp: Timestamp) -> Self {
1733 self.end_timestamp = Some(end_timestamp);
1734 self
1735 }
1736
1737 pub fn build(self) -> Result<TradeQuery, RequestValidationError> {
1744 validate_search_range(self.start_timestamp, self.end_timestamp)?;
1745 Ok(TradeQuery {
1746 account_id: self.account_id,
1747 start_timestamp: self.start_timestamp,
1748 end_timestamp: self.end_timestamp,
1749 })
1750 }
1751}
1752
1753impl TradeSearch {
1754 pub fn new(
1761 account_id: AccountId,
1762 start_timestamp: Timestamp,
1763 end_timestamp: Option<Timestamp>,
1764 ) -> Result<Self, RequestValidationError> {
1765 validate_search_range(Some(start_timestamp), end_timestamp)?;
1766 Ok(Self {
1767 account_id,
1768 start_timestamp,
1769 end_timestamp,
1770 })
1771 }
1772
1773 #[must_use]
1775 pub const fn account_id(&self) -> AccountId {
1776 self.account_id
1777 }
1778
1779 #[must_use]
1781 pub const fn start_timestamp(&self) -> Timestamp {
1782 self.start_timestamp
1783 }
1784
1785 #[must_use]
1787 pub const fn end_timestamp(&self) -> Option<Timestamp> {
1788 self.end_timestamp
1789 }
1790}
1791
1792fn validate_search_range(
1793 start_timestamp: Option<Timestamp>,
1794 end_timestamp: Option<Timestamp>,
1795) -> Result<(), RequestValidationError> {
1796 if start_timestamp
1797 .zip(end_timestamp)
1798 .is_some_and(|(start, end)| end <= start)
1799 {
1800 Err(RequestValidationError::SearchRangeNotIncreasing)
1801 } else {
1802 Ok(())
1803 }
1804}
1805
1806#[derive(Clone, Debug, Deserialize, PartialEq)]
1808#[non_exhaustive]
1809#[serde(rename_all = "camelCase")]
1810pub struct Trade {
1811 pub id: TradeId,
1813 pub account_id: AccountId,
1815 pub contract_id: ContractId,
1817 pub creation_timestamp: Timestamp,
1819 #[serde(with = "crate::decimal_serde")]
1821 pub price: Decimal,
1822 #[serde(default, with = "crate::decimal_serde::option")]
1824 pub profit_and_loss: Option<Decimal>,
1825 #[serde(with = "crate::decimal_serde")]
1827 pub fees: Decimal,
1828 #[serde(default, with = "crate::decimal_serde::option")]
1830 pub commissions: Option<Decimal>,
1831 pub side: Side,
1833 pub size: i32,
1835 pub voided: bool,
1837 pub order_id: OrderId,
1839}
1840
1841#[derive(Clone, Debug, Deserialize, PartialEq)]
1847#[non_exhaustive]
1848#[serde(rename_all = "camelCase")]
1849pub struct MarketQuote {
1850 #[serde(alias = "symbol")]
1852 pub raw_symbol: SymbolId,
1853 #[serde(default)]
1855 pub symbol_name: Option<String>,
1856 #[serde(default, with = "crate::decimal_serde::option")]
1858 pub last_price: Option<Decimal>,
1859 #[serde(default, with = "crate::decimal_serde::option")]
1861 pub best_bid: Option<Decimal>,
1862 #[serde(default, with = "crate::decimal_serde::option")]
1864 pub best_ask: Option<Decimal>,
1865 #[serde(default, with = "crate::decimal_serde::option")]
1867 pub change: Option<Decimal>,
1868 #[serde(default, with = "crate::decimal_serde::option")]
1870 pub change_percent: Option<Decimal>,
1871 #[serde(default, with = "crate::decimal_serde::option")]
1873 pub open: Option<Decimal>,
1874 #[serde(default, with = "crate::decimal_serde::option")]
1876 pub high: Option<Decimal>,
1877 #[serde(default, with = "crate::decimal_serde::option")]
1879 pub low: Option<Decimal>,
1880 #[serde(default)]
1882 pub volume: Option<i64>,
1883 pub last_updated: Timestamp,
1885 #[serde(default)]
1887 pub timestamp: Option<Timestamp>,
1888}
1889
1890#[derive(Clone, Debug, Deserialize, PartialEq)]
1892#[non_exhaustive]
1893#[serde(rename_all = "camelCase")]
1894pub struct MarketDepth {
1895 #[serde(default, alias = "symbolId")]
1897 pub symbol_id: Option<SymbolId>,
1898 pub timestamp: Timestamp,
1900 #[serde(rename = "type")]
1902 pub depth_type: DepthType,
1903 #[serde(with = "crate::decimal_serde")]
1905 pub price: Decimal,
1906 pub volume: i64,
1908 pub current_volume: i64,
1910 #[serde(default)]
1912 pub index: Option<i32>,
1913}
1914
1915#[derive(Clone, Debug, Deserialize, PartialEq)]
1917#[non_exhaustive]
1918#[serde(rename_all = "camelCase")]
1919pub struct MarketTrade {
1920 pub symbol_id: SymbolId,
1922 #[serde(with = "crate::decimal_serde")]
1924 pub price: Decimal,
1925 pub timestamp: Timestamp,
1927 #[serde(rename = "type")]
1929 pub trade_type: TradeLogType,
1930 pub volume: i64,
1932}
1933
1934#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1936#[non_exhaustive]
1937pub struct OperationResponse;
1938
1939#[derive(Debug)]
1940pub(crate) enum Envelope<T> {
1941 Accepted(T),
1942 Rejected { error_code: i32 },
1943 InconsistentStatus { success: bool, error_code: i32 },
1944}
1945
1946impl<'de, T> Deserialize<'de> for Envelope<T>
1947where
1948 T: serde::de::DeserializeOwned,
1949{
1950 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1951 where
1952 D: serde::Deserializer<'de>,
1953 {
1954 use serde::de::Error as _;
1955
1956 let mut object = BTreeMap::<String, Box<RawValue>>::deserialize(deserializer)?;
1957 let success = object
1958 .remove("success")
1959 .ok_or_else(|| D::Error::custom("provider response success flag is missing"))
1960 .and_then(|value| serde_json::from_str(value.get()).map_err(D::Error::custom))?;
1961 let error_code = object
1962 .remove("errorCode")
1963 .ok_or_else(|| D::Error::custom("provider response error code is missing"))
1964 .and_then(|value| serde_json::from_str(value.get()).map_err(D::Error::custom))?;
1965 object.remove("errorMessage");
1966 if success != (error_code == 0) {
1967 return Ok(Self::InconsistentStatus {
1968 success,
1969 error_code,
1970 });
1971 }
1972 if !success {
1973 return Ok(Self::Rejected { error_code });
1974 }
1975 let mut body_json = String::from("{");
1976 for (index, (key, value)) in object.into_iter().enumerate() {
1977 if index > 0 {
1978 body_json.push(',');
1979 }
1980 body_json.push_str(&serde_json::to_string(&key).map_err(D::Error::custom)?);
1981 body_json.push(':');
1982 body_json.push_str(value.get());
1983 }
1984 body_json.push('}');
1985 let body = serde_json::from_str(&body_json).map_err(D::Error::custom)?;
1986 Ok(Self::Accepted(body))
1987 }
1988}
1989
1990#[derive(Debug, Deserialize)]
1991pub(crate) struct AccountsBody {
1992 #[serde(default, deserialize_with = "null_to_empty")]
1993 pub(crate) accounts: Vec<Account>,
1994}
1995
1996#[derive(Debug, Deserialize)]
1997pub(crate) struct ContractsBody {
1998 #[serde(default, deserialize_with = "null_to_empty")]
1999 pub(crate) contracts: Vec<Contract>,
2000}
2001
2002#[derive(Debug, Deserialize)]
2003pub(crate) struct ContractBody {
2004 pub(crate) contract: Contract,
2005}
2006
2007#[derive(Debug, Deserialize)]
2008pub(crate) struct BarsBody {
2009 #[serde(default, deserialize_with = "null_to_empty")]
2010 pub(crate) bars: Vec<Bar>,
2011}
2012
2013#[derive(Debug, Deserialize)]
2014pub(crate) struct OrdersBody {
2015 #[serde(default, deserialize_with = "null_to_empty")]
2016 pub(crate) orders: Vec<Order>,
2017}
2018
2019#[derive(Debug, Deserialize)]
2020pub(crate) struct OrderBody {
2021 pub(crate) order: Order,
2022}
2023
2024#[derive(Debug, Deserialize)]
2025#[serde(rename_all = "camelCase")]
2026pub(crate) struct PlaceOrderBody {
2027 pub(crate) order_id: Option<OrderId>,
2028}
2029
2030#[derive(Debug, Deserialize)]
2031pub(crate) struct PositionsBody {
2032 #[serde(default, deserialize_with = "null_to_empty")]
2033 pub(crate) positions: Vec<Position>,
2034}
2035
2036#[derive(Debug, Deserialize)]
2037pub(crate) struct TradesBody {
2038 #[serde(default, deserialize_with = "null_to_empty")]
2039 pub(crate) trades: Vec<Trade>,
2040}
2041
2042#[derive(Debug, Deserialize)]
2043pub(crate) struct EmptyBody {}
2044
2045pub(crate) fn null_to_empty<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
2046where
2047 D: serde::Deserializer<'de>,
2048 T: Deserialize<'de>,
2049{
2050 Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
2051}
2052
2053#[cfg(test)]
2054mod tests {
2055 use super::*;
2056
2057 macro_rules! assert_empty_list {
2058 ($body:ty, $field:ident, $json:literal) => {{
2059 let envelope: Envelope<$body> = serde_json::from_str($json)
2060 .unwrap_or_else(|error| panic!("fixture envelope must decode: {error}"));
2061 let Envelope::Accepted(body) = envelope else {
2062 panic!("fixture envelope must be accepted");
2063 };
2064 assert!(body.$field.is_empty());
2065 }};
2066 }
2067
2068 #[test]
2069 fn optional_list_bodies_normalize_missing_and_null_to_empty() {
2070 assert_empty_list!(
2071 AccountsBody,
2072 accounts,
2073 r#"{"success":true,"errorCode":0,"accounts":null}"#
2074 );
2075 assert_empty_list!(
2076 ContractsBody,
2077 contracts,
2078 r#"{"success":true,"errorCode":0}"#
2079 );
2080 assert_empty_list!(
2081 OrdersBody,
2082 orders,
2083 r#"{"success":true,"errorCode":0,"orders":null}"#
2084 );
2085 assert_empty_list!(
2086 OrderPage,
2087 orders,
2088 r#"{"success":true,"errorCode":0,"orders":null}"#
2089 );
2090 assert_empty_list!(
2091 PositionsBody,
2092 positions,
2093 r#"{"success":true,"errorCode":0}"#
2094 );
2095 assert_empty_list!(
2096 TradesBody,
2097 trades,
2098 r#"{"success":true,"errorCode":0,"trades":null}"#
2099 );
2100 }
2101
2102 #[test]
2103 fn rejected_envelope_does_not_require_an_endpoint_body() {
2104 let envelope: Envelope<AccountsBody> =
2105 serde_json::from_str(r#"{"success":false,"errorCode":17,"errorMessage":"synthetic"}"#)
2106 .unwrap_or_else(|error| panic!("rejection envelope must decode: {error}"));
2107
2108 assert!(matches!(envelope, Envelope::Rejected { error_code: 17 }));
2109 }
2110
2111 #[test]
2112 fn envelope_requires_a_consistent_provider_status() {
2113 assert!(
2114 serde_json::from_str::<Envelope<AccountsBody>>(r#"{"success":true,"accounts":[]}"#)
2115 .is_err()
2116 );
2117 for (json, success, error_code) in [
2118 (r#"{"success":true,"errorCode":17}"#, true, 17),
2119 (r#"{"success":false,"errorCode":0}"#, false, 0),
2120 ] {
2121 let envelope: Envelope<AccountsBody> = serde_json::from_str(json)
2122 .unwrap_or_else(|error| panic!("inconsistent envelope must decode: {error}"));
2123 assert!(matches!(
2124 envelope,
2125 Envelope::InconsistentStatus {
2126 success: actual_success,
2127 error_code: actual_error_code,
2128 } if actual_success == success && actual_error_code == error_code
2129 ));
2130 }
2131 }
2132}