1use 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#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)]
22#[non_exhaustive]
23pub struct Order {
24 #[serde(default)]
26 pub session: Option<Session>,
27 #[serde(default)]
29 pub duration: Option<Duration>,
30 #[serde(default, rename = "orderType")]
32 pub order_type: Option<OrderType>,
33 #[serde(default, rename = "cancelTime")]
35 pub cancel_time: Option<DateTime<Utc>>,
36 #[serde(default, rename = "complexOrderStrategyType")]
38 pub complex_order_strategy_type: Option<ComplexOrderStrategyType>,
39 #[serde(default, with = "decimal_opt")]
41 pub quantity: Option<Decimal>,
42 #[serde(default, with = "decimal_opt", rename = "filledQuantity")]
44 pub filled_quantity: Option<Decimal>,
45 #[serde(default, with = "decimal_opt", rename = "remainingQuantity")]
47 pub remaining_quantity: Option<Decimal>,
48 #[serde(default, rename = "requestedDestination")]
50 pub requested_destination: Option<RequestedDestination>,
51 #[serde(default, rename = "destinationLinkName")]
53 pub destination_link_name: Option<String>,
54 #[serde(default, rename = "releaseTime")]
56 pub release_time: Option<DateTime<Utc>>,
57 #[serde(default, with = "decimal_opt", rename = "stopPrice")]
59 pub stop_price: Option<Decimal>,
60 #[serde(default, rename = "stopPriceLinkBasis")]
62 pub stop_price_link_basis: Option<StopPriceLinkBasis>,
63 #[serde(default, rename = "stopPriceLinkType")]
65 pub stop_price_link_type: Option<StopPriceLinkType>,
66 #[serde(default, with = "decimal_opt", rename = "stopPriceOffset")]
68 pub stop_price_offset: Option<Decimal>,
69 #[serde(default, rename = "stopType")]
71 pub stop_type: Option<StopType>,
72 #[serde(default, rename = "priceLinkBasis")]
74 pub price_link_basis: Option<PriceLinkBasis>,
75 #[serde(default, rename = "priceLinkType")]
77 pub price_link_type: Option<PriceLinkType>,
78 #[serde(default, with = "decimal_opt")]
80 pub price: Option<Decimal>,
81 #[serde(default, rename = "taxLotMethod")]
83 pub tax_lot_method: Option<TaxLotMethod>,
84 #[serde(default, rename = "orderLegCollection")]
86 pub order_leg_collection: Vec<OrderLegCollection>,
87 #[serde(default, with = "decimal_opt", rename = "activationPrice")]
89 pub activation_price: Option<Decimal>,
90 #[serde(default, rename = "specialInstruction")]
92 pub special_instruction: Option<SpecialInstruction>,
93 #[serde(default, rename = "orderStrategyType")]
95 pub order_strategy_type: Option<OrderStrategyType>,
96 #[serde(default, rename = "orderId")]
98 pub order_id: Option<OrderId>,
99 #[serde(default)]
101 pub cancelable: Option<bool>,
102 #[serde(default)]
104 pub editable: Option<bool>,
105 #[serde(default)]
107 pub status: Option<ApiOrderStatus>,
108 #[serde(default, rename = "enteredTime")]
110 pub entered_time: Option<DateTime<Utc>>,
111 #[serde(default, rename = "closeTime")]
113 pub close_time: Option<DateTime<Utc>>,
114 #[serde(default)]
118 pub tag: Option<String>,
119 #[serde(default, rename = "accountNumber")]
121 pub account_number: Option<AccountNumber>,
122 #[serde(default, rename = "orderActivityCollection")]
124 pub order_activity_collection: Vec<OrderActivity>,
125 #[serde(default, rename = "replacingOrderCollection")]
127 pub replacing_order_collection: Vec<Order>,
128 #[serde(default, rename = "childOrderStrategies")]
130 pub child_order_strategies: Vec<Order>,
131 #[serde(default, rename = "statusDescription")]
134 pub status_description: Option<String>,
135}
136
137#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
139#[non_exhaustive]
140pub struct OrderLegCollection {
141 #[serde(default, rename = "orderLegType")]
143 pub order_leg_type: Option<OrderLegType>,
144 #[serde(default, rename = "legId")]
146 pub leg_id: Option<i64>,
147 #[serde(default)]
149 pub instrument: Option<AccountsInstrument>,
150 #[serde(default)]
152 pub instruction: Option<Instruction>,
153 #[serde(default, rename = "positionEffect")]
155 pub position_effect: Option<PositionEffect>,
156 #[serde(default, with = "decimal_opt")]
158 pub quantity: Option<Decimal>,
159 #[serde(default, rename = "quantityType")]
161 pub quantity_type: Option<QuantityType>,
162 #[serde(default, rename = "divCapGains")]
164 pub div_cap_gains: Option<DivCapGains>,
165 #[serde(default, rename = "toSymbol")]
167 pub to_symbol: Option<String>,
168}
169
170#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
173#[non_exhaustive]
174pub struct OrderActivity {
175 #[serde(default, rename = "activityType")]
177 pub activity_type: Option<OrderActivityType>,
178 #[serde(default, rename = "executionType")]
180 pub execution_type: Option<ExecutionType>,
181 #[serde(default, with = "decimal_opt")]
183 pub quantity: Option<Decimal>,
184 #[serde(default, with = "decimal_opt", rename = "orderRemainingQuantity")]
186 pub order_remaining_quantity: Option<Decimal>,
187 #[serde(default, rename = "executionLegs")]
189 pub execution_legs: Vec<ExecutionLeg>,
190}
191
192#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
194#[non_exhaustive]
195pub struct ExecutionLeg {
196 #[serde(default, rename = "legId")]
198 pub leg_id: Option<i64>,
199 #[serde(default, with = "decimal_opt")]
201 pub price: Option<Decimal>,
202 #[serde(default, with = "decimal_opt")]
204 pub quantity: Option<Decimal>,
205 #[serde(default, with = "decimal_opt", rename = "mismarkedQuantity")]
207 pub mismarked_quantity: Option<Decimal>,
208 #[serde(default, rename = "instrumentId")]
210 pub instrument_id: Option<i64>,
211 #[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 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 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}