1use std::marker::PhantomData;
44
45use rust_decimal::Decimal;
46use serde::Serialize;
47
48use crate::accounts::AssetType;
49use crate::error::Error;
50use crate::orders::enums::{
51 ComplexOrderStrategyType, Duration, Instruction, OrderStrategyType, OrderType, PositionEffect,
52 PriceLinkBasis, PriceLinkType, QuantityType, Session, SpecialInstruction, StopPriceLinkBasis,
53 StopPriceLinkType, StopType, TaxLotMethod,
54};
55use crate::orders::response::{Order, OrderLegCollection};
56
57pub trait IntoQuantity: sealed::Sealed {
68 fn into_quantity(self) -> Decimal;
70}
71
72impl IntoQuantity for Decimal {
73 fn into_quantity(self) -> Decimal {
74 self
75 }
76}
77
78macro_rules! impl_into_quantity_int {
79 ($($t:ty),* $(,)?) => {
80 $(
81 impl sealed::Sealed for $t {}
82 impl IntoQuantity for $t {
83 fn into_quantity(self) -> Decimal {
84 Decimal::from(self)
85 }
86 }
87 )*
88 };
89}
90
91impl_into_quantity_int!(u8, u16, u32, u64, i8, i16, i32, i64);
92
93mod decimal_opt {
98 use rust_decimal::Decimal;
99 use serde::{Serialize, Serializer};
100
101 pub fn serialize<S: Serializer>(value: &Option<Decimal>, s: S) -> Result<S::Ok, S::Error> {
102 match value {
103 Some(d) => {
104 let n: serde_json::Number =
105 d.to_string().parse().map_err(serde::ser::Error::custom)?;
106 n.serialize(s)
107 }
108 None => s.serialize_none(),
109 }
110 }
111}
112
113#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)]
123#[non_exhaustive]
124pub struct OrderRequest {
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub(crate) session: Option<Session>,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub(crate) duration: Option<Duration>,
129 #[serde(rename = "orderType", skip_serializing_if = "Option::is_none")]
130 pub(crate) order_type: Option<OrderType>,
131 #[serde(
132 rename = "complexOrderStrategyType",
133 skip_serializing_if = "Option::is_none"
134 )]
135 pub(crate) complex_order_strategy_type: Option<ComplexOrderStrategyType>,
136 #[serde(skip_serializing_if = "Option::is_none", with = "decimal_opt")]
137 pub(crate) quantity: Option<Decimal>,
138 #[serde(
139 rename = "destinationLinkName",
140 skip_serializing_if = "Option::is_none"
141 )]
142 pub(crate) destination_link_name: Option<String>,
143 #[serde(
144 rename = "stopPrice",
145 skip_serializing_if = "Option::is_none",
146 with = "decimal_opt"
147 )]
148 pub(crate) stop_price: Option<Decimal>,
149 #[serde(rename = "stopPriceLinkBasis", skip_serializing_if = "Option::is_none")]
150 pub(crate) stop_price_link_basis: Option<StopPriceLinkBasis>,
151 #[serde(rename = "stopPriceLinkType", skip_serializing_if = "Option::is_none")]
152 pub(crate) stop_price_link_type: Option<StopPriceLinkType>,
153 #[serde(
154 rename = "stopPriceOffset",
155 skip_serializing_if = "Option::is_none",
156 with = "decimal_opt"
157 )]
158 pub(crate) stop_price_offset: Option<Decimal>,
159 #[serde(rename = "stopType", skip_serializing_if = "Option::is_none")]
160 pub(crate) stop_type: Option<StopType>,
161 #[serde(rename = "priceLinkBasis", skip_serializing_if = "Option::is_none")]
162 pub(crate) price_link_basis: Option<PriceLinkBasis>,
163 #[serde(rename = "priceLinkType", skip_serializing_if = "Option::is_none")]
164 pub(crate) price_link_type: Option<PriceLinkType>,
165 #[serde(skip_serializing_if = "Option::is_none", with = "decimal_opt")]
166 pub(crate) price: Option<Decimal>,
167 #[serde(rename = "taxLotMethod", skip_serializing_if = "Option::is_none")]
168 pub(crate) tax_lot_method: Option<TaxLotMethod>,
169 #[serde(rename = "orderLegCollection", skip_serializing_if = "Vec::is_empty")]
170 pub(crate) order_leg_collection: Vec<OrderLegRequest>,
171 #[serde(
172 rename = "activationPrice",
173 skip_serializing_if = "Option::is_none",
174 with = "decimal_opt"
175 )]
176 pub(crate) activation_price: Option<Decimal>,
177 #[serde(rename = "specialInstruction", skip_serializing_if = "Option::is_none")]
178 pub(crate) special_instruction: Option<SpecialInstruction>,
179 #[serde(rename = "orderStrategyType", skip_serializing_if = "Option::is_none")]
180 pub(crate) order_strategy_type: Option<OrderStrategyType>,
181 #[serde(rename = "childOrderStrategies", skip_serializing_if = "Vec::is_empty")]
182 pub(crate) child_order_strategies: Vec<OrderRequest>,
183}
184
185impl OrderRequest {
186 pub(crate) fn empty() -> Self {
187 Self {
188 session: None,
189 duration: None,
190 order_type: None,
191 complex_order_strategy_type: None,
192 quantity: None,
193 destination_link_name: None,
194 stop_price: None,
195 stop_price_link_basis: None,
196 stop_price_link_type: None,
197 stop_price_offset: None,
198 stop_type: None,
199 price_link_basis: None,
200 price_link_type: None,
201 price: None,
202 tax_lot_method: None,
203 order_leg_collection: Vec::new(),
204 activation_price: None,
205 special_instruction: None,
206 order_strategy_type: None,
207 child_order_strategies: Vec::new(),
208 }
209 }
210}
211
212impl OrderRequest {
213 pub fn session(&self) -> Option<&Session> {
215 self.session.as_ref()
216 }
217
218 pub fn duration(&self) -> Option<&Duration> {
220 self.duration.as_ref()
221 }
222
223 pub fn order_type(&self) -> Option<&OrderType> {
225 self.order_type.as_ref()
226 }
227
228 pub fn complex_order_strategy_type(&self) -> Option<&ComplexOrderStrategyType> {
230 self.complex_order_strategy_type.as_ref()
231 }
232
233 pub fn quantity(&self) -> Option<Decimal> {
236 self.quantity
237 }
238
239 pub fn price(&self) -> Option<Decimal> {
241 self.price
242 }
243
244 pub fn stop_price(&self) -> Option<Decimal> {
246 self.stop_price
247 }
248
249 pub fn special_instruction(&self) -> Option<&SpecialInstruction> {
251 self.special_instruction.as_ref()
252 }
253
254 pub fn order_strategy_type(&self) -> Option<&OrderStrategyType> {
256 self.order_strategy_type.as_ref()
257 }
258
259 pub fn legs(&self) -> &[OrderLegRequest] {
261 &self.order_leg_collection
262 }
263
264 pub fn child_strategies(&self) -> &[OrderRequest] {
267 &self.child_order_strategies
268 }
269}
270
271#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq, Hash)]
274#[non_exhaustive]
275pub struct OrderLegRequest {
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub(crate) instruction: Option<Instruction>,
278 #[serde(skip_serializing_if = "Option::is_none", with = "decimal_opt")]
279 pub(crate) quantity: Option<Decimal>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub(crate) instrument: Option<OrderInstrumentRequest>,
282 #[serde(rename = "positionEffect", skip_serializing_if = "Option::is_none")]
283 pub(crate) position_effect: Option<PositionEffect>,
284 #[serde(rename = "quantityType", skip_serializing_if = "Option::is_none")]
285 pub(crate) quantity_type: Option<QuantityType>,
286}
287
288#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq, Hash)]
292#[non_exhaustive]
293pub struct OrderInstrumentRequest {
294 #[serde(skip_serializing_if = "Option::is_none")]
295 pub(crate) symbol: Option<String>,
296 #[serde(rename = "assetType", skip_serializing_if = "Option::is_none")]
297 pub(crate) asset_type: Option<AssetType>,
298}
299
300impl OrderLegRequest {
301 pub fn instruction(&self) -> Option<&Instruction> {
303 self.instruction.as_ref()
304 }
305
306 pub fn quantity(&self) -> Option<Decimal> {
308 self.quantity
309 }
310
311 pub fn instrument(&self) -> Option<&OrderInstrumentRequest> {
313 self.instrument.as_ref()
314 }
315
316 pub fn position_effect(&self) -> Option<&PositionEffect> {
318 self.position_effect.as_ref()
319 }
320
321 pub fn quantity_type(&self) -> Option<&QuantityType> {
324 self.quantity_type.as_ref()
325 }
326}
327
328impl OrderInstrumentRequest {
329 pub fn symbol(&self) -> Option<&str> {
331 self.symbol.as_deref()
332 }
333
334 pub fn asset_type(&self) -> Option<&AssetType> {
336 self.asset_type.as_ref()
337 }
338}
339
340#[derive(Debug)]
344pub struct NeedsType;
345#[derive(Debug)]
347pub struct NeedsLeg;
348#[derive(Debug)]
352pub struct Ready;
353
354pub trait AcceptsLeg: sealed::Sealed {
359 type AfterLeg;
361}
362
363mod sealed {
364 pub trait Sealed {}
365 impl Sealed for super::NeedsLeg {}
366 impl Sealed for super::Ready {}
367 impl Sealed for rust_decimal::Decimal {}
368}
369
370impl AcceptsLeg for NeedsLeg {
371 type AfterLeg = Ready;
372}
373
374impl AcceptsLeg for Ready {
375 type AfterLeg = Ready;
376}
377
378#[derive(Debug)]
381#[must_use = "call .build() to finalize the OrderRequest"]
382pub struct SingleOrderBuilder<State> {
383 inner: OrderRequest,
384 _state: PhantomData<State>,
385}
386
387impl OrderRequest {
388 pub fn single() -> SingleOrderBuilder<NeedsType> {
392 let inner = OrderRequest {
393 session: Some(Session::Normal),
394 duration: Some(Duration::Day),
395 order_strategy_type: Some(OrderStrategyType::Single),
396 ..OrderRequest::empty()
397 };
398 SingleOrderBuilder {
399 inner,
400 _state: PhantomData,
401 }
402 }
403
404 pub fn buy_market(
417 symbol: impl Into<String>,
418 qty: impl IntoQuantity,
419 ) -> SingleOrderBuilder<Ready> {
420 Self::single().market().equity_buy(symbol, qty)
421 }
422
423 pub fn buy_limit(
425 symbol: impl Into<String>,
426 qty: impl IntoQuantity,
427 price: Decimal,
428 ) -> SingleOrderBuilder<Ready> {
429 Self::single().limit(price).equity_buy(symbol, qty)
430 }
431
432 pub fn sell_market(
434 symbol: impl Into<String>,
435 qty: impl IntoQuantity,
436 ) -> SingleOrderBuilder<Ready> {
437 Self::single().market().equity_sell(symbol, qty)
438 }
439
440 pub fn sell_limit(
442 symbol: impl Into<String>,
443 qty: impl IntoQuantity,
444 price: Decimal,
445 ) -> SingleOrderBuilder<Ready> {
446 Self::single().limit(price).equity_sell(symbol, qty)
447 }
448
449 pub fn sell_stop(
452 symbol: impl Into<String>,
453 qty: impl IntoQuantity,
454 stop_price: Decimal,
455 ) -> SingleOrderBuilder<Ready> {
456 Self::single().stop(stop_price).equity_sell(symbol, qty)
457 }
458
459 pub fn sell_stop_limit(
463 symbol: impl Into<String>,
464 qty: impl IntoQuantity,
465 stop_price: Decimal,
466 limit_price: Decimal,
467 ) -> SingleOrderBuilder<Ready> {
468 Self::single()
469 .stop_limit(stop_price, limit_price)
470 .equity_sell(symbol, qty)
471 }
472
473 pub fn buy_to_open_market(
484 symbol: impl Into<String>,
485 qty: impl IntoQuantity,
486 ) -> SingleOrderBuilder<Ready> {
487 Self::single().market().option_buy_to_open(symbol, qty)
488 }
489
490 pub fn buy_to_open_limit(
492 symbol: impl Into<String>,
493 qty: impl IntoQuantity,
494 price: Decimal,
495 ) -> SingleOrderBuilder<Ready> {
496 Self::single().limit(price).option_buy_to_open(symbol, qty)
497 }
498
499 pub fn sell_to_open_market(
502 symbol: impl Into<String>,
503 qty: impl IntoQuantity,
504 ) -> SingleOrderBuilder<Ready> {
505 Self::single().market().option_sell_to_open(symbol, qty)
506 }
507
508 pub fn sell_to_open_limit(
510 symbol: impl Into<String>,
511 qty: impl IntoQuantity,
512 price: Decimal,
513 ) -> SingleOrderBuilder<Ready> {
514 Self::single().limit(price).option_sell_to_open(symbol, qty)
515 }
516
517 pub fn buy_to_close_market(
520 symbol: impl Into<String>,
521 qty: impl IntoQuantity,
522 ) -> SingleOrderBuilder<Ready> {
523 Self::single().market().option_buy_to_close(symbol, qty)
524 }
525
526 pub fn buy_to_close_limit(
528 symbol: impl Into<String>,
529 qty: impl IntoQuantity,
530 price: Decimal,
531 ) -> SingleOrderBuilder<Ready> {
532 Self::single().limit(price).option_buy_to_close(symbol, qty)
533 }
534
535 pub fn sell_to_close_market(
538 symbol: impl Into<String>,
539 qty: impl IntoQuantity,
540 ) -> SingleOrderBuilder<Ready> {
541 Self::single().market().option_sell_to_close(symbol, qty)
542 }
543
544 pub fn sell_to_close_limit(
546 symbol: impl Into<String>,
547 qty: impl IntoQuantity,
548 price: Decimal,
549 ) -> SingleOrderBuilder<Ready> {
550 Self::single()
551 .limit(price)
552 .option_sell_to_close(symbol, qty)
553 }
554
555 pub fn oco(child_a: impl Into<OrderRequest>, child_b: impl Into<OrderRequest>) -> OrderRequest {
595 OrderRequest {
596 order_strategy_type: Some(OrderStrategyType::Oco),
597 child_order_strategies: vec![child_a.into(), child_b.into()],
598 ..OrderRequest::empty()
599 }
600 }
601
602 pub fn trigger(
638 parent: impl Into<OrderRequest>,
639 child: impl Into<OrderRequest>,
640 ) -> OrderRequest {
641 let mut parent: OrderRequest = parent.into();
642 parent.order_strategy_type = Some(OrderStrategyType::Trigger);
643 parent.child_order_strategies.push(child.into());
644 parent
645 }
646}
647
648impl From<SingleOrderBuilder<Ready>> for OrderRequest {
649 fn from(builder: SingleOrderBuilder<Ready>) -> Self {
650 builder.build()
651 }
652}
653
654impl SingleOrderBuilder<NeedsType> {
655 pub fn market(mut self) -> SingleOrderBuilder<NeedsLeg> {
657 self.inner.order_type = Some(OrderType::Market);
658 self.transition()
659 }
660
661 pub fn limit(mut self, price: Decimal) -> SingleOrderBuilder<NeedsLeg> {
663 self.inner.order_type = Some(OrderType::Limit);
664 self.inner.price = Some(price);
665 self.transition()
666 }
667
668 pub fn stop(mut self, stop_price: Decimal) -> SingleOrderBuilder<NeedsLeg> {
670 self.inner.order_type = Some(OrderType::Stop);
671 self.inner.stop_price = Some(stop_price);
672 self.transition()
673 }
674
675 pub fn stop_limit(
678 mut self,
679 stop_price: Decimal,
680 limit_price: Decimal,
681 ) -> SingleOrderBuilder<NeedsLeg> {
682 self.inner.order_type = Some(OrderType::StopLimit);
683 self.inner.stop_price = Some(stop_price);
684 self.inner.price = Some(limit_price);
685 self.transition()
686 }
687
688 pub fn net_debit(mut self, price: Decimal) -> SingleOrderBuilder<NeedsLeg> {
691 self.inner.order_type = Some(OrderType::NetDebit);
692 self.inner.price = Some(price);
693 self.transition()
694 }
695
696 pub fn net_credit(mut self, price: Decimal) -> SingleOrderBuilder<NeedsLeg> {
699 self.inner.order_type = Some(OrderType::NetCredit);
700 self.inner.price = Some(price);
701 self.transition()
702 }
703
704 fn transition(self) -> SingleOrderBuilder<NeedsLeg> {
705 SingleOrderBuilder {
706 inner: self.inner,
707 _state: PhantomData,
708 }
709 }
710}
711
712impl<S: AcceptsLeg> SingleOrderBuilder<S> {
713 fn push_leg(mut self, leg: OrderLegRequest) -> SingleOrderBuilder<S::AfterLeg> {
714 self.inner.order_leg_collection.push(leg);
715 SingleOrderBuilder {
716 inner: self.inner,
717 _state: PhantomData,
718 }
719 }
720
721 pub fn equity_buy(
723 self,
724 symbol: impl Into<String>,
725 qty: impl IntoQuantity,
726 ) -> SingleOrderBuilder<S::AfterLeg> {
727 self.push_leg(equity_leg(Instruction::Buy, symbol, qty))
728 }
729
730 pub fn equity_sell(
732 self,
733 symbol: impl Into<String>,
734 qty: impl IntoQuantity,
735 ) -> SingleOrderBuilder<S::AfterLeg> {
736 self.push_leg(equity_leg(Instruction::Sell, symbol, qty))
737 }
738
739 pub fn equity_sell_short(
741 self,
742 symbol: impl Into<String>,
743 qty: impl IntoQuantity,
744 ) -> SingleOrderBuilder<S::AfterLeg> {
745 self.push_leg(equity_leg(Instruction::SellShort, symbol, qty))
746 }
747
748 pub fn equity_buy_to_cover(
750 self,
751 symbol: impl Into<String>,
752 qty: impl IntoQuantity,
753 ) -> SingleOrderBuilder<S::AfterLeg> {
754 self.push_leg(equity_leg(Instruction::BuyToCover, symbol, qty))
755 }
756
757 pub fn option_buy_to_open(
759 self,
760 symbol: impl Into<String>,
761 qty: impl IntoQuantity,
762 ) -> SingleOrderBuilder<S::AfterLeg> {
763 self.push_leg(option_leg(Instruction::BuyToOpen, symbol, qty))
764 }
765
766 pub fn option_sell_to_open(
768 self,
769 symbol: impl Into<String>,
770 qty: impl IntoQuantity,
771 ) -> SingleOrderBuilder<S::AfterLeg> {
772 self.push_leg(option_leg(Instruction::SellToOpen, symbol, qty))
773 }
774
775 pub fn option_buy_to_close(
777 self,
778 symbol: impl Into<String>,
779 qty: impl IntoQuantity,
780 ) -> SingleOrderBuilder<S::AfterLeg> {
781 self.push_leg(option_leg(Instruction::BuyToClose, symbol, qty))
782 }
783
784 pub fn option_sell_to_close(
786 self,
787 symbol: impl Into<String>,
788 qty: impl IntoQuantity,
789 ) -> SingleOrderBuilder<S::AfterLeg> {
790 self.push_leg(option_leg(Instruction::SellToClose, symbol, qty))
791 }
792}
793
794impl SingleOrderBuilder<Ready> {
795 pub fn duration(mut self, duration: Duration) -> Self {
797 self.inner.duration = Some(duration);
798 self
799 }
800
801 pub fn session(mut self, session: Session) -> Self {
803 self.inner.session = Some(session);
804 self
805 }
806
807 pub fn special_instruction(mut self, instr: SpecialInstruction) -> Self {
809 self.inner.special_instruction = Some(instr);
810 self
811 }
812
813 pub fn complex_order_strategy_type(mut self, t: ComplexOrderStrategyType) -> Self {
816 self.inner.complex_order_strategy_type = Some(t);
817 self
818 }
819
820 pub fn build(self) -> OrderRequest {
822 self.inner
823 }
824}
825
826fn equity_leg(
827 instruction: Instruction,
828 symbol: impl Into<String>,
829 qty: impl IntoQuantity,
830) -> OrderLegRequest {
831 OrderLegRequest {
832 instruction: Some(instruction),
833 quantity: Some(qty.into_quantity()),
834 instrument: Some(OrderInstrumentRequest {
835 symbol: Some(symbol.into()),
836 asset_type: Some(AssetType::Equity),
837 }),
838 ..Default::default()
839 }
840}
841
842fn option_leg(
843 instruction: Instruction,
844 symbol: impl Into<String>,
845 qty: impl IntoQuantity,
846) -> OrderLegRequest {
847 OrderLegRequest {
848 instruction: Some(instruction),
849 quantity: Some(qty.into_quantity()),
850 instrument: Some(OrderInstrumentRequest {
851 symbol: Some(symbol.into()),
852 asset_type: Some(AssetType::Option),
853 }),
854 ..Default::default()
855 }
856}
857
858impl TryFrom<Order> for OrderRequest {
870 type Error = Error;
871
872 fn try_from(order: Order) -> Result<Self, Self::Error> {
884 let Order {
885 session,
886 duration,
887 order_type,
888 complex_order_strategy_type,
889 quantity,
890 destination_link_name,
891 stop_price,
892 stop_price_link_basis,
893 stop_price_link_type,
894 stop_price_offset,
895 stop_type,
896 price_link_basis,
897 price_link_type,
898 price,
899 tax_lot_method,
900 order_leg_collection,
901 activation_price,
902 special_instruction,
903 order_strategy_type,
904 child_order_strategies,
905 ..
908 } = order;
909
910 let order_leg_collection = order_leg_collection
911 .into_iter()
912 .map(OrderLegRequest::try_from)
913 .collect::<Result<Vec<_>, _>>()?;
914
915 let child_order_strategies = child_order_strategies
916 .into_iter()
917 .map(OrderRequest::try_from)
918 .collect::<Result<Vec<_>, _>>()?;
919
920 Ok(OrderRequest {
921 session,
922 duration,
923 order_type,
924 complex_order_strategy_type,
925 quantity,
926 destination_link_name,
927 stop_price,
928 stop_price_link_basis,
929 stop_price_link_type,
930 stop_price_offset,
931 stop_type,
932 price_link_basis,
933 price_link_type,
934 price,
935 tax_lot_method,
936 order_leg_collection,
937 activation_price,
938 special_instruction,
939 order_strategy_type,
940 child_order_strategies,
941 })
942 }
943}
944
945impl TryFrom<OrderLegCollection> for OrderLegRequest {
946 type Error = Error;
947
948 fn try_from(leg: OrderLegCollection) -> Result<Self, Self::Error> {
949 let OrderLegCollection {
950 instruction,
951 quantity,
952 instrument,
953 position_effect,
954 quantity_type,
955 ..
958 } = leg;
959
960 let instrument = instrument
961 .map(|inst| {
962 let symbol = inst
963 .symbol
964 .ok_or_else(|| Error::OrderResponseNotRepresentable {
965 reason: "order leg instrument is missing `symbol`".to_string(),
966 })?;
967 if let AssetType::Unknown(raw) = &inst.asset_type {
968 return Err(Error::OrderResponseNotRepresentable {
969 reason: format!("order leg instrument has unknown assetType `{raw}`"),
970 });
971 }
972 Ok(OrderInstrumentRequest {
973 symbol: Some(symbol),
974 asset_type: Some(inst.asset_type),
975 })
976 })
977 .ok_or_else(|| Error::OrderResponseNotRepresentable {
978 reason: "order leg is missing its instrument".to_string(),
979 })
980 .flatten()?;
981
982 Ok(OrderLegRequest {
983 instruction,
984 quantity,
985 instrument: Some(instrument),
986 position_effect,
987 quantity_type,
988 })
989 }
990}
991
992#[cfg(test)]
993mod tests {
994 use super::*;
995 use rust_decimal_macros::dec;
996
997 fn pretty(value: &serde_json::Value) -> String {
998 serde_json::to_string_pretty(value).unwrap()
999 }
1000
1001 #[test]
1002 fn builder_buy_market_equity_matches_schwab_example() {
1003 let req = OrderRequest::single()
1004 .market()
1005 .equity_buy("XYZ", dec!(15))
1006 .build();
1007 let actual: serde_json::Value = serde_json::to_value(&req).unwrap();
1008 let expected: serde_json::Value = serde_json::from_str(
1009 r#"{
1010 "session": "NORMAL",
1011 "duration": "DAY",
1012 "orderType": "MARKET",
1013 "orderStrategyType": "SINGLE",
1014 "orderLegCollection": [{
1015 "instruction": "BUY",
1016 "quantity": 15,
1017 "instrument": {
1018 "symbol": "XYZ",
1019 "assetType": "EQUITY"
1020 }
1021 }]
1022 }"#,
1023 )
1024 .unwrap();
1025 assert_eq!(actual, expected, "got: {}", pretty(&actual));
1026 }
1027
1028 #[test]
1029 fn builder_buy_limit_option_matches_schwab_example() {
1030 let req = OrderRequest::single()
1031 .limit(dec!(6.45))
1032 .option_buy_to_open("XYZ 240315C00500000", dec!(10))
1033 .complex_order_strategy_type(ComplexOrderStrategyType::None)
1034 .build();
1035 let actual: serde_json::Value = serde_json::to_value(&req).unwrap();
1036 let expected: serde_json::Value = serde_json::from_str(
1037 r#"{
1038 "complexOrderStrategyType": "NONE",
1039 "orderType": "LIMIT",
1040 "session": "NORMAL",
1041 "price": 6.45,
1042 "duration": "DAY",
1043 "orderStrategyType": "SINGLE",
1044 "orderLegCollection": [{
1045 "instruction": "BUY_TO_OPEN",
1046 "quantity": 10,
1047 "instrument": {
1048 "symbol": "XYZ 240315C00500000",
1049 "assetType": "OPTION"
1050 }
1051 }]
1052 }"#,
1053 )
1054 .unwrap();
1055 assert_eq!(actual, expected, "got: {}", pretty(&actual));
1056 }
1057
1058 #[test]
1059 fn builder_vertical_spread_uses_net_debit_with_two_legs() {
1060 let req = OrderRequest::single()
1061 .net_debit(dec!(0.10))
1062 .option_buy_to_open("XYZ 240315P00045000", dec!(2))
1063 .option_sell_to_open("XYZ 240315P00043000", dec!(2))
1064 .build();
1065 let actual: serde_json::Value = serde_json::to_value(&req).unwrap();
1066 let expected: serde_json::Value = serde_json::from_str(
1067 r#"{
1068 "orderType": "NET_DEBIT",
1069 "session": "NORMAL",
1070 "price": 0.10,
1071 "duration": "DAY",
1072 "orderStrategyType": "SINGLE",
1073 "orderLegCollection": [
1074 {
1075 "instruction": "BUY_TO_OPEN",
1076 "quantity": 2,
1077 "instrument": {
1078 "symbol": "XYZ 240315P00045000",
1079 "assetType": "OPTION"
1080 }
1081 },
1082 {
1083 "instruction": "SELL_TO_OPEN",
1084 "quantity": 2,
1085 "instrument": {
1086 "symbol": "XYZ 240315P00043000",
1087 "assetType": "OPTION"
1088 }
1089 }
1090 ]
1091 }"#,
1092 )
1093 .unwrap();
1094 assert_eq!(actual, expected, "got: {}", pretty(&actual));
1095 }
1096
1097 #[test]
1098 fn builder_optional_setters_override_defaults() {
1099 let req = OrderRequest::single()
1100 .limit(dec!(140.00))
1101 .equity_buy("AAPL", dec!(5))
1102 .duration(Duration::GoodTillCancel)
1103 .session(Session::Seamless)
1104 .special_instruction(SpecialInstruction::AllOrNone)
1105 .build();
1106 assert_eq!(req.duration, Some(Duration::GoodTillCancel));
1107 assert_eq!(req.session, Some(Session::Seamless));
1108 assert_eq!(req.special_instruction, Some(SpecialInstruction::AllOrNone));
1109 }
1110
1111 #[test]
1112 fn builder_serialization_omits_response_only_fields() {
1113 let req = OrderRequest::single()
1114 .market()
1115 .equity_buy("AAPL", dec!(1))
1116 .build();
1117 let json = serde_json::to_string(&req).unwrap();
1118 for forbidden in [
1119 "status",
1120 "orderId",
1121 "accountNumber",
1122 "tag",
1123 "requestedDestination",
1124 "filledQuantity",
1125 "remainingQuantity",
1126 "enteredTime",
1127 "closeTime",
1128 "cancelable",
1129 "editable",
1130 "orderActivityCollection",
1131 ] {
1132 assert!(
1133 !json.contains(forbidden),
1134 "request body should not contain {forbidden}, got: {json}"
1135 );
1136 }
1137 }
1138
1139 #[test]
1142 fn shortcut_buy_market_equals_explicit_builder() {
1143 let a = OrderRequest::buy_market("AAPL", dec!(10)).build();
1144 let b = OrderRequest::single()
1145 .market()
1146 .equity_buy("AAPL", dec!(10))
1147 .build();
1148 assert_eq!(
1149 serde_json::to_value(&a).unwrap(),
1150 serde_json::to_value(&b).unwrap()
1151 );
1152 }
1153
1154 #[test]
1155 fn shortcut_buy_limit_equals_explicit_builder() {
1156 let a = OrderRequest::buy_limit("AAPL", dec!(10), dec!(150.00)).build();
1157 let b = OrderRequest::single()
1158 .limit(dec!(150.00))
1159 .equity_buy("AAPL", dec!(10))
1160 .build();
1161 assert_eq!(
1162 serde_json::to_value(&a).unwrap(),
1163 serde_json::to_value(&b).unwrap()
1164 );
1165 }
1166
1167 #[test]
1168 fn shortcut_sell_stop_equals_explicit_builder() {
1169 let a = OrderRequest::sell_stop("AAPL", dec!(10), dec!(140.00)).build();
1170 let b = OrderRequest::single()
1171 .stop(dec!(140.00))
1172 .equity_sell("AAPL", dec!(10))
1173 .build();
1174 assert_eq!(
1175 serde_json::to_value(&a).unwrap(),
1176 serde_json::to_value(&b).unwrap()
1177 );
1178 }
1179
1180 #[test]
1181 fn shortcut_sell_stop_limit_equals_explicit_builder() {
1182 let a = OrderRequest::sell_stop_limit("AAPL", dec!(10), dec!(140.00), dec!(139.50)).build();
1183 let b = OrderRequest::single()
1184 .stop_limit(dec!(140.00), dec!(139.50))
1185 .equity_sell("AAPL", dec!(10))
1186 .build();
1187 assert_eq!(
1188 serde_json::to_value(&a).unwrap(),
1189 serde_json::to_value(&b).unwrap()
1190 );
1191 }
1192
1193 #[test]
1194 fn option_shortcut_buy_to_open_market_equals_explicit_builder() {
1195 let symbol = "AAPL 240315C00200000";
1196 let a = OrderRequest::buy_to_open_market(symbol, dec!(2)).build();
1197 let b = OrderRequest::single()
1198 .market()
1199 .option_buy_to_open(symbol, dec!(2))
1200 .build();
1201 assert_eq!(
1202 serde_json::to_value(&a).unwrap(),
1203 serde_json::to_value(&b).unwrap()
1204 );
1205 }
1206
1207 #[test]
1208 fn option_shortcuts_cover_all_four_instructions() {
1209 let cases: [(OrderRequest, &str); 4] = [
1212 (
1213 OrderRequest::buy_to_open_limit("XYZ 240315C00500000", dec!(1), dec!(6.45))
1214 .build(),
1215 "BUY_TO_OPEN",
1216 ),
1217 (
1218 OrderRequest::sell_to_open_limit("XYZ 240315C00500000", dec!(1), dec!(6.45))
1219 .build(),
1220 "SELL_TO_OPEN",
1221 ),
1222 (
1223 OrderRequest::buy_to_close_limit("XYZ 240315C00500000", dec!(1), dec!(6.45))
1224 .build(),
1225 "BUY_TO_CLOSE",
1226 ),
1227 (
1228 OrderRequest::sell_to_close_limit("XYZ 240315C00500000", dec!(1), dec!(6.45))
1229 .build(),
1230 "SELL_TO_CLOSE",
1231 ),
1232 ];
1233 for (req, expected_instruction) in cases {
1234 let v = serde_json::to_value(&req).unwrap();
1235 let leg = &v["orderLegCollection"][0];
1236 assert_eq!(leg["instruction"], expected_instruction);
1237 assert_eq!(leg["instrument"]["assetType"], "OPTION");
1238 assert_eq!(v["orderStrategyType"], "SINGLE");
1239 }
1240 }
1241
1242 #[test]
1243 fn shortcut_supports_chaining_optional_setters() {
1244 let req = OrderRequest::buy_limit("AAPL", dec!(10), dec!(150.00))
1245 .duration(Duration::GoodTillCancel)
1246 .session(Session::Seamless)
1247 .special_instruction(SpecialInstruction::AllOrNone)
1248 .build();
1249 assert_eq!(req.duration, Some(Duration::GoodTillCancel));
1250 assert_eq!(req.session, Some(Session::Seamless));
1251 assert_eq!(req.special_instruction, Some(SpecialInstruction::AllOrNone));
1252 assert_eq!(req.order_type, Some(OrderType::Limit));
1254 assert_eq!(req.price, Some(dec!(150.00)));
1255 }
1256
1257 #[test]
1258 fn oco_accepts_shortcut_builders_via_into() {
1259 let oco = OrderRequest::oco(
1263 OrderRequest::sell_limit("XYZ", dec!(1), dec!(50)),
1264 OrderRequest::sell_stop("XYZ", dec!(1), dec!(40)),
1265 );
1266 let v = serde_json::to_value(&oco).unwrap();
1267 assert_eq!(v["orderStrategyType"], "OCO");
1268 assert_eq!(v["childOrderStrategies"].as_array().unwrap().len(), 2);
1269 }
1270
1271 #[test]
1274 fn oco_pair_matches_schwab_example() {
1275 let limit_leg = OrderRequest::single()
1278 .limit(dec!(45.97))
1279 .equity_sell("XYZ", dec!(2))
1280 .build();
1281 let stop_limit_leg = OrderRequest::single()
1282 .stop_limit(dec!(37.03), dec!(37.00))
1283 .equity_sell("XYZ", dec!(2))
1284 .build();
1285 let req = OrderRequest::oco(limit_leg, stop_limit_leg);
1286 let actual: serde_json::Value = serde_json::to_value(&req).unwrap();
1287 let expected: serde_json::Value = serde_json::from_str(
1288 r#"{
1289 "orderStrategyType": "OCO",
1290 "childOrderStrategies": [
1291 {
1292 "orderType": "LIMIT",
1293 "session": "NORMAL",
1294 "price": 45.97,
1295 "duration": "DAY",
1296 "orderStrategyType": "SINGLE",
1297 "orderLegCollection": [{
1298 "instruction": "SELL",
1299 "quantity": 2,
1300 "instrument": { "symbol": "XYZ", "assetType": "EQUITY" }
1301 }]
1302 },
1303 {
1304 "orderType": "STOP_LIMIT",
1305 "session": "NORMAL",
1306 "price": 37.00,
1307 "stopPrice": 37.03,
1308 "duration": "DAY",
1309 "orderStrategyType": "SINGLE",
1310 "orderLegCollection": [{
1311 "instruction": "SELL",
1312 "quantity": 2,
1313 "instrument": { "symbol": "XYZ", "assetType": "EQUITY" }
1314 }]
1315 }
1316 ]
1317 }"#,
1318 )
1319 .unwrap();
1320 assert_eq!(actual, expected, "got: {}", pretty(&actual));
1321 }
1322
1323 #[test]
1324 fn trigger_buy_then_sell_matches_schwab_example() {
1325 let entry = OrderRequest::buy_limit("XYZ", dec!(10), dec!(34.97));
1328 let exit = OrderRequest::sell_limit("XYZ", dec!(10), dec!(42.03));
1329 let req = OrderRequest::trigger(entry, exit);
1330 let actual: serde_json::Value = serde_json::to_value(&req).unwrap();
1331 let expected: serde_json::Value = serde_json::from_str(
1332 r#"{
1333 "orderType": "LIMIT",
1334 "session": "NORMAL",
1335 "price": 34.97,
1336 "duration": "DAY",
1337 "orderStrategyType": "TRIGGER",
1338 "orderLegCollection": [{
1339 "instruction": "BUY",
1340 "quantity": 10,
1341 "instrument": { "symbol": "XYZ", "assetType": "EQUITY" }
1342 }],
1343 "childOrderStrategies": [{
1344 "orderType": "LIMIT",
1345 "session": "NORMAL",
1346 "price": 42.03,
1347 "duration": "DAY",
1348 "orderStrategyType": "SINGLE",
1349 "orderLegCollection": [{
1350 "instruction": "SELL",
1351 "quantity": 10,
1352 "instrument": { "symbol": "XYZ", "assetType": "EQUITY" }
1353 }]
1354 }]
1355 }"#,
1356 )
1357 .unwrap();
1358 assert_eq!(actual, expected, "got: {}", pretty(&actual));
1359 }
1360
1361 #[test]
1362 fn one_triggers_oco_matches_schwab_example() {
1363 let entry = OrderRequest::buy_limit("XYZ", dec!(5), dec!(14.97));
1366 let take_profit = OrderRequest::single()
1367 .limit(dec!(15.27))
1368 .equity_sell("XYZ", dec!(5))
1369 .duration(Duration::GoodTillCancel)
1370 .build();
1371 let stop_loss = OrderRequest::single()
1372 .stop(dec!(11.27))
1373 .equity_sell("XYZ", dec!(5))
1374 .duration(Duration::GoodTillCancel)
1375 .build();
1376 let oco = OrderRequest::oco(take_profit, stop_loss);
1377 let req = OrderRequest::trigger(entry, oco);
1378 let actual: serde_json::Value = serde_json::to_value(&req).unwrap();
1379 let expected: serde_json::Value = serde_json::from_str(
1380 r#"{
1381 "orderStrategyType": "TRIGGER",
1382 "session": "NORMAL",
1383 "duration": "DAY",
1384 "orderType": "LIMIT",
1385 "price": 14.97,
1386 "orderLegCollection": [{
1387 "instruction": "BUY",
1388 "quantity": 5,
1389 "instrument": { "assetType": "EQUITY", "symbol": "XYZ" }
1390 }],
1391 "childOrderStrategies": [{
1392 "orderStrategyType": "OCO",
1393 "childOrderStrategies": [
1394 {
1395 "orderStrategyType": "SINGLE",
1396 "session": "NORMAL",
1397 "duration": "GOOD_TILL_CANCEL",
1398 "orderType": "LIMIT",
1399 "price": 15.27,
1400 "orderLegCollection": [{
1401 "instruction": "SELL",
1402 "quantity": 5,
1403 "instrument": { "assetType": "EQUITY", "symbol": "XYZ" }
1404 }]
1405 },
1406 {
1407 "orderStrategyType": "SINGLE",
1408 "session": "NORMAL",
1409 "duration": "GOOD_TILL_CANCEL",
1410 "orderType": "STOP",
1411 "stopPrice": 11.27,
1412 "orderLegCollection": [{
1413 "instruction": "SELL",
1414 "quantity": 5,
1415 "instrument": { "assetType": "EQUITY", "symbol": "XYZ" }
1416 }]
1417 }
1418 ]
1419 }]
1420 }"#,
1421 )
1422 .unwrap();
1423 assert_eq!(actual, expected, "got: {}", pretty(&actual));
1424 }
1425
1426 #[test]
1429 fn into_quantity_decimal_is_identity() {
1430 assert_eq!(dec!(10).into_quantity(), dec!(10));
1431 assert_eq!(dec!(0.5).into_quantity(), dec!(0.5));
1432 }
1433
1434 #[test]
1435 fn into_quantity_accepts_unsigned_and_signed_ints() {
1436 assert_eq!(IntoQuantity::into_quantity(10u8), dec!(10));
1437 assert_eq!(IntoQuantity::into_quantity(10u16), dec!(10));
1438 assert_eq!(IntoQuantity::into_quantity(10u32), dec!(10));
1439 assert_eq!(IntoQuantity::into_quantity(10u64), dec!(10));
1440 assert_eq!(IntoQuantity::into_quantity(10i8), dec!(10));
1441 assert_eq!(IntoQuantity::into_quantity(10i16), dec!(10));
1442 assert_eq!(IntoQuantity::into_quantity(10i32), dec!(10));
1443 assert_eq!(IntoQuantity::into_quantity(10i64), dec!(10));
1444 }
1445
1446 #[test]
1447 fn factory_shortcuts_accept_integer_literal_for_qty() {
1448 let a = OrderRequest::buy_market("AAPL", 10).build();
1451 let b = OrderRequest::buy_market("AAPL", dec!(10)).build();
1452 assert_eq!(
1453 serde_json::to_value(&a).unwrap(),
1454 serde_json::to_value(&b).unwrap()
1455 );
1456 }
1457
1458 #[test]
1459 fn oco_top_level_has_no_session_or_duration() {
1460 let a = OrderRequest::sell_limit("XYZ", dec!(1), dec!(50));
1464 let b = OrderRequest::sell_stop("XYZ", dec!(1), dec!(40));
1465 let req = OrderRequest::oco(a, b);
1466 let v = serde_json::to_value(&req).unwrap();
1467 let obj = v.as_object().unwrap();
1468 assert_eq!(obj.len(), 2);
1469 assert!(obj.contains_key("orderStrategyType"));
1470 assert!(obj.contains_key("childOrderStrategies"));
1471 }
1472
1473 fn try_round_trip(req: &OrderRequest) -> OrderRequest {
1476 let wire = serde_json::to_string(req).expect("serialize OrderRequest");
1479 let order: crate::orders::Order =
1480 serde_json::from_str(&wire).expect("deserialize as Order");
1481 OrderRequest::try_from(order).expect("Order -> OrderRequest")
1482 }
1483
1484 #[test]
1485 fn try_from_round_trips_equity_limit_buy() {
1486 let req = OrderRequest::single()
1487 .limit(dec!(140.00))
1488 .equity_buy("AAPL", dec!(5))
1489 .duration(Duration::GoodTillCancel)
1490 .session(Session::Seamless)
1491 .special_instruction(SpecialInstruction::AllOrNone)
1492 .build();
1493 let after = try_round_trip(&req);
1494 assert_eq!(req, after);
1495 }
1496
1497 #[test]
1498 fn try_from_round_trips_vertical_spread() {
1499 let req = OrderRequest::single()
1500 .net_debit(dec!(0.10))
1501 .option_buy_to_open("XYZ 240315P00045000", dec!(2))
1502 .option_sell_to_open("XYZ 240315P00043000", dec!(2))
1503 .build();
1504 let after = try_round_trip(&req);
1505 assert_eq!(req, after);
1506 }
1507
1508 #[test]
1509 fn try_from_round_trips_oco_pair() {
1510 let limit_leg = OrderRequest::single()
1511 .limit(dec!(45.97))
1512 .equity_sell("XYZ", dec!(2))
1513 .build();
1514 let stop_limit_leg = OrderRequest::single()
1515 .stop_limit(dec!(37.03), dec!(37.00))
1516 .equity_sell("XYZ", dec!(2))
1517 .build();
1518 let req = OrderRequest::oco(limit_leg, stop_limit_leg);
1519 let after = try_round_trip(&req);
1520 assert_eq!(req, after);
1521 }
1522
1523 #[test]
1524 fn try_from_round_trips_one_triggers_oco() {
1525 let entry = OrderRequest::buy_limit("XYZ", dec!(5), dec!(14.97));
1526 let take_profit = OrderRequest::single()
1527 .limit(dec!(15.27))
1528 .equity_sell("XYZ", dec!(5))
1529 .duration(Duration::GoodTillCancel)
1530 .build();
1531 let stop_loss = OrderRequest::single()
1532 .stop(dec!(11.27))
1533 .equity_sell("XYZ", dec!(5))
1534 .duration(Duration::GoodTillCancel)
1535 .build();
1536 let oco = OrderRequest::oco(take_profit, stop_loss);
1537 let req = OrderRequest::trigger(entry, oco);
1538 let after = try_round_trip(&req);
1539 assert_eq!(req, after);
1540 }
1541
1542 #[test]
1543 fn try_from_drops_broker_assigned_fields_on_a_live_order() {
1544 let live: crate::orders::Order = serde_json::from_str(
1550 r#"{
1551 "orderId": 100000001,
1552 "accountNumber": 12345678,
1553 "status": "WORKING",
1554 "orderType": "LIMIT",
1555 "session": "NORMAL",
1556 "duration": "DAY",
1557 "orderStrategyType": "SINGLE",
1558 "quantity": 10.0,
1559 "filledQuantity": 0.0,
1560 "remainingQuantity": 10.0,
1561 "price": 140.00,
1562 "enteredTime": "2024-03-15T15:30:00.000Z",
1563 "cancelable": true,
1564 "editable": true,
1565 "orderLegCollection": [{
1566 "orderLegType": "EQUITY",
1567 "legId": 1,
1568 "instruction": "BUY",
1569 "quantity": 10.0,
1570 "instrument": {
1571 "assetType": "EQUITY",
1572 "symbol": "AAPL",
1573 "cusip": "037833100",
1574 "instrumentId": 12345
1575 }
1576 }]
1577 }"#,
1578 )
1579 .unwrap();
1580 let replace_body = OrderRequest::try_from(live).expect("convert live order");
1581
1582 assert_eq!(replace_body.order_type, Some(OrderType::Limit));
1584 assert_eq!(replace_body.price, Some(dec!(140.00)));
1585 assert_eq!(replace_body.session, Some(Session::Normal));
1586 assert_eq!(replace_body.duration, Some(Duration::Day));
1587 assert_eq!(
1588 replace_body.order_strategy_type,
1589 Some(OrderStrategyType::Single)
1590 );
1591 assert_eq!(replace_body.order_leg_collection.len(), 1);
1592 let leg = &replace_body.order_leg_collection[0];
1593 assert_eq!(leg.instruction, Some(Instruction::Buy));
1594 assert_eq!(leg.quantity, Some(dec!(10)));
1595 let inst = leg.instrument.as_ref().unwrap();
1596 assert_eq!(inst.symbol.as_deref(), Some("AAPL"));
1597 assert_eq!(inst.asset_type, Some(AssetType::Equity));
1598
1599 let json = serde_json::to_string(&replace_body).unwrap();
1601 for forbidden in [
1602 "orderId",
1603 "accountNumber",
1604 "status",
1605 "enteredTime",
1606 "filledQuantity",
1607 "remainingQuantity",
1608 "cancelable",
1609 "editable",
1610 "cusip",
1611 "instrumentId",
1612 "legId",
1613 ] {
1614 assert!(
1615 !json.contains(forbidden),
1616 "replace body should not contain {forbidden}, got: {json}"
1617 );
1618 }
1619 }
1620
1621 #[test]
1622 fn try_from_errors_when_leg_has_no_instrument() {
1623 let order: crate::orders::Order = serde_json::from_str(
1624 r#"{
1625 "orderId": 1,
1626 "orderStrategyType": "SINGLE",
1627 "orderType": "MARKET",
1628 "orderLegCollection": [{
1629 "instruction": "BUY",
1630 "quantity": 1
1631 }]
1632 }"#,
1633 )
1634 .unwrap();
1635 match OrderRequest::try_from(order) {
1636 Err(Error::OrderResponseNotRepresentable { reason }) => {
1637 assert!(reason.contains("instrument"), "unexpected reason: {reason}");
1638 }
1639 other => panic!("expected OrderResponseNotRepresentable, got {other:?}"),
1640 }
1641 }
1642
1643 #[test]
1644 fn try_from_errors_when_instrument_has_no_symbol() {
1645 let order: crate::orders::Order = serde_json::from_str(
1646 r#"{
1647 "orderId": 1,
1648 "orderStrategyType": "SINGLE",
1649 "orderType": "MARKET",
1650 "orderLegCollection": [{
1651 "instruction": "BUY",
1652 "quantity": 1,
1653 "instrument": { "assetType": "EQUITY" }
1654 }]
1655 }"#,
1656 )
1657 .unwrap();
1658 match OrderRequest::try_from(order) {
1659 Err(Error::OrderResponseNotRepresentable { reason }) => {
1660 assert!(reason.contains("symbol"), "unexpected reason: {reason}");
1661 }
1662 other => panic!("expected OrderResponseNotRepresentable, got {other:?}"),
1663 }
1664 }
1665
1666 #[test]
1667 fn try_from_errors_when_asset_type_is_unknown() {
1668 let order: crate::orders::Order = serde_json::from_str(
1673 r#"{
1674 "orderId": 1,
1675 "orderStrategyType": "SINGLE",
1676 "orderType": "MARKET",
1677 "orderLegCollection": [{
1678 "instruction": "BUY",
1679 "quantity": 1,
1680 "instrument": { "assetType": "NEW_ASSET_CLASS", "symbol": "X" }
1681 }]
1682 }"#,
1683 )
1684 .unwrap();
1685 match OrderRequest::try_from(order) {
1686 Err(Error::OrderResponseNotRepresentable { reason }) => {
1687 assert!(
1688 reason.contains("NEW_ASSET_CLASS"),
1689 "unexpected reason: {reason}"
1690 );
1691 }
1692 other => panic!("expected OrderResponseNotRepresentable, got {other:?}"),
1693 }
1694 }
1695
1696 #[test]
1697 fn try_from_error_is_not_retryable() {
1698 let err = Error::OrderResponseNotRepresentable {
1699 reason: "leg missing instrument".to_string(),
1700 };
1701 assert!(!err.is_retryable());
1702 assert_eq!(err.retry_after(), None);
1703 }
1704
1705 #[test]
1706 fn accessors_read_single_limit_order() {
1707 let req = OrderRequest::buy_limit("AAPL", dec!(10), dec!(150.25)).build();
1708
1709 assert_eq!(req.session(), Some(&Session::Normal));
1710 assert_eq!(req.duration(), Some(&Duration::Day));
1711 assert_eq!(req.order_type(), Some(&OrderType::Limit));
1712 assert_eq!(req.price(), Some(dec!(150.25)));
1713 assert_eq!(req.stop_price(), None);
1714 assert_eq!(req.order_strategy_type(), Some(&OrderStrategyType::Single));
1715 assert!(req.child_strategies().is_empty());
1716
1717 let legs = req.legs();
1718 assert_eq!(legs.len(), 1);
1719 let leg = &legs[0];
1720 assert_eq!(leg.instruction(), Some(&Instruction::Buy));
1721 assert_eq!(leg.quantity(), Some(dec!(10)));
1722 let instrument = leg.instrument().expect("leg has instrument");
1723 assert_eq!(instrument.symbol(), Some("AAPL"));
1724 assert_eq!(instrument.asset_type(), Some(&AssetType::Equity));
1725 }
1726
1727 #[test]
1728 fn accessors_walk_oco_composite() {
1729 let take_profit = OrderRequest::sell_limit("XYZ", dec!(1), dec!(50));
1730 let stop_loss = OrderRequest::sell_stop("XYZ", dec!(1), dec!(40));
1731 let req = OrderRequest::oco(take_profit, stop_loss);
1732
1733 assert_eq!(req.order_strategy_type(), Some(&OrderStrategyType::Oco));
1734 assert!(req.legs().is_empty());
1735
1736 let children = req.child_strategies();
1737 assert_eq!(children.len(), 2);
1738 assert_eq!(children[0].order_type(), Some(&OrderType::Limit));
1739 assert_eq!(children[0].price(), Some(dec!(50)));
1740 assert_eq!(children[1].order_type(), Some(&OrderType::Stop));
1741 assert_eq!(children[1].stop_price(), Some(dec!(40)));
1742 }
1743}