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")]
1019 pub trail_price: Option<Decimal>,
1020 #[serde(default)]
1022 pub parent_order_id: Option<OrderId>,
1023 #[serde(default)]
1025 pub linked_order_id: Option<OrderId>,
1026}
1027
1028#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
1030#[non_exhaustive]
1031pub enum RequestValidationError {
1032 #[error("order size must be positive")]
1034 NonPositiveOrderSize,
1035 #[error("replacement order size must be positive")]
1037 NonPositiveReplacementSize,
1038 #[error("order modification requires at least one replacement value")]
1040 EmptyModification,
1041 #[error("bracket ticks must be positive")]
1043 NonPositiveBracketTicks,
1044 #[error("unsupported order type code {code}")]
1046 UnsupportedOrderType {
1047 code: i32,
1049 },
1050 #[error("unsupported order side code {code}")]
1052 UnsupportedOrderSide {
1053 code: i32,
1055 },
1056 #[error("unsupported order status code {code}")]
1058 UnsupportedOrderStatus {
1059 code: i32,
1061 },
1062 #[error("order-query page size must be positive")]
1064 NonPositiveOrderPageSize,
1065 #[error("order-query page offset must not be negative")]
1067 NegativeOrderPageOffset,
1068 #[error("historical-bar unit number must be positive")]
1070 NonPositiveHistoryUnitNumber,
1071 #[error("historical-bar limit must be between 1 and 20,000")]
1073 HistoryLimitOutOfRange,
1074 #[error("historical-bar end time must be later than its start time")]
1076 HistoryRangeNotIncreasing,
1077 #[error("search end time must be later than its start time")]
1079 SearchRangeNotIncreasing,
1080 #[error("partial-close size must be positive")]
1082 NonPositivePartialCloseSize,
1083}
1084
1085#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1087#[serde(rename_all = "camelCase")]
1088pub struct Bracket {
1089 ticks: i32,
1091 #[serde(rename = "type")]
1093 order_type: OrderType,
1094}
1095
1096impl Bracket {
1097 pub fn new(ticks: i32, order_type: OrderType) -> Result<Self, RequestValidationError> {
1104 if ticks <= 0 {
1105 return Err(RequestValidationError::NonPositiveBracketTicks);
1106 }
1107 validate_request_order_type(order_type)?;
1108 Ok(Self { ticks, order_type })
1109 }
1110
1111 #[must_use]
1113 pub const fn ticks(&self) -> i32 {
1114 self.ticks
1115 }
1116
1117 #[must_use]
1119 pub const fn order_type(&self) -> OrderType {
1120 self.order_type
1121 }
1122}
1123
1124#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1129#[serde(rename_all = "camelCase")]
1130pub struct PlaceOrder {
1131 account_id: AccountId,
1133 contract_id: ContractId,
1135 #[serde(rename = "type")]
1137 order_type: OrderType,
1138 side: Side,
1140 size: i32,
1142 #[serde(
1144 skip_serializing_if = "Option::is_none",
1145 with = "crate::decimal_serde::option"
1146 )]
1147 limit_price: Option<Decimal>,
1148 #[serde(
1150 skip_serializing_if = "Option::is_none",
1151 with = "crate::decimal_serde::option"
1152 )]
1153 stop_price: Option<Decimal>,
1154 #[serde(
1156 skip_serializing_if = "Option::is_none",
1157 with = "crate::decimal_serde::option"
1158 )]
1159 trail_price: Option<Decimal>,
1160 #[serde(skip_serializing_if = "Option::is_none")]
1162 custom_tag: Option<String>,
1163 #[serde(skip_serializing_if = "Option::is_none")]
1165 stop_loss_bracket: Option<Bracket>,
1166 #[serde(skip_serializing_if = "Option::is_none")]
1168 take_profit_bracket: Option<Bracket>,
1169}
1170
1171impl PlaceOrder {
1172 pub fn builder(
1174 account_id: AccountId,
1175 contract_id: ContractId,
1176 order_type: OrderType,
1177 side: Side,
1178 quantity: i32,
1179 ) -> PlaceOrderBuilder {
1180 PlaceOrderBuilder {
1181 account_id,
1182 contract_id,
1183 order_type,
1184 side,
1185 size: quantity,
1186 limit_price: None,
1187 stop_price: None,
1188 trail_price: None,
1189 custom_tag: None,
1190 stop_loss_bracket: None,
1191 take_profit_bracket: None,
1192 }
1193 }
1194
1195 #[must_use]
1197 pub const fn account_id(&self) -> AccountId {
1198 self.account_id
1199 }
1200
1201 #[must_use]
1203 pub const fn contract_id(&self) -> &ContractId {
1204 &self.contract_id
1205 }
1206
1207 #[must_use]
1209 pub const fn order_type(&self) -> OrderType {
1210 self.order_type
1211 }
1212
1213 #[must_use]
1215 pub const fn side(&self) -> Side {
1216 self.side
1217 }
1218
1219 #[must_use]
1221 pub const fn size(&self) -> i32 {
1222 self.size
1223 }
1224
1225 #[must_use]
1227 pub const fn limit_price(&self) -> Option<Decimal> {
1228 self.limit_price
1229 }
1230
1231 #[must_use]
1233 pub const fn stop_price(&self) -> Option<Decimal> {
1234 self.stop_price
1235 }
1236
1237 #[must_use]
1239 pub const fn trail_price(&self) -> Option<Decimal> {
1240 self.trail_price
1241 }
1242
1243 #[must_use]
1245 pub fn custom_tag(&self) -> Option<&str> {
1246 self.custom_tag.as_deref()
1247 }
1248
1249 #[must_use]
1251 pub const fn stop_loss_bracket(&self) -> Option<&Bracket> {
1252 self.stop_loss_bracket.as_ref()
1253 }
1254
1255 #[must_use]
1257 pub const fn take_profit_bracket(&self) -> Option<&Bracket> {
1258 self.take_profit_bracket.as_ref()
1259 }
1260}
1261
1262#[derive(Clone, Debug)]
1264#[must_use = "a PlaceOrderBuilder does nothing until build is called"]
1265pub struct PlaceOrderBuilder {
1266 account_id: AccountId,
1267 contract_id: ContractId,
1268 order_type: OrderType,
1269 side: Side,
1270 size: i32,
1271 limit_price: Option<Decimal>,
1272 stop_price: Option<Decimal>,
1273 trail_price: Option<Decimal>,
1274 custom_tag: Option<String>,
1275 stop_loss_bracket: Option<Bracket>,
1276 take_profit_bracket: Option<Bracket>,
1277}
1278
1279impl PlaceOrderBuilder {
1280 pub const fn limit_price(mut self, limit_price: Decimal) -> Self {
1282 self.limit_price = Some(limit_price);
1283 self
1284 }
1285
1286 pub const fn stop_price(mut self, stop_price: Decimal) -> Self {
1288 self.stop_price = Some(stop_price);
1289 self
1290 }
1291
1292 pub const fn trail_price(mut self, trail_price: Decimal) -> Self {
1294 self.trail_price = Some(trail_price);
1295 self
1296 }
1297
1298 pub fn custom_tag(mut self, custom_tag: impl Into<String>) -> Self {
1300 self.custom_tag = Some(custom_tag.into());
1301 self
1302 }
1303
1304 pub fn stop_loss_bracket(mut self, stop_loss_bracket: Bracket) -> Self {
1306 self.stop_loss_bracket = Some(stop_loss_bracket);
1307 self
1308 }
1309
1310 pub fn take_profit_bracket(mut self, take_profit_bracket: Bracket) -> Self {
1312 self.take_profit_bracket = Some(take_profit_bracket);
1313 self
1314 }
1315
1316 pub fn build(self) -> Result<PlaceOrder, RequestValidationError> {
1324 if self.size <= 0 {
1325 return Err(RequestValidationError::NonPositiveOrderSize);
1326 }
1327 validate_request_order_type(self.order_type)?;
1328 if let Side::Unknown(code) = self.side {
1329 return Err(RequestValidationError::UnsupportedOrderSide { code });
1330 }
1331 Ok(PlaceOrder {
1332 account_id: self.account_id,
1333 contract_id: self.contract_id,
1334 order_type: self.order_type,
1335 side: self.side,
1336 size: self.size,
1337 limit_price: self.limit_price,
1338 stop_price: self.stop_price,
1339 trail_price: self.trail_price,
1340 custom_tag: self.custom_tag,
1341 stop_loss_bracket: self.stop_loss_bracket,
1342 take_profit_bracket: self.take_profit_bracket,
1343 })
1344 }
1345}
1346
1347fn validate_request_order_type(order_type: OrderType) -> Result<(), RequestValidationError> {
1348 match order_type {
1349 OrderType::Limit
1350 | OrderType::Market
1351 | OrderType::Stop
1352 | OrderType::TrailingStop
1353 | OrderType::JoinBid
1354 | OrderType::JoinAsk => Ok(()),
1355 OrderType::StopLimit => Err(RequestValidationError::UnsupportedOrderType { code: 3 }),
1356 OrderType::Unknown(code) => Err(RequestValidationError::UnsupportedOrderType { code }),
1357 }
1358}
1359
1360#[derive(Clone, Debug, Eq, PartialEq)]
1362#[non_exhaustive]
1363pub struct OrderResponse {
1364 pub order_id: OrderId,
1366}
1367
1368#[derive(Clone, Debug, Serialize)]
1370#[serde(rename_all = "camelCase")]
1371pub struct CancelOrder {
1372 pub account_id: AccountId,
1374 pub order_id: OrderId,
1376}
1377
1378#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1383#[serde(rename_all = "camelCase")]
1384pub struct ModifyOrder {
1385 account_id: AccountId,
1387 order_id: OrderId,
1389 #[serde(skip_serializing_if = "Option::is_none")]
1391 size: Option<i32>,
1392 #[serde(
1394 skip_serializing_if = "Option::is_none",
1395 with = "crate::decimal_serde::option"
1396 )]
1397 limit_price: Option<Decimal>,
1398 #[serde(
1400 skip_serializing_if = "Option::is_none",
1401 with = "crate::decimal_serde::option"
1402 )]
1403 stop_price: Option<Decimal>,
1404 #[serde(
1406 skip_serializing_if = "Option::is_none",
1407 with = "crate::decimal_serde::option"
1408 )]
1409 trail_price: Option<Decimal>,
1410}
1411
1412impl ModifyOrder {
1413 pub const fn builder(account_id: AccountId, order_id: OrderId) -> ModifyOrderBuilder {
1415 ModifyOrderBuilder {
1416 account_id,
1417 order_id,
1418 size: None,
1419 limit_price: None,
1420 stop_price: None,
1421 trail_price: None,
1422 }
1423 }
1424
1425 #[must_use]
1427 pub const fn account_id(&self) -> AccountId {
1428 self.account_id
1429 }
1430
1431 #[must_use]
1433 pub const fn order_id(&self) -> OrderId {
1434 self.order_id
1435 }
1436
1437 #[must_use]
1439 pub const fn size(&self) -> Option<i32> {
1440 self.size
1441 }
1442
1443 #[must_use]
1445 pub const fn limit_price(&self) -> Option<Decimal> {
1446 self.limit_price
1447 }
1448
1449 #[must_use]
1451 pub const fn stop_price(&self) -> Option<Decimal> {
1452 self.stop_price
1453 }
1454
1455 #[must_use]
1457 pub const fn trail_price(&self) -> Option<Decimal> {
1458 self.trail_price
1459 }
1460}
1461
1462#[derive(Clone, Copy, Debug)]
1464#[must_use = "a ModifyOrderBuilder does nothing until build is called"]
1465pub struct ModifyOrderBuilder {
1466 account_id: AccountId,
1467 order_id: OrderId,
1468 size: Option<i32>,
1469 limit_price: Option<Decimal>,
1470 stop_price: Option<Decimal>,
1471 trail_price: Option<Decimal>,
1472}
1473
1474impl ModifyOrderBuilder {
1475 pub const fn size(mut self, size: i32) -> Self {
1477 self.size = Some(size);
1478 self
1479 }
1480
1481 pub const fn limit_price(mut self, limit_price: Decimal) -> Self {
1483 self.limit_price = Some(limit_price);
1484 self
1485 }
1486
1487 pub const fn stop_price(mut self, stop_price: Decimal) -> Self {
1489 self.stop_price = Some(stop_price);
1490 self
1491 }
1492
1493 pub const fn trail_price(mut self, trail_price: Decimal) -> Self {
1495 self.trail_price = Some(trail_price);
1496 self
1497 }
1498
1499 pub fn build(self) -> Result<ModifyOrder, RequestValidationError> {
1508 if self.size.is_some_and(|size| size <= 0) {
1509 return Err(RequestValidationError::NonPositiveReplacementSize);
1510 }
1511 if self.size.is_none()
1512 && self.limit_price.is_none()
1513 && self.stop_price.is_none()
1514 && self.trail_price.is_none()
1515 {
1516 return Err(RequestValidationError::EmptyModification);
1517 }
1518 Ok(ModifyOrder {
1519 account_id: self.account_id,
1520 order_id: self.order_id,
1521 size: self.size,
1522 limit_price: self.limit_price,
1523 stop_price: self.stop_price,
1524 trail_price: self.trail_price,
1525 })
1526 }
1527}
1528
1529#[derive(Clone, Debug, Serialize)]
1531#[serde(rename_all = "camelCase")]
1532pub struct CloseContract {
1533 pub account_id: AccountId,
1535 pub contract_id: ContractId,
1537}
1538
1539#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1541#[serde(rename_all = "camelCase")]
1542pub struct PartialCloseContract {
1543 account_id: AccountId,
1545 contract_id: ContractId,
1547 size: i32,
1549}
1550
1551impl PartialCloseContract {
1552 pub fn new(
1559 account_id: AccountId,
1560 contract_id: ContractId,
1561 size: i32,
1562 ) -> Result<Self, RequestValidationError> {
1563 if size <= 0 {
1564 return Err(RequestValidationError::NonPositivePartialCloseSize);
1565 }
1566 Ok(Self {
1567 account_id,
1568 contract_id,
1569 size,
1570 })
1571 }
1572
1573 #[must_use]
1575 pub const fn account_id(&self) -> AccountId {
1576 self.account_id
1577 }
1578
1579 #[must_use]
1581 pub const fn contract_id(&self) -> &ContractId {
1582 &self.contract_id
1583 }
1584
1585 #[must_use]
1587 pub const fn size(&self) -> i32 {
1588 self.size
1589 }
1590}
1591
1592#[derive(Clone, Debug, Deserialize, PartialEq)]
1594#[non_exhaustive]
1595#[serde(rename_all = "camelCase")]
1596pub struct Position {
1597 pub id: PositionId,
1599 pub account_id: AccountId,
1601 pub contract_id: ContractId,
1603 #[serde(default)]
1605 pub contract_display_name: Option<String>,
1606 pub creation_timestamp: Timestamp,
1608 #[serde(rename = "type")]
1610 pub position_type: PositionType,
1611 pub size: i32,
1613 #[serde(with = "crate::decimal_serde")]
1615 pub average_price: Decimal,
1616}
1617
1618#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1620#[serde(rename_all = "camelCase")]
1621pub struct TradeSearch {
1622 account_id: AccountId,
1624 start_timestamp: Timestamp,
1626 #[serde(skip_serializing_if = "Option::is_none")]
1628 end_timestamp: Option<Timestamp>,
1629}
1630
1631#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1636#[serde(rename_all = "camelCase")]
1637pub struct TradeQuery {
1638 account_id: AccountId,
1640 #[serde(skip_serializing_if = "Option::is_none")]
1642 start_timestamp: Option<Timestamp>,
1643 #[serde(skip_serializing_if = "Option::is_none")]
1645 end_timestamp: Option<Timestamp>,
1646}
1647
1648impl TradeQuery {
1649 pub const fn builder(account_id: AccountId) -> TradeQueryBuilder {
1651 TradeQueryBuilder {
1652 account_id,
1653 start_timestamp: None,
1654 end_timestamp: None,
1655 }
1656 }
1657
1658 #[must_use]
1660 pub const fn account_id(&self) -> AccountId {
1661 self.account_id
1662 }
1663
1664 #[must_use]
1666 pub const fn start_timestamp(&self) -> Option<Timestamp> {
1667 self.start_timestamp
1668 }
1669
1670 #[must_use]
1672 pub const fn end_timestamp(&self) -> Option<Timestamp> {
1673 self.end_timestamp
1674 }
1675}
1676
1677#[derive(Clone, Copy, Debug)]
1679#[must_use = "a TradeQueryBuilder does nothing until build is called"]
1680pub struct TradeQueryBuilder {
1681 account_id: AccountId,
1682 start_timestamp: Option<Timestamp>,
1683 end_timestamp: Option<Timestamp>,
1684}
1685
1686impl TradeQueryBuilder {
1687 pub const fn start_timestamp(mut self, start_timestamp: Timestamp) -> Self {
1689 self.start_timestamp = Some(start_timestamp);
1690 self
1691 }
1692
1693 pub const fn end_timestamp(mut self, end_timestamp: Timestamp) -> Self {
1695 self.end_timestamp = Some(end_timestamp);
1696 self
1697 }
1698
1699 pub fn build(self) -> Result<TradeQuery, RequestValidationError> {
1706 validate_search_range(self.start_timestamp, self.end_timestamp)?;
1707 Ok(TradeQuery {
1708 account_id: self.account_id,
1709 start_timestamp: self.start_timestamp,
1710 end_timestamp: self.end_timestamp,
1711 })
1712 }
1713}
1714
1715impl TradeSearch {
1716 pub fn new(
1723 account_id: AccountId,
1724 start_timestamp: Timestamp,
1725 end_timestamp: Option<Timestamp>,
1726 ) -> Result<Self, RequestValidationError> {
1727 validate_search_range(Some(start_timestamp), end_timestamp)?;
1728 Ok(Self {
1729 account_id,
1730 start_timestamp,
1731 end_timestamp,
1732 })
1733 }
1734
1735 #[must_use]
1737 pub const fn account_id(&self) -> AccountId {
1738 self.account_id
1739 }
1740
1741 #[must_use]
1743 pub const fn start_timestamp(&self) -> Timestamp {
1744 self.start_timestamp
1745 }
1746
1747 #[must_use]
1749 pub const fn end_timestamp(&self) -> Option<Timestamp> {
1750 self.end_timestamp
1751 }
1752}
1753
1754fn validate_search_range(
1755 start_timestamp: Option<Timestamp>,
1756 end_timestamp: Option<Timestamp>,
1757) -> Result<(), RequestValidationError> {
1758 if start_timestamp
1759 .zip(end_timestamp)
1760 .is_some_and(|(start, end)| end <= start)
1761 {
1762 Err(RequestValidationError::SearchRangeNotIncreasing)
1763 } else {
1764 Ok(())
1765 }
1766}
1767
1768#[derive(Clone, Debug, Deserialize, PartialEq)]
1770#[non_exhaustive]
1771#[serde(rename_all = "camelCase")]
1772pub struct Trade {
1773 pub id: TradeId,
1775 pub account_id: AccountId,
1777 pub contract_id: ContractId,
1779 pub creation_timestamp: Timestamp,
1781 #[serde(with = "crate::decimal_serde")]
1783 pub price: Decimal,
1784 #[serde(default, with = "crate::decimal_serde::option")]
1786 pub profit_and_loss: Option<Decimal>,
1787 #[serde(with = "crate::decimal_serde")]
1789 pub fees: Decimal,
1790 #[serde(default, with = "crate::decimal_serde::option")]
1792 pub commissions: Option<Decimal>,
1793 pub side: Side,
1795 pub size: i32,
1797 pub voided: bool,
1799 pub order_id: OrderId,
1801}
1802
1803#[derive(Clone, Debug, Deserialize, PartialEq)]
1809#[non_exhaustive]
1810#[serde(rename_all = "camelCase")]
1811pub struct MarketQuote {
1812 #[serde(alias = "symbol")]
1814 pub raw_symbol: SymbolId,
1815 #[serde(default)]
1817 pub symbol_name: Option<String>,
1818 #[serde(default, with = "crate::decimal_serde::option")]
1820 pub last_price: Option<Decimal>,
1821 #[serde(default, with = "crate::decimal_serde::option")]
1823 pub best_bid: Option<Decimal>,
1824 #[serde(default, with = "crate::decimal_serde::option")]
1826 pub best_ask: Option<Decimal>,
1827 #[serde(default, with = "crate::decimal_serde::option")]
1829 pub change: Option<Decimal>,
1830 #[serde(default, with = "crate::decimal_serde::option")]
1832 pub change_percent: Option<Decimal>,
1833 #[serde(default, with = "crate::decimal_serde::option")]
1835 pub open: Option<Decimal>,
1836 #[serde(default, with = "crate::decimal_serde::option")]
1838 pub high: Option<Decimal>,
1839 #[serde(default, with = "crate::decimal_serde::option")]
1841 pub low: Option<Decimal>,
1842 #[serde(default)]
1844 pub volume: Option<i64>,
1845 pub last_updated: Timestamp,
1847 #[serde(default)]
1849 pub timestamp: Option<Timestamp>,
1850}
1851
1852#[derive(Clone, Debug, Deserialize, PartialEq)]
1854#[non_exhaustive]
1855#[serde(rename_all = "camelCase")]
1856pub struct MarketDepth {
1857 #[serde(default, alias = "symbolId")]
1859 pub symbol_id: Option<SymbolId>,
1860 pub timestamp: Timestamp,
1862 #[serde(rename = "type")]
1864 pub depth_type: DepthType,
1865 #[serde(with = "crate::decimal_serde")]
1867 pub price: Decimal,
1868 pub volume: i64,
1870 pub current_volume: i64,
1872 #[serde(default)]
1874 pub index: Option<i32>,
1875}
1876
1877#[derive(Clone, Debug, Deserialize, PartialEq)]
1879#[non_exhaustive]
1880#[serde(rename_all = "camelCase")]
1881pub struct MarketTrade {
1882 pub symbol_id: SymbolId,
1884 #[serde(with = "crate::decimal_serde")]
1886 pub price: Decimal,
1887 pub timestamp: Timestamp,
1889 #[serde(rename = "type")]
1891 pub trade_type: TradeLogType,
1892 pub volume: i64,
1894}
1895
1896#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1898#[non_exhaustive]
1899pub struct OperationResponse;
1900
1901#[derive(Debug)]
1902pub(crate) enum Envelope<T> {
1903 Accepted(T),
1904 Rejected { error_code: i32 },
1905 InconsistentStatus { success: bool, error_code: i32 },
1906}
1907
1908impl<'de, T> Deserialize<'de> for Envelope<T>
1909where
1910 T: serde::de::DeserializeOwned,
1911{
1912 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1913 where
1914 D: serde::Deserializer<'de>,
1915 {
1916 use serde::de::Error as _;
1917
1918 let mut object = BTreeMap::<String, Box<RawValue>>::deserialize(deserializer)?;
1919 let success = object
1920 .remove("success")
1921 .ok_or_else(|| D::Error::custom("provider response success flag is missing"))
1922 .and_then(|value| serde_json::from_str(value.get()).map_err(D::Error::custom))?;
1923 let error_code = object
1924 .remove("errorCode")
1925 .ok_or_else(|| D::Error::custom("provider response error code is missing"))
1926 .and_then(|value| serde_json::from_str(value.get()).map_err(D::Error::custom))?;
1927 object.remove("errorMessage");
1928 if success != (error_code == 0) {
1929 return Ok(Self::InconsistentStatus {
1930 success,
1931 error_code,
1932 });
1933 }
1934 if !success {
1935 return Ok(Self::Rejected { error_code });
1936 }
1937 let mut body_json = String::from("{");
1938 for (index, (key, value)) in object.into_iter().enumerate() {
1939 if index > 0 {
1940 body_json.push(',');
1941 }
1942 body_json.push_str(&serde_json::to_string(&key).map_err(D::Error::custom)?);
1943 body_json.push(':');
1944 body_json.push_str(value.get());
1945 }
1946 body_json.push('}');
1947 let body = serde_json::from_str(&body_json).map_err(D::Error::custom)?;
1948 Ok(Self::Accepted(body))
1949 }
1950}
1951
1952#[derive(Debug, Deserialize)]
1953pub(crate) struct AccountsBody {
1954 #[serde(default, deserialize_with = "null_to_empty")]
1955 pub(crate) accounts: Vec<Account>,
1956}
1957
1958#[derive(Debug, Deserialize)]
1959pub(crate) struct ContractsBody {
1960 #[serde(default, deserialize_with = "null_to_empty")]
1961 pub(crate) contracts: Vec<Contract>,
1962}
1963
1964#[derive(Debug, Deserialize)]
1965pub(crate) struct ContractBody {
1966 pub(crate) contract: Contract,
1967}
1968
1969#[derive(Debug, Deserialize)]
1970pub(crate) struct BarsBody {
1971 #[serde(default, deserialize_with = "null_to_empty")]
1972 pub(crate) bars: Vec<Bar>,
1973}
1974
1975#[derive(Debug, Deserialize)]
1976pub(crate) struct OrdersBody {
1977 #[serde(default, deserialize_with = "null_to_empty")]
1978 pub(crate) orders: Vec<Order>,
1979}
1980
1981#[derive(Debug, Deserialize)]
1982pub(crate) struct OrderBody {
1983 pub(crate) order: Order,
1984}
1985
1986#[derive(Debug, Deserialize)]
1987#[serde(rename_all = "camelCase")]
1988pub(crate) struct PlaceOrderBody {
1989 pub(crate) order_id: Option<OrderId>,
1990}
1991
1992#[derive(Debug, Deserialize)]
1993pub(crate) struct PositionsBody {
1994 #[serde(default, deserialize_with = "null_to_empty")]
1995 pub(crate) positions: Vec<Position>,
1996}
1997
1998#[derive(Debug, Deserialize)]
1999pub(crate) struct TradesBody {
2000 #[serde(default, deserialize_with = "null_to_empty")]
2001 pub(crate) trades: Vec<Trade>,
2002}
2003
2004#[derive(Debug, Deserialize)]
2005pub(crate) struct EmptyBody {}
2006
2007pub(crate) fn null_to_empty<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
2008where
2009 D: serde::Deserializer<'de>,
2010 T: Deserialize<'de>,
2011{
2012 Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
2013}
2014
2015#[cfg(test)]
2016mod tests {
2017 use super::*;
2018
2019 macro_rules! assert_empty_list {
2020 ($body:ty, $field:ident, $json:literal) => {{
2021 let envelope: Envelope<$body> = serde_json::from_str($json)
2022 .unwrap_or_else(|error| panic!("fixture envelope must decode: {error}"));
2023 let Envelope::Accepted(body) = envelope else {
2024 panic!("fixture envelope must be accepted");
2025 };
2026 assert!(body.$field.is_empty());
2027 }};
2028 }
2029
2030 #[test]
2031 fn optional_list_bodies_normalize_missing_and_null_to_empty() {
2032 assert_empty_list!(
2033 AccountsBody,
2034 accounts,
2035 r#"{"success":true,"errorCode":0,"accounts":null}"#
2036 );
2037 assert_empty_list!(
2038 ContractsBody,
2039 contracts,
2040 r#"{"success":true,"errorCode":0}"#
2041 );
2042 assert_empty_list!(
2043 OrdersBody,
2044 orders,
2045 r#"{"success":true,"errorCode":0,"orders":null}"#
2046 );
2047 assert_empty_list!(
2048 OrderPage,
2049 orders,
2050 r#"{"success":true,"errorCode":0,"orders":null}"#
2051 );
2052 assert_empty_list!(
2053 PositionsBody,
2054 positions,
2055 r#"{"success":true,"errorCode":0}"#
2056 );
2057 assert_empty_list!(
2058 TradesBody,
2059 trades,
2060 r#"{"success":true,"errorCode":0,"trades":null}"#
2061 );
2062 }
2063
2064 #[test]
2065 fn rejected_envelope_does_not_require_an_endpoint_body() {
2066 let envelope: Envelope<AccountsBody> =
2067 serde_json::from_str(r#"{"success":false,"errorCode":17,"errorMessage":"synthetic"}"#)
2068 .unwrap_or_else(|error| panic!("rejection envelope must decode: {error}"));
2069
2070 assert!(matches!(envelope, Envelope::Rejected { error_code: 17 }));
2071 }
2072
2073 #[test]
2074 fn envelope_requires_a_consistent_provider_status() {
2075 assert!(
2076 serde_json::from_str::<Envelope<AccountsBody>>(r#"{"success":true,"accounts":[]}"#)
2077 .is_err()
2078 );
2079 for (json, success, error_code) in [
2080 (r#"{"success":true,"errorCode":17}"#, true, 17),
2081 (r#"{"success":false,"errorCode":0}"#, false, 0),
2082 ] {
2083 let envelope: Envelope<AccountsBody> = serde_json::from_str(json)
2084 .unwrap_or_else(|error| panic!("inconsistent envelope must decode: {error}"));
2085 assert!(matches!(
2086 envelope,
2087 Envelope::InconsistentStatus {
2088 success: actual_success,
2089 error_code: actual_error_code,
2090 } if actual_success == success && actual_error_code == error_code
2091 ));
2092 }
2093 }
2094}