1use super::{
5 ComplexOrderStrategyType, OrderDuration, OrderInstruction, OrderRequest,
6 OrderRequestInstrument, OrderRequestLeg, OrderSession, OrderStrategyType, OrderType,
7 PositionEffect, PriceLinkBasis, PriceLinkType, RequestedDestination, SpecialInstruction,
8 StopType, TaxLotMethod,
9};
10use crate::common::{AssetType, PutCall};
11use chrono::{Datelike, NaiveDate};
12use rust_decimal::Decimal;
13use std::{fmt, str::FromStr};
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub struct OptionSymbol {
32 underlying: String,
33 expiration: NaiveDate,
34 put_call: PutCall,
35 strike: Decimal,
36}
37
38impl OptionSymbol {
39 pub fn new(
45 underlying: impl AsRef<str>,
46 expiration: NaiveDate,
47 put_call: PutCall,
48 strike: Decimal,
49 ) -> Result<Self, String> {
50 let underlying = underlying.as_ref().trim().to_ascii_uppercase();
51 if underlying.is_empty()
52 || underlying.len() > 6
53 || !underlying.bytes().all(|b| b.is_ascii_alphanumeric())
54 {
55 return Err(format!(
56 "Option underlying must be one to six letters or digits, got {underlying:?}"
57 ));
58 }
59 if !(2000..=2099).contains(&expiration.year()) {
60 return Err(format!(
61 "Option expiration must be in 2000-2099, got {expiration}"
62 ));
63 }
64 let strike = strike.normalize();
65 if strike <= Decimal::ZERO || strike.scale() > 3 || strike >= Decimal::from(100_000) {
66 return Err(format!(
67 "Option strike must be positive, below 100000, \
68 and a multiple of 0.001, got {strike}"
69 ));
70 }
71 Ok(Self {
72 underlying,
73 expiration,
74 put_call,
75 strike,
76 })
77 }
78
79 pub fn call(
81 underlying: impl AsRef<str>,
82 expiration: NaiveDate,
83 strike: Decimal,
84 ) -> Result<Self, String> {
85 Self::new(underlying, expiration, PutCall::Call, strike)
86 }
87
88 pub fn put(
90 underlying: impl AsRef<str>,
91 expiration: NaiveDate,
92 strike: Decimal,
93 ) -> Result<Self, String> {
94 Self::new(underlying, expiration, PutCall::Put, strike)
95 }
96
97 pub fn underlying(&self) -> &str {
98 &self.underlying
99 }
100
101 pub fn expiration(&self) -> NaiveDate {
102 self.expiration
103 }
104
105 pub fn put_call(&self) -> PutCall {
106 self.put_call
107 }
108
109 pub fn strike(&self) -> Decimal {
110 self.strike
111 }
112}
113
114impl fmt::Display for OptionSymbol {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 let right = match self.put_call {
117 PutCall::Call => 'C',
118 PutCall::Put => 'P',
119 };
120 let thousandths = (self.strike * Decimal::from(1000)).normalize().mantissa();
122 write!(
123 f,
124 "{:<6}{}{right}{thousandths:08}",
125 self.underlying,
126 self.expiration.format("%y%m%d")
127 )
128 }
129}
130
131impl FromStr for OptionSymbol {
132 type Err = String;
133
134 fn from_str(s: &str) -> Result<Self, Self::Err> {
139 let invalid = || format!("Expected a 21-character OSI option symbol, got {s:?}");
140 if s.len() != 21 || !s.is_ascii() {
141 return Err(invalid());
142 }
143 let (underlying, rest) = s.split_at(6);
144 let (date, rest) = rest.split_at(6);
145 let (right, strike) = rest.split_at(1);
146 let digits = |t: &str| t.bytes().all(|b| b.is_ascii_digit());
147 if !digits(date) {
148 return Err(invalid());
149 }
150 let field = |range: std::ops::Range<usize>| date[range].parse::<u32>().ok();
151 let expiration = field(0..2)
152 .zip(field(2..4))
153 .zip(field(4..6))
154 .and_then(|((y, m), d)| NaiveDate::from_ymd_opt(2000 + y as i32, m, d))
155 .ok_or_else(invalid)?;
156 let put_call = match right {
157 "C" => PutCall::Call,
158 "P" => PutCall::Put,
159 _ => return Err(invalid()),
160 };
161 let thousandths: i64 = strike
162 .parse()
163 .ok()
164 .filter(|_| digits(strike))
165 .ok_or_else(invalid)?;
166 let symbol = Self::new(
167 underlying,
168 expiration,
169 put_call,
170 Decimal::new(thousandths, 3),
171 )?;
172 if symbol.to_string() != s {
173 return Err(format!(
174 "{s:?} is not in canonical OSI form; expected {:?}",
175 symbol.to_string()
176 ));
177 }
178 Ok(symbol)
179 }
180}
181
182impl From<OptionSymbol> for String {
183 fn from(symbol: OptionSymbol) -> Self {
184 symbol.to_string()
185 }
186}
187
188impl From<&OptionSymbol> for String {
189 fn from(symbol: &OptionSymbol) -> Self {
190 symbol.to_string()
191 }
192}
193
194impl OrderInstruction {
195 pub fn closing(self) -> Option<Self> {
200 match self {
201 Self::Buy => Some(Self::Sell),
202 Self::SellShort | Self::SellShortExempt => Some(Self::BuyToCover),
203 Self::BuyToOpen => Some(Self::SellToClose),
204 Self::SellToOpen => Some(Self::BuyToClose),
205 Self::Sell
206 | Self::BuyToCover
207 | Self::BuyToClose
208 | Self::SellToClose
209 | Self::Exchange
210 | Self::Unknown => None,
211 }
212 }
213
214 pub fn opens_long(self) -> Option<bool> {
218 match self {
219 Self::Buy | Self::BuyToOpen => Some(true),
220 Self::SellShort | Self::SellShortExempt | Self::SellToOpen => Some(false),
221 _ => None,
222 }
223 }
224
225 pub fn buys(self) -> Option<bool> {
228 match self {
229 Self::Buy | Self::BuyToCover | Self::BuyToOpen | Self::BuyToClose => Some(true),
230 Self::Sell
231 | Self::SellShort
232 | Self::SellShortExempt
233 | Self::SellToOpen
234 | Self::SellToClose => Some(false),
235 Self::Exchange | Self::Unknown => None,
236 }
237 }
238}
239
240impl OrderRequestInstrument {
241 pub fn equity(symbol: impl Into<String>) -> Self {
242 Self {
243 symbol: symbol.into(),
244 asset_type: AssetType::Equity,
245 }
246 }
247
248 pub fn option(symbol: impl Into<String>) -> Self {
251 Self {
252 symbol: symbol.into(),
253 asset_type: AssetType::Option,
254 }
255 }
256}
257
258impl OrderRequestLeg {
259 pub fn new(
260 instruction: OrderInstruction,
261 instrument: OrderRequestInstrument,
262 quantity: impl Into<Decimal>,
263 ) -> Self {
264 Self {
265 instruction,
266 quantity: quantity.into(),
267 instrument,
268 position_effect: None,
269 quantity_type: None,
270 div_cap_gains: None,
271 to_symbol: None,
272 }
273 }
274
275 pub fn equity(
276 instruction: OrderInstruction,
277 symbol: impl Into<String>,
278 quantity: impl Into<Decimal>,
279 ) -> Self {
280 Self::new(
281 instruction,
282 OrderRequestInstrument::equity(symbol),
283 quantity,
284 )
285 }
286
287 pub fn option(
289 instruction: OrderInstruction,
290 symbol: impl Into<String>,
291 quantity: impl Into<Decimal>,
292 ) -> Self {
293 Self::new(
294 instruction,
295 OrderRequestInstrument::option(symbol),
296 quantity,
297 )
298 }
299
300 pub fn with_position_effect(mut self, effect: PositionEffect) -> Self {
301 self.position_effect = Some(effect);
302 self
303 }
304}
305
306impl OrderRequest {
307 fn single(order_type: OrderType, legs: Vec<OrderRequestLeg>) -> Self {
308 Self {
309 session: Some(OrderSession::Normal),
310 duration: Some(OrderDuration::Day),
311 order_type: Some(order_type),
312 order_strategy_type: OrderStrategyType::Single,
313 order_leg_collection: legs,
314 ..Self::default()
315 }
316 }
317
318 pub fn market(leg: OrderRequestLeg) -> Self {
319 Self::single(OrderType::Market, vec![leg])
320 }
321
322 pub fn limit(leg: OrderRequestLeg, price: Decimal) -> Self {
323 Self {
324 price: Some(price),
325 ..Self::single(OrderType::Limit, vec![leg])
326 }
327 }
328
329 pub fn stop(leg: OrderRequestLeg, stop_price: Decimal) -> Self {
331 Self {
332 stop_price: Some(stop_price),
333 ..Self::single(OrderType::Stop, vec![leg])
334 }
335 }
336
337 pub fn stop_limit(leg: OrderRequestLeg, stop_price: Decimal, price: Decimal) -> Self {
340 Self {
341 price: Some(price),
342 stop_price: Some(stop_price),
343 ..Self::single(OrderType::StopLimit, vec![leg])
344 }
345 }
346
347 pub fn trailing_stop(
357 leg: OrderRequestLeg,
358 basis: PriceLinkBasis,
359 link: PriceLinkType,
360 offset: Decimal,
361 ) -> Self {
362 Self {
363 stop_price_link_basis: Some(basis),
364 stop_price_link_type: Some(link),
365 stop_price_offset: Some(offset),
366 ..Self::single(OrderType::TrailingStop, vec![leg])
367 }
368 }
369
370 pub fn net_debit(legs: Vec<OrderRequestLeg>, price: Decimal) -> Self {
372 Self {
373 price: Some(price),
374 ..Self::single(OrderType::NetDebit, legs)
375 }
376 }
377
378 pub fn net_credit(legs: Vec<OrderRequestLeg>, price: Decimal) -> Self {
380 Self {
381 price: Some(price),
382 ..Self::single(OrderType::NetCredit, legs)
383 }
384 }
385
386 pub fn net_zero(legs: Vec<OrderRequestLeg>) -> Self {
388 Self::single(OrderType::NetZero, legs)
389 }
390
391 pub fn with_session(mut self, session: OrderSession) -> Self {
392 self.session = Some(session);
393 self
394 }
395
396 pub fn with_duration(mut self, duration: OrderDuration) -> Self {
397 self.duration = Some(duration);
398 self
399 }
400
401 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
402 self.tag = Some(tag.into());
403 self
404 }
405
406 pub fn with_complex_strategy(mut self, strategy: ComplexOrderStrategyType) -> Self {
407 self.complex_order_strategy_type = Some(strategy);
408 self
409 }
410
411 pub fn with_special_instruction(mut self, instruction: SpecialInstruction) -> Self {
412 self.special_instruction = Some(instruction);
413 self
414 }
415
416 pub fn with_destination(mut self, destination: RequestedDestination) -> Self {
417 self.requested_destination = Some(destination);
418 self
419 }
420
421 pub fn with_tax_lot_method(mut self, method: TaxLotMethod) -> Self {
422 self.tax_lot_method = Some(method);
423 self
424 }
425
426 pub fn with_stop_type(mut self, stop_type: StopType) -> Self {
427 self.stop_type = Some(stop_type);
428 self
429 }
430
431 pub fn one_cancels_other(first: OrderRequest, second: OrderRequest) -> Self {
433 Self {
434 order_strategy_type: OrderStrategyType::Oco,
435 child_order_strategies: vec![first, second],
436 ..Self::default()
437 }
438 }
439
440 pub fn triggers(mut self, child: OrderRequest) -> Self {
443 self.order_strategy_type = OrderStrategyType::Trigger;
444 self.child_order_strategies.push(child);
445 self
446 }
447
448 pub fn bracket(self, take_profit: Decimal, stop_loss: Decimal) -> Result<Self, String> {
462 if self.order_strategy_type != OrderStrategyType::Single
463 || !self.child_order_strategies.is_empty()
464 {
465 return Err("A bracket entry must be a SINGLE order without children".into());
466 }
467 let [leg] = self.order_leg_collection.as_slice() else {
468 return Err("A bracket entry must have exactly one leg".into());
469 };
470 let (Some(closing), Some(long)) = (leg.instruction.closing(), leg.instruction.opens_long())
471 else {
472 return Err(format!(
473 "{:?} does not open a position, so it cannot be bracketed",
474 leg.instruction
475 ));
476 };
477 if (long && take_profit <= stop_loss) || (!long && take_profit >= stop_loss) {
478 let side = if long { "long" } else { "short" };
479 return Err(format!(
480 "Bracket exits are inverted for a {side} entry: \
481 take profit {take_profit}, stop loss {stop_loss}"
482 ));
483 }
484 if let Some(entry) = self.price.or(self.stop_price) {
485 let (below, above) = if long {
486 (stop_loss, take_profit)
487 } else {
488 (take_profit, stop_loss)
489 };
490 if !(below < entry && entry < above) {
491 let side = if long { "long" } else { "short" };
492 return Err(format!(
493 "Bracket exits must straddle a {side} entry at {entry}: \
494 take profit {take_profit}, stop loss {stop_loss}"
495 ));
496 }
497 }
498 let exit = || OrderRequestLeg {
499 position_effect: leg.position_effect.map(|_| PositionEffect::Closing),
500 ..OrderRequestLeg::new(closing, leg.instrument.clone(), leg.quantity)
501 };
502 let exits = Self::one_cancels_other(
503 Self::limit(exit(), take_profit).with_duration(OrderDuration::Gtc),
504 Self::stop(exit(), stop_loss).with_duration(OrderDuration::Gtc),
505 );
506 Ok(self.triggers(exits))
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use serde_json::json;
514
515 fn d(s: &str) -> Decimal {
516 s.parse().unwrap()
517 }
518
519 fn expiry() -> NaiveDate {
520 NaiveDate::from_ymd_opt(2024, 5, 17).unwrap()
521 }
522
523 #[test]
524 fn option_symbols_round_trip_the_osi_format() {
525 let put = OptionSymbol::put("aapl", expiry(), d("190")).unwrap();
526 assert_eq!(put.to_string(), "AAPL 240517P00190000");
527 assert_eq!(put.underlying(), "AAPL");
528 let parsed: OptionSymbol = "AAPL 240517P00190000".parse().unwrap();
529 assert_eq!(parsed, put);
530 let call = OptionSymbol::call("SPXW", expiry(), d("4512.5")).unwrap();
532 assert_eq!(call.to_string(), "SPXW 240517C04512500");
533 assert_eq!(call.to_string().parse::<OptionSymbol>().unwrap(), call);
534 let long = OptionSymbol::call("ABCDEF", expiry(), d("0.5")).unwrap();
535 assert_eq!(long.to_string(), "ABCDEF240517C00000500");
536 assert_eq!(long.to_string().parse::<OptionSymbol>().unwrap(), long);
537 assert_eq!(
538 "AAPL 240517P00190000"
539 .parse::<OptionSymbol>()
540 .unwrap()
541 .strike(),
542 d("190")
543 );
544 }
545
546 #[test]
547 fn option_symbols_reject_what_osi_cannot_express() {
548 assert!(OptionSymbol::call("ABCDEFG", expiry(), d("1")).is_err());
549 assert!(OptionSymbol::call("", expiry(), d("1")).is_err());
550 assert!(OptionSymbol::call("BRK.B", expiry(), d("1")).is_err());
551 assert!(OptionSymbol::call("AAPL", expiry(), d("0")).is_err());
552 assert!(OptionSymbol::call("AAPL", expiry(), d("-1")).is_err());
553 assert!(OptionSymbol::call("AAPL", expiry(), d("100000")).is_err());
554 assert!(OptionSymbol::call("AAPL", expiry(), d("1.0005")).is_err());
555 for year in [1999, 2100, 2125] {
557 let date = NaiveDate::from_ymd_opt(year, 6, 20).unwrap();
558 assert!(OptionSymbol::call("SPY", date, d("550")).is_err(), "{year}");
559 }
560 for bad in [
561 "AAPL 240517P00190000",
562 "AAPL 240517X00190000",
563 "AAPL 241317P00190000",
564 "AAPL 240517P0019000a",
565 "AAPL 240517P-0190000",
566 ] {
567 assert!(bad.parse::<OptionSymbol>().is_err(), "{bad}");
568 }
569 }
570
571 #[test]
572 fn single_orders_match_the_documented_shapes() {
573 let market =
574 OrderRequest::market(OrderRequestLeg::equity(OrderInstruction::Buy, "AMD", 10));
575 market.validate().unwrap();
576 assert_eq!(
577 serde_json::to_value(&market).unwrap(),
578 json!({
579 "orderType": "MARKET", "session": "NORMAL", "duration": "DAY",
580 "orderStrategyType": "SINGLE",
581 "orderLegCollection": [{"instruction": "BUY", "quantity": 10,
582 "instrument": {"symbol": "AMD", "assetType": "EQUITY"}}]
583 })
584 );
585 let put = OptionSymbol::put("AAPL", expiry(), d("190")).unwrap();
586 let sell = OrderRequest::limit(
587 OrderRequestLeg::option(OrderInstruction::SellToOpen, &put, 3),
588 d("1.0"),
589 )
590 .with_duration(OrderDuration::Gtc)
591 .with_complex_strategy(ComplexOrderStrategyType::None);
592 sell.validate().unwrap();
593 assert_eq!(
594 serde_json::to_value(&sell).unwrap(),
595 json!({
596 "orderType": "LIMIT", "session": "NORMAL", "price": "1.0",
597 "duration": "GOOD_TILL_CANCEL", "orderStrategyType": "SINGLE",
598 "complexOrderStrategyType": "NONE",
599 "orderLegCollection": [{"instruction": "SELL_TO_OPEN", "quantity": 3,
600 "instrument": {"symbol": "AAPL 240517P00190000", "assetType": "OPTION"}}]
601 })
602 );
603 let trailing = OrderRequest::trailing_stop(
604 OrderRequestLeg::equity(OrderInstruction::Sell, "XYZ", 10),
605 PriceLinkBasis::Bid,
606 PriceLinkType::Value,
607 d("10"),
608 );
609 trailing.validate().unwrap();
610 assert_eq!(
611 serde_json::to_value(&trailing).unwrap(),
612 json!({
613 "orderType": "TRAILING_STOP", "session": "NORMAL", "stopPriceLinkBasis": "BID",
614 "stopPriceLinkType": "VALUE", "stopPriceOffset": "10", "duration": "DAY",
615 "orderStrategyType": "SINGLE",
616 "orderLegCollection": [{"instruction": "SELL", "quantity": 10,
617 "instrument": {"symbol": "XYZ", "assetType": "EQUITY"}}]
618 })
619 );
620 let spread = OrderRequest::net_debit(
621 vec![
622 OrderRequestLeg::option(OrderInstruction::BuyToOpen, "XYZ 240315P00045000", 2),
623 OrderRequestLeg::option(OrderInstruction::SellToOpen, "XYZ 240315P00043000", 2),
624 ],
625 d("0.10"),
626 );
627 spread.validate().unwrap();
628 let value = serde_json::to_value(&spread).unwrap();
629 assert_eq!(value["orderType"], "NET_DEBIT");
630 assert_eq!(value["price"], "0.10");
631 assert_eq!(value["orderLegCollection"].as_array().unwrap().len(), 2);
632 }
633
634 #[test]
635 fn setters_cover_every_typed_request_field() {
636 let order = OrderRequest::stop(
637 OrderRequestLeg::equity(OrderInstruction::Sell, "XYZ", 1)
638 .with_position_effect(PositionEffect::Closing),
639 d("9"),
640 )
641 .with_duration(OrderDuration::Gtc)
642 .with_tag("t")
643 .with_special_instruction(SpecialInstruction::AllOrNone)
644 .with_destination(RequestedDestination::Auto)
645 .with_tax_lot_method(TaxLotMethod::Lifo)
646 .with_stop_type(StopType::Mark);
647 order.validate().unwrap();
648 let value = serde_json::to_value(&order).unwrap();
649 assert_eq!(value["session"], "NORMAL");
650 assert_eq!(value["duration"], "GOOD_TILL_CANCEL");
651 assert!(
653 order
654 .clone()
655 .with_session(OrderSession::Seamless)
656 .validate()
657 .is_err()
658 );
659 let seamless = OrderRequest::limit(
660 OrderRequestLeg::equity(OrderInstruction::Sell, "XYZ", 1),
661 d("9"),
662 )
663 .with_session(OrderSession::Seamless);
664 seamless.validate().unwrap();
665 assert_eq!(
666 serde_json::to_value(&seamless).unwrap()["session"],
667 "SEAMLESS"
668 );
669 assert_eq!(value["tag"], "t");
670 assert_eq!(value["specialInstruction"], "ALL_OR_NONE");
671 assert_eq!(value["requestedDestination"], "AUTO");
672 assert_eq!(value["taxLotMethod"], "LIFO");
673 assert_eq!(value["stopType"], "MARK");
674 assert_eq!(value["orderLegCollection"][0]["positionEffect"], "CLOSING");
675 }
676
677 #[test]
678 fn composites_nest_like_the_documented_examples() {
679 let sell = |symbol: &str, quantity: u32| {
680 OrderRequestLeg::equity(OrderInstruction::Sell, symbol, quantity)
681 };
682 let oco = OrderRequest::one_cancels_other(
683 OrderRequest::limit(sell("XYZ", 2), d("45.97")),
684 OrderRequest::stop_limit(sell("ABC", 2), d("37.03"), d("37.00")),
685 );
686 oco.validate().unwrap();
687 let value = serde_json::to_value(&oco).unwrap();
688 assert_eq!(
689 value,
690 json!({
691 "orderStrategyType": "OCO",
692 "childOrderStrategies": [
693 {"orderType": "LIMIT", "session": "NORMAL", "price": "45.97", "duration": "DAY",
694 "orderStrategyType": "SINGLE",
695 "orderLegCollection": [{"instruction": "SELL", "quantity": 2,
696 "instrument": {"symbol": "XYZ", "assetType": "EQUITY"}}]},
697 {"orderType": "STOP_LIMIT", "session": "NORMAL", "price": "37.00",
698 "stopPrice": "37.03", "duration": "DAY", "orderStrategyType": "SINGLE",
699 "orderLegCollection": [{"instruction": "SELL", "quantity": 2,
700 "instrument": {"symbol": "ABC", "assetType": "EQUITY"}}]}
701 ]
702 })
703 );
704 let trigger = OrderRequest::limit(
705 OrderRequestLeg::equity(OrderInstruction::Buy, "XYZ", 5),
706 d("14.97"),
707 )
708 .triggers(OrderRequest::one_cancels_other(
709 OrderRequest::limit(sell("ABC", 5), d("15.27")).with_duration(OrderDuration::Gtc),
710 OrderRequest::stop(sell("IJK", 5), d("11.27")).with_duration(OrderDuration::Gtc),
711 ));
712 trigger.validate().unwrap();
713 let value = serde_json::to_value(&trigger).unwrap();
714 assert_eq!(value["orderStrategyType"], "TRIGGER");
715 assert_eq!(value["childOrderStrategies"][0]["orderStrategyType"], "OCO");
716 assert_eq!(
717 value["childOrderStrategies"][0]["childOrderStrategies"][1]["stopPrice"],
718 "11.27"
719 );
720 assert_eq!(
721 value["childOrderStrategies"][0]["childOrderStrategies"][1]["duration"],
722 "GOOD_TILL_CANCEL"
723 );
724 }
725
726 #[test]
727 fn brackets_derive_exits_from_the_entry() {
728 let entry = OrderRequest::limit(
729 OrderRequestLeg::equity(OrderInstruction::Buy, "AAPL", 10)
730 .with_position_effect(PositionEffect::Opening),
731 d("150.25"),
732 );
733 let bracket = entry.clone().bracket(d("160"), d("145")).unwrap();
734 bracket.validate().unwrap();
735 let value = serde_json::to_value(&bracket).unwrap();
736 assert_eq!(value["orderStrategyType"], "TRIGGER");
737 assert_eq!(value["price"], "150.25");
738 let exits = &value["childOrderStrategies"][0];
739 assert_eq!(exits["orderStrategyType"], "OCO");
740 let (profit, loss) = (
741 &exits["childOrderStrategies"][0],
742 &exits["childOrderStrategies"][1],
743 );
744 assert_eq!(profit["orderType"], "LIMIT");
745 assert_eq!(profit["price"], "160");
746 assert_eq!(profit["duration"], "GOOD_TILL_CANCEL");
747 assert_eq!(loss["orderType"], "STOP");
748 assert_eq!(loss["stopPrice"], "145");
749 for exit in [profit, loss] {
750 let leg = &exit["orderLegCollection"][0];
751 assert_eq!(leg["instruction"], "SELL");
752 assert_eq!(leg["quantity"], 10);
753 assert_eq!(leg["instrument"]["symbol"], "AAPL");
754 assert_eq!(leg["positionEffect"], "CLOSING");
755 }
756 let short = OrderRequest::market(OrderRequestLeg::equity(
758 OrderInstruction::SellShort,
759 "AAPL",
760 10,
761 ));
762 let covered = short.clone().bracket(d("140"), d("155")).unwrap();
763 let exits = &serde_json::to_value(&covered).unwrap()["childOrderStrategies"][0];
764 assert_eq!(
765 exits["childOrderStrategies"][0]["orderLegCollection"][0]["instruction"],
766 "BUY_TO_COVER"
767 );
768 assert!(short.bracket(d("155"), d("140")).is_err());
769 assert!(entry.clone().bracket(d("145"), d("160")).is_err());
770 assert!(entry.clone().bracket(d("150"), d("150")).is_err());
771 assert!(
773 OrderRequest::market(OrderRequestLeg::equity(OrderInstruction::Sell, "AAPL", 10))
774 .bracket(d("160"), d("145"))
775 .is_err()
776 );
777 assert!(
778 OrderRequest::net_debit(
779 vec![
780 OrderRequestLeg::option(
781 OrderInstruction::BuyToOpen,
782 "XYZ 240315P00045000",
783 1
784 ),
785 OrderRequestLeg::option(
786 OrderInstruction::SellToOpen,
787 "XYZ 240315P00043000",
788 1
789 ),
790 ],
791 d("0.10"),
792 )
793 .bracket(d("1"), d("0.05"))
794 .is_err()
795 );
796 assert!(
797 entry
798 .triggers(OrderRequest::market(OrderRequestLeg::equity(
799 OrderInstruction::Sell,
800 "AAPL",
801 10
802 )))
803 .bracket(d("160"), d("145"))
804 .is_err()
805 );
806 }
807
808 #[test]
809 fn closing_instructions_pair_with_opening_ones() {
810 use OrderInstruction::*;
811 assert_eq!(Buy.closing(), Some(Sell));
812 assert_eq!(SellShort.closing(), Some(BuyToCover));
813 assert_eq!(BuyToOpen.closing(), Some(SellToClose));
814 assert_eq!(SellToOpen.closing(), Some(BuyToClose));
815 assert_eq!(SellShortExempt.closing(), Some(BuyToCover));
816 assert_eq!(SellShortExempt.opens_long(), Some(false));
817 for closing in [Sell, BuyToCover, BuyToClose, SellToClose, Exchange] {
818 assert_eq!(closing.closing(), None);
819 }
820 }
821}