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, Default, Eq, PartialEq)]
985pub enum ProviderList<T> {
986 Listed(Vec<T>),
988 #[default]
990 Absent,
991}
992
993impl<T> ProviderList<T> {
994 #[must_use]
996 pub fn as_listed(&self) -> Option<&[T]> {
997 match self {
998 Self::Listed(rows) => Some(rows),
999 Self::Absent => None,
1000 }
1001 }
1002
1003 #[must_use]
1005 pub fn into_listed(self) -> Option<Vec<T>> {
1006 match self {
1007 Self::Listed(rows) => Some(rows),
1008 Self::Absent => None,
1009 }
1010 }
1011
1012 #[must_use]
1018 pub fn unwrap_or_empty(self) -> Vec<T> {
1019 self.into_listed().unwrap_or_default()
1020 }
1021
1022 #[must_use]
1024 pub fn is_listed(&self) -> bool {
1025 matches!(self, Self::Listed(_))
1026 }
1027
1028 #[must_use]
1033 pub fn is_explicitly_empty(&self) -> bool {
1034 matches!(self, Self::Listed(rows) if rows.is_empty())
1035 }
1036}
1037
1038impl<T> From<ProviderList<T>> for Option<Vec<T>> {
1039 fn from(list: ProviderList<T>) -> Self {
1040 list.into_listed()
1041 }
1042}
1043
1044impl<'de, T> Deserialize<'de> for ProviderList<T>
1045where
1046 T: Deserialize<'de>,
1047{
1048 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1049 where
1050 D: serde::Deserializer<'de>,
1051 {
1052 match Option::<Vec<T>>::deserialize(deserializer)? {
1053 Some(rows) => Ok(Self::Listed(rows)),
1054 None => Ok(Self::Absent),
1055 }
1056 }
1057}
1058
1059#[derive(Clone, Debug, Deserialize, PartialEq)]
1061#[non_exhaustive]
1062#[serde(rename_all = "camelCase")]
1063pub struct OrderPage {
1064 #[serde(default)]
1066 pub orders: ProviderList<Order>,
1067 #[serde(default)]
1069 pub total_count: Option<i32>,
1070}
1071
1072#[derive(Clone, Debug, Deserialize, PartialEq)]
1074#[non_exhaustive]
1075#[serde(rename_all = "camelCase")]
1076pub struct Order {
1077 pub id: OrderId,
1079 pub account_id: AccountId,
1081 pub contract_id: ContractId,
1083 #[serde(default)]
1085 pub symbol_id: Option<SymbolId>,
1086 pub creation_timestamp: Timestamp,
1088 pub update_timestamp: Timestamp,
1090 pub status: OrderStatus,
1092 #[serde(rename = "type")]
1094 pub order_type: OrderType,
1095 pub side: Side,
1097 pub size: i32,
1099 #[serde(default, with = "crate::decimal_serde::option")]
1101 pub limit_price: Option<Decimal>,
1102 #[serde(default, with = "crate::decimal_serde::option")]
1104 pub stop_price: Option<Decimal>,
1105 #[serde(default)]
1107 pub fill_volume: Option<i32>,
1108 #[serde(default, with = "crate::decimal_serde::option")]
1110 pub filled_price: Option<Decimal>,
1111 #[serde(default)]
1113 pub custom_tag: Option<String>,
1114 #[serde(default)]
1116 pub trail_distance: Option<i32>,
1117 #[serde(default, with = "crate::decimal_serde::option")]
1123 pub trail_price: Option<Decimal>,
1124 #[serde(default)]
1126 pub parent_order_id: Option<OrderId>,
1127 #[serde(default)]
1129 pub linked_order_id: Option<OrderId>,
1130}
1131
1132#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
1134#[non_exhaustive]
1135pub enum RequestValidationError {
1136 #[error("order size must be positive")]
1138 NonPositiveOrderSize,
1139 #[error("trailing-stop placement requires an absolute trail price")]
1141 MissingTrailPrice,
1142 #[error("replacement order size must be positive")]
1144 NonPositiveReplacementSize,
1145 #[error("order modification requires at least one replacement value")]
1147 EmptyModification,
1148 #[error("bracket ticks must be positive")]
1150 NonPositiveBracketTicks,
1151 #[error("unsupported order type code {code}")]
1153 UnsupportedOrderType {
1154 code: i32,
1156 },
1157 #[error("unsupported order side code {code}")]
1159 UnsupportedOrderSide {
1160 code: i32,
1162 },
1163 #[error("unsupported order status code {code}")]
1165 UnsupportedOrderStatus {
1166 code: i32,
1168 },
1169 #[error("order-query page size must be positive")]
1171 NonPositiveOrderPageSize,
1172 #[error("order-query page offset must not be negative")]
1174 NegativeOrderPageOffset,
1175 #[error("historical-bar unit number must be positive")]
1177 NonPositiveHistoryUnitNumber,
1178 #[error("historical-bar limit must be between 1 and 20,000")]
1180 HistoryLimitOutOfRange,
1181 #[error("historical-bar end time must be later than its start time")]
1183 HistoryRangeNotIncreasing,
1184 #[error("search end time must be later than its start time")]
1186 SearchRangeNotIncreasing,
1187 #[error("partial-close size must be positive")]
1189 NonPositivePartialCloseSize,
1190}
1191
1192#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1198#[serde(rename_all = "camelCase")]
1199pub struct Bracket {
1200 ticks: i32,
1202 #[serde(rename = "type")]
1204 order_type: OrderType,
1205}
1206
1207impl Bracket {
1208 pub fn new(ticks: i32, order_type: OrderType) -> Result<Self, RequestValidationError> {
1215 if ticks <= 0 {
1216 return Err(RequestValidationError::NonPositiveBracketTicks);
1217 }
1218 validate_request_order_type(order_type)?;
1219 Ok(Self { ticks, order_type })
1220 }
1221
1222 #[must_use]
1224 pub const fn ticks(&self) -> i32 {
1225 self.ticks
1226 }
1227
1228 #[must_use]
1230 pub const fn order_type(&self) -> OrderType {
1231 self.order_type
1232 }
1233}
1234
1235#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1241#[serde(rename_all = "camelCase")]
1242pub struct PlaceOrder {
1243 account_id: AccountId,
1245 contract_id: ContractId,
1247 #[serde(rename = "type")]
1249 order_type: OrderType,
1250 side: Side,
1252 size: i32,
1254 #[serde(
1256 skip_serializing_if = "Option::is_none",
1257 with = "crate::decimal_serde::option"
1258 )]
1259 limit_price: Option<Decimal>,
1260 #[serde(
1262 skip_serializing_if = "Option::is_none",
1263 with = "crate::decimal_serde::option"
1264 )]
1265 stop_price: Option<Decimal>,
1266 #[serde(
1268 skip_serializing_if = "Option::is_none",
1269 with = "crate::decimal_serde::option"
1270 )]
1271 trail_price: Option<Decimal>,
1272 #[serde(skip_serializing_if = "Option::is_none")]
1274 custom_tag: Option<String>,
1275 #[serde(skip_serializing_if = "Option::is_none")]
1277 stop_loss_bracket: Option<Bracket>,
1278 #[serde(skip_serializing_if = "Option::is_none")]
1280 take_profit_bracket: Option<Bracket>,
1281}
1282
1283impl PlaceOrder {
1284 pub fn builder(
1286 account_id: AccountId,
1287 contract_id: ContractId,
1288 order_type: OrderType,
1289 side: Side,
1290 quantity: i32,
1291 ) -> PlaceOrderBuilder {
1292 PlaceOrderBuilder {
1293 account_id,
1294 contract_id,
1295 order_type,
1296 side,
1297 size: quantity,
1298 limit_price: None,
1299 stop_price: None,
1300 trail_price: None,
1301 custom_tag: None,
1302 stop_loss_bracket: None,
1303 take_profit_bracket: None,
1304 }
1305 }
1306
1307 #[must_use]
1309 pub const fn account_id(&self) -> AccountId {
1310 self.account_id
1311 }
1312
1313 #[must_use]
1315 pub const fn contract_id(&self) -> &ContractId {
1316 &self.contract_id
1317 }
1318
1319 #[must_use]
1321 pub const fn order_type(&self) -> OrderType {
1322 self.order_type
1323 }
1324
1325 #[must_use]
1327 pub const fn side(&self) -> Side {
1328 self.side
1329 }
1330
1331 #[must_use]
1333 pub const fn size(&self) -> i32 {
1334 self.size
1335 }
1336
1337 #[must_use]
1339 pub const fn limit_price(&self) -> Option<Decimal> {
1340 self.limit_price
1341 }
1342
1343 #[must_use]
1345 pub const fn stop_price(&self) -> Option<Decimal> {
1346 self.stop_price
1347 }
1348
1349 #[must_use]
1353 pub const fn trail_price(&self) -> Option<Decimal> {
1354 self.trail_price
1355 }
1356
1357 #[must_use]
1359 pub fn custom_tag(&self) -> Option<&str> {
1360 self.custom_tag.as_deref()
1361 }
1362
1363 #[must_use]
1365 pub const fn stop_loss_bracket(&self) -> Option<&Bracket> {
1366 self.stop_loss_bracket.as_ref()
1367 }
1368
1369 #[must_use]
1371 pub const fn take_profit_bracket(&self) -> Option<&Bracket> {
1372 self.take_profit_bracket.as_ref()
1373 }
1374}
1375
1376#[derive(Clone, Debug)]
1378#[must_use = "a PlaceOrderBuilder does nothing until build is called"]
1379pub struct PlaceOrderBuilder {
1380 account_id: AccountId,
1381 contract_id: ContractId,
1382 order_type: OrderType,
1383 side: Side,
1384 size: i32,
1385 limit_price: Option<Decimal>,
1386 stop_price: Option<Decimal>,
1387 trail_price: Option<Decimal>,
1388 custom_tag: Option<String>,
1389 stop_loss_bracket: Option<Bracket>,
1390 take_profit_bracket: Option<Bracket>,
1391}
1392
1393impl PlaceOrderBuilder {
1394 pub const fn limit_price(mut self, limit_price: Decimal) -> Self {
1396 self.limit_price = Some(limit_price);
1397 self
1398 }
1399
1400 pub const fn stop_price(mut self, stop_price: Decimal) -> Self {
1402 self.stop_price = Some(stop_price);
1403 self
1404 }
1405
1406 pub const fn trail_price(mut self, trail_price: Decimal) -> Self {
1414 self.trail_price = Some(trail_price);
1415 self
1416 }
1417
1418 pub fn custom_tag(mut self, custom_tag: impl Into<String>) -> Self {
1420 self.custom_tag = Some(custom_tag.into());
1421 self
1422 }
1423
1424 pub fn stop_loss_bracket(mut self, stop_loss_bracket: Bracket) -> Self {
1428 self.stop_loss_bracket = Some(stop_loss_bracket);
1429 self
1430 }
1431
1432 pub fn take_profit_bracket(mut self, take_profit_bracket: Bracket) -> Self {
1436 self.take_profit_bracket = Some(take_profit_bracket);
1437 self
1438 }
1439
1440 pub fn build(self) -> Result<PlaceOrder, RequestValidationError> {
1450 if self.size <= 0 {
1451 return Err(RequestValidationError::NonPositiveOrderSize);
1452 }
1453 validate_request_order_type(self.order_type)?;
1454 if let Side::Unknown(code) = self.side {
1455 return Err(RequestValidationError::UnsupportedOrderSide { code });
1456 }
1457 if self.order_type == OrderType::TrailingStop && self.trail_price.is_none() {
1458 return Err(RequestValidationError::MissingTrailPrice);
1459 }
1460 Ok(PlaceOrder {
1461 account_id: self.account_id,
1462 contract_id: self.contract_id,
1463 order_type: self.order_type,
1464 side: self.side,
1465 size: self.size,
1466 limit_price: self.limit_price,
1467 stop_price: self.stop_price,
1468 trail_price: self.trail_price,
1469 custom_tag: self.custom_tag,
1470 stop_loss_bracket: self.stop_loss_bracket,
1471 take_profit_bracket: self.take_profit_bracket,
1472 })
1473 }
1474}
1475
1476fn validate_request_order_type(order_type: OrderType) -> Result<(), RequestValidationError> {
1477 match order_type {
1478 OrderType::Limit
1479 | OrderType::Market
1480 | OrderType::Stop
1481 | OrderType::TrailingStop
1482 | OrderType::JoinBid
1483 | OrderType::JoinAsk => Ok(()),
1484 OrderType::StopLimit => Err(RequestValidationError::UnsupportedOrderType { code: 3 }),
1485 OrderType::Unknown(code) => Err(RequestValidationError::UnsupportedOrderType { code }),
1486 }
1487}
1488
1489#[derive(Clone, Debug, Eq, PartialEq)]
1491#[non_exhaustive]
1492pub struct OrderResponse {
1493 pub order_id: OrderId,
1495}
1496
1497#[derive(Clone, Debug, Serialize)]
1499#[serde(rename_all = "camelCase")]
1500pub struct CancelOrder {
1501 pub account_id: AccountId,
1503 pub order_id: OrderId,
1505}
1506
1507#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1512#[serde(rename_all = "camelCase")]
1513pub struct ModifyOrder {
1514 account_id: AccountId,
1516 order_id: OrderId,
1518 #[serde(skip_serializing_if = "Option::is_none")]
1520 size: Option<i32>,
1521 #[serde(
1523 skip_serializing_if = "Option::is_none",
1524 with = "crate::decimal_serde::option"
1525 )]
1526 limit_price: Option<Decimal>,
1527 #[serde(
1529 skip_serializing_if = "Option::is_none",
1530 with = "crate::decimal_serde::option"
1531 )]
1532 stop_price: Option<Decimal>,
1533 #[serde(
1535 skip_serializing_if = "Option::is_none",
1536 with = "crate::decimal_serde::option"
1537 )]
1538 trail_price: Option<Decimal>,
1539}
1540
1541impl ModifyOrder {
1542 pub const fn builder(account_id: AccountId, order_id: OrderId) -> ModifyOrderBuilder {
1544 ModifyOrderBuilder {
1545 account_id,
1546 order_id,
1547 size: None,
1548 limit_price: None,
1549 stop_price: None,
1550 trail_price: None,
1551 }
1552 }
1553
1554 #[must_use]
1556 pub const fn account_id(&self) -> AccountId {
1557 self.account_id
1558 }
1559
1560 #[must_use]
1562 pub const fn order_id(&self) -> OrderId {
1563 self.order_id
1564 }
1565
1566 #[must_use]
1568 pub const fn size(&self) -> Option<i32> {
1569 self.size
1570 }
1571
1572 #[must_use]
1574 pub const fn limit_price(&self) -> Option<Decimal> {
1575 self.limit_price
1576 }
1577
1578 #[must_use]
1580 pub const fn stop_price(&self) -> Option<Decimal> {
1581 self.stop_price
1582 }
1583
1584 #[must_use]
1588 pub const fn trail_price(&self) -> Option<Decimal> {
1589 self.trail_price
1590 }
1591}
1592
1593#[derive(Clone, Copy, Debug)]
1595#[must_use = "a ModifyOrderBuilder does nothing until build is called"]
1596pub struct ModifyOrderBuilder {
1597 account_id: AccountId,
1598 order_id: OrderId,
1599 size: Option<i32>,
1600 limit_price: Option<Decimal>,
1601 stop_price: Option<Decimal>,
1602 trail_price: Option<Decimal>,
1603}
1604
1605impl ModifyOrderBuilder {
1606 pub const fn size(mut self, size: i32) -> Self {
1608 self.size = Some(size);
1609 self
1610 }
1611
1612 pub const fn limit_price(mut self, limit_price: Decimal) -> Self {
1614 self.limit_price = Some(limit_price);
1615 self
1616 }
1617
1618 pub const fn stop_price(mut self, stop_price: Decimal) -> Self {
1620 self.stop_price = Some(stop_price);
1621 self
1622 }
1623
1624 pub const fn trail_price(mut self, trail_price: Decimal) -> Self {
1633 self.trail_price = Some(trail_price);
1634 self
1635 }
1636
1637 pub fn build(self) -> Result<ModifyOrder, RequestValidationError> {
1646 if self.size.is_some_and(|size| size <= 0) {
1647 return Err(RequestValidationError::NonPositiveReplacementSize);
1648 }
1649 if self.size.is_none()
1650 && self.limit_price.is_none()
1651 && self.stop_price.is_none()
1652 && self.trail_price.is_none()
1653 {
1654 return Err(RequestValidationError::EmptyModification);
1655 }
1656 Ok(ModifyOrder {
1657 account_id: self.account_id,
1658 order_id: self.order_id,
1659 size: self.size,
1660 limit_price: self.limit_price,
1661 stop_price: self.stop_price,
1662 trail_price: self.trail_price,
1663 })
1664 }
1665}
1666
1667#[derive(Clone, Debug, Serialize)]
1669#[serde(rename_all = "camelCase")]
1670pub struct CloseContract {
1671 pub account_id: AccountId,
1673 pub contract_id: ContractId,
1675}
1676
1677#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1679#[serde(rename_all = "camelCase")]
1680pub struct PartialCloseContract {
1681 account_id: AccountId,
1683 contract_id: ContractId,
1685 size: i32,
1687}
1688
1689impl PartialCloseContract {
1690 pub fn new(
1697 account_id: AccountId,
1698 contract_id: ContractId,
1699 size: i32,
1700 ) -> Result<Self, RequestValidationError> {
1701 if size <= 0 {
1702 return Err(RequestValidationError::NonPositivePartialCloseSize);
1703 }
1704 Ok(Self {
1705 account_id,
1706 contract_id,
1707 size,
1708 })
1709 }
1710
1711 #[must_use]
1713 pub const fn account_id(&self) -> AccountId {
1714 self.account_id
1715 }
1716
1717 #[must_use]
1719 pub const fn contract_id(&self) -> &ContractId {
1720 &self.contract_id
1721 }
1722
1723 #[must_use]
1725 pub const fn size(&self) -> i32 {
1726 self.size
1727 }
1728}
1729
1730#[derive(Clone, Debug, Deserialize, PartialEq)]
1732#[non_exhaustive]
1733#[serde(rename_all = "camelCase")]
1734pub struct Position {
1735 pub id: PositionId,
1737 pub account_id: AccountId,
1739 pub contract_id: ContractId,
1741 #[serde(default)]
1743 pub contract_display_name: Option<String>,
1744 pub creation_timestamp: Timestamp,
1746 #[serde(rename = "type")]
1748 pub position_type: PositionType,
1749 pub size: i32,
1751 #[serde(with = "crate::decimal_serde")]
1753 pub average_price: Decimal,
1754}
1755
1756#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1758#[serde(rename_all = "camelCase")]
1759pub struct TradeSearch {
1760 account_id: AccountId,
1762 start_timestamp: Timestamp,
1764 #[serde(skip_serializing_if = "Option::is_none")]
1766 end_timestamp: Option<Timestamp>,
1767}
1768
1769#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1774#[serde(rename_all = "camelCase")]
1775pub struct TradeQuery {
1776 account_id: AccountId,
1778 #[serde(skip_serializing_if = "Option::is_none")]
1780 start_timestamp: Option<Timestamp>,
1781 #[serde(skip_serializing_if = "Option::is_none")]
1783 end_timestamp: Option<Timestamp>,
1784}
1785
1786impl TradeQuery {
1787 pub const fn builder(account_id: AccountId) -> TradeQueryBuilder {
1789 TradeQueryBuilder {
1790 account_id,
1791 start_timestamp: None,
1792 end_timestamp: None,
1793 }
1794 }
1795
1796 #[must_use]
1798 pub const fn account_id(&self) -> AccountId {
1799 self.account_id
1800 }
1801
1802 #[must_use]
1804 pub const fn start_timestamp(&self) -> Option<Timestamp> {
1805 self.start_timestamp
1806 }
1807
1808 #[must_use]
1810 pub const fn end_timestamp(&self) -> Option<Timestamp> {
1811 self.end_timestamp
1812 }
1813}
1814
1815#[derive(Clone, Copy, Debug)]
1817#[must_use = "a TradeQueryBuilder does nothing until build is called"]
1818pub struct TradeQueryBuilder {
1819 account_id: AccountId,
1820 start_timestamp: Option<Timestamp>,
1821 end_timestamp: Option<Timestamp>,
1822}
1823
1824impl TradeQueryBuilder {
1825 pub const fn start_timestamp(mut self, start_timestamp: Timestamp) -> Self {
1827 self.start_timestamp = Some(start_timestamp);
1828 self
1829 }
1830
1831 pub const fn end_timestamp(mut self, end_timestamp: Timestamp) -> Self {
1833 self.end_timestamp = Some(end_timestamp);
1834 self
1835 }
1836
1837 pub fn build(self) -> Result<TradeQuery, RequestValidationError> {
1844 validate_search_range(self.start_timestamp, self.end_timestamp)?;
1845 Ok(TradeQuery {
1846 account_id: self.account_id,
1847 start_timestamp: self.start_timestamp,
1848 end_timestamp: self.end_timestamp,
1849 })
1850 }
1851}
1852
1853impl TradeSearch {
1854 pub fn new(
1861 account_id: AccountId,
1862 start_timestamp: Timestamp,
1863 end_timestamp: Option<Timestamp>,
1864 ) -> Result<Self, RequestValidationError> {
1865 validate_search_range(Some(start_timestamp), end_timestamp)?;
1866 Ok(Self {
1867 account_id,
1868 start_timestamp,
1869 end_timestamp,
1870 })
1871 }
1872
1873 #[must_use]
1875 pub const fn account_id(&self) -> AccountId {
1876 self.account_id
1877 }
1878
1879 #[must_use]
1881 pub const fn start_timestamp(&self) -> Timestamp {
1882 self.start_timestamp
1883 }
1884
1885 #[must_use]
1887 pub const fn end_timestamp(&self) -> Option<Timestamp> {
1888 self.end_timestamp
1889 }
1890}
1891
1892fn validate_search_range(
1893 start_timestamp: Option<Timestamp>,
1894 end_timestamp: Option<Timestamp>,
1895) -> Result<(), RequestValidationError> {
1896 if start_timestamp
1897 .zip(end_timestamp)
1898 .is_some_and(|(start, end)| end <= start)
1899 {
1900 Err(RequestValidationError::SearchRangeNotIncreasing)
1901 } else {
1902 Ok(())
1903 }
1904}
1905
1906#[derive(Clone, Debug, Deserialize, PartialEq)]
1908#[non_exhaustive]
1909#[serde(rename_all = "camelCase")]
1910pub struct Trade {
1911 pub id: TradeId,
1913 pub account_id: AccountId,
1915 pub contract_id: ContractId,
1917 pub creation_timestamp: Timestamp,
1919 #[serde(with = "crate::decimal_serde")]
1921 pub price: Decimal,
1922 #[serde(default, with = "crate::decimal_serde::option")]
1924 pub profit_and_loss: Option<Decimal>,
1925 #[serde(with = "crate::decimal_serde")]
1927 pub fees: Decimal,
1928 #[serde(default, with = "crate::decimal_serde::option")]
1930 pub commissions: Option<Decimal>,
1931 pub side: Side,
1933 pub size: i32,
1935 pub voided: bool,
1937 pub order_id: OrderId,
1939}
1940
1941#[derive(Clone, Debug, Deserialize, PartialEq)]
1947#[non_exhaustive]
1948#[serde(rename_all = "camelCase")]
1949pub struct MarketQuote {
1950 #[serde(alias = "symbol")]
1952 pub raw_symbol: SymbolId,
1953 #[serde(default)]
1955 pub symbol_name: Option<String>,
1956 #[serde(default, with = "crate::decimal_serde::option")]
1958 pub last_price: Option<Decimal>,
1959 #[serde(default, with = "crate::decimal_serde::option")]
1961 pub best_bid: Option<Decimal>,
1962 #[serde(default, with = "crate::decimal_serde::option")]
1964 pub best_ask: Option<Decimal>,
1965 #[serde(default, with = "crate::decimal_serde::option")]
1967 pub change: Option<Decimal>,
1968 #[serde(default, with = "crate::decimal_serde::option")]
1970 pub change_percent: Option<Decimal>,
1971 #[serde(default, with = "crate::decimal_serde::option")]
1973 pub open: Option<Decimal>,
1974 #[serde(default, with = "crate::decimal_serde::option")]
1976 pub high: Option<Decimal>,
1977 #[serde(default, with = "crate::decimal_serde::option")]
1979 pub low: Option<Decimal>,
1980 #[serde(default)]
1982 pub volume: Option<i64>,
1983 pub last_updated: Timestamp,
1985 #[serde(default)]
1987 pub timestamp: Option<Timestamp>,
1988}
1989
1990#[derive(Clone, Debug, Deserialize, PartialEq)]
1992#[non_exhaustive]
1993#[serde(rename_all = "camelCase")]
1994pub struct MarketDepth {
1995 #[serde(default, alias = "symbolId")]
1997 pub symbol_id: Option<SymbolId>,
1998 pub timestamp: Timestamp,
2000 #[serde(rename = "type")]
2002 pub depth_type: DepthType,
2003 #[serde(with = "crate::decimal_serde")]
2005 pub price: Decimal,
2006 pub volume: i64,
2008 pub current_volume: i64,
2010 #[serde(default)]
2012 pub index: Option<i32>,
2013}
2014
2015#[derive(Clone, Debug, Deserialize, PartialEq)]
2017#[non_exhaustive]
2018#[serde(rename_all = "camelCase")]
2019pub struct MarketTrade {
2020 pub symbol_id: SymbolId,
2022 #[serde(with = "crate::decimal_serde")]
2024 pub price: Decimal,
2025 pub timestamp: Timestamp,
2027 #[serde(rename = "type")]
2029 pub trade_type: TradeLogType,
2030 pub volume: i64,
2032}
2033
2034#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2036#[non_exhaustive]
2037pub struct OperationResponse;
2038
2039#[derive(Debug)]
2045pub(crate) enum Envelope<T, R = EmptyBody> {
2046 Accepted(T),
2047 Rejected {
2048 error_code: i32,
2049 error_message: Option<String>,
2050 detail: R,
2051 },
2052 InconsistentStatus {
2053 success: bool,
2054 error_code: i32,
2055 },
2056}
2057
2058impl<'de, T, R> Deserialize<'de> for Envelope<T, R>
2059where
2060 T: serde::de::DeserializeOwned,
2061 R: serde::de::DeserializeOwned,
2062{
2063 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2064 where
2065 D: serde::Deserializer<'de>,
2066 {
2067 use serde::de::Error as _;
2068
2069 let mut object = BTreeMap::<String, Box<RawValue>>::deserialize(deserializer)?;
2070 let success = object
2071 .remove("success")
2072 .ok_or_else(|| D::Error::custom("provider response success flag is missing"))
2073 .and_then(|value| serde_json::from_str(value.get()).map_err(D::Error::custom))?;
2074 let error_code = object
2075 .remove("errorCode")
2076 .ok_or_else(|| D::Error::custom("provider response error code is missing"))
2077 .and_then(|value| serde_json::from_str(value.get()).map_err(D::Error::custom))?;
2078 let error_message = object.remove("errorMessage");
2079 if success != (error_code == 0) {
2080 return Ok(Self::InconsistentStatus {
2081 success,
2082 error_code,
2083 });
2084 }
2085 if !success {
2086 let error_message = error_message
2087 .map(|value| serde_json::from_str::<Option<String>>(value.get()))
2088 .transpose()
2089 .map_err(D::Error::custom)?
2090 .flatten();
2091 let detail = decode_remaining(object).map_err(D::Error::custom)?;
2092 return Ok(Self::Rejected {
2093 error_code,
2094 error_message,
2095 detail,
2096 });
2097 }
2098 let body = decode_remaining(object).map_err(D::Error::custom)?;
2099 Ok(Self::Accepted(body))
2100 }
2101}
2102
2103fn decode_remaining<T>(object: BTreeMap<String, Box<RawValue>>) -> serde_json::Result<T>
2105where
2106 T: serde::de::DeserializeOwned,
2107{
2108 let mut body_json = String::from("{");
2109 for (index, (key, value)) in object.into_iter().enumerate() {
2110 if index > 0 {
2111 body_json.push(',');
2112 }
2113 body_json.push_str(&serde_json::to_string(&key)?);
2114 body_json.push(':');
2115 body_json.push_str(value.get());
2116 }
2117 body_json.push('}');
2118 serde_json::from_str(&body_json)
2119}
2120
2121#[derive(Debug, Deserialize)]
2122pub(crate) struct AccountsBody {
2123 #[serde(default)]
2124 pub(crate) accounts: ProviderList<Account>,
2125}
2126
2127#[derive(Debug, Deserialize)]
2128pub(crate) struct ContractsBody {
2129 #[serde(default)]
2130 pub(crate) contracts: ProviderList<Contract>,
2131}
2132
2133#[derive(Debug, Deserialize)]
2134pub(crate) struct ContractBody {
2135 pub(crate) contract: Contract,
2136}
2137
2138#[derive(Debug, Deserialize)]
2139pub(crate) struct BarsBody {
2140 #[serde(default)]
2141 pub(crate) bars: ProviderList<Bar>,
2142}
2143
2144#[derive(Debug, Deserialize)]
2145pub(crate) struct OrdersBody {
2146 #[serde(default)]
2147 pub(crate) orders: ProviderList<Order>,
2148}
2149
2150#[derive(Debug, Deserialize)]
2151pub(crate) struct OrderBody {
2152 pub(crate) order: Order,
2153}
2154
2155#[derive(Debug, Deserialize)]
2156#[serde(rename_all = "camelCase")]
2157pub(crate) struct PlaceOrderBody {
2158 pub(crate) order_id: Option<OrderId>,
2159}
2160
2161#[derive(Debug, Deserialize)]
2162pub(crate) struct PositionsBody {
2163 #[serde(default)]
2164 pub(crate) positions: ProviderList<Position>,
2165}
2166
2167#[derive(Debug, Deserialize)]
2168pub(crate) struct TradesBody {
2169 #[serde(default)]
2170 pub(crate) trades: ProviderList<Trade>,
2171}
2172
2173#[derive(Debug, Deserialize)]
2174pub(crate) struct EmptyBody {}
2175
2176#[cfg(test)]
2177mod tests {
2178 use super::*;
2179
2180 macro_rules! assert_absent {
2181 ($body:ty, $field:ident, $json:literal) => {{
2182 let envelope: Envelope<$body> = serde_json::from_str($json)
2183 .unwrap_or_else(|error| panic!("fixture envelope must decode: {error}"));
2184 let Envelope::Accepted(body) = envelope else {
2185 panic!("fixture envelope must be accepted");
2186 };
2187 assert_eq!(body.$field, ProviderList::Absent);
2188 }};
2189 }
2190
2191 #[test]
2192 fn list_bodies_keep_explicit_lists_distinct_from_missing_and_null() {
2193 assert_absent!(
2194 AccountsBody,
2195 accounts,
2196 r#"{"success":true,"errorCode":0,"accounts":null}"#
2197 );
2198 assert_absent!(
2199 ContractsBody,
2200 contracts,
2201 r#"{"success":true,"errorCode":0}"#
2202 );
2203 assert_absent!(
2204 OrdersBody,
2205 orders,
2206 r#"{"success":true,"errorCode":0,"orders":null}"#
2207 );
2208 assert_absent!(
2209 OrderPage,
2210 orders,
2211 r#"{"success":true,"errorCode":0,"orders":null}"#
2212 );
2213 assert_absent!(
2214 PositionsBody,
2215 positions,
2216 r#"{"success":true,"errorCode":0}"#
2217 );
2218 assert_absent!(
2219 TradesBody,
2220 trades,
2221 r#"{"success":true,"errorCode":0,"trades":null}"#
2222 );
2223 }
2224
2225 #[test]
2226 fn list_bodies_list_explicit_arrays_including_the_empty_array() {
2227 assert_eq!(
2228 serde_json::from_str::<PositionsBody>(r#"{"positions":[]}"#)
2229 .unwrap_or_else(|error| panic!("explicit empty list must decode: {error}"))
2230 .positions,
2231 ProviderList::Listed(Vec::new())
2232 );
2233 }
2234
2235 #[test]
2236 fn rejected_envelope_does_not_require_an_endpoint_body() {
2237 let envelope: Envelope<AccountsBody> =
2238 serde_json::from_str(r#"{"success":false,"errorCode":17,"errorMessage":"synthetic"}"#)
2239 .unwrap_or_else(|error| panic!("rejection envelope must decode: {error}"));
2240
2241 assert!(matches!(
2242 envelope,
2243 Envelope::Rejected {
2244 error_code: 17,
2245 error_message: Some(ref message),
2246 ..
2247 } if message == "synthetic"
2248 ));
2249 }
2250
2251 #[test]
2252 fn rejected_envelope_reads_a_null_or_absent_message_as_none() {
2253 for json in [
2254 r#"{"success":false,"errorCode":17,"errorMessage":null}"#,
2255 r#"{"success":false,"errorCode":17}"#,
2256 ] {
2257 let envelope: Envelope<AccountsBody> = serde_json::from_str(json)
2258 .unwrap_or_else(|error| panic!("rejection envelope must decode: {error}"));
2259 assert!(matches!(
2260 envelope,
2261 Envelope::Rejected {
2262 error_code: 17,
2263 error_message: None,
2264 ..
2265 }
2266 ));
2267 }
2268 }
2269
2270 #[test]
2271 fn rejected_envelope_refuses_a_message_that_is_not_text() {
2272 assert!(
2273 serde_json::from_str::<Envelope<AccountsBody>>(
2274 r#"{"success":false,"errorCode":17,"errorMessage":17}"#
2275 )
2276 .is_err()
2277 );
2278 }
2279
2280 #[test]
2281 fn rejected_placement_envelope_reads_the_order_record() {
2282 let envelope: Envelope<PlaceOrderBody, PlaceOrderBody> =
2283 serde_json::from_str(r#"{"orderId":84,"success":false,"errorCode":2}"#)
2284 .unwrap_or_else(|error| panic!("placement rejection must decode: {error}"));
2285 let Envelope::Rejected { detail, .. } = envelope else {
2286 panic!("placement rejection must stay a rejection");
2287 };
2288 assert_eq!(detail.order_id.map(OrderId::get), Some(84));
2289 assert!(
2290 serde_json::from_str::<Envelope<PlaceOrderBody, PlaceOrderBody>>(
2291 r#"{"orderId":"not-an-id","success":false,"errorCode":2}"#
2292 )
2293 .is_err()
2294 );
2295 }
2296
2297 #[test]
2298 fn envelope_requires_a_consistent_provider_status() {
2299 assert!(
2300 serde_json::from_str::<Envelope<AccountsBody>>(r#"{"success":true,"accounts":[]}"#)
2301 .is_err()
2302 );
2303 for (json, success, error_code) in [
2304 (r#"{"success":true,"errorCode":17}"#, true, 17),
2305 (r#"{"success":false,"errorCode":0}"#, false, 0),
2306 ] {
2307 let envelope: Envelope<AccountsBody> = serde_json::from_str(json)
2308 .unwrap_or_else(|error| panic!("inconsistent envelope must decode: {error}"));
2309 assert!(matches!(
2310 envelope,
2311 Envelope::InconsistentStatus {
2312 success: actual_success,
2313 error_code: actual_error_code,
2314 } if actual_success == success && actual_error_code == error_code
2315 ));
2316 }
2317 }
2318}