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 not be zero")]
1153 NonPositiveBracketTicks,
1154 #[error("unsupported order type code {code}")]
1156 UnsupportedOrderType {
1157 code: i32,
1159 },
1160 #[error("unsupported order side code {code}")]
1162 UnsupportedOrderSide {
1163 code: i32,
1165 },
1166 #[error("unsupported order status code {code}")]
1168 UnsupportedOrderStatus {
1169 code: i32,
1171 },
1172 #[error("order-query page size must be positive")]
1174 NonPositiveOrderPageSize,
1175 #[error("order-query page offset must not be negative")]
1177 NegativeOrderPageOffset,
1178 #[error("historical-bar unit number must be positive")]
1180 NonPositiveHistoryUnitNumber,
1181 #[error("historical-bar limit must be between 1 and 20,000")]
1183 HistoryLimitOutOfRange,
1184 #[error("historical-bar end time must be later than its start time")]
1186 HistoryRangeNotIncreasing,
1187 #[error("search end time must be later than its start time")]
1189 SearchRangeNotIncreasing,
1190 #[error("partial-close size must be positive")]
1192 NonPositivePartialCloseSize,
1193}
1194
1195#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1209#[serde(rename_all = "camelCase")]
1210pub struct Bracket {
1211 ticks: i32,
1213 #[serde(rename = "type")]
1215 order_type: OrderType,
1216}
1217
1218impl Bracket {
1219 pub fn new(ticks: i32, order_type: OrderType) -> Result<Self, RequestValidationError> {
1226 if ticks == 0 {
1227 return Err(RequestValidationError::NonPositiveBracketTicks);
1228 }
1229 validate_request_order_type(order_type)?;
1230 Ok(Self { ticks, order_type })
1231 }
1232
1233 #[must_use]
1235 pub const fn ticks(&self) -> i32 {
1236 self.ticks
1237 }
1238
1239 #[must_use]
1241 pub const fn order_type(&self) -> OrderType {
1242 self.order_type
1243 }
1244}
1245
1246#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1252#[serde(rename_all = "camelCase")]
1253pub struct PlaceOrder {
1254 account_id: AccountId,
1256 contract_id: ContractId,
1258 #[serde(rename = "type")]
1260 order_type: OrderType,
1261 side: Side,
1263 size: i32,
1265 #[serde(
1267 skip_serializing_if = "Option::is_none",
1268 with = "crate::decimal_serde::option"
1269 )]
1270 limit_price: Option<Decimal>,
1271 #[serde(
1273 skip_serializing_if = "Option::is_none",
1274 with = "crate::decimal_serde::option"
1275 )]
1276 stop_price: Option<Decimal>,
1277 #[serde(
1279 skip_serializing_if = "Option::is_none",
1280 with = "crate::decimal_serde::option"
1281 )]
1282 trail_price: Option<Decimal>,
1283 #[serde(skip_serializing_if = "Option::is_none")]
1285 custom_tag: Option<String>,
1286 #[serde(skip_serializing_if = "Option::is_none")]
1288 stop_loss_bracket: Option<Bracket>,
1289 #[serde(skip_serializing_if = "Option::is_none")]
1291 take_profit_bracket: Option<Bracket>,
1292}
1293
1294impl PlaceOrder {
1295 pub fn builder(
1297 account_id: AccountId,
1298 contract_id: ContractId,
1299 order_type: OrderType,
1300 side: Side,
1301 quantity: i32,
1302 ) -> PlaceOrderBuilder {
1303 PlaceOrderBuilder {
1304 account_id,
1305 contract_id,
1306 order_type,
1307 side,
1308 size: quantity,
1309 limit_price: None,
1310 stop_price: None,
1311 trail_price: None,
1312 custom_tag: None,
1313 stop_loss_bracket: None,
1314 take_profit_bracket: None,
1315 }
1316 }
1317
1318 #[must_use]
1320 pub const fn account_id(&self) -> AccountId {
1321 self.account_id
1322 }
1323
1324 #[must_use]
1326 pub const fn contract_id(&self) -> &ContractId {
1327 &self.contract_id
1328 }
1329
1330 #[must_use]
1332 pub const fn order_type(&self) -> OrderType {
1333 self.order_type
1334 }
1335
1336 #[must_use]
1338 pub const fn side(&self) -> Side {
1339 self.side
1340 }
1341
1342 #[must_use]
1344 pub const fn size(&self) -> i32 {
1345 self.size
1346 }
1347
1348 #[must_use]
1350 pub const fn limit_price(&self) -> Option<Decimal> {
1351 self.limit_price
1352 }
1353
1354 #[must_use]
1356 pub const fn stop_price(&self) -> Option<Decimal> {
1357 self.stop_price
1358 }
1359
1360 #[must_use]
1364 pub const fn trail_price(&self) -> Option<Decimal> {
1365 self.trail_price
1366 }
1367
1368 #[must_use]
1370 pub fn custom_tag(&self) -> Option<&str> {
1371 self.custom_tag.as_deref()
1372 }
1373
1374 #[must_use]
1376 pub const fn stop_loss_bracket(&self) -> Option<&Bracket> {
1377 self.stop_loss_bracket.as_ref()
1378 }
1379
1380 #[must_use]
1382 pub const fn take_profit_bracket(&self) -> Option<&Bracket> {
1383 self.take_profit_bracket.as_ref()
1384 }
1385}
1386
1387#[derive(Clone, Debug)]
1389#[must_use = "a PlaceOrderBuilder does nothing until build is called"]
1390pub struct PlaceOrderBuilder {
1391 account_id: AccountId,
1392 contract_id: ContractId,
1393 order_type: OrderType,
1394 side: Side,
1395 size: i32,
1396 limit_price: Option<Decimal>,
1397 stop_price: Option<Decimal>,
1398 trail_price: Option<Decimal>,
1399 custom_tag: Option<String>,
1400 stop_loss_bracket: Option<Bracket>,
1401 take_profit_bracket: Option<Bracket>,
1402}
1403
1404impl PlaceOrderBuilder {
1405 pub const fn limit_price(mut self, limit_price: Decimal) -> Self {
1407 self.limit_price = Some(limit_price);
1408 self
1409 }
1410
1411 pub const fn stop_price(mut self, stop_price: Decimal) -> Self {
1413 self.stop_price = Some(stop_price);
1414 self
1415 }
1416
1417 pub const fn trail_price(mut self, trail_price: Decimal) -> Self {
1425 self.trail_price = Some(trail_price);
1426 self
1427 }
1428
1429 pub fn custom_tag(mut self, custom_tag: impl Into<String>) -> Self {
1431 self.custom_tag = Some(custom_tag.into());
1432 self
1433 }
1434
1435 pub fn stop_loss_bracket(mut self, stop_loss_bracket: Bracket) -> Self {
1439 self.stop_loss_bracket = Some(stop_loss_bracket);
1440 self
1441 }
1442
1443 pub fn take_profit_bracket(mut self, take_profit_bracket: Bracket) -> Self {
1447 self.take_profit_bracket = Some(take_profit_bracket);
1448 self
1449 }
1450
1451 pub fn build(self) -> Result<PlaceOrder, RequestValidationError> {
1461 if self.size <= 0 {
1462 return Err(RequestValidationError::NonPositiveOrderSize);
1463 }
1464 validate_request_order_type(self.order_type)?;
1465 if let Side::Unknown(code) = self.side {
1466 return Err(RequestValidationError::UnsupportedOrderSide { code });
1467 }
1468 if self.order_type == OrderType::TrailingStop && self.trail_price.is_none() {
1469 return Err(RequestValidationError::MissingTrailPrice);
1470 }
1471 Ok(PlaceOrder {
1472 account_id: self.account_id,
1473 contract_id: self.contract_id,
1474 order_type: self.order_type,
1475 side: self.side,
1476 size: self.size,
1477 limit_price: self.limit_price,
1478 stop_price: self.stop_price,
1479 trail_price: self.trail_price,
1480 custom_tag: self.custom_tag,
1481 stop_loss_bracket: self.stop_loss_bracket,
1482 take_profit_bracket: self.take_profit_bracket,
1483 })
1484 }
1485}
1486
1487fn validate_request_order_type(order_type: OrderType) -> Result<(), RequestValidationError> {
1488 match order_type {
1489 OrderType::Limit
1490 | OrderType::Market
1491 | OrderType::Stop
1492 | OrderType::TrailingStop
1493 | OrderType::JoinBid
1494 | OrderType::JoinAsk => Ok(()),
1495 OrderType::StopLimit => Err(RequestValidationError::UnsupportedOrderType { code: 3 }),
1496 OrderType::Unknown(code) => Err(RequestValidationError::UnsupportedOrderType { code }),
1497 }
1498}
1499
1500#[derive(Clone, Debug, Eq, PartialEq)]
1502#[non_exhaustive]
1503pub struct OrderResponse {
1504 pub order_id: OrderId,
1506}
1507
1508#[derive(Clone, Debug, Serialize)]
1510#[serde(rename_all = "camelCase")]
1511pub struct CancelOrder {
1512 pub account_id: AccountId,
1514 pub order_id: OrderId,
1516}
1517
1518#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1523#[serde(rename_all = "camelCase")]
1524pub struct ModifyOrder {
1525 account_id: AccountId,
1527 order_id: OrderId,
1529 #[serde(skip_serializing_if = "Option::is_none")]
1531 size: Option<i32>,
1532 #[serde(
1534 skip_serializing_if = "Option::is_none",
1535 with = "crate::decimal_serde::option"
1536 )]
1537 limit_price: Option<Decimal>,
1538 #[serde(
1540 skip_serializing_if = "Option::is_none",
1541 with = "crate::decimal_serde::option"
1542 )]
1543 stop_price: Option<Decimal>,
1544 #[serde(
1546 skip_serializing_if = "Option::is_none",
1547 with = "crate::decimal_serde::option"
1548 )]
1549 trail_price: Option<Decimal>,
1550}
1551
1552impl ModifyOrder {
1553 pub const fn builder(account_id: AccountId, order_id: OrderId) -> ModifyOrderBuilder {
1555 ModifyOrderBuilder {
1556 account_id,
1557 order_id,
1558 size: None,
1559 limit_price: None,
1560 stop_price: None,
1561 trail_price: None,
1562 }
1563 }
1564
1565 #[must_use]
1567 pub const fn account_id(&self) -> AccountId {
1568 self.account_id
1569 }
1570
1571 #[must_use]
1573 pub const fn order_id(&self) -> OrderId {
1574 self.order_id
1575 }
1576
1577 #[must_use]
1579 pub const fn size(&self) -> Option<i32> {
1580 self.size
1581 }
1582
1583 #[must_use]
1585 pub const fn limit_price(&self) -> Option<Decimal> {
1586 self.limit_price
1587 }
1588
1589 #[must_use]
1591 pub const fn stop_price(&self) -> Option<Decimal> {
1592 self.stop_price
1593 }
1594
1595 #[must_use]
1599 pub const fn trail_price(&self) -> Option<Decimal> {
1600 self.trail_price
1601 }
1602}
1603
1604#[derive(Clone, Copy, Debug)]
1606#[must_use = "a ModifyOrderBuilder does nothing until build is called"]
1607pub struct ModifyOrderBuilder {
1608 account_id: AccountId,
1609 order_id: OrderId,
1610 size: Option<i32>,
1611 limit_price: Option<Decimal>,
1612 stop_price: Option<Decimal>,
1613 trail_price: Option<Decimal>,
1614}
1615
1616impl ModifyOrderBuilder {
1617 pub const fn size(mut self, size: i32) -> Self {
1619 self.size = Some(size);
1620 self
1621 }
1622
1623 pub const fn limit_price(mut self, limit_price: Decimal) -> Self {
1625 self.limit_price = Some(limit_price);
1626 self
1627 }
1628
1629 pub const fn stop_price(mut self, stop_price: Decimal) -> Self {
1631 self.stop_price = Some(stop_price);
1632 self
1633 }
1634
1635 pub const fn trail_price(mut self, trail_price: Decimal) -> Self {
1644 self.trail_price = Some(trail_price);
1645 self
1646 }
1647
1648 pub fn build(self) -> Result<ModifyOrder, RequestValidationError> {
1657 if self.size.is_some_and(|size| size <= 0) {
1658 return Err(RequestValidationError::NonPositiveReplacementSize);
1659 }
1660 if self.size.is_none()
1661 && self.limit_price.is_none()
1662 && self.stop_price.is_none()
1663 && self.trail_price.is_none()
1664 {
1665 return Err(RequestValidationError::EmptyModification);
1666 }
1667 Ok(ModifyOrder {
1668 account_id: self.account_id,
1669 order_id: self.order_id,
1670 size: self.size,
1671 limit_price: self.limit_price,
1672 stop_price: self.stop_price,
1673 trail_price: self.trail_price,
1674 })
1675 }
1676}
1677
1678#[derive(Clone, Debug, Serialize)]
1680#[serde(rename_all = "camelCase")]
1681pub struct CloseContract {
1682 pub account_id: AccountId,
1684 pub contract_id: ContractId,
1686}
1687
1688#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1690#[serde(rename_all = "camelCase")]
1691pub struct PartialCloseContract {
1692 account_id: AccountId,
1694 contract_id: ContractId,
1696 size: i32,
1698}
1699
1700impl PartialCloseContract {
1701 pub fn new(
1708 account_id: AccountId,
1709 contract_id: ContractId,
1710 size: i32,
1711 ) -> Result<Self, RequestValidationError> {
1712 if size <= 0 {
1713 return Err(RequestValidationError::NonPositivePartialCloseSize);
1714 }
1715 Ok(Self {
1716 account_id,
1717 contract_id,
1718 size,
1719 })
1720 }
1721
1722 #[must_use]
1724 pub const fn account_id(&self) -> AccountId {
1725 self.account_id
1726 }
1727
1728 #[must_use]
1730 pub const fn contract_id(&self) -> &ContractId {
1731 &self.contract_id
1732 }
1733
1734 #[must_use]
1736 pub const fn size(&self) -> i32 {
1737 self.size
1738 }
1739}
1740
1741#[derive(Clone, Debug, Deserialize, PartialEq)]
1743#[non_exhaustive]
1744#[serde(rename_all = "camelCase")]
1745pub struct Position {
1746 pub id: PositionId,
1748 pub account_id: AccountId,
1750 pub contract_id: ContractId,
1752 #[serde(default)]
1754 pub contract_display_name: Option<String>,
1755 pub creation_timestamp: Timestamp,
1757 #[serde(rename = "type")]
1759 pub position_type: PositionType,
1760 pub size: i32,
1762 #[serde(with = "crate::decimal_serde")]
1764 pub average_price: Decimal,
1765}
1766
1767#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1769#[serde(rename_all = "camelCase")]
1770pub struct TradeSearch {
1771 account_id: AccountId,
1773 start_timestamp: Timestamp,
1775 #[serde(skip_serializing_if = "Option::is_none")]
1777 end_timestamp: Option<Timestamp>,
1778}
1779
1780#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1785#[serde(rename_all = "camelCase")]
1786pub struct TradeQuery {
1787 account_id: AccountId,
1789 #[serde(skip_serializing_if = "Option::is_none")]
1791 start_timestamp: Option<Timestamp>,
1792 #[serde(skip_serializing_if = "Option::is_none")]
1794 end_timestamp: Option<Timestamp>,
1795}
1796
1797impl TradeQuery {
1798 pub const fn builder(account_id: AccountId) -> TradeQueryBuilder {
1800 TradeQueryBuilder {
1801 account_id,
1802 start_timestamp: None,
1803 end_timestamp: None,
1804 }
1805 }
1806
1807 #[must_use]
1809 pub const fn account_id(&self) -> AccountId {
1810 self.account_id
1811 }
1812
1813 #[must_use]
1815 pub const fn start_timestamp(&self) -> Option<Timestamp> {
1816 self.start_timestamp
1817 }
1818
1819 #[must_use]
1821 pub const fn end_timestamp(&self) -> Option<Timestamp> {
1822 self.end_timestamp
1823 }
1824}
1825
1826#[derive(Clone, Copy, Debug)]
1828#[must_use = "a TradeQueryBuilder does nothing until build is called"]
1829pub struct TradeQueryBuilder {
1830 account_id: AccountId,
1831 start_timestamp: Option<Timestamp>,
1832 end_timestamp: Option<Timestamp>,
1833}
1834
1835impl TradeQueryBuilder {
1836 pub const fn start_timestamp(mut self, start_timestamp: Timestamp) -> Self {
1838 self.start_timestamp = Some(start_timestamp);
1839 self
1840 }
1841
1842 pub const fn end_timestamp(mut self, end_timestamp: Timestamp) -> Self {
1844 self.end_timestamp = Some(end_timestamp);
1845 self
1846 }
1847
1848 pub fn build(self) -> Result<TradeQuery, RequestValidationError> {
1855 validate_search_range(self.start_timestamp, self.end_timestamp)?;
1856 Ok(TradeQuery {
1857 account_id: self.account_id,
1858 start_timestamp: self.start_timestamp,
1859 end_timestamp: self.end_timestamp,
1860 })
1861 }
1862}
1863
1864impl TradeSearch {
1865 pub fn new(
1872 account_id: AccountId,
1873 start_timestamp: Timestamp,
1874 end_timestamp: Option<Timestamp>,
1875 ) -> Result<Self, RequestValidationError> {
1876 validate_search_range(Some(start_timestamp), end_timestamp)?;
1877 Ok(Self {
1878 account_id,
1879 start_timestamp,
1880 end_timestamp,
1881 })
1882 }
1883
1884 #[must_use]
1886 pub const fn account_id(&self) -> AccountId {
1887 self.account_id
1888 }
1889
1890 #[must_use]
1892 pub const fn start_timestamp(&self) -> Timestamp {
1893 self.start_timestamp
1894 }
1895
1896 #[must_use]
1898 pub const fn end_timestamp(&self) -> Option<Timestamp> {
1899 self.end_timestamp
1900 }
1901}
1902
1903fn validate_search_range(
1904 start_timestamp: Option<Timestamp>,
1905 end_timestamp: Option<Timestamp>,
1906) -> Result<(), RequestValidationError> {
1907 if start_timestamp
1908 .zip(end_timestamp)
1909 .is_some_and(|(start, end)| end <= start)
1910 {
1911 Err(RequestValidationError::SearchRangeNotIncreasing)
1912 } else {
1913 Ok(())
1914 }
1915}
1916
1917#[derive(Clone, Debug, Deserialize, PartialEq)]
1919#[non_exhaustive]
1920#[serde(rename_all = "camelCase")]
1921pub struct Trade {
1922 pub id: TradeId,
1924 pub account_id: AccountId,
1926 pub contract_id: ContractId,
1928 pub creation_timestamp: Timestamp,
1930 #[serde(with = "crate::decimal_serde")]
1932 pub price: Decimal,
1933 #[serde(default, with = "crate::decimal_serde::option")]
1935 pub profit_and_loss: Option<Decimal>,
1936 #[serde(with = "crate::decimal_serde")]
1938 pub fees: Decimal,
1939 #[serde(default, with = "crate::decimal_serde::option")]
1941 pub commissions: Option<Decimal>,
1942 pub side: Side,
1944 pub size: i32,
1946 pub voided: bool,
1948 pub order_id: OrderId,
1950}
1951
1952#[derive(Clone, Debug, Deserialize, PartialEq)]
1958#[non_exhaustive]
1959#[serde(rename_all = "camelCase")]
1960pub struct MarketQuote {
1961 #[serde(alias = "symbol")]
1963 pub raw_symbol: SymbolId,
1964 #[serde(default)]
1966 pub symbol_name: Option<String>,
1967 #[serde(default, with = "crate::decimal_serde::option")]
1969 pub last_price: Option<Decimal>,
1970 #[serde(default, with = "crate::decimal_serde::option")]
1972 pub best_bid: Option<Decimal>,
1973 #[serde(default, with = "crate::decimal_serde::option")]
1975 pub best_ask: Option<Decimal>,
1976 #[serde(default, with = "crate::decimal_serde::option")]
1978 pub change: Option<Decimal>,
1979 #[serde(default, with = "crate::decimal_serde::option")]
1981 pub change_percent: Option<Decimal>,
1982 #[serde(default, with = "crate::decimal_serde::option")]
1984 pub open: Option<Decimal>,
1985 #[serde(default, with = "crate::decimal_serde::option")]
1987 pub high: Option<Decimal>,
1988 #[serde(default, with = "crate::decimal_serde::option")]
1990 pub low: Option<Decimal>,
1991 #[serde(default)]
1993 pub volume: Option<i64>,
1994 pub last_updated: Timestamp,
1996 #[serde(default)]
1998 pub timestamp: Option<Timestamp>,
1999}
2000
2001#[derive(Clone, Debug, Deserialize, PartialEq)]
2003#[non_exhaustive]
2004#[serde(rename_all = "camelCase")]
2005pub struct MarketDepth {
2006 #[serde(default, alias = "symbolId")]
2008 pub symbol_id: Option<SymbolId>,
2009 pub timestamp: Timestamp,
2011 #[serde(rename = "type")]
2013 pub depth_type: DepthType,
2014 #[serde(with = "crate::decimal_serde")]
2016 pub price: Decimal,
2017 pub volume: i64,
2019 pub current_volume: i64,
2021 #[serde(default)]
2023 pub index: Option<i32>,
2024}
2025
2026#[derive(Clone, Debug, Deserialize, PartialEq)]
2028#[non_exhaustive]
2029#[serde(rename_all = "camelCase")]
2030pub struct MarketTrade {
2031 pub symbol_id: SymbolId,
2033 #[serde(with = "crate::decimal_serde")]
2035 pub price: Decimal,
2036 pub timestamp: Timestamp,
2038 #[serde(rename = "type")]
2040 pub trade_type: TradeLogType,
2041 pub volume: i64,
2043}
2044
2045#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2047#[non_exhaustive]
2048pub struct OperationResponse;
2049
2050#[derive(Debug)]
2056pub(crate) enum Envelope<T, R = EmptyBody> {
2057 Accepted(T),
2058 Rejected {
2059 error_code: i32,
2060 error_message: Option<String>,
2061 detail: R,
2062 },
2063 InconsistentStatus {
2064 success: bool,
2065 error_code: i32,
2066 },
2067}
2068
2069impl<'de, T, R> Deserialize<'de> for Envelope<T, R>
2070where
2071 T: serde::de::DeserializeOwned,
2072 R: serde::de::DeserializeOwned,
2073{
2074 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2075 where
2076 D: serde::Deserializer<'de>,
2077 {
2078 use serde::de::Error as _;
2079
2080 let mut object = BTreeMap::<String, Box<RawValue>>::deserialize(deserializer)?;
2081 let success = object
2082 .remove("success")
2083 .ok_or_else(|| D::Error::custom("provider response success flag is missing"))
2084 .and_then(|value| serde_json::from_str(value.get()).map_err(D::Error::custom))?;
2085 let error_code = object
2086 .remove("errorCode")
2087 .ok_or_else(|| D::Error::custom("provider response error code is missing"))
2088 .and_then(|value| serde_json::from_str(value.get()).map_err(D::Error::custom))?;
2089 let error_message = object.remove("errorMessage");
2090 if success != (error_code == 0) {
2091 return Ok(Self::InconsistentStatus {
2092 success,
2093 error_code,
2094 });
2095 }
2096 if !success {
2097 let error_message = error_message
2098 .map(|value| serde_json::from_str::<Option<String>>(value.get()))
2099 .transpose()
2100 .map_err(D::Error::custom)?
2101 .flatten();
2102 let detail = decode_remaining(object).map_err(D::Error::custom)?;
2103 return Ok(Self::Rejected {
2104 error_code,
2105 error_message,
2106 detail,
2107 });
2108 }
2109 let body = decode_remaining(object).map_err(D::Error::custom)?;
2110 Ok(Self::Accepted(body))
2111 }
2112}
2113
2114fn decode_remaining<T>(object: BTreeMap<String, Box<RawValue>>) -> serde_json::Result<T>
2116where
2117 T: serde::de::DeserializeOwned,
2118{
2119 let mut body_json = String::from("{");
2120 for (index, (key, value)) in object.into_iter().enumerate() {
2121 if index > 0 {
2122 body_json.push(',');
2123 }
2124 body_json.push_str(&serde_json::to_string(&key)?);
2125 body_json.push(':');
2126 body_json.push_str(value.get());
2127 }
2128 body_json.push('}');
2129 serde_json::from_str(&body_json)
2130}
2131
2132#[derive(Debug, Deserialize)]
2133pub(crate) struct AccountsBody {
2134 #[serde(default)]
2135 pub(crate) accounts: ProviderList<Account>,
2136}
2137
2138#[derive(Debug, Deserialize)]
2139pub(crate) struct ContractsBody {
2140 #[serde(default)]
2141 pub(crate) contracts: ProviderList<Contract>,
2142}
2143
2144#[derive(Debug, Deserialize)]
2145pub(crate) struct ContractBody {
2146 pub(crate) contract: Contract,
2147}
2148
2149#[derive(Debug, Deserialize)]
2150pub(crate) struct BarsBody {
2151 #[serde(default)]
2152 pub(crate) bars: ProviderList<Bar>,
2153}
2154
2155#[derive(Debug, Deserialize)]
2156pub(crate) struct OrdersBody {
2157 #[serde(default)]
2158 pub(crate) orders: ProviderList<Order>,
2159}
2160
2161#[derive(Debug, Deserialize)]
2162pub(crate) struct OrderBody {
2163 pub(crate) order: Order,
2164}
2165
2166#[derive(Debug, Deserialize)]
2167#[serde(rename_all = "camelCase")]
2168pub(crate) struct PlaceOrderBody {
2169 pub(crate) order_id: Option<OrderId>,
2170}
2171
2172#[derive(Debug, Deserialize)]
2173pub(crate) struct PositionsBody {
2174 #[serde(default)]
2175 pub(crate) positions: ProviderList<Position>,
2176}
2177
2178#[derive(Debug, Deserialize)]
2179pub(crate) struct TradesBody {
2180 #[serde(default)]
2181 pub(crate) trades: ProviderList<Trade>,
2182}
2183
2184#[derive(Debug, Deserialize)]
2185pub(crate) struct EmptyBody {}
2186
2187#[cfg(test)]
2188mod tests {
2189 use super::*;
2190
2191 macro_rules! assert_absent {
2192 ($body:ty, $field:ident, $json:literal) => {{
2193 let envelope: Envelope<$body> = serde_json::from_str($json)
2194 .unwrap_or_else(|error| panic!("fixture envelope must decode: {error}"));
2195 let Envelope::Accepted(body) = envelope else {
2196 panic!("fixture envelope must be accepted");
2197 };
2198 assert_eq!(body.$field, ProviderList::Absent);
2199 }};
2200 }
2201
2202 #[test]
2203 fn list_bodies_keep_explicit_lists_distinct_from_missing_and_null() {
2204 assert_absent!(
2205 AccountsBody,
2206 accounts,
2207 r#"{"success":true,"errorCode":0,"accounts":null}"#
2208 );
2209 assert_absent!(
2210 ContractsBody,
2211 contracts,
2212 r#"{"success":true,"errorCode":0}"#
2213 );
2214 assert_absent!(
2215 OrdersBody,
2216 orders,
2217 r#"{"success":true,"errorCode":0,"orders":null}"#
2218 );
2219 assert_absent!(
2220 OrderPage,
2221 orders,
2222 r#"{"success":true,"errorCode":0,"orders":null}"#
2223 );
2224 assert_absent!(
2225 PositionsBody,
2226 positions,
2227 r#"{"success":true,"errorCode":0}"#
2228 );
2229 assert_absent!(
2230 TradesBody,
2231 trades,
2232 r#"{"success":true,"errorCode":0,"trades":null}"#
2233 );
2234 }
2235
2236 #[test]
2237 fn list_bodies_list_explicit_arrays_including_the_empty_array() {
2238 assert_eq!(
2239 serde_json::from_str::<PositionsBody>(r#"{"positions":[]}"#)
2240 .unwrap_or_else(|error| panic!("explicit empty list must decode: {error}"))
2241 .positions,
2242 ProviderList::Listed(Vec::new())
2243 );
2244 }
2245
2246 #[test]
2247 fn rejected_envelope_does_not_require_an_endpoint_body() {
2248 let envelope: Envelope<AccountsBody> =
2249 serde_json::from_str(r#"{"success":false,"errorCode":17,"errorMessage":"synthetic"}"#)
2250 .unwrap_or_else(|error| panic!("rejection envelope must decode: {error}"));
2251
2252 assert!(matches!(
2253 envelope,
2254 Envelope::Rejected {
2255 error_code: 17,
2256 error_message: Some(ref message),
2257 ..
2258 } if message == "synthetic"
2259 ));
2260 }
2261
2262 #[test]
2263 fn rejected_envelope_reads_a_null_or_absent_message_as_none() {
2264 for json in [
2265 r#"{"success":false,"errorCode":17,"errorMessage":null}"#,
2266 r#"{"success":false,"errorCode":17}"#,
2267 ] {
2268 let envelope: Envelope<AccountsBody> = serde_json::from_str(json)
2269 .unwrap_or_else(|error| panic!("rejection envelope must decode: {error}"));
2270 assert!(matches!(
2271 envelope,
2272 Envelope::Rejected {
2273 error_code: 17,
2274 error_message: None,
2275 ..
2276 }
2277 ));
2278 }
2279 }
2280
2281 #[test]
2282 fn rejected_envelope_refuses_a_message_that_is_not_text() {
2283 assert!(
2284 serde_json::from_str::<Envelope<AccountsBody>>(
2285 r#"{"success":false,"errorCode":17,"errorMessage":17}"#
2286 )
2287 .is_err()
2288 );
2289 }
2290
2291 #[test]
2292 fn rejected_placement_envelope_reads_the_order_record() {
2293 let envelope: Envelope<PlaceOrderBody, PlaceOrderBody> =
2294 serde_json::from_str(r#"{"orderId":84,"success":false,"errorCode":2}"#)
2295 .unwrap_or_else(|error| panic!("placement rejection must decode: {error}"));
2296 let Envelope::Rejected { detail, .. } = envelope else {
2297 panic!("placement rejection must stay a rejection");
2298 };
2299 assert_eq!(detail.order_id.map(OrderId::get), Some(84));
2300 assert!(
2301 serde_json::from_str::<Envelope<PlaceOrderBody, PlaceOrderBody>>(
2302 r#"{"orderId":"not-an-id","success":false,"errorCode":2}"#
2303 )
2304 .is_err()
2305 );
2306 }
2307
2308 #[test]
2309 fn envelope_requires_a_consistent_provider_status() {
2310 assert!(
2311 serde_json::from_str::<Envelope<AccountsBody>>(r#"{"success":true,"accounts":[]}"#)
2312 .is_err()
2313 );
2314 for (json, success, error_code) in [
2315 (r#"{"success":true,"errorCode":17}"#, true, 17),
2316 (r#"{"success":false,"errorCode":0}"#, false, 0),
2317 ] {
2318 let envelope: Envelope<AccountsBody> = serde_json::from_str(json)
2319 .unwrap_or_else(|error| panic!("inconsistent envelope must decode: {error}"));
2320 assert!(matches!(
2321 envelope,
2322 Envelope::InconsistentStatus {
2323 success: actual_success,
2324 error_code: actual_error_code,
2325 } if actual_success == success && actual_error_code == error_code
2326 ));
2327 }
2328 }
2329}