Skip to main content

schwab_sdk/orders/
response.rs

1//! Response shapes for the `/orders` and `/accounts/{n}/orders*` GET
2//! endpoints. The construction-side types live in
3//! [`super::request`](crate::orders::request).
4
5use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use rust_decimal::serde::float_option as decimal_opt;
8use serde::Deserialize;
9
10use crate::accounts::AccountsInstrument;
11use crate::orders::OrderId;
12use crate::orders::enums::*;
13use crate::secrets::AccountNumber;
14
15/// One order, as returned by the read endpoints. Schwab marks almost no
16/// field as required, so everything outside the discriminator-bearing
17/// enums is `Option`.
18///
19/// `order_id` is wrapped in [`OrderId`], which serializes transparently as
20/// the same `int64`.
21#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)]
22#[non_exhaustive]
23pub struct Order {
24    /// Trading session the order is valid in.
25    #[serde(default)]
26    pub session: Option<Session>,
27    /// Time-in-force.
28    #[serde(default)]
29    pub duration: Option<Duration>,
30    /// Order type (market / limit / stop / ...).
31    #[serde(default, rename = "orderType")]
32    pub order_type: Option<OrderType>,
33    /// Scheduled cancel time for time-bound orders.
34    #[serde(default, rename = "cancelTime")]
35    pub cancel_time: Option<DateTime<Utc>>,
36    /// Multi-leg option strategy shape; `NONE` for single-leg orders.
37    #[serde(default, rename = "complexOrderStrategyType")]
38    pub complex_order_strategy_type: Option<ComplexOrderStrategyType>,
39    /// Total order quantity.
40    #[serde(default, with = "decimal_opt")]
41    pub quantity: Option<Decimal>,
42    /// Quantity filled so far.
43    #[serde(default, with = "decimal_opt", rename = "filledQuantity")]
44    pub filled_quantity: Option<Decimal>,
45    /// Quantity still working.
46    #[serde(default, with = "decimal_opt", rename = "remainingQuantity")]
47    pub remaining_quantity: Option<Decimal>,
48    /// Response-only: the venue Schwab routed the order to.
49    #[serde(default, rename = "requestedDestination")]
50    pub requested_destination: Option<RequestedDestination>,
51    /// Schwab-internal name for the routing destination.
52    #[serde(default, rename = "destinationLinkName")]
53    pub destination_link_name: Option<String>,
54    /// Scheduled release time for orders held for later activation.
55    #[serde(default, rename = "releaseTime")]
56    pub release_time: Option<DateTime<Utc>>,
57    /// Stop trigger price, USD.
58    #[serde(default, with = "decimal_opt", rename = "stopPrice")]
59    pub stop_price: Option<Decimal>,
60    /// Reference price the stop is linked to.
61    #[serde(default, rename = "stopPriceLinkBasis")]
62    pub stop_price_link_basis: Option<StopPriceLinkBasis>,
63    /// How the linked stop offset is interpreted.
64    #[serde(default, rename = "stopPriceLinkType")]
65    pub stop_price_link_type: Option<StopPriceLinkType>,
66    /// Offset from the linked reference price.
67    #[serde(default, with = "decimal_opt", rename = "stopPriceOffset")]
68    pub stop_price_offset: Option<Decimal>,
69    /// Which feed triggers the stop (bid / ask / last / mark).
70    #[serde(default, rename = "stopType")]
71    pub stop_type: Option<StopType>,
72    /// Reference price the limit is linked to.
73    #[serde(default, rename = "priceLinkBasis")]
74    pub price_link_basis: Option<PriceLinkBasis>,
75    /// How the linked limit offset is interpreted.
76    #[serde(default, rename = "priceLinkType")]
77    pub price_link_type: Option<PriceLinkType>,
78    /// Limit price, USD.
79    #[serde(default, with = "decimal_opt")]
80    pub price: Option<Decimal>,
81    /// Tax-lot relief method to apply when closing.
82    #[serde(default, rename = "taxLotMethod")]
83    pub tax_lot_method: Option<TaxLotMethod>,
84    /// One entry per order leg.
85    #[serde(default, rename = "orderLegCollection")]
86    pub order_leg_collection: Vec<OrderLegCollection>,
87    /// Activation price for stop / trigger orders.
88    #[serde(default, with = "decimal_opt", rename = "activationPrice")]
89    pub activation_price: Option<Decimal>,
90    /// Schwab special-instruction flag (e.g. all-or-none).
91    #[serde(default, rename = "specialInstruction")]
92    pub special_instruction: Option<SpecialInstruction>,
93    /// Top-level structure of the order envelope.
94    #[serde(default, rename = "orderStrategyType")]
95    pub order_strategy_type: Option<OrderStrategyType>,
96    /// Schwab-assigned order id.
97    #[serde(default, rename = "orderId")]
98    pub order_id: Option<OrderId>,
99    /// `true` if the order can currently be cancelled.
100    #[serde(default)]
101    pub cancelable: Option<bool>,
102    /// `true` if the order can currently be replaced.
103    #[serde(default)]
104    pub editable: Option<bool>,
105    /// Lifecycle status.
106    #[serde(default)]
107    pub status: Option<ApiOrderStatus>,
108    /// Time Schwab recorded the order.
109    #[serde(default, rename = "enteredTime")]
110    pub entered_time: Option<DateTime<Utc>>,
111    /// Time the order reached a terminal state.
112    #[serde(default, rename = "closeTime")]
113    pub close_time: Option<DateTime<Utc>>,
114    /// Response-only: Schwab-assigned classification of the order's origin.
115    /// Not settable on the request; consumers cannot use this for
116    /// client-side correlation.
117    #[serde(default)]
118    pub tag: Option<String>,
119    /// Plain account number that owns this order.
120    #[serde(default, rename = "accountNumber")]
121    pub account_number: Option<AccountNumber>,
122    /// Per-event activity history (fills, lifecycle actions).
123    #[serde(default, rename = "orderActivityCollection")]
124    pub order_activity_collection: Vec<OrderActivity>,
125    /// Orders that have replaced this one (replace lineage).
126    #[serde(default, rename = "replacingOrderCollection")]
127    pub replacing_order_collection: Vec<Order>,
128    /// Child legs for `OCO` / `TRIGGER` / other compound strategies.
129    #[serde(default, rename = "childOrderStrategies")]
130    pub child_order_strategies: Vec<Order>,
131    /// Schwab's free-form description of the current status (rejection
132    /// reason, etc.).
133    #[serde(default, rename = "statusDescription")]
134    pub status_description: Option<String>,
135}
136
137/// One leg of an order (the security being traded plus its side / quantity).
138#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
139#[non_exhaustive]
140pub struct OrderLegCollection {
141    /// Asset class of the leg.
142    #[serde(default, rename = "orderLegType")]
143    pub order_leg_type: Option<OrderLegType>,
144    /// Schwab-assigned leg id within the order.
145    #[serde(default, rename = "legId")]
146    pub leg_id: Option<i64>,
147    /// Instrument being traded.
148    #[serde(default)]
149    pub instrument: Option<AccountsInstrument>,
150    /// Side / intent (buy / sell / buy-to-cover / ...).
151    #[serde(default)]
152    pub instruction: Option<Instruction>,
153    /// Whether the leg opens or closes a position.
154    #[serde(default, rename = "positionEffect")]
155    pub position_effect: Option<PositionEffect>,
156    /// Leg quantity (shares / contracts / dollars per `quantity_type`).
157    #[serde(default, with = "decimal_opt")]
158    pub quantity: Option<Decimal>,
159    /// How `quantity` is denominated.
160    #[serde(default, rename = "quantityType")]
161    pub quantity_type: Option<QuantityType>,
162    /// Dividend / capital-gains handling for mutual-fund legs.
163    #[serde(default, rename = "divCapGains")]
164    pub div_cap_gains: Option<DivCapGains>,
165    /// Destination symbol for mutual-fund exchanges.
166    #[serde(default, rename = "toSymbol")]
167    pub to_symbol: Option<String>,
168}
169
170/// One lifecycle event in an order's activity history (a fill or an order
171/// action).
172#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
173#[non_exhaustive]
174pub struct OrderActivity {
175    /// Whether this row is an execution or an order action.
176    #[serde(default, rename = "activityType")]
177    pub activity_type: Option<OrderActivityType>,
178    /// For executions, the kind of execution.
179    #[serde(default, rename = "executionType")]
180    pub execution_type: Option<ExecutionType>,
181    /// Quantity affected by this activity.
182    #[serde(default, with = "decimal_opt")]
183    pub quantity: Option<Decimal>,
184    /// Order quantity still working after this activity.
185    #[serde(default, with = "decimal_opt", rename = "orderRemainingQuantity")]
186    pub order_remaining_quantity: Option<Decimal>,
187    /// Per-leg detail for executions.
188    #[serde(default, rename = "executionLegs")]
189    pub execution_legs: Vec<ExecutionLeg>,
190}
191
192/// One executed leg within an [`OrderActivity`] fill row.
193#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
194#[non_exhaustive]
195pub struct ExecutionLeg {
196    /// Schwab-assigned leg id this fill is against.
197    #[serde(default, rename = "legId")]
198    pub leg_id: Option<i64>,
199    /// Fill price, USD.
200    #[serde(default, with = "decimal_opt")]
201    pub price: Option<Decimal>,
202    /// Quantity filled in this leg.
203    #[serde(default, with = "decimal_opt")]
204    pub quantity: Option<Decimal>,
205    /// Quantity that was mis-marked at fill time.
206    #[serde(default, with = "decimal_opt", rename = "mismarkedQuantity")]
207    pub mismarked_quantity: Option<Decimal>,
208    /// Schwab-internal instrument id of the security filled.
209    #[serde(default, rename = "instrumentId")]
210    pub instrument_id: Option<i64>,
211    /// Execution time.
212    #[serde(default)]
213    pub time: Option<DateTime<Utc>>,
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use rust_decimal_macros::dec;
220
221    #[test]
222    fn filled_equity_order_parses_with_execution() {
223        let json = r#"{
224            "orderId": 100000001,
225            "accountNumber": 12345678,
226            "status": "FILLED",
227            "orderType": "LIMIT",
228            "session": "NORMAL",
229            "duration": "DAY",
230            "orderStrategyType": "SINGLE",
231            "complexOrderStrategyType": "NONE",
232            "quantity": 10.0,
233            "filledQuantity": 10.0,
234            "remainingQuantity": 0.0,
235            "price": 145.32,
236            "enteredTime": "2024-03-15T15:30:00.000Z",
237            "closeTime": "2024-03-15T15:30:02.500Z",
238            "cancelable": false,
239            "editable": false,
240            "orderLegCollection": [{
241                "orderLegType": "EQUITY",
242                "legId": 1,
243                "instruction": "BUY",
244                "positionEffect": "OPENING",
245                "quantity": 10.0,
246                "quantityType": "SHARES",
247                "instrument": {
248                    "assetType": "EQUITY",
249                    "symbol": "AAPL",
250                    "cusip": "037833100",
251                    "instrumentId": 12345
252                }
253            }],
254            "orderActivityCollection": [{
255                "activityType": "EXECUTION",
256                "executionType": "FILL",
257                "quantity": 10.0,
258                "orderRemainingQuantity": 0.0,
259                "executionLegs": [{
260                    "legId": 1,
261                    "price": 145.32,
262                    "quantity": 10.0,
263                    "mismarkedQuantity": 0.0,
264                    "instrumentId": 12345,
265                    "time": "2024-03-15T15:30:02.500Z"
266                }]
267            }]
268        }"#;
269        let order: Order = serde_json::from_str(json).unwrap();
270        assert_eq!(order.order_id, Some(OrderId::new(100000001)));
271        assert_eq!(
272            order
273                .account_number
274                .as_ref()
275                .map(AccountNumber::expose_secret),
276            Some("12345678"),
277        );
278        assert_eq!(order.status, Some(ApiOrderStatus::Filled));
279        assert_eq!(order.order_type, Some(OrderType::Limit));
280        assert_eq!(order.order_strategy_type, Some(OrderStrategyType::Single));
281        assert_eq!(order.quantity, Some(dec!(10.0)));
282        assert_eq!(order.filled_quantity, Some(dec!(10.0)));
283        assert_eq!(order.price, Some(dec!(145.32)));
284        assert_eq!(order.cancelable, Some(false));
285
286        assert_eq!(order.order_leg_collection.len(), 1);
287        let leg = &order.order_leg_collection[0];
288        assert_eq!(leg.instruction, Some(Instruction::Buy));
289        assert_eq!(leg.position_effect, Some(PositionEffect::Opening));
290        assert_eq!(leg.quantity, Some(dec!(10.0)));
291        assert_eq!(leg.quantity_type, Some(QuantityType::Shares));
292
293        assert_eq!(order.order_activity_collection.len(), 1);
294        let activity = &order.order_activity_collection[0];
295        assert_eq!(activity.activity_type, Some(OrderActivityType::Execution));
296        assert_eq!(activity.execution_type, Some(ExecutionType::Fill));
297        assert_eq!(activity.execution_legs.len(), 1);
298        let exec = &activity.execution_legs[0];
299        assert_eq!(exec.price, Some(dec!(145.32)));
300        assert_eq!(exec.quantity, Some(dec!(10.0)));
301    }
302
303    #[test]
304    fn working_order_with_no_fills_parses() {
305        let json = r#"{
306            "orderId": 100000002,
307            "status": "WORKING",
308            "orderType": "LIMIT",
309            "orderStrategyType": "SINGLE",
310            "quantity": 5.0,
311            "filledQuantity": 0.0,
312            "remainingQuantity": 5.0,
313            "price": 140.00,
314            "cancelable": true,
315            "editable": true,
316            "orderLegCollection": [{
317                "orderLegType": "EQUITY",
318                "instruction": "BUY",
319                "quantity": 5.0,
320                "instrument": {
321                    "assetType": "EQUITY",
322                    "symbol": "AAPL"
323                }
324            }]
325        }"#;
326        let order: Order = serde_json::from_str(json).unwrap();
327        assert_eq!(order.status, Some(ApiOrderStatus::Working));
328        assert_eq!(order.filled_quantity, Some(dec!(0.0)));
329        assert_eq!(order.remaining_quantity, Some(dec!(5.0)));
330        assert!(order.order_activity_collection.is_empty());
331        assert_eq!(order.cancelable, Some(true));
332    }
333
334    #[test]
335    fn trigger_strategy_parses_with_child_orders() {
336        let json = r#"{
337            "orderId": 100000003,
338            "orderStrategyType": "TRIGGER",
339            "orderType": "LIMIT",
340            "price": 34.97,
341            "quantity": 10.0,
342            "orderLegCollection": [{
343                "instruction": "BUY",
344                "quantity": 10.0,
345                "instrument": { "assetType": "EQUITY", "symbol": "XYZ" }
346            }],
347            "childOrderStrategies": [{
348                "orderId": 100000004,
349                "orderStrategyType": "SINGLE",
350                "orderType": "LIMIT",
351                "price": 42.03,
352                "quantity": 10.0,
353                "orderLegCollection": [{
354                    "instruction": "SELL",
355                    "quantity": 10.0,
356                    "instrument": { "assetType": "EQUITY", "symbol": "XYZ" }
357                }]
358            }]
359        }"#;
360        let order: Order = serde_json::from_str(json).unwrap();
361        assert_eq!(order.order_strategy_type, Some(OrderStrategyType::Trigger));
362        assert_eq!(order.child_order_strategies.len(), 1);
363        let child = &order.child_order_strategies[0];
364        assert_eq!(child.order_id, Some(OrderId::new(100000004)));
365        assert_eq!(child.order_strategy_type, Some(OrderStrategyType::Single));
366        assert_eq!(child.price, Some(dec!(42.03)));
367    }
368
369    #[test]
370    fn account_number_accepts_both_string_and_int_forms() {
371        // The OpenAPI spec types `accountNumber` here as `int64`. For
372        // robustness, check that both string and int forms decode to the
373        // same `AccountNumber`, and the resulting value is redacted in Debug.
374        let as_int: Order =
375            serde_json::from_str(r#"{"orderId": 1, "accountNumber": 12345678}"#).unwrap();
376        let as_str: Order =
377            serde_json::from_str(r#"{"orderId": 1, "accountNumber": "12345678"}"#).unwrap();
378
379        assert_eq!(
380            as_int
381                .account_number
382                .as_ref()
383                .map(AccountNumber::expose_secret),
384            Some("12345678"),
385        );
386        assert_eq!(as_int.account_number, as_str.account_number);
387
388        let debug = format!("{:?}", as_str.account_number.as_ref().unwrap());
389        assert!(!debug.contains("12345678"), "Debug leaked: {debug}");
390        assert!(debug.contains("REDACTED"), "expected REDACTED in {debug}");
391    }
392
393    #[test]
394    fn missing_account_number_decodes_as_none() {
395        let order: Order = serde_json::from_str(r#"{"orderId": 1}"#).unwrap();
396        assert!(order.account_number.is_none());
397    }
398
399    #[test]
400    fn empty_collections_default_to_empty_vecs() {
401        let json = r#"{"orderId": 1}"#;
402        let order: Order = serde_json::from_str(json).unwrap();
403        assert!(order.order_leg_collection.is_empty());
404        assert!(order.order_activity_collection.is_empty());
405        assert!(order.child_order_strategies.is_empty());
406        assert!(order.replacing_order_collection.is_empty());
407    }
408
409    #[test]
410    fn oco_strategy_parses_with_two_child_orders_and_no_top_level_legs() {
411        // OCO ("one cancels other"): the parent carries no orderLegCollection;
412        // each child is an independent SINGLE strategy with its own leg.
413        let json = r#"{
414            "orderId": 100000005,
415            "orderStrategyType": "OCO",
416            "childOrderStrategies": [
417                {
418                    "orderId": 100000006,
419                    "orderStrategyType": "SINGLE",
420                    "orderType": "LIMIT",
421                    "price": 155.00,
422                    "quantity": 10.0,
423                    "orderLegCollection": [{
424                        "instruction": "SELL",
425                        "quantity": 10.0,
426                        "instrument": { "assetType": "EQUITY", "symbol": "AAPL" }
427                    }]
428                },
429                {
430                    "orderId": 100000007,
431                    "orderStrategyType": "SINGLE",
432                    "orderType": "STOP",
433                    "stopPrice": 135.00,
434                    "quantity": 10.0,
435                    "orderLegCollection": [{
436                        "instruction": "SELL",
437                        "quantity": 10.0,
438                        "instrument": { "assetType": "EQUITY", "symbol": "AAPL" }
439                    }]
440                }
441            ]
442        }"#;
443        let order: Order = serde_json::from_str(json).unwrap();
444        assert_eq!(order.order_id, Some(OrderId::new(100000005)));
445        assert_eq!(order.order_strategy_type, Some(OrderStrategyType::Oco));
446        assert!(order.order_leg_collection.is_empty());
447
448        assert_eq!(order.child_order_strategies.len(), 2);
449        let limit_leg = &order.child_order_strategies[0];
450        assert_eq!(limit_leg.order_id, Some(OrderId::new(100000006)));
451        assert_eq!(
452            limit_leg.order_strategy_type,
453            Some(OrderStrategyType::Single)
454        );
455        assert_eq!(limit_leg.order_type, Some(OrderType::Limit));
456        assert_eq!(limit_leg.price, Some(dec!(155.00)));
457        assert_eq!(limit_leg.order_leg_collection.len(), 1);
458        assert_eq!(
459            limit_leg.order_leg_collection[0].instruction,
460            Some(Instruction::Sell)
461        );
462
463        let stop_leg = &order.child_order_strategies[1];
464        assert_eq!(stop_leg.order_id, Some(OrderId::new(100000007)));
465        assert_eq!(stop_leg.order_type, Some(OrderType::Stop));
466        assert_eq!(stop_leg.stop_price, Some(dec!(135.00)));
467        assert_eq!(stop_leg.order_leg_collection.len(), 1);
468    }
469}