use std::marker::PhantomData;
use rust_decimal::Decimal;
use serde::Serialize;
use crate::accounts::AssetType;
use crate::orders::enums::{
ComplexOrderStrategyType, Duration, Instruction, OrderStrategyType, OrderType, PositionEffect,
PriceLinkBasis, PriceLinkType, QuantityType, Session, SpecialInstruction, StopPriceLinkBasis,
StopPriceLinkType, StopType, TaxLotMethod,
};
mod decimal_opt {
use rust_decimal::Decimal;
use serde::{Serialize, Serializer};
pub fn serialize<S: Serializer>(value: &Option<Decimal>, s: S) -> Result<S::Ok, S::Error> {
match value {
Some(d) => {
let n: serde_json::Number =
d.to_string().parse().map_err(serde::ser::Error::custom)?;
n.serialize(s)
}
None => s.serialize_none(),
}
}
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct OrderRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) session: Option<Session>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) duration: Option<Duration>,
#[serde(rename = "orderType", skip_serializing_if = "Option::is_none")]
pub(crate) order_type: Option<OrderType>,
#[serde(
rename = "complexOrderStrategyType",
skip_serializing_if = "Option::is_none"
)]
pub(crate) complex_order_strategy_type: Option<ComplexOrderStrategyType>,
#[serde(skip_serializing_if = "Option::is_none", with = "decimal_opt")]
pub(crate) quantity: Option<Decimal>,
#[serde(
rename = "destinationLinkName",
skip_serializing_if = "Option::is_none"
)]
pub(crate) destination_link_name: Option<String>,
#[serde(
rename = "stopPrice",
skip_serializing_if = "Option::is_none",
with = "decimal_opt"
)]
pub(crate) stop_price: Option<Decimal>,
#[serde(rename = "stopPriceLinkBasis", skip_serializing_if = "Option::is_none")]
pub(crate) stop_price_link_basis: Option<StopPriceLinkBasis>,
#[serde(rename = "stopPriceLinkType", skip_serializing_if = "Option::is_none")]
pub(crate) stop_price_link_type: Option<StopPriceLinkType>,
#[serde(
rename = "stopPriceOffset",
skip_serializing_if = "Option::is_none",
with = "decimal_opt"
)]
pub(crate) stop_price_offset: Option<Decimal>,
#[serde(rename = "stopType", skip_serializing_if = "Option::is_none")]
pub(crate) stop_type: Option<StopType>,
#[serde(rename = "priceLinkBasis", skip_serializing_if = "Option::is_none")]
pub(crate) price_link_basis: Option<PriceLinkBasis>,
#[serde(rename = "priceLinkType", skip_serializing_if = "Option::is_none")]
pub(crate) price_link_type: Option<PriceLinkType>,
#[serde(skip_serializing_if = "Option::is_none", with = "decimal_opt")]
pub(crate) price: Option<Decimal>,
#[serde(rename = "taxLotMethod", skip_serializing_if = "Option::is_none")]
pub(crate) tax_lot_method: Option<TaxLotMethod>,
#[serde(rename = "orderLegCollection", skip_serializing_if = "Vec::is_empty")]
pub(crate) order_leg_collection: Vec<OrderLegRequest>,
#[serde(
rename = "activationPrice",
skip_serializing_if = "Option::is_none",
with = "decimal_opt"
)]
pub(crate) activation_price: Option<Decimal>,
#[serde(rename = "specialInstruction", skip_serializing_if = "Option::is_none")]
pub(crate) special_instruction: Option<SpecialInstruction>,
#[serde(rename = "orderStrategyType", skip_serializing_if = "Option::is_none")]
pub(crate) order_strategy_type: Option<OrderStrategyType>,
#[serde(rename = "childOrderStrategies", skip_serializing_if = "Vec::is_empty")]
pub(crate) child_order_strategies: Vec<OrderRequest>,
}
impl OrderRequest {
pub(crate) fn empty() -> Self {
Self {
session: None,
duration: None,
order_type: None,
complex_order_strategy_type: None,
quantity: None,
destination_link_name: None,
stop_price: None,
stop_price_link_basis: None,
stop_price_link_type: None,
stop_price_offset: None,
stop_type: None,
price_link_basis: None,
price_link_type: None,
price: None,
tax_lot_method: None,
order_leg_collection: Vec::new(),
activation_price: None,
special_instruction: None,
order_strategy_type: None,
child_order_strategies: Vec::new(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct OrderLegRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) instruction: Option<Instruction>,
#[serde(skip_serializing_if = "Option::is_none", with = "decimal_opt")]
pub(crate) quantity: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) instrument: Option<OrderInstrumentRequest>,
#[serde(rename = "positionEffect", skip_serializing_if = "Option::is_none")]
pub(crate) position_effect: Option<PositionEffect>,
#[serde(rename = "quantityType", skip_serializing_if = "Option::is_none")]
pub(crate) quantity_type: Option<QuantityType>,
}
#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct OrderInstrumentRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) symbol: Option<String>,
#[serde(rename = "assetType", skip_serializing_if = "Option::is_none")]
pub(crate) asset_type: Option<AssetType>,
}
#[derive(Debug)]
pub struct NeedsType;
#[derive(Debug)]
pub struct NeedsLeg;
#[derive(Debug)]
pub struct Ready;
pub trait AcceptsLeg: sealed::Sealed {
type AfterLeg;
}
mod sealed {
pub trait Sealed {}
impl Sealed for super::NeedsLeg {}
impl Sealed for super::Ready {}
}
impl AcceptsLeg for NeedsLeg {
type AfterLeg = Ready;
}
impl AcceptsLeg for Ready {
type AfterLeg = Ready;
}
#[derive(Debug)]
#[must_use = "call .build() to finalize the OrderRequest"]
pub struct SingleOrderBuilder<State> {
inner: OrderRequest,
_state: PhantomData<State>,
}
impl OrderRequest {
pub fn single() -> SingleOrderBuilder<NeedsType> {
let inner = OrderRequest {
session: Some(Session::Normal),
duration: Some(Duration::Day),
order_strategy_type: Some(OrderStrategyType::Single),
..OrderRequest::empty()
};
SingleOrderBuilder {
inner,
_state: PhantomData,
}
}
pub fn buy_market(symbol: impl Into<String>, qty: Decimal) -> SingleOrderBuilder<Ready> {
Self::single().market().equity_buy(symbol, qty)
}
pub fn buy_limit(
symbol: impl Into<String>,
qty: Decimal,
price: Decimal,
) -> SingleOrderBuilder<Ready> {
Self::single().limit(price).equity_buy(symbol, qty)
}
pub fn sell_market(symbol: impl Into<String>, qty: Decimal) -> SingleOrderBuilder<Ready> {
Self::single().market().equity_sell(symbol, qty)
}
pub fn sell_limit(
symbol: impl Into<String>,
qty: Decimal,
price: Decimal,
) -> SingleOrderBuilder<Ready> {
Self::single().limit(price).equity_sell(symbol, qty)
}
pub fn sell_stop(
symbol: impl Into<String>,
qty: Decimal,
stop_price: Decimal,
) -> SingleOrderBuilder<Ready> {
Self::single().stop(stop_price).equity_sell(symbol, qty)
}
pub fn sell_stop_limit(
symbol: impl Into<String>,
qty: Decimal,
stop_price: Decimal,
limit_price: Decimal,
) -> SingleOrderBuilder<Ready> {
Self::single()
.stop_limit(stop_price, limit_price)
.equity_sell(symbol, qty)
}
pub fn buy_to_open_market(
symbol: impl Into<String>,
qty: Decimal,
) -> SingleOrderBuilder<Ready> {
Self::single().market().option_buy_to_open(symbol, qty)
}
pub fn buy_to_open_limit(
symbol: impl Into<String>,
qty: Decimal,
price: Decimal,
) -> SingleOrderBuilder<Ready> {
Self::single().limit(price).option_buy_to_open(symbol, qty)
}
pub fn sell_to_open_market(
symbol: impl Into<String>,
qty: Decimal,
) -> SingleOrderBuilder<Ready> {
Self::single().market().option_sell_to_open(symbol, qty)
}
pub fn sell_to_open_limit(
symbol: impl Into<String>,
qty: Decimal,
price: Decimal,
) -> SingleOrderBuilder<Ready> {
Self::single().limit(price).option_sell_to_open(symbol, qty)
}
pub fn buy_to_close_market(
symbol: impl Into<String>,
qty: Decimal,
) -> SingleOrderBuilder<Ready> {
Self::single().market().option_buy_to_close(symbol, qty)
}
pub fn buy_to_close_limit(
symbol: impl Into<String>,
qty: Decimal,
price: Decimal,
) -> SingleOrderBuilder<Ready> {
Self::single().limit(price).option_buy_to_close(symbol, qty)
}
pub fn sell_to_close_market(
symbol: impl Into<String>,
qty: Decimal,
) -> SingleOrderBuilder<Ready> {
Self::single().market().option_sell_to_close(symbol, qty)
}
pub fn sell_to_close_limit(
symbol: impl Into<String>,
qty: Decimal,
price: Decimal,
) -> SingleOrderBuilder<Ready> {
Self::single()
.limit(price)
.option_sell_to_close(symbol, qty)
}
pub fn oco(child_a: impl Into<OrderRequest>, child_b: impl Into<OrderRequest>) -> OrderRequest {
OrderRequest {
order_strategy_type: Some(OrderStrategyType::Oco),
child_order_strategies: vec![child_a.into(), child_b.into()],
..OrderRequest::empty()
}
}
pub fn trigger(
parent: impl Into<OrderRequest>,
child: impl Into<OrderRequest>,
) -> OrderRequest {
let mut parent: OrderRequest = parent.into();
parent.order_strategy_type = Some(OrderStrategyType::Trigger);
parent.child_order_strategies.push(child.into());
parent
}
}
impl From<SingleOrderBuilder<Ready>> for OrderRequest {
fn from(builder: SingleOrderBuilder<Ready>) -> Self {
builder.build()
}
}
impl SingleOrderBuilder<NeedsType> {
pub fn market(mut self) -> SingleOrderBuilder<NeedsLeg> {
self.inner.order_type = Some(OrderType::Market);
self.transition()
}
pub fn limit(mut self, price: Decimal) -> SingleOrderBuilder<NeedsLeg> {
self.inner.order_type = Some(OrderType::Limit);
self.inner.price = Some(price);
self.transition()
}
pub fn stop(mut self, stop_price: Decimal) -> SingleOrderBuilder<NeedsLeg> {
self.inner.order_type = Some(OrderType::Stop);
self.inner.stop_price = Some(stop_price);
self.transition()
}
pub fn stop_limit(
mut self,
stop_price: Decimal,
limit_price: Decimal,
) -> SingleOrderBuilder<NeedsLeg> {
self.inner.order_type = Some(OrderType::StopLimit);
self.inner.stop_price = Some(stop_price);
self.inner.price = Some(limit_price);
self.transition()
}
pub fn net_debit(mut self, price: Decimal) -> SingleOrderBuilder<NeedsLeg> {
self.inner.order_type = Some(OrderType::NetDebit);
self.inner.price = Some(price);
self.transition()
}
pub fn net_credit(mut self, price: Decimal) -> SingleOrderBuilder<NeedsLeg> {
self.inner.order_type = Some(OrderType::NetCredit);
self.inner.price = Some(price);
self.transition()
}
fn transition(self) -> SingleOrderBuilder<NeedsLeg> {
SingleOrderBuilder {
inner: self.inner,
_state: PhantomData,
}
}
}
impl<S: AcceptsLeg> SingleOrderBuilder<S> {
fn push_leg(mut self, leg: OrderLegRequest) -> SingleOrderBuilder<S::AfterLeg> {
self.inner.order_leg_collection.push(leg);
SingleOrderBuilder {
inner: self.inner,
_state: PhantomData,
}
}
pub fn equity_buy(
self,
symbol: impl Into<String>,
qty: Decimal,
) -> SingleOrderBuilder<S::AfterLeg> {
self.push_leg(equity_leg(Instruction::Buy, symbol, qty))
}
pub fn equity_sell(
self,
symbol: impl Into<String>,
qty: Decimal,
) -> SingleOrderBuilder<S::AfterLeg> {
self.push_leg(equity_leg(Instruction::Sell, symbol, qty))
}
pub fn equity_sell_short(
self,
symbol: impl Into<String>,
qty: Decimal,
) -> SingleOrderBuilder<S::AfterLeg> {
self.push_leg(equity_leg(Instruction::SellShort, symbol, qty))
}
pub fn equity_buy_to_cover(
self,
symbol: impl Into<String>,
qty: Decimal,
) -> SingleOrderBuilder<S::AfterLeg> {
self.push_leg(equity_leg(Instruction::BuyToCover, symbol, qty))
}
pub fn option_buy_to_open(
self,
symbol: impl Into<String>,
qty: Decimal,
) -> SingleOrderBuilder<S::AfterLeg> {
self.push_leg(option_leg(Instruction::BuyToOpen, symbol, qty))
}
pub fn option_sell_to_open(
self,
symbol: impl Into<String>,
qty: Decimal,
) -> SingleOrderBuilder<S::AfterLeg> {
self.push_leg(option_leg(Instruction::SellToOpen, symbol, qty))
}
pub fn option_buy_to_close(
self,
symbol: impl Into<String>,
qty: Decimal,
) -> SingleOrderBuilder<S::AfterLeg> {
self.push_leg(option_leg(Instruction::BuyToClose, symbol, qty))
}
pub fn option_sell_to_close(
self,
symbol: impl Into<String>,
qty: Decimal,
) -> SingleOrderBuilder<S::AfterLeg> {
self.push_leg(option_leg(Instruction::SellToClose, symbol, qty))
}
}
impl SingleOrderBuilder<Ready> {
pub fn duration(mut self, duration: Duration) -> Self {
self.inner.duration = Some(duration);
self
}
pub fn session(mut self, session: Session) -> Self {
self.inner.session = Some(session);
self
}
pub fn special_instruction(mut self, instr: SpecialInstruction) -> Self {
self.inner.special_instruction = Some(instr);
self
}
pub fn complex_order_strategy_type(mut self, t: ComplexOrderStrategyType) -> Self {
self.inner.complex_order_strategy_type = Some(t);
self
}
pub fn build(self) -> OrderRequest {
self.inner
}
}
fn equity_leg(
instruction: Instruction,
symbol: impl Into<String>,
qty: Decimal,
) -> OrderLegRequest {
OrderLegRequest {
instruction: Some(instruction),
quantity: Some(qty),
instrument: Some(OrderInstrumentRequest {
symbol: Some(symbol.into()),
asset_type: Some(AssetType::Equity),
}),
..Default::default()
}
}
fn option_leg(
instruction: Instruction,
symbol: impl Into<String>,
qty: Decimal,
) -> OrderLegRequest {
OrderLegRequest {
instruction: Some(instruction),
quantity: Some(qty),
instrument: Some(OrderInstrumentRequest {
symbol: Some(symbol.into()),
asset_type: Some(AssetType::Option),
}),
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn pretty(value: &serde_json::Value) -> String {
serde_json::to_string_pretty(value).unwrap()
}
#[test]
fn builder_buy_market_equity_matches_schwab_example() {
let req = OrderRequest::single()
.market()
.equity_buy("XYZ", dec!(15))
.build();
let actual: serde_json::Value = serde_json::to_value(&req).unwrap();
let expected: serde_json::Value = serde_json::from_str(
r#"{
"session": "NORMAL",
"duration": "DAY",
"orderType": "MARKET",
"orderStrategyType": "SINGLE",
"orderLegCollection": [{
"instruction": "BUY",
"quantity": 15,
"instrument": {
"symbol": "XYZ",
"assetType": "EQUITY"
}
}]
}"#,
)
.unwrap();
assert_eq!(actual, expected, "got: {}", pretty(&actual));
}
#[test]
fn builder_buy_limit_option_matches_schwab_example() {
let req = OrderRequest::single()
.limit(dec!(6.45))
.option_buy_to_open("XYZ 240315C00500000", dec!(10))
.complex_order_strategy_type(ComplexOrderStrategyType::None)
.build();
let actual: serde_json::Value = serde_json::to_value(&req).unwrap();
let expected: serde_json::Value = serde_json::from_str(
r#"{
"complexOrderStrategyType": "NONE",
"orderType": "LIMIT",
"session": "NORMAL",
"price": 6.45,
"duration": "DAY",
"orderStrategyType": "SINGLE",
"orderLegCollection": [{
"instruction": "BUY_TO_OPEN",
"quantity": 10,
"instrument": {
"symbol": "XYZ 240315C00500000",
"assetType": "OPTION"
}
}]
}"#,
)
.unwrap();
assert_eq!(actual, expected, "got: {}", pretty(&actual));
}
#[test]
fn builder_vertical_spread_uses_net_debit_with_two_legs() {
let req = OrderRequest::single()
.net_debit(dec!(0.10))
.option_buy_to_open("XYZ 240315P00045000", dec!(2))
.option_sell_to_open("XYZ 240315P00043000", dec!(2))
.build();
let actual: serde_json::Value = serde_json::to_value(&req).unwrap();
let expected: serde_json::Value = serde_json::from_str(
r#"{
"orderType": "NET_DEBIT",
"session": "NORMAL",
"price": 0.10,
"duration": "DAY",
"orderStrategyType": "SINGLE",
"orderLegCollection": [
{
"instruction": "BUY_TO_OPEN",
"quantity": 2,
"instrument": {
"symbol": "XYZ 240315P00045000",
"assetType": "OPTION"
}
},
{
"instruction": "SELL_TO_OPEN",
"quantity": 2,
"instrument": {
"symbol": "XYZ 240315P00043000",
"assetType": "OPTION"
}
}
]
}"#,
)
.unwrap();
assert_eq!(actual, expected, "got: {}", pretty(&actual));
}
#[test]
fn builder_optional_setters_override_defaults() {
let req = OrderRequest::single()
.limit(dec!(140.00))
.equity_buy("AAPL", dec!(5))
.duration(Duration::GoodTillCancel)
.session(Session::Seamless)
.special_instruction(SpecialInstruction::AllOrNone)
.build();
assert_eq!(req.duration, Some(Duration::GoodTillCancel));
assert_eq!(req.session, Some(Session::Seamless));
assert_eq!(req.special_instruction, Some(SpecialInstruction::AllOrNone));
}
#[test]
fn builder_serialization_omits_response_only_fields() {
let req = OrderRequest::single()
.market()
.equity_buy("AAPL", dec!(1))
.build();
let json = serde_json::to_string(&req).unwrap();
for forbidden in [
"status",
"orderId",
"accountNumber",
"tag",
"requestedDestination",
"filledQuantity",
"remainingQuantity",
"enteredTime",
"closeTime",
"cancelable",
"editable",
"orderActivityCollection",
] {
assert!(
!json.contains(forbidden),
"request body should not contain {forbidden}, got: {json}"
);
}
}
#[test]
fn shortcut_buy_market_equals_explicit_builder() {
let a = OrderRequest::buy_market("AAPL", dec!(10)).build();
let b = OrderRequest::single()
.market()
.equity_buy("AAPL", dec!(10))
.build();
assert_eq!(
serde_json::to_value(&a).unwrap(),
serde_json::to_value(&b).unwrap()
);
}
#[test]
fn shortcut_buy_limit_equals_explicit_builder() {
let a = OrderRequest::buy_limit("AAPL", dec!(10), dec!(150.00)).build();
let b = OrderRequest::single()
.limit(dec!(150.00))
.equity_buy("AAPL", dec!(10))
.build();
assert_eq!(
serde_json::to_value(&a).unwrap(),
serde_json::to_value(&b).unwrap()
);
}
#[test]
fn shortcut_sell_stop_equals_explicit_builder() {
let a = OrderRequest::sell_stop("AAPL", dec!(10), dec!(140.00)).build();
let b = OrderRequest::single()
.stop(dec!(140.00))
.equity_sell("AAPL", dec!(10))
.build();
assert_eq!(
serde_json::to_value(&a).unwrap(),
serde_json::to_value(&b).unwrap()
);
}
#[test]
fn shortcut_sell_stop_limit_equals_explicit_builder() {
let a = OrderRequest::sell_stop_limit("AAPL", dec!(10), dec!(140.00), dec!(139.50)).build();
let b = OrderRequest::single()
.stop_limit(dec!(140.00), dec!(139.50))
.equity_sell("AAPL", dec!(10))
.build();
assert_eq!(
serde_json::to_value(&a).unwrap(),
serde_json::to_value(&b).unwrap()
);
}
#[test]
fn option_shortcut_buy_to_open_market_equals_explicit_builder() {
let symbol = "AAPL 240315C00200000";
let a = OrderRequest::buy_to_open_market(symbol, dec!(2)).build();
let b = OrderRequest::single()
.market()
.option_buy_to_open(symbol, dec!(2))
.build();
assert_eq!(
serde_json::to_value(&a).unwrap(),
serde_json::to_value(&b).unwrap()
);
}
#[test]
fn option_shortcuts_cover_all_four_instructions() {
let cases: [(OrderRequest, &str); 4] = [
(
OrderRequest::buy_to_open_limit("XYZ 240315C00500000", dec!(1), dec!(6.45))
.build(),
"BUY_TO_OPEN",
),
(
OrderRequest::sell_to_open_limit("XYZ 240315C00500000", dec!(1), dec!(6.45))
.build(),
"SELL_TO_OPEN",
),
(
OrderRequest::buy_to_close_limit("XYZ 240315C00500000", dec!(1), dec!(6.45))
.build(),
"BUY_TO_CLOSE",
),
(
OrderRequest::sell_to_close_limit("XYZ 240315C00500000", dec!(1), dec!(6.45))
.build(),
"SELL_TO_CLOSE",
),
];
for (req, expected_instruction) in cases {
let v = serde_json::to_value(&req).unwrap();
let leg = &v["orderLegCollection"][0];
assert_eq!(leg["instruction"], expected_instruction);
assert_eq!(leg["instrument"]["assetType"], "OPTION");
assert_eq!(v["orderStrategyType"], "SINGLE");
}
}
#[test]
fn shortcut_supports_chaining_optional_setters() {
let req = OrderRequest::buy_limit("AAPL", dec!(10), dec!(150.00))
.duration(Duration::GoodTillCancel)
.session(Session::Seamless)
.special_instruction(SpecialInstruction::AllOrNone)
.build();
assert_eq!(req.duration, Some(Duration::GoodTillCancel));
assert_eq!(req.session, Some(Session::Seamless));
assert_eq!(req.special_instruction, Some(SpecialInstruction::AllOrNone));
assert_eq!(req.order_type, Some(OrderType::Limit));
assert_eq!(req.price, Some(dec!(150.00)));
}
#[test]
fn oco_accepts_shortcut_builders_via_into() {
let oco = OrderRequest::oco(
OrderRequest::sell_limit("XYZ", dec!(1), dec!(50)),
OrderRequest::sell_stop("XYZ", dec!(1), dec!(40)),
);
let v = serde_json::to_value(&oco).unwrap();
assert_eq!(v["orderStrategyType"], "OCO");
assert_eq!(v["childOrderStrategies"].as_array().unwrap().len(), 2);
}
#[test]
fn oco_pair_matches_schwab_example() {
let limit_leg = OrderRequest::single()
.limit(dec!(45.97))
.equity_sell("XYZ", dec!(2))
.build();
let stop_limit_leg = OrderRequest::single()
.stop_limit(dec!(37.03), dec!(37.00))
.equity_sell("XYZ", dec!(2))
.build();
let req = OrderRequest::oco(limit_leg, stop_limit_leg);
let actual: serde_json::Value = serde_json::to_value(&req).unwrap();
let expected: serde_json::Value = serde_json::from_str(
r#"{
"orderStrategyType": "OCO",
"childOrderStrategies": [
{
"orderType": "LIMIT",
"session": "NORMAL",
"price": 45.97,
"duration": "DAY",
"orderStrategyType": "SINGLE",
"orderLegCollection": [{
"instruction": "SELL",
"quantity": 2,
"instrument": { "symbol": "XYZ", "assetType": "EQUITY" }
}]
},
{
"orderType": "STOP_LIMIT",
"session": "NORMAL",
"price": 37.00,
"stopPrice": 37.03,
"duration": "DAY",
"orderStrategyType": "SINGLE",
"orderLegCollection": [{
"instruction": "SELL",
"quantity": 2,
"instrument": { "symbol": "XYZ", "assetType": "EQUITY" }
}]
}
]
}"#,
)
.unwrap();
assert_eq!(actual, expected, "got: {}", pretty(&actual));
}
#[test]
fn trigger_buy_then_sell_matches_schwab_example() {
let entry = OrderRequest::buy_limit("XYZ", dec!(10), dec!(34.97));
let exit = OrderRequest::sell_limit("XYZ", dec!(10), dec!(42.03));
let req = OrderRequest::trigger(entry, exit);
let actual: serde_json::Value = serde_json::to_value(&req).unwrap();
let expected: serde_json::Value = serde_json::from_str(
r#"{
"orderType": "LIMIT",
"session": "NORMAL",
"price": 34.97,
"duration": "DAY",
"orderStrategyType": "TRIGGER",
"orderLegCollection": [{
"instruction": "BUY",
"quantity": 10,
"instrument": { "symbol": "XYZ", "assetType": "EQUITY" }
}],
"childOrderStrategies": [{
"orderType": "LIMIT",
"session": "NORMAL",
"price": 42.03,
"duration": "DAY",
"orderStrategyType": "SINGLE",
"orderLegCollection": [{
"instruction": "SELL",
"quantity": 10,
"instrument": { "symbol": "XYZ", "assetType": "EQUITY" }
}]
}]
}"#,
)
.unwrap();
assert_eq!(actual, expected, "got: {}", pretty(&actual));
}
#[test]
fn one_triggers_oco_matches_schwab_example() {
let entry = OrderRequest::buy_limit("XYZ", dec!(5), dec!(14.97));
let take_profit = OrderRequest::single()
.limit(dec!(15.27))
.equity_sell("XYZ", dec!(5))
.duration(Duration::GoodTillCancel)
.build();
let stop_loss = OrderRequest::single()
.stop(dec!(11.27))
.equity_sell("XYZ", dec!(5))
.duration(Duration::GoodTillCancel)
.build();
let oco = OrderRequest::oco(take_profit, stop_loss);
let req = OrderRequest::trigger(entry, oco);
let actual: serde_json::Value = serde_json::to_value(&req).unwrap();
let expected: serde_json::Value = serde_json::from_str(
r#"{
"orderStrategyType": "TRIGGER",
"session": "NORMAL",
"duration": "DAY",
"orderType": "LIMIT",
"price": 14.97,
"orderLegCollection": [{
"instruction": "BUY",
"quantity": 5,
"instrument": { "assetType": "EQUITY", "symbol": "XYZ" }
}],
"childOrderStrategies": [{
"orderStrategyType": "OCO",
"childOrderStrategies": [
{
"orderStrategyType": "SINGLE",
"session": "NORMAL",
"duration": "GOOD_TILL_CANCEL",
"orderType": "LIMIT",
"price": 15.27,
"orderLegCollection": [{
"instruction": "SELL",
"quantity": 5,
"instrument": { "assetType": "EQUITY", "symbol": "XYZ" }
}]
},
{
"orderStrategyType": "SINGLE",
"session": "NORMAL",
"duration": "GOOD_TILL_CANCEL",
"orderType": "STOP",
"stopPrice": 11.27,
"orderLegCollection": [{
"instruction": "SELL",
"quantity": 5,
"instrument": { "assetType": "EQUITY", "symbol": "XYZ" }
}]
}
]
}]
}"#,
)
.unwrap();
assert_eq!(actual, expected, "got: {}", pretty(&actual));
}
#[test]
fn oco_top_level_has_no_session_or_duration() {
let a = OrderRequest::sell_limit("XYZ", dec!(1), dec!(50));
let b = OrderRequest::sell_stop("XYZ", dec!(1), dec!(40));
let req = OrderRequest::oco(a, b);
let v = serde_json::to_value(&req).unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.len(), 2);
assert!(obj.contains_key("orderStrategyType"));
assert!(obj.contains_key("childOrderStrategies"));
}
}