1use crate::common::{AccountsInstrument, AssetType, PutCall};
48use crate::validation::{ValidationError, ValidationError as VE, ValidationErrorKind as K};
49use chrono::{DateTime, Utc};
50use serde::{Deserialize, Serialize};
51use serde_with::skip_serializing_none;
52use std::collections::HashMap;
53
54mod builder;
55mod payoff;
56mod spec;
57pub use builder::OptionSymbol;
58pub use payoff::{DefinedRisk, defined_risk};
59pub use spec::{InstrumentSpec, LegSpec, OrderSettings, OrderSpec};
60
61#[skip_serializing_none]
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
70#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
71#[serde(rename_all = "camelCase")]
72#[non_exhaustive]
73pub struct Order {
74 #[serde(default, deserialize_with = "crate::common::optional_id")]
75 pub order_id: Option<String>,
76 pub session: Option<OrderSession>,
77 pub duration: Option<OrderDuration>,
78 pub order_type: Option<OrderType>,
79 pub complex_order_strategy_type: Option<ComplexOrderStrategyType>,
80 pub quantity: Option<f64>,
81 pub filled_quantity: Option<f64>,
82 pub remaining_quantity: Option<f64>,
83 pub requested_destination: Option<RequestedDestination>,
84 pub destination_link_name: Option<String>,
85 pub price: Option<f64>,
86 #[serde(default)]
87 pub order_leg_collection: Vec<OrderLeg>,
88 pub order_strategy_type: Option<OrderStrategyType>,
89 pub cancelable: Option<bool>,
90 pub editable: Option<bool>,
91 pub status: Option<OrderStatus>,
92 pub status_description: Option<String>,
93 pub entered_time: Option<String>,
94 pub close_time: Option<String>,
95 pub cancel_time: Option<String>,
96 pub release_time: Option<String>,
97 pub stop_price: Option<f64>,
98 pub stop_price_link_basis: Option<PriceLinkBasis>,
99 pub stop_price_link_type: Option<PriceLinkType>,
100 pub stop_price_offset: Option<f64>,
101 pub stop_type: Option<StopType>,
102 pub price_link_basis: Option<PriceLinkBasis>,
103 pub price_link_type: Option<PriceLinkType>,
104 pub activation_price: Option<f64>,
105 pub tax_lot_method: Option<TaxLotMethod>,
106 pub special_instruction: Option<SpecialInstruction>,
107 pub tag: Option<String>,
108 #[serde(default)]
109 pub child_order_strategies: Vec<Order>,
110 #[serde(default)]
111 pub replacing_order_collection: Vec<Order>,
112 #[serde(default)]
113 pub order_activity_collection: Vec<OrderActivity>,
114 #[serde(flatten)]
115 pub extra: HashMap<String, serde_json::Value>,
116 #[serde(default, deserialize_with = "crate::common::optional_id")]
117 pub account_number: Option<String>,
118}
119
120#[skip_serializing_none]
121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
122#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
123#[serde(rename_all = "camelCase", default)]
125#[non_exhaustive]
126pub struct OrderLeg {
127 pub order_leg_type: OrderLegType,
128 pub leg_id: Option<i64>,
129 pub instrument: AccountsInstrument,
130 pub instruction: OrderInstruction,
131 pub position_effect: Option<PositionEffect>,
132 pub quantity: f64,
133 pub quantity_type: Option<QuantityType>,
134 #[serde(flatten)]
135 pub extra: HashMap<String, serde_json::Value>,
136}
137
138#[skip_serializing_none]
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
144#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
145#[serde(rename_all = "camelCase")]
146#[non_exhaustive]
147pub struct OrderActivity {
148 pub activity_type: Option<OrderActivityType>,
149 pub execution_type: Option<String>,
151 pub quantity: Option<f64>,
152 pub order_remaining_quantity: Option<f64>,
153 #[serde(default)]
154 pub execution_legs: Vec<ExecutionLeg>,
155 #[serde(flatten)]
156 pub extra: HashMap<String, serde_json::Value>,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
160#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
161#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
162#[non_exhaustive]
163pub enum OrderActivityType {
164 Execution,
165 OrderAction,
166 #[serde(other)]
167 #[cfg_attr(feature = "schemars", schemars(skip))]
168 Unknown,
169}
170
171#[skip_serializing_none]
173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
174#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
175#[serde(rename_all = "camelCase")]
176#[non_exhaustive]
177pub struct ExecutionLeg {
178 pub leg_id: Option<i64>,
179 pub price: Option<f64>,
180 pub quantity: Option<f64>,
181 pub mismarked_quantity: Option<f64>,
182 pub instrument_id: Option<i64>,
183 pub time: Option<String>,
184 #[serde(flatten)]
185 pub extra: HashMap<String, serde_json::Value>,
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
190#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
191#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
192#[non_exhaustive]
193pub enum QuantityType {
194 AllShares,
195 Dollars,
196 Shares,
197 #[serde(other)]
198 #[cfg_attr(feature = "schemars", schemars(skip))]
199 Unknown,
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
203#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
204#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
205#[non_exhaustive]
206pub enum OrderSession {
207 #[default]
208 Normal,
209 Am,
210 Pm,
211 Seamless,
212 #[serde(other)]
213 #[cfg_attr(feature = "schemars", schemars(skip))]
214 Unknown,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
218#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
219#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
220#[non_exhaustive]
221pub enum OrderDuration {
222 #[default]
223 Day,
224 #[serde(rename = "GOOD_TILL_CANCEL")]
225 Gtc,
226 FillOrKill,
227 ImmediateOrCancel,
228 EndOfWeek,
229 EndOfMonth,
230 NextEndOfMonth,
231 #[serde(other)]
232 #[cfg_attr(feature = "schemars", schemars(skip))]
233 Unknown,
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
240#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
241#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
242#[non_exhaustive]
243pub enum OrderType {
244 Market,
245 Limit,
246 Stop,
247 StopLimit,
248 TrailingStop,
249 #[cfg_attr(feature = "schemars", schemars(skip))]
250 Cabinet,
251 #[cfg_attr(feature = "schemars", schemars(skip))]
252 NonMarketable,
253 MarketOnClose,
254 #[cfg_attr(feature = "schemars", schemars(skip))]
255 Exercise,
256 TrailingStopLimit,
257 NetDebit,
258 NetCredit,
259 NetZero,
260 LimitOnClose,
261 #[serde(other)]
262 #[cfg_attr(feature = "schemars", schemars(skip))]
263 Unknown,
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
267#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
268#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
269#[non_exhaustive]
270pub enum ComplexOrderStrategyType {
271 None,
272 Covered,
273 Vertical,
274 BackRatio,
275 Calendar,
276 Diagonal,
277 Straddle,
278 Strangle,
279 CollarSynthetic,
280 Butterfly,
281 Condor,
282 IronCondor,
283 VerticalRoll,
284 CollarWithStock,
285 DoubleDiagonal,
286 UnbalancedButterfly,
287 UnbalancedCondor,
288 UnbalancedIronCondor,
289 UnbalancedVerticalRoll,
290 MutualFundSwap,
291 Custom,
292 #[serde(other)]
293 #[cfg_attr(feature = "schemars", schemars(skip))]
294 Unknown,
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
301#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
302#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
303#[non_exhaustive]
304pub enum OrderStrategyType {
305 #[default]
306 Single,
307 #[cfg_attr(feature = "schemars", schemars(skip))]
308 Cancel,
309 #[cfg_attr(feature = "schemars", schemars(skip))]
310 Recall,
311 #[cfg_attr(feature = "schemars", schemars(skip))]
312 Pair,
313 #[cfg_attr(feature = "schemars", schemars(skip))]
314 Flatten,
315 #[cfg_attr(feature = "schemars", schemars(skip))]
316 TwoDaySwap,
317 #[cfg_attr(feature = "schemars", schemars(skip))]
318 BlastAll,
319 Oco,
320 Trigger,
321 #[serde(other)]
322 #[cfg_attr(feature = "schemars", schemars(skip))]
323 Unknown,
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
327#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
328#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
329#[non_exhaustive]
330pub enum OrderStatus {
331 AwaitingParentOrder,
332 AwaitingCondition,
333 AwaitingStopCondition,
334 AwaitingManualReview,
335 Accepted,
336 AwaitingUrOut,
337 PendingActivation,
338 Queued,
339 Working,
340 Rejected,
341 PendingCancel,
342 Canceled,
343 PendingReplace,
344 Replaced,
345 Filled,
346 Expired,
347 New,
348 AwaitingReleaseTime,
349 PendingAcknowledgement,
350 PendingRecall,
351 #[serde(other)]
352 #[cfg_attr(feature = "schemars", schemars(skip))]
353 Unknown,
354}
355
356impl OrderStatus {
357 pub const fn is_terminal(self) -> bool {
361 matches!(
362 self,
363 Self::Filled | Self::Canceled | Self::Rejected | Self::Expired | Self::Replaced
364 )
365 }
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
369#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
370#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
371#[non_exhaustive]
372pub enum OrderLegType {
373 #[default]
374 Equity,
375 Option,
376 Index,
377 MutualFund,
378 CashEquivalent,
379 FixedIncome,
380 Currency,
381 CollectiveInvestment,
382 #[serde(other)]
383 #[cfg_attr(feature = "schemars", schemars(skip))]
384 Unknown,
385}
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
388#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
389#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
390#[non_exhaustive]
391pub enum OrderInstruction {
392 #[default]
393 Buy,
394 Sell,
395 BuyToCover,
396 SellShort,
397 BuyToOpen,
398 BuyToClose,
399 SellToOpen,
400 SellToClose,
401 Exchange,
402 SellShortExempt,
403 #[serde(other)]
404 #[cfg_attr(feature = "schemars", schemars(skip))]
405 Unknown,
406}
407
408#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
409#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
410#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
411#[non_exhaustive]
412pub enum PositionEffect {
413 Opening,
414 Closing,
415 #[default]
416 Automatic,
417 #[serde(other)]
418 #[cfg_attr(feature = "schemars", schemars(skip))]
419 Unknown,
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
425#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
426#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
427#[non_exhaustive]
428pub enum PriceLinkBasis {
429 Manual,
430 Base,
431 Trigger,
432 Last,
433 Bid,
434 Ask,
435 AskBid,
436 Mark,
437 Average,
438 #[serde(other)]
439 #[cfg_attr(feature = "schemars", schemars(skip))]
440 Unknown,
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
446#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
447#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
448#[non_exhaustive]
449pub enum PriceLinkType {
450 Value,
451 Percent,
452 Tick,
453 #[serde(other)]
454 #[cfg_attr(feature = "schemars", schemars(skip))]
455 Unknown,
456}
457
458#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
460#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
461#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
462#[non_exhaustive]
463pub enum StopType {
464 Standard,
465 Bid,
466 Ask,
467 Last,
468 Mark,
469 #[serde(other)]
470 #[cfg_attr(feature = "schemars", schemars(skip))]
471 Unknown,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
475#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
476#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
477#[non_exhaustive]
478pub enum TaxLotMethod {
479 Fifo,
480 Lifo,
481 HighCost,
482 LowCost,
483 AverageCost,
484 SpecificLot,
485 LossHarvester,
486 #[serde(other)]
487 #[cfg_attr(feature = "schemars", schemars(skip))]
488 Unknown,
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
492#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
493#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
494#[non_exhaustive]
495pub enum SpecialInstruction {
496 AllOrNone,
497 DoNotReduce,
498 AllOrNoneDoNotReduce,
499 #[serde(other)]
500 #[cfg_attr(feature = "schemars", schemars(skip))]
501 Unknown,
502}
503
504#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
506#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
507#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
508#[non_exhaustive]
509pub enum RequestedDestination {
510 Inet,
511 EcnArca,
512 Cboe,
513 Amex,
514 Phlx,
515 Ise,
516 Box,
517 Nyse,
518 Nasdaq,
519 Bats,
520 C2,
521 Auto,
522 #[serde(other)]
523 #[cfg_attr(feature = "schemars", schemars(skip))]
524 Unknown,
525}
526
527#[skip_serializing_none]
534#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
535#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
536#[serde(rename_all = "camelCase", default)]
537#[non_exhaustive]
538pub struct OrderPreview {
539 #[serde(deserialize_with = "crate::common::optional_id")]
540 pub order_id: Option<String>,
541 pub order_strategy: Option<PreviewOrderStrategy>,
542 pub order_validation_result: OrderValidationResult,
543 pub commission_and_fee: Option<serde_json::Value>,
545 #[serde(flatten)]
546 pub extra: HashMap<String, serde_json::Value>,
547}
548
549impl OrderPreview {
550 pub fn is_rejected(&self) -> bool {
553 !self.order_validation_result.rejects.is_empty()
554 || self
555 .order_strategy
556 .as_ref()
557 .is_some_and(|s| s.status == Some(OrderStatus::Rejected))
558 }
559 pub fn rejection_reasons(&self) -> Vec<&str> {
561 self.order_validation_result
562 .rejects
563 .iter()
564 .filter_map(ValidationFinding::text)
565 .collect()
566 }
567}
568
569#[skip_serializing_none]
572#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
573#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
574#[serde(rename_all = "camelCase", default)]
575#[non_exhaustive]
576pub struct PreviewOrderStrategy {
577 #[serde(deserialize_with = "crate::common::lenient")]
578 pub status: Option<OrderStatus>,
579 pub order_balance: Option<PreviewOrderBalance>,
580 pub order_value: Option<f64>,
581 #[serde(flatten)]
582 pub extra: HashMap<String, serde_json::Value>,
583}
584
585#[skip_serializing_none]
587#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
588#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
589#[serde(rename_all = "camelCase", default)]
590#[non_exhaustive]
591pub struct PreviewOrderBalance {
592 pub order_value: Option<f64>,
593 pub projected_available_fund: Option<f64>,
594 pub projected_buying_power: Option<f64>,
595 pub projected_commission: Option<f64>,
596 #[serde(flatten)]
597 pub extra: HashMap<String, serde_json::Value>,
598}
599
600#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
602#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
603#[serde(rename_all = "camelCase", default)]
604#[non_exhaustive]
605pub struct OrderValidationResult {
606 pub alerts: Vec<ValidationFinding>,
607 pub accepts: Vec<ValidationFinding>,
608 pub rejects: Vec<ValidationFinding>,
609 pub reviews: Vec<ValidationFinding>,
610 pub warns: Vec<ValidationFinding>,
611 #[serde(flatten)]
612 pub extra: HashMap<String, serde_json::Value>,
613}
614
615#[skip_serializing_none]
617#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
618#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
619#[serde(rename_all = "camelCase", default)]
620#[non_exhaustive]
621pub struct ValidationFinding {
622 pub validation_rule_name: Option<String>,
623 pub message: Option<String>,
624 pub activity_message: Option<String>,
625 pub original_severity: Option<String>,
626 pub override_name: Option<String>,
627 pub override_severity: Option<String>,
628 #[serde(flatten)]
629 pub extra: HashMap<String, serde_json::Value>,
630}
631
632impl ValidationFinding {
633 pub fn text(&self) -> Option<&str> {
635 self.activity_message.as_deref().or(self.message.as_deref())
636 }
637}
638
639#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
646#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
647#[serde(rename_all = "camelCase")]
648pub struct OrderSubmitResponse {
649 pub order_id: Option<String>,
651 pub location: Option<String>,
653}
654
655#[skip_serializing_none]
664#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
665#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
666#[serde(rename_all = "camelCase")]
667pub struct OrdersParams {
668 #[serde(serialize_with = "crate::common::serialize_timestamp")]
670 pub from_entered_time: DateTime<Utc>,
671 #[serde(serialize_with = "crate::common::serialize_timestamp")]
673 pub to_entered_time: DateTime<Utc>,
674 pub max_results: Option<u32>,
676 pub status: Option<OrderStatus>,
678}
679
680impl OrdersParams {
681 pub fn new(from_entered_time: DateTime<Utc>, to_entered_time: DateTime<Utc>) -> Self {
683 Self {
684 from_entered_time,
685 to_entered_time,
686 max_results: None,
687 status: None,
688 }
689 }
690
691 pub fn validate(&self) -> Result<(), ValidationError> {
693 if self.from_entered_time > self.to_entered_time {
694 return Err(VE::new(
695 K::OutOfRange,
696 Some("fromEnteredTime"),
697 "fromEnteredTime must not be after toEnteredTime",
698 ));
699 }
700 if self.max_results == Some(0) {
701 return Err(VE::new(
702 K::OutOfRange,
703 Some("maxResults"),
704 "maxResults must be at least 1",
705 ));
706 }
707 crate::common::known("status", self.status.as_ref())
708 }
709}
710
711#[skip_serializing_none]
717#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
718#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
719#[serde(rename_all = "camelCase")]
720#[non_exhaustive]
721pub struct Transaction {
722 pub activity_id: Option<i64>,
723 pub time: Option<String>,
724 pub user: Option<TransactionUser>,
725 pub description: Option<String>,
726 #[serde(default, deserialize_with = "crate::common::optional_id")]
727 pub account_number: Option<String>,
728 #[serde(rename = "type")]
730 pub type_: Option<TransactionType>,
731 pub status: Option<TransactionStatus>,
732 pub sub_account: Option<SubAccount>,
733 pub trade_date: Option<String>,
734 pub settlement_date: Option<String>,
735 pub position_id: Option<i64>,
736 #[serde(default, deserialize_with = "crate::common::optional_id")]
737 pub order_id: Option<String>,
738 pub net_amount: Option<f64>,
739 pub activity_type: Option<TransactionActivityType>,
740 #[serde(default)]
741 pub transfer_items: Vec<TransferItem>,
742 #[serde(flatten)]
743 pub extra: HashMap<String, serde_json::Value>,
744}
745
746#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
749#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
750#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
751#[non_exhaustive]
752pub enum TransactionType {
753 Trade,
754 ReceiveAndDeliver,
755 DividendOrInterest,
756 AchReceipt,
757 AchDisbursement,
758 CashReceipt,
759 CashDisbursement,
760 ElectronicFund,
761 WireOut,
762 WireIn,
763 Journal,
764 Memorandum,
765 MarginCall,
766 MoneyMarket,
767 SmaAdjustment,
768 #[serde(other)]
769 #[cfg_attr(feature = "schemars", schemars(skip))]
770 Unknown,
771}
772
773impl TransactionType {
774 pub const fn as_str(self) -> &'static str {
776 match self {
777 Self::Trade => "TRADE",
778 Self::ReceiveAndDeliver => "RECEIVE_AND_DELIVER",
779 Self::DividendOrInterest => "DIVIDEND_OR_INTEREST",
780 Self::AchReceipt => "ACH_RECEIPT",
781 Self::AchDisbursement => "ACH_DISBURSEMENT",
782 Self::CashReceipt => "CASH_RECEIPT",
783 Self::CashDisbursement => "CASH_DISBURSEMENT",
784 Self::ElectronicFund => "ELECTRONIC_FUND",
785 Self::WireOut => "WIRE_OUT",
786 Self::WireIn => "WIRE_IN",
787 Self::Journal => "JOURNAL",
788 Self::Memorandum => "MEMORANDUM",
789 Self::MarginCall => "MARGIN_CALL",
790 Self::MoneyMarket => "MONEY_MARKET",
791 Self::SmaAdjustment => "SMA_ADJUSTMENT",
792 Self::Unknown => "UNKNOWN",
793 }
794 }
795}
796
797impl std::fmt::Display for TransactionType {
798 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
799 f.write_str(self.as_str())
800 }
801}
802
803#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
804#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
805#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
806#[non_exhaustive]
807pub enum TransactionStatus {
808 Valid,
809 Invalid,
810 Pending,
811 #[serde(other)]
812 #[cfg_attr(feature = "schemars", schemars(skip))]
813 Unknown,
814}
815
816#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
818#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
819#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
820#[non_exhaustive]
821pub enum SubAccount {
822 Cash,
823 Margin,
824 Short,
825 Div,
826 Income,
827 #[serde(other)]
828 #[cfg_attr(feature = "schemars", schemars(skip))]
829 Unknown,
830}
831
832#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
833#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
834#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
835#[non_exhaustive]
836pub enum TransactionActivityType {
837 ActivityCorrection,
838 Execution,
839 OrderAction,
840 Transfer,
841 #[serde(other)]
842 #[cfg_attr(feature = "schemars", schemars(skip))]
843 Unknown,
844}
845
846#[skip_serializing_none]
848#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
849#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
850#[serde(rename_all = "camelCase")]
851#[non_exhaustive]
852pub struct TransferItem {
853 pub instrument: Option<TransactionInstrument>,
854 pub amount: Option<f64>,
856 pub cost: Option<f64>,
857 pub price: Option<f64>,
858 pub fee_type: Option<FeeType>,
860 pub position_effect: Option<PositionEffect>,
862 #[serde(flatten)]
863 pub extra: HashMap<String, serde_json::Value>,
864}
865
866#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
867#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
868#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
869#[non_exhaustive]
870pub enum FeeType {
871 Commission,
872 SecFee,
873 StrFee,
874 RFee,
875 CdscFee,
876 OptRegFee,
877 AdditionalFee,
878 MiscellaneousFee,
879 Ftt,
880 FuturesClearingFee,
881 FuturesDeskOfficeFee,
882 FuturesExchangeFee,
883 FuturesGlobexFee,
884 FuturesNfaFee,
885 FuturesPitBrokerageFee,
886 FuturesTransactionFee,
887 LowProceedsCommission,
888 BaseCharge,
889 GeneralCharge,
890 GstFee,
891 TafFee,
892 IndexOptionFee,
893 TefraTax,
894 StateTax,
895 #[serde(other)]
896 #[cfg_attr(feature = "schemars", schemars(skip))]
897 Unknown,
898}
899
900#[skip_serializing_none]
901#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
902#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
903#[serde(rename_all = "camelCase")]
904#[non_exhaustive]
905pub struct TransactionUser {
906 pub cd_domain_id: Option<String>,
907 pub login: Option<String>,
908 #[serde(rename = "type")]
909 pub type_: Option<String>,
910 pub user_id: Option<i64>,
911 pub system_user_name: Option<String>,
912 pub first_name: Option<String>,
913 pub last_name: Option<String>,
914 pub broker_rep_code: Option<String>,
915}
916
917#[skip_serializing_none]
921#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
922#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
923#[serde(rename_all = "camelCase", default)]
924#[non_exhaustive]
925pub struct TransactionInstrument {
926 pub asset_type: AssetType,
927 pub symbol: String,
928 pub cusip: Option<String>,
929 pub description: Option<String>,
930 pub instrument_id: Option<i64>,
931 pub net_change: Option<f64>,
932 #[serde(rename = "type")]
935 pub type_: Option<String>,
936 pub status: Option<String>,
938 pub closing_price: Option<f64>,
940 pub expiration_date: Option<String>,
942 #[serde(default, deserialize_with = "crate::common::lenient")]
944 pub put_call: Option<PutCall>,
945 pub strike_price: Option<f64>,
947 pub option_premium_multiplier: Option<i64>,
949 pub underlying_symbol: Option<String>,
951 pub underlying_cusip: Option<String>,
953 pub maturity_date: Option<String>,
955 pub factor: Option<f64>,
957 pub variable_rate: Option<f64>,
959 pub multiplier: Option<f64>,
961 #[serde(flatten)]
962 pub extra: HashMap<String, serde_json::Value>,
963}
964
965#[skip_serializing_none]
974#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
975#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
976#[serde(rename_all = "camelCase")]
977pub struct TransactionsParams {
978 #[serde(serialize_with = "crate::common::serialize_timestamp")]
980 pub start_date: DateTime<Utc>,
981 #[serde(serialize_with = "crate::common::serialize_timestamp")]
983 pub end_date: DateTime<Utc>,
984 #[serde(
988 serialize_with = "serialize_transaction_types",
989 deserialize_with = "deserialize_transaction_types"
990 )]
991 pub types: Vec<TransactionType>,
992 pub symbol: Option<String>,
994}
995
996impl TransactionsParams {
997 pub fn new(
999 start_date: DateTime<Utc>,
1000 end_date: DateTime<Utc>,
1001 types: impl IntoIterator<Item = TransactionType>,
1002 ) -> Self {
1003 Self {
1004 start_date,
1005 end_date,
1006 types: types.into_iter().collect(),
1007 symbol: None,
1008 }
1009 }
1010
1011 pub fn validate(&self) -> Result<(), ValidationError> {
1013 if self.start_date > self.end_date {
1014 return Err(VE::new(
1015 K::OutOfRange,
1016 Some("startDate"),
1017 "startDate must not be after endDate",
1018 ));
1019 }
1020 if self.types.is_empty() {
1021 return Err(VE::new(
1022 K::Missing,
1023 Some("types"),
1024 "types must name at least one transaction type",
1025 ));
1026 }
1027 for t in &self.types {
1028 crate::common::known("types", Some(t))?;
1029 }
1030 if self.symbol.as_deref().is_some_and(|s| s.trim().is_empty()) {
1031 return Err(VE::new(
1032 K::Symbol,
1033 Some("symbol"),
1034 "symbol must not be blank",
1035 ));
1036 }
1037 Ok(())
1038 }
1039}
1040
1041fn serialize_transaction_types<S: serde::Serializer>(
1043 types: &[TransactionType],
1044 serializer: S,
1045) -> Result<S::Ok, S::Error> {
1046 let csv = types
1047 .iter()
1048 .map(|t| t.as_str())
1049 .collect::<Vec<_>>()
1050 .join(",");
1051 serializer.serialize_str(&csv)
1052}
1053
1054fn deserialize_transaction_types<'de, D: serde::Deserializer<'de>>(
1056 deserializer: D,
1057) -> Result<Vec<TransactionType>, D::Error> {
1058 use serde::de::IntoDeserializer;
1059
1060 #[derive(Deserialize)]
1061 #[serde(untagged)]
1062 enum Types {
1063 List(Vec<TransactionType>),
1064 Csv(String),
1065 }
1066
1067 match Types::deserialize(deserializer)? {
1068 Types::List(types) => Ok(types),
1069 Types::Csv(csv) => csv
1070 .split(',')
1071 .map(|name| TransactionType::deserialize(name.trim().into_deserializer()))
1072 .collect(),
1073 }
1074}
1075
1076#[skip_serializing_none]
1080#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1081#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1082#[serde(rename_all = "camelCase", deny_unknown_fields)]
1083pub struct OrderRequest {
1084 pub session: Option<OrderSession>,
1085 pub duration: Option<OrderDuration>,
1086 pub order_type: Option<OrderType>,
1087 pub complex_order_strategy_type: Option<ComplexOrderStrategyType>,
1088 #[serde(
1092 default,
1093 deserialize_with = "crate::common::exact_decimal::option::deserialize",
1094 serialize_with = "crate::common::exact_decimal::option::serialize_number"
1095 )]
1096 #[cfg_attr(
1097 feature = "schemars",
1098 schemars(with = "Option<crate::common::QuantityNumber>")
1099 )]
1100 pub quantity: Option<rust_decimal::Decimal>,
1101 #[serde(
1102 default,
1103 deserialize_with = "crate::common::exact_decimal::option::deserialize",
1104 serialize_with = "crate::common::exact_decimal::option::serialize_string"
1105 )]
1106 #[cfg_attr(
1107 feature = "schemars",
1108 schemars(with = "Option<crate::common::DecimalString>")
1109 )]
1110 pub price: Option<rust_decimal::Decimal>,
1111 #[serde(
1112 default,
1113 deserialize_with = "crate::common::exact_decimal::option::deserialize",
1114 serialize_with = "crate::common::exact_decimal::option::serialize_string"
1115 )]
1116 #[cfg_attr(
1117 feature = "schemars",
1118 schemars(with = "Option<crate::common::DecimalString>")
1119 )]
1120 pub stop_price: Option<rust_decimal::Decimal>,
1121 pub stop_price_link_basis: Option<PriceLinkBasis>,
1122 pub stop_price_link_type: Option<PriceLinkType>,
1123 #[serde(
1124 default,
1125 deserialize_with = "crate::common::exact_decimal::option::deserialize",
1126 serialize_with = "crate::common::exact_decimal::option::serialize_string"
1127 )]
1128 #[cfg_attr(
1129 feature = "schemars",
1130 schemars(with = "Option<crate::common::DecimalString>")
1131 )]
1132 pub stop_price_offset: Option<rust_decimal::Decimal>,
1133 pub stop_type: Option<StopType>,
1134 pub price_link_basis: Option<PriceLinkBasis>,
1135 pub price_link_type: Option<PriceLinkType>,
1136 pub tax_lot_method: Option<TaxLotMethod>,
1137 pub tag: Option<String>,
1138 pub special_instruction: Option<SpecialInstruction>,
1139 pub requested_destination: Option<RequestedDestination>,
1140 #[serde(
1143 default,
1144 serialize_with = "crate::common::serialize_optional_timestamp"
1145 )]
1146 pub release_time: Option<DateTime<Utc>>,
1147 #[serde(
1149 default,
1150 serialize_with = "crate::common::serialize_optional_timestamp"
1151 )]
1152 pub cancel_time: Option<DateTime<Utc>>,
1153 #[serde(
1155 default,
1156 deserialize_with = "crate::common::exact_decimal::option::deserialize",
1157 serialize_with = "crate::common::exact_decimal::option::serialize_string"
1158 )]
1159 #[cfg_attr(
1160 feature = "schemars",
1161 schemars(with = "Option<crate::common::DecimalString>")
1162 )]
1163 pub activation_price: Option<rust_decimal::Decimal>,
1164 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1165 pub order_leg_collection: Vec<OrderRequestLeg>,
1166 pub order_strategy_type: OrderStrategyType,
1167 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1168 pub child_order_strategies: Vec<OrderRequest>,
1169}
1170
1171#[skip_serializing_none]
1172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1173#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1174#[serde(rename_all = "camelCase", deny_unknown_fields)]
1175pub struct OrderRequestLeg {
1176 pub instruction: OrderInstruction,
1177 #[serde(
1180 deserialize_with = "crate::common::exact_decimal::deserialize",
1181 serialize_with = "crate::common::exact_decimal::serialize_number"
1182 )]
1183 #[cfg_attr(feature = "schemars", schemars(with = "crate::common::QuantityNumber"))]
1184 pub quantity: rust_decimal::Decimal,
1185 pub instrument: OrderRequestInstrument,
1186 pub position_effect: Option<PositionEffect>,
1187 #[serde(default)]
1190 pub quantity_type: Option<QuantityType>,
1191 #[serde(default)]
1193 pub div_cap_gains: Option<DivCapGains>,
1194 #[serde(default)]
1196 pub to_symbol: Option<String>,
1197}
1198
1199#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1201#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1202#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1203#[non_exhaustive]
1204pub enum DivCapGains {
1205 Reinvest,
1206 Payout,
1207 #[serde(other)]
1208 #[cfg_attr(feature = "schemars", schemars(skip))]
1209 Unknown,
1210}
1211
1212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1213#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1214#[serde(rename_all = "camelCase", deny_unknown_fields)]
1215pub struct OrderRequestInstrument {
1216 pub symbol: String,
1217 pub asset_type: AssetType,
1218}
1219
1220impl TryFrom<&Order> for OrderRequest {
1225 type Error = ValidationError;
1226
1227 fn try_from(order: &Order) -> Result<Self, ValidationError> {
1228 fn exact(field: &str, value: f64) -> Result<rust_decimal::Decimal, ValidationError> {
1229 crate::common::exact_decimal::parse(&value.to_string())
1230 .map_err(|e| VE::new(K::OutOfRange, Some(field), e))
1231 }
1232 let optional = |field: &str, value: Option<f64>| value.map(|v| exact(field, v)).transpose();
1233 let mut legs = Vec::with_capacity(order.order_leg_collection.len());
1234 for (i, leg) in order.order_leg_collection.iter().enumerate() {
1235 legs.push(OrderRequestLeg {
1236 instruction: leg.instruction,
1237 quantity: exact("quantity", leg.quantity)
1238 .map_err(|e| e.within(&format!("orderLegCollection[{i}]")))?,
1239 instrument: OrderRequestInstrument {
1240 symbol: leg.instrument.symbol.clone(),
1241 asset_type: leg.instrument.asset_type,
1242 },
1243 position_effect: leg.position_effect,
1244 quantity_type: leg.quantity_type,
1245 div_cap_gains: None,
1246 to_symbol: None,
1247 });
1248 }
1249 let mut children = Vec::with_capacity(order.child_order_strategies.len());
1250 for (i, child) in order.child_order_strategies.iter().enumerate() {
1251 children.push(
1252 Self::try_from(child)
1253 .map_err(|e| e.within(&format!("childOrderStrategies[{i}]")))?,
1254 );
1255 }
1256 let time = |value: &Option<String>| {
1257 value
1258 .as_deref()
1259 .and_then(|t| crate::common::parse_schwab_time(t).ok())
1260 };
1261 Ok(Self {
1262 session: order.session,
1263 duration: order.duration,
1264 order_type: order.order_type,
1265 complex_order_strategy_type: order.complex_order_strategy_type,
1266 quantity: optional("quantity", order.quantity)?,
1267 price: optional("price", order.price)?,
1268 stop_price: optional("stopPrice", order.stop_price)?,
1269 stop_price_link_basis: order.stop_price_link_basis,
1270 stop_price_link_type: order.stop_price_link_type,
1271 stop_price_offset: optional("stopPriceOffset", order.stop_price_offset)?,
1272 stop_type: order.stop_type,
1273 price_link_basis: order.price_link_basis,
1274 price_link_type: order.price_link_type,
1275 tax_lot_method: order.tax_lot_method,
1276 tag: None,
1277 special_instruction: order.special_instruction,
1278 requested_destination: order.requested_destination,
1279 release_time: time(&order.release_time),
1280 cancel_time: None,
1281 activation_price: optional("activationPrice", order.activation_price)?,
1282 order_leg_collection: legs,
1283 order_strategy_type: order.order_strategy_type.unwrap_or_default(),
1284 child_order_strategies: children,
1285 })
1286 }
1287}
1288
1289crate::common::wire_enum!(
1290 QuantityType,
1291 OrderSession,
1292 OrderDuration,
1293 OrderType,
1294 ComplexOrderStrategyType,
1295 OrderStrategyType,
1296 OrderStatus,
1297 OrderLegType,
1298 OrderInstruction,
1299 PositionEffect,
1300 PriceLinkBasis,
1301 PriceLinkType,
1302 StopType,
1303 TaxLotMethod,
1304 SpecialInstruction,
1305 RequestedDestination,
1306 OrderActivityType,
1307 TransactionType,
1308 TransactionStatus,
1309 SubAccount,
1310 TransactionActivityType,
1311 FeeType,
1312 DivCapGains,
1313);
1314
1315const MAX_STRATEGY_DEPTH: usize = 8;
1317
1318struct TypeRules {
1321 price: bool,
1322 stop_price: bool,
1323 trailing: bool,
1324 price_link: bool,
1326 stop_type: bool,
1328 net: bool,
1330}
1331
1332impl OrderType {
1333 fn rules(self) -> Option<TypeRules> {
1336 let base = TypeRules {
1337 price: false,
1338 stop_price: false,
1339 trailing: false,
1340 price_link: false,
1341 stop_type: false,
1342 net: false,
1343 };
1344 Some(match self {
1345 Self::Market | Self::MarketOnClose => base,
1346 Self::Limit | Self::LimitOnClose => TypeRules {
1347 price: true,
1348 ..base
1349 },
1350 Self::Stop => TypeRules {
1351 stop_price: true,
1352 stop_type: true,
1353 ..base
1354 },
1355 Self::StopLimit => TypeRules {
1356 price: true,
1357 stop_price: true,
1358 stop_type: true,
1359 ..base
1360 },
1361 Self::TrailingStop => TypeRules {
1362 trailing: true,
1363 stop_type: true,
1364 ..base
1365 },
1366 Self::TrailingStopLimit => TypeRules {
1367 price: true,
1368 trailing: true,
1369 price_link: true,
1370 stop_type: true,
1371 ..base
1372 },
1373 Self::NetDebit | Self::NetCredit => TypeRules {
1374 price: true,
1375 net: true,
1376 ..base
1377 },
1378 Self::NetZero => TypeRules { net: true, ..base },
1379 Self::Cabinet | Self::NonMarketable | Self::Exercise | Self::Unknown => return None,
1380 })
1381 }
1382}
1383
1384impl OrderRequest {
1385 pub fn validate(&self) -> Result<(), ValidationError> {
1391 self.validate_depth(1)
1392 }
1393
1394 fn validate_depth(&self, depth: usize) -> Result<(), ValidationError> {
1395 use crate::common::known;
1396 if depth > MAX_STRATEGY_DEPTH {
1397 return Err(VE::new(
1398 K::Structure,
1399 None,
1400 format!("Order strategies nest at most {MAX_STRATEGY_DEPTH} levels"),
1401 ));
1402 }
1403 known("session", self.session.as_ref())?;
1404 known("duration", self.duration.as_ref())?;
1405 known("orderType", self.order_type.as_ref())?;
1406 known(
1407 "complexOrderStrategyType",
1408 self.complex_order_strategy_type.as_ref(),
1409 )?;
1410 known("stopPriceLinkBasis", self.stop_price_link_basis.as_ref())?;
1411 known("stopPriceLinkType", self.stop_price_link_type.as_ref())?;
1412 known("stopType", self.stop_type.as_ref())?;
1413 known("priceLinkBasis", self.price_link_basis.as_ref())?;
1414 known("priceLinkType", self.price_link_type.as_ref())?;
1415 known("taxLotMethod", self.tax_lot_method.as_ref())?;
1416 known("specialInstruction", self.special_instruction.as_ref())?;
1417 known("requestedDestination", self.requested_destination.as_ref())?;
1418 known("orderStrategyType", Some(&self.order_strategy_type))?;
1419 for (name, value) in [
1420 ("quantity", self.quantity),
1421 ("price", self.price),
1422 ("stopPrice", self.stop_price),
1423 ("stopPriceOffset", self.stop_price_offset),
1424 ("activationPrice", self.activation_price),
1425 ] {
1426 if value.is_some_and(|v| v <= rust_decimal::Decimal::ZERO) {
1427 return Err(VE::new(
1428 K::OutOfRange,
1429 Some(name),
1430 format!("{name} must be positive"),
1431 ));
1432 }
1433 }
1434 if let (Some(release), Some(cancel)) = (self.release_time, self.cancel_time)
1435 && cancel <= release
1436 {
1437 return Err(VE::new(
1438 K::Structure,
1439 Some("cancelTime"),
1440 "cancelTime must be after releaseTime",
1441 ));
1442 }
1443 match self.order_strategy_type {
1444 OrderStrategyType::Oco => self.validate_oco_parent()?,
1445 OrderStrategyType::Single | OrderStrategyType::Trigger => self.validate_executable()?,
1446 _ => {
1447 return Err(VE::new(
1448 K::Unsupported,
1449 Some("orderStrategyType"),
1450 "Only SINGLE, OCO, and TRIGGER strategies can be submitted",
1451 ));
1452 }
1453 }
1454 for (i, child) in self.child_order_strategies.iter().enumerate() {
1455 child
1456 .validate_depth(depth + 1)
1457 .map_err(|e| e.within(&format!("childOrderStrategies[{i}]")))?;
1458 }
1459 Ok(())
1460 }
1461
1462 fn validate_oco_parent(&self) -> Result<(), ValidationError> {
1465 let executable = self.session.is_some()
1466 || self.duration.is_some()
1467 || self.order_type.is_some()
1468 || self.complex_order_strategy_type.is_some()
1469 || self.quantity.is_some()
1470 || self.price.is_some()
1471 || self.stop_price.is_some()
1472 || self.stop_price_link_basis.is_some()
1473 || self.stop_price_link_type.is_some()
1474 || self.stop_price_offset.is_some()
1475 || self.stop_type.is_some()
1476 || self.price_link_basis.is_some()
1477 || self.price_link_type.is_some()
1478 || self.tax_lot_method.is_some()
1479 || self.special_instruction.is_some()
1480 || self.requested_destination.is_some()
1481 || self.release_time.is_some()
1482 || self.cancel_time.is_some()
1483 || self.activation_price.is_some()
1484 || !self.order_leg_collection.is_empty();
1485 if self.child_order_strategies.len() != 2 || executable {
1486 return Err(VE::new(
1487 K::Structure,
1488 None,
1489 "OCO must contain exactly two children and no executable parent",
1490 ));
1491 }
1492 Ok(())
1493 }
1494
1495 fn validate_executable(&self) -> Result<(), ValidationError> {
1496 let missing = [
1497 ("orderType", self.order_type.is_none()),
1498 ("session", self.session.is_none()),
1499 ("duration", self.duration.is_none()),
1500 ("orderLegCollection", self.order_leg_collection.is_empty()),
1501 ]
1502 .into_iter()
1503 .find_map(|(field, absent)| absent.then_some(field));
1504 let (Some(kind), Some(session), Some(duration), None) =
1505 (self.order_type, self.session, self.duration, missing)
1506 else {
1507 return Err(VE::new(
1508 K::Missing,
1509 missing,
1510 "Executable orders require legs, session, duration, and orderType",
1511 ));
1512 };
1513 let name = serde_json::to_value(kind).unwrap_or_default();
1514 let name = name.as_str().unwrap_or("this order type");
1515 let rules = kind.rules().ok_or_else(|| {
1516 VE::new(
1517 K::Unsupported,
1518 Some("orderType"),
1519 format!("{name} orders cannot be submitted"),
1520 )
1521 })?;
1522 let field = |present: bool, required: bool, field: &str| -> Result<(), ValidationError> {
1523 match (present, required) {
1524 (false, true) => Err(VE::new(
1525 K::Missing,
1526 Some(field),
1527 format!("{name} requires {field}"),
1528 )),
1529 (true, false) => Err(VE::new(
1530 K::NotAllowed,
1531 Some(field),
1532 format!("{name} does not take {field}"),
1533 )),
1534 _ => Ok(()),
1535 }
1536 };
1537 field(self.price.is_some(), rules.price, "price")?;
1538 field(self.stop_price.is_some(), rules.stop_price, "stopPrice")?;
1539 for (present, f) in [
1540 (self.stop_price_offset.is_some(), "stopPriceOffset"),
1541 (self.stop_price_link_basis.is_some(), "stopPriceLinkBasis"),
1542 (self.stop_price_link_type.is_some(), "stopPriceLinkType"),
1543 ] {
1544 field(present, rules.trailing, f)?;
1545 }
1546 if self.price_link_basis.is_some() != self.price_link_type.is_some() {
1547 return Err(VE::new(
1548 K::Missing,
1549 Some(if self.price_link_basis.is_some() {
1550 "priceLinkType"
1551 } else {
1552 "priceLinkBasis"
1553 }),
1554 "priceLinkBasis and priceLinkType go together",
1555 ));
1556 }
1557 if self.price_link_basis.is_some() && !rules.price_link {
1558 field(true, false, "priceLinkBasis")?;
1559 }
1560 if self.stop_type.is_some() && !rules.stop_type {
1561 field(true, false, "stopType")?;
1562 }
1563 if rules.trailing {
1564 if self.stop_price_link_basis == Some(PriceLinkBasis::Manual) {
1565 return Err(VE::new(
1566 K::OutOfRange,
1567 Some("stopPriceLinkBasis"),
1568 "A trailing stop cannot trail a MANUAL basis",
1569 ));
1570 }
1571 if self.stop_price_link_type == Some(PriceLinkType::Percent)
1572 && self
1573 .stop_price_offset
1574 .is_some_and(|o| o >= rust_decimal::Decimal::ONE_HUNDRED)
1575 {
1576 return Err(VE::new(
1577 K::OutOfRange,
1578 Some("stopPriceOffset"),
1579 "A percentage trailing offset must be below 100",
1580 ));
1581 }
1582 }
1583 if rules.net && self.order_leg_collection.len() < 2 {
1584 return Err(VE::new(
1585 K::Structure,
1586 Some("orderLegCollection"),
1587 format!("{name} needs two or more legs"),
1588 ));
1589 }
1590 if session != OrderSession::Normal && kind != OrderType::Limit {
1591 return Err(VE::new(
1592 K::Structure,
1593 Some("session"),
1594 "Extended-hours sessions accept only LIMIT orders",
1595 ));
1596 }
1597 if matches!(kind, OrderType::MarketOnClose | OrderType::LimitOnClose)
1598 && (session != OrderSession::Normal || duration != OrderDuration::Day)
1599 {
1600 return Err(VE::new(
1601 K::Structure,
1602 Some(if session != OrderSession::Normal {
1603 "session"
1604 } else {
1605 "duration"
1606 }),
1607 format!("{name} requires the NORMAL session and DAY duration"),
1608 ));
1609 }
1610 let single = self.order_leg_collection.len() == 1;
1611 for (i, leg) in self.order_leg_collection.iter().enumerate() {
1612 leg.validate()
1613 .map_err(|e| e.within(&format!("orderLegCollection[{i}]")))?;
1614 let fits = |q: rust_decimal::Decimal| {
1618 if single {
1619 q == leg.quantity
1620 } else {
1621 (leg.quantity % q).is_zero()
1622 }
1623 };
1624 if self.quantity.is_some_and(|q| !fits(q)) {
1625 return Err(VE::new(
1626 K::Structure,
1627 Some("quantity"),
1628 if single {
1629 "quantity must equal the leg's quantity; size orders on the legs"
1630 } else {
1631 "quantity is the unit count; every leg's quantity must be a whole \
1632 multiple of it"
1633 },
1634 ));
1635 }
1636 }
1637 self.validate_leg_set(kind, session)?;
1638 match (
1639 self.order_strategy_type,
1640 self.child_order_strategies.is_empty(),
1641 ) {
1642 (OrderStrategyType::Single, false) => Err(VE::new(
1643 K::Structure,
1644 Some("childOrderStrategies"),
1645 "SINGLE cannot contain child strategies",
1646 )),
1647 (OrderStrategyType::Trigger, true) => Err(VE::new(
1648 K::Missing,
1649 Some("childOrderStrategies"),
1650 "TRIGGER requires child strategies",
1651 )),
1652 _ => Ok(()),
1653 }
1654 }
1655
1656 fn validate_leg_set(
1662 &self,
1663 kind: OrderType,
1664 session: OrderSession,
1665 ) -> Result<(), ValidationError> {
1666 let legs = &self.order_leg_collection;
1667 let options: Vec<OptionSymbol> = legs
1668 .iter()
1669 .filter(|leg| leg.instrument.asset_type == AssetType::Option)
1670 .filter_map(|leg| leg.instrument.symbol.parse().ok())
1671 .collect();
1672 if !options.is_empty() && session != OrderSession::Normal {
1673 return Err(VE::new(
1674 K::Structure,
1675 Some("session"),
1676 "Options trade only in the NORMAL session",
1677 ));
1678 }
1679 if legs.len() > 1
1680 && !matches!(
1681 kind,
1682 OrderType::NetDebit | OrderType::NetCredit | OrderType::NetZero | OrderType::Market
1683 )
1684 {
1685 return Err(VE::new(
1686 K::Structure,
1687 Some("orderType"),
1688 "Several legs are priced together: NET_DEBIT, NET_CREDIT, NET_ZERO or MARKET",
1689 ));
1690 }
1691 for (i, leg) in legs.iter().enumerate() {
1692 if legs[..i].iter().any(|other| {
1693 other.instrument.symbol == leg.instrument.symbol
1694 && other.instruction == leg.instruction
1695 }) {
1696 return Err(VE::new(
1697 K::Structure,
1698 Some(&format!("orderLegCollection[{i}]")),
1699 "A leg repeats another; trade the combined quantity on one leg",
1700 ));
1701 }
1702 }
1703 use ComplexOrderStrategyType as C;
1704 if let Some(strategy) = self.complex_order_strategy_type {
1705 let expected = match strategy {
1706 C::Vertical
1707 | C::BackRatio
1708 | C::Calendar
1709 | C::Diagonal
1710 | C::Straddle
1711 | C::Strangle
1712 | C::CollarSynthetic
1713 | C::Covered => Some(2),
1714 C::Butterfly | C::UnbalancedButterfly | C::CollarWithStock => Some(3),
1715 C::Condor
1716 | C::IronCondor
1717 | C::VerticalRoll
1718 | C::DoubleDiagonal
1719 | C::UnbalancedCondor
1720 | C::UnbalancedIronCondor
1721 | C::UnbalancedVerticalRoll => Some(4),
1722 _ => None,
1723 };
1724 if expected.is_some_and(|n| n != legs.len()) {
1725 return Err(VE::new(
1726 K::Structure,
1727 Some("orderLegCollection"),
1728 format!("{strategy:?} takes {} legs", expected.unwrap_or_default()),
1729 ));
1730 }
1731 if expected.is_some() {
1734 let mut underlyings = options.iter().map(OptionSymbol::underlying).chain(
1735 legs.iter()
1736 .filter(|leg| leg.instrument.asset_type != AssetType::Option)
1737 .map(|leg| leg.instrument.symbol.as_str()),
1738 );
1739 if let Some(first) = underlyings.next()
1740 && underlyings.any(|u| u != first)
1741 {
1742 return Err(VE::new(
1743 K::Structure,
1744 Some("orderLegCollection"),
1745 format!("{strategy:?} legs must share one underlying"),
1746 ));
1747 }
1748 }
1749 }
1750 if self.stop_price_link_type == Some(PriceLinkType::Tick)
1751 && self.stop_price_offset.is_some_and(|o| !o.fract().is_zero())
1752 {
1753 return Err(VE::new(
1754 K::NotWhole,
1755 Some("stopPriceOffset"),
1756 "A trailing offset in ticks is a whole number of ticks",
1757 ));
1758 }
1759 Ok(())
1760 }
1761
1762 pub fn validate_price_increment(
1765 &self,
1766 tick: rust_decimal::Decimal,
1767 ) -> Result<(), ValidationError> {
1768 self.validate()?;
1769 self.validate_ticks(tick)
1770 }
1771
1772 fn validate_ticks(&self, tick: rust_decimal::Decimal) -> Result<(), ValidationError> {
1773 if tick <= rust_decimal::Decimal::ZERO {
1774 return Err(VE::new(
1775 K::OutOfRange,
1776 None,
1777 "Tick size must be finite and positive",
1778 ));
1779 }
1780 let value_offset = self
1781 .stop_price_offset
1782 .filter(|_| self.stop_price_link_type == Some(PriceLinkType::Value));
1783 for (field, price) in [
1784 ("price", self.price),
1785 ("stopPrice", self.stop_price),
1786 ("stopPriceOffset", value_offset),
1787 ] {
1788 if price.is_some_and(|price| price % tick != rust_decimal::Decimal::ZERO) {
1789 return Err(VE::new(
1790 K::Tick,
1791 Some(field),
1792 "Price does not match the supplied tick size",
1793 ));
1794 }
1795 }
1796 for (i, child) in self.child_order_strategies.iter().enumerate() {
1797 child
1798 .validate_ticks(tick)
1799 .map_err(|e| e.within(&format!("childOrderStrategies[{i}]")))?;
1800 }
1801 Ok(())
1802 }
1803}
1804
1805impl OrderRequestLeg {
1806 pub fn validate(&self) -> Result<(), ValidationError> {
1812 use crate::common::known;
1813 use OrderInstruction as I;
1814 known("instruction", Some(&self.instruction))?;
1815 known("positionEffect", self.position_effect.as_ref())?;
1816 known("instrument.assetType", Some(&self.instrument.asset_type))?;
1817 known("quantityType", self.quantity_type.as_ref())?;
1818 known("divCapGains", self.div_cap_gains.as_ref())?;
1819 let symbol = &self.instrument.symbol;
1820 if symbol.trim().is_empty()
1821 || symbol.trim() != symbol
1822 || symbol.bytes().any(|b| b.is_ascii_lowercase())
1823 {
1824 return Err(VE::new(
1825 K::Symbol,
1826 Some("instrument.symbol"),
1827 format!("Instrument symbol {symbol:?} must be non-empty, upper case and unpadded"),
1828 ));
1829 }
1830 if self.quantity <= rust_decimal::Decimal::ZERO {
1831 return Err(VE::new(
1832 K::OutOfRange,
1833 Some("quantity"),
1834 "Leg quantity must be positive",
1835 ));
1836 }
1837 let whole = self.quantity.fract().is_zero();
1838 let instruction = |allowed: bool, expected: &str| {
1839 if allowed {
1840 Ok(())
1841 } else {
1842 Err(VE::new(
1843 K::NotAllowed,
1844 Some("instruction"),
1845 format!(
1846 "{:?} does not fit this asset type; it takes {expected}",
1847 self.instruction
1848 ),
1849 ))
1850 }
1851 };
1852 let not_whole = |message: &str| VE::new(K::NotWhole, Some("quantity"), message);
1853 match self.instrument.asset_type {
1854 AssetType::Equity | AssetType::CollectiveInvestment => {
1855 instruction(
1856 matches!(
1857 self.instruction,
1858 I::Buy | I::Sell | I::SellShort | I::BuyToCover | I::SellShortExempt
1859 ),
1860 "BUY, SELL, SELL_SHORT, SELL_SHORT_EXEMPT or BUY_TO_COVER",
1861 )?;
1862 if !whole {
1863 return Err(not_whole("Equity orders are for whole shares"));
1864 }
1865 }
1866 AssetType::Option => {
1867 instruction(
1868 matches!(
1869 self.instruction,
1870 I::BuyToOpen | I::BuyToClose | I::SellToOpen | I::SellToClose
1871 ),
1872 "BUY_TO_OPEN, BUY_TO_CLOSE, SELL_TO_OPEN or SELL_TO_CLOSE",
1873 )?;
1874 symbol
1875 .parse::<OptionSymbol>()
1876 .map_err(|e| VE::new(K::Symbol, Some("instrument.symbol"), e))?;
1877 if !whole {
1878 return Err(not_whole("Option orders are for whole contracts"));
1879 }
1880 }
1881 AssetType::MutualFund => {
1882 instruction(
1883 matches!(self.instruction, I::Buy | I::Sell | I::Exchange),
1884 "BUY, SELL or EXCHANGE",
1885 )?;
1886 }
1887 _ => {
1888 return Err(VE::new(
1889 K::Unsupported,
1890 Some("instrument.assetType"),
1891 "Order legs may be EQUITY, COLLECTIVE_INVESTMENT, OPTION or MUTUAL_FUND",
1892 ));
1893 }
1894 }
1895 if self.instrument.asset_type != AssetType::MutualFund {
1896 for (present, field) in [
1897 (self.quantity_type.is_some(), "quantityType"),
1898 (self.div_cap_gains.is_some(), "divCapGains"),
1899 (self.to_symbol.is_some(), "toSymbol"),
1900 ] {
1901 if present {
1902 return Err(VE::new(
1903 K::NotAllowed,
1904 Some(field),
1905 "quantityType, divCapGains and toSymbol apply to mutual funds only",
1906 ));
1907 }
1908 }
1909 }
1910 if (self.instruction == I::Exchange) != self.to_symbol.is_some() {
1911 return Err(VE::new(
1912 K::Structure,
1913 Some("toSymbol"),
1914 "EXCHANGE takes a toSymbol, and only EXCHANGE does",
1915 ));
1916 }
1917 Ok(())
1918 }
1919}