1use super::{
7 ComplexOrderStrategyType, OptionSymbol, OrderDuration, OrderInstruction, OrderRequest,
8 OrderRequestInstrument, OrderRequestLeg, OrderSession, PositionEffect, PriceLinkBasis,
9 PriceLinkType, RequestedDestination, SpecialInstruction, StopType, TaxLotMethod,
10};
11use crate::common::PutCall;
12use chrono::NaiveDate;
13use rust_decimal::Decimal;
14use serde::{Deserialize, Serialize};
15use serde_with::skip_serializing_none;
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
43#[cfg_attr(
45 feature = "schemars",
46 schemars(
47 description = "One order, keyed by `kind`. Single orders take a `leg` (or `legs` \
48 for the net-priced multi-leg kinds) and optional `settings`; `oco`, \
49 `trigger` and `bracket` nest other specs. Quantities and prices are \
50 decimal strings."
51 )
52)]
53#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
54#[non_exhaustive]
55pub enum OrderSpec {
56 Market {
58 leg: LegSpec,
59 #[serde(default)]
60 settings: OrderSettings,
61 },
62 Limit {
64 leg: LegSpec,
65 #[serde(deserialize_with = "crate::common::exact_decimal::deserialize")]
66 #[cfg_attr(feature = "schemars", schemars(with = "crate::common::DecimalString"))]
67 price: Decimal,
68 #[serde(default)]
69 settings: OrderSettings,
70 },
71 Stop {
73 leg: LegSpec,
74 #[serde(deserialize_with = "crate::common::exact_decimal::deserialize")]
75 #[cfg_attr(feature = "schemars", schemars(with = "crate::common::DecimalString"))]
76 stop_price: Decimal,
77 #[serde(default)]
78 settings: OrderSettings,
79 },
80 StopLimit {
82 leg: LegSpec,
83 #[serde(deserialize_with = "crate::common::exact_decimal::deserialize")]
84 #[cfg_attr(feature = "schemars", schemars(with = "crate::common::DecimalString"))]
85 stop_price: Decimal,
86 #[serde(deserialize_with = "crate::common::exact_decimal::deserialize")]
87 #[cfg_attr(feature = "schemars", schemars(with = "crate::common::DecimalString"))]
88 price: Decimal,
89 #[serde(default)]
90 settings: OrderSettings,
91 },
92 TrailingStop {
94 leg: LegSpec,
95 basis: PriceLinkBasis,
96 link: PriceLinkType,
97 #[serde(deserialize_with = "crate::common::exact_decimal::deserialize")]
98 #[cfg_attr(feature = "schemars", schemars(with = "crate::common::DecimalString"))]
99 offset: Decimal,
100 #[serde(default)]
101 settings: OrderSettings,
102 },
103 NetDebit {
105 legs: Vec<LegSpec>,
106 #[serde(deserialize_with = "crate::common::exact_decimal::deserialize")]
107 #[cfg_attr(feature = "schemars", schemars(with = "crate::common::DecimalString"))]
108 price: Decimal,
109 #[serde(default)]
110 settings: OrderSettings,
111 },
112 NetCredit {
114 legs: Vec<LegSpec>,
115 #[serde(deserialize_with = "crate::common::exact_decimal::deserialize")]
116 #[cfg_attr(feature = "schemars", schemars(with = "crate::common::DecimalString"))]
117 price: Decimal,
118 #[serde(default)]
119 settings: OrderSettings,
120 },
121 NetZero {
123 legs: Vec<LegSpec>,
124 #[serde(default)]
125 settings: OrderSettings,
126 },
127 Oco {
129 first: Box<OrderSpec>,
130 second: Box<OrderSpec>,
131 },
132 Trigger {
134 entry: Box<OrderSpec>,
135 children: Vec<OrderSpec>,
136 },
137 Bracket {
140 entry: Box<OrderSpec>,
141 #[serde(deserialize_with = "crate::common::exact_decimal::deserialize")]
142 #[cfg_attr(feature = "schemars", schemars(with = "crate::common::DecimalString"))]
143 take_profit: Decimal,
144 #[serde(deserialize_with = "crate::common::exact_decimal::deserialize")]
145 #[cfg_attr(feature = "schemars", schemars(with = "crate::common::DecimalString"))]
146 stop_loss: Decimal,
147 },
148}
149
150#[skip_serializing_none]
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
153#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
154#[serde(rename_all = "snake_case", deny_unknown_fields)]
155pub struct LegSpec {
156 pub instruction: OrderInstruction,
157 #[serde(deserialize_with = "crate::common::exact_decimal::deserialize")]
159 #[cfg_attr(feature = "schemars", schemars(with = "crate::common::DecimalString"))]
160 pub quantity: Decimal,
161 pub instrument: InstrumentSpec,
162 #[serde(default)]
163 pub position_effect: Option<PositionEffect>,
164}
165
166#[skip_serializing_none]
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
170#[serde(
171 tag = "asset_type",
172 rename_all = "SCREAMING_SNAKE_CASE",
173 deny_unknown_fields
174)]
175#[non_exhaustive]
176pub enum InstrumentSpec {
177 Equity {
178 symbol: String,
179 },
180 Option {
184 #[serde(default)]
185 symbol: Option<String>,
186 #[serde(default)]
187 underlying: Option<String>,
188 #[serde(default)]
189 expiration: Option<NaiveDate>,
190 #[serde(default)]
191 right: Option<PutCall>,
192 #[serde(
193 default,
194 deserialize_with = "crate::common::exact_decimal::option::deserialize"
195 )]
196 #[cfg_attr(
197 feature = "schemars",
198 schemars(with = "Option<crate::common::DecimalString>")
199 )]
200 strike: Option<Decimal>,
201 },
202}
203
204#[skip_serializing_none]
207#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
208#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
209#[serde(rename_all = "snake_case", deny_unknown_fields, default)]
210pub struct OrderSettings {
211 pub session: Option<OrderSession>,
212 pub duration: Option<OrderDuration>,
213 pub tag: Option<String>,
215 pub complex_strategy: Option<ComplexOrderStrategyType>,
216 pub special_instruction: Option<SpecialInstruction>,
217 pub destination: Option<RequestedDestination>,
218 pub tax_lot_method: Option<TaxLotMethod>,
219 pub stop_type: Option<StopType>,
220}
221
222impl OrderSettings {
223 fn apply(self, mut order: OrderRequest) -> OrderRequest {
224 if let Some(session) = self.session {
225 order = order.with_session(session);
226 }
227 if let Some(duration) = self.duration {
228 order = order.with_duration(duration);
229 }
230 if let Some(tag) = self.tag {
231 order = order.with_tag(tag);
232 }
233 if let Some(strategy) = self.complex_strategy {
234 order = order.with_complex_strategy(strategy);
235 }
236 if let Some(instruction) = self.special_instruction {
237 order = order.with_special_instruction(instruction);
238 }
239 if let Some(destination) = self.destination {
240 order = order.with_destination(destination);
241 }
242 if let Some(method) = self.tax_lot_method {
243 order = order.with_tax_lot_method(method);
244 }
245 if let Some(stop_type) = self.stop_type {
246 order = order.with_stop_type(stop_type);
247 }
248 order
249 }
250}
251
252impl InstrumentSpec {
253 fn build(self) -> Result<OrderRequestInstrument, String> {
254 match self {
255 Self::Equity { symbol } => Ok(OrderRequestInstrument::equity(symbol)),
256 Self::Option {
257 symbol: Some(symbol),
258 underlying: None,
259 expiration: None,
260 right: None,
261 strike: None,
262 } => {
263 let parsed: OptionSymbol = symbol.parse()?;
264 Ok(OrderRequestInstrument::option(parsed))
265 }
266 Self::Option {
267 symbol: None,
268 underlying: Some(underlying),
269 expiration: Some(expiration),
270 right: Some(right),
271 strike: Some(strike),
272 } => Ok(OrderRequestInstrument::option(OptionSymbol::new(
273 underlying, expiration, right, strike,
274 )?)),
275 Self::Option { .. } => Err("An option instrument takes either an OSI symbol \
276 or all of underlying, expiration, right and strike"
277 .into()),
278 }
279 }
280}
281
282impl LegSpec {
283 fn build(self) -> Result<OrderRequestLeg, String> {
284 let leg = OrderRequestLeg::new(self.instruction, self.instrument.build()?, self.quantity);
285 Ok(match self.position_effect {
286 Some(effect) => leg.with_position_effect(effect),
287 None => leg,
288 })
289 }
290}
291
292fn build_legs(legs: Vec<LegSpec>) -> Result<Vec<OrderRequestLeg>, String> {
293 legs.into_iter().map(LegSpec::build).collect()
294}
295
296impl OrderSpec {
297 pub fn build(self) -> Result<OrderRequest, String> {
299 let order = self.expand()?;
300 order.validate().map_err(|e| e.to_string())?;
301 Ok(order)
302 }
303
304 fn expand(self) -> Result<OrderRequest, String> {
305 Ok(match self {
306 Self::Market { leg, settings } => settings.apply(OrderRequest::market(leg.build()?)),
307 Self::Limit {
308 leg,
309 price,
310 settings,
311 } => settings.apply(OrderRequest::limit(leg.build()?, price)),
312 Self::Stop {
313 leg,
314 stop_price,
315 settings,
316 } => settings.apply(OrderRequest::stop(leg.build()?, stop_price)),
317 Self::StopLimit {
318 leg,
319 stop_price,
320 price,
321 settings,
322 } => settings.apply(OrderRequest::stop_limit(leg.build()?, stop_price, price)),
323 Self::TrailingStop {
324 leg,
325 basis,
326 link,
327 offset,
328 settings,
329 } => settings.apply(OrderRequest::trailing_stop(
330 leg.build()?,
331 basis,
332 link,
333 offset,
334 )),
335 Self::NetDebit {
336 legs,
337 price,
338 settings,
339 } => settings.apply(OrderRequest::net_debit(build_legs(legs)?, price)),
340 Self::NetCredit {
341 legs,
342 price,
343 settings,
344 } => settings.apply(OrderRequest::net_credit(build_legs(legs)?, price)),
345 Self::NetZero { legs, settings } => {
346 settings.apply(OrderRequest::net_zero(build_legs(legs)?))
347 }
348 Self::Oco { first, second } => {
349 OrderRequest::one_cancels_other(first.expand()?, second.expand()?)
350 }
351 Self::Trigger { entry, children } => {
352 if children.is_empty() {
353 return Err("A trigger needs at least one child order".into());
354 }
355 children
356 .into_iter()
357 .try_fold(entry.expand()?, |order, child| {
358 Ok::<_, String>(order.triggers(child.expand()?))
359 })?
360 }
361 Self::Bracket {
362 entry,
363 take_profit,
364 stop_loss,
365 } => entry.expand()?.bracket(take_profit, stop_loss)?,
366 })
367 }
368}
369
370impl TryFrom<OrderSpec> for OrderRequest {
371 type Error = String;
372
373 fn try_from(spec: OrderSpec) -> Result<Self, Self::Error> {
374 spec.build()
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use serde_json::json;
382
383 fn build(spec: serde_json::Value) -> Result<serde_json::Value, String> {
384 let spec: OrderSpec = serde_json::from_value(spec).map_err(|e| e.to_string())?;
385 Ok(serde_json::to_value(spec.build()?).unwrap())
386 }
387
388 #[test]
389 fn specs_expand_to_the_documented_request_shapes() {
390 let leg = json!({"instruction": "BUY", "quantity": "10",
391 "instrument": {"asset_type": "EQUITY", "symbol": "AMD"}});
392 assert_eq!(
393 build(json!({"kind": "market", "leg": leg})).unwrap(),
394 json!({
395 "orderType": "MARKET", "session": "NORMAL", "duration": "DAY",
396 "orderStrategyType": "SINGLE",
397 "orderLegCollection": [{"instruction": "BUY", "quantity": 10,
398 "instrument": {"symbol": "AMD", "assetType": "EQUITY"}}]
399 })
400 );
401 let bracket = build(json!({
402 "kind": "bracket",
403 "entry": {"kind": "limit", "leg": leg, "price": "150.25",
404 "settings": {"duration": "GOOD_TILL_CANCEL", "tag": "swing-1"}},
405 "take_profit": "160",
406 "stop_loss": "145"
407 }))
408 .unwrap();
409 assert_eq!(bracket["orderStrategyType"], "TRIGGER");
410 assert_eq!(bracket["duration"], "GOOD_TILL_CANCEL");
411 assert_eq!(bracket["tag"], "swing-1");
412 let exits = &bracket["childOrderStrategies"][0];
413 assert_eq!(exits["orderStrategyType"], "OCO");
414 assert_eq!(exits["childOrderStrategies"][0]["price"], "160");
415 assert_eq!(exits["childOrderStrategies"][1]["stopPrice"], "145");
416 assert_eq!(
417 exits["childOrderStrategies"][1]["orderLegCollection"][0]["instruction"],
418 "SELL"
419 );
420 let sell = json!({"instruction": "SELL", "quantity": "2",
421 "instrument": {"asset_type": "EQUITY", "symbol": "XYZ"}});
422 let trigger = build(json!({
423 "kind": "trigger",
424 "entry": {"kind": "limit", "leg": leg, "price": "14.97"},
425 "children": [{
426 "kind": "oco",
427 "first": {"kind": "limit", "leg": sell, "price": "15.27"},
428 "second": {"kind": "stop_limit", "leg": sell, "stop_price": "11.30",
429 "price": "11.27"}
430 }]
431 }))
432 .unwrap();
433 assert_eq!(trigger["orderStrategyType"], "TRIGGER");
434 assert_eq!(
435 trigger["childOrderStrategies"][0]["childOrderStrategies"][1]["orderType"],
436 "STOP_LIMIT"
437 );
438 let trailing = build(json!({
439 "kind": "trailing_stop", "leg": sell, "basis": "BID", "link": "VALUE",
440 "offset": "10", "settings": {"stop_type": "MARK", "destination": "AUTO"}
441 }))
442 .unwrap();
443 assert_eq!(trailing["stopPriceLinkBasis"], "BID");
444 assert_eq!(trailing["stopPriceOffset"], "10");
445 assert_eq!(trailing["stopType"], "MARK");
446 assert_eq!(trailing["requestedDestination"], "AUTO");
447 }
448
449 #[test]
450 fn option_instruments_accept_osi_symbols_or_contract_parts() {
451 let by_parts = json!({"asset_type": "OPTION", "underlying": "aapl",
452 "expiration": "2024-05-17", "right": "PUT", "strike": "190"});
453 let by_symbol = json!({"asset_type": "OPTION", "symbol": "AAPL 240517P00190000"});
454 for instrument in [by_parts, by_symbol] {
455 let order = build(json!({
456 "kind": "net_credit",
457 "legs": [
458 {"instruction": "SELL_TO_OPEN", "quantity": "1", "instrument": instrument,
459 "position_effect": "OPENING"},
460 {"instruction": "BUY_TO_OPEN", "quantity": "1",
461 "instrument": {"asset_type": "OPTION", "symbol": "AAPL 240517P00185000"}}
462 ],
463 "price": "1.05",
464 "settings": {"complex_strategy": "VERTICAL"}
465 }))
466 .unwrap();
467 assert_eq!(order["orderType"], "NET_CREDIT");
468 assert_eq!(order["complexOrderStrategyType"], "VERTICAL");
469 let leg = &order["orderLegCollection"][0];
470 assert_eq!(leg["instrument"]["symbol"], "AAPL 240517P00190000");
471 assert_eq!(leg["instrument"]["assetType"], "OPTION");
472 assert_eq!(leg["positionEffect"], "OPENING");
473 }
474 }
475
476 #[test]
477 fn malformed_specs_are_rejected() {
478 let leg = json!({"instruction": "BUY", "quantity": "1",
479 "instrument": {"asset_type": "EQUITY", "symbol": "AMD"}});
480 assert!(
482 build(json!({"kind": "limit", "leg": leg, "price": "1", "duration": "DAY"})).is_err()
483 );
484 assert!(
485 build(json!({"kind": "limit", "leg": leg, "price": "1",
486 "settings": {"durration": "DAY"}}))
487 .is_err()
488 );
489 assert!(build(json!({"kind": "iceberg", "leg": leg})).is_err());
490 assert!(build(json!({"kind": "limit", "leg": leg})).is_err());
491 for instrument in [
493 json!({"asset_type": "OPTION"}),
494 json!({"asset_type": "OPTION", "symbol": "AAPL 240517P00190000", "strike": "190"}),
495 json!({"asset_type": "OPTION", "underlying": "AAPL", "strike": "190"}),
496 json!({"asset_type": "OPTION", "symbol": "AAPL"}),
497 json!({"asset_type": "FUTURE", "symbol": "/ES"}),
498 ] {
499 assert!(
500 build(
501 json!({"kind": "market", "leg": {"instruction": "BUY_TO_OPEN",
502 "quantity": "1", "instrument": instrument}})
503 )
504 .is_err(),
505 "{instrument}"
506 );
507 }
508 assert!(
510 build(
511 json!({"kind": "trigger", "entry": {"kind": "market", "leg": leg},
512 "children": []})
513 )
514 .is_err()
515 );
516 assert!(
517 build(
518 json!({"kind": "bracket", "entry": {"kind": "market", "leg": leg},
519 "take_profit": "1", "stop_loss": "2"})
520 )
521 .is_err()
522 );
523 assert!(build(json!({"kind": "limit", "leg": leg, "price": "0"})).is_err());
524 }
525
526 #[test]
527 fn specs_round_trip_through_serde() {
528 let spec = OrderSpec::Bracket {
529 entry: Box::new(OrderSpec::Limit {
530 leg: LegSpec {
531 instruction: OrderInstruction::Buy,
532 quantity: Decimal::from(10),
533 instrument: InstrumentSpec::Equity {
534 symbol: "AAPL".into(),
535 },
536 position_effect: None,
537 },
538 price: Decimal::new(15025, 2),
539 settings: OrderSettings {
540 duration: Some(OrderDuration::Gtc),
541 ..OrderSettings::default()
542 },
543 }),
544 take_profit: Decimal::from(160),
545 stop_loss: Decimal::from(145),
546 };
547 let json = serde_json::to_value(&spec).unwrap();
548 assert_eq!(json["kind"], "bracket");
549 assert_eq!(json["entry"]["kind"], "limit");
550 assert_eq!(json["entry"]["settings"]["duration"], "GOOD_TILL_CANCEL");
551 assert_eq!(serde_json::from_value::<OrderSpec>(json).unwrap(), spec);
552 }
553}