use derive_builder::Builder;
use serde::Deserialize;
use serde::Serialize;
use serde_with::skip_serializing_none;
use super::accounts::AccountsInstrument;
use super::order::ComplexOrderStrategyType;
use super::order::Duration;
use super::order::Order;
use super::order::OrderActivity;
use super::order::OrderLegCollection;
use super::order::OrderStrategyType;
use super::order::OrderType;
use super::order::PriceLinkBasis;
use super::order::PriceLinkType;
use super::order::Session;
use super::order::SpecialInstruction;
use super::order::Status;
use super::order::StopPriceLinkBasis;
use super::order::StopPriceLinkType;
use super::order::StopType;
use super::order::TaxLotMethod;
use super::preview_order::Instruction;
use crate::Error;
use crate::model::InstrumentResponse;
use crate::model::market_data::instrument::InstrumentAssetType;
#[skip_serializing_none]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Builder)]
#[builder(setter(strip_option), default)]
#[serde(rename_all = "camelCase")]
pub struct OrderRequest {
pub session: Option<Session>,
pub duration: Option<Duration>,
pub order_type: Option<OrderTypeRequest>,
pub cancel_time: Option<chrono::DateTime<chrono::Utc>>,
pub complex_order_strategy_type: Option<ComplexOrderStrategyType>,
pub quantity: Option<f64>,
pub filled_quantity: Option<f64>,
pub remaining_quantity: Option<f64>,
pub destination_link_name: Option<String>,
pub release_time: Option<chrono::DateTime<chrono::Utc>>,
pub stop_price: Option<f64>,
pub stop_price_link_basis: Option<StopPriceLinkBasis>,
pub stop_price_link_type: Option<StopPriceLinkType>,
pub stop_price_offset: Option<f64>,
pub stop_type: Option<StopType>,
pub price_link_basis: Option<PriceLinkBasis>,
pub price_link_type: Option<PriceLinkType>,
pub price: Option<f64>,
pub tax_lot_method: Option<TaxLotMethod>,
pub order_leg_collection: Option<Vec<OrderLegCollectionRequest>>,
pub activation_price: Option<f64>,
pub special_instruction: Option<SpecialInstruction>,
pub order_strategy_type: OrderStrategyType,
pub order_id: Option<i64>,
pub cancelable: Option<bool>,
pub editable: Option<bool>,
pub status: Option<Status>,
pub entered_time: Option<chrono::DateTime<chrono::Utc>>,
pub close_time: Option<chrono::DateTime<chrono::Utc>>,
pub account_number: Option<i64>,
pub order_activity_collection: Option<Vec<OrderActivity>>,
pub replacing_order_collection: Option<Vec<String>>,
pub child_order_strategies: Option<Vec<OrderRequest>>,
pub status_description: Option<String>,
}
impl From<Order> for OrderRequest {
fn from(value: Order) -> Self {
Self {
session: Some(value.session),
duration: Some(value.duration),
order_type: Some(value.order_type.into()),
cancel_time: value.cancel_time,
complex_order_strategy_type: Some(value.complex_order_strategy_type),
quantity: Some(value.quantity),
filled_quantity: Some(value.filled_quantity),
remaining_quantity: Some(value.remaining_quantity),
destination_link_name: Some(value.destination_link_name),
release_time: value.release_time,
stop_price: value.stop_price,
stop_price_link_basis: value.stop_price_link_basis,
stop_price_link_type: value.stop_price_link_type,
stop_price_offset: value.stop_price_offset,
stop_type: value.stop_type,
price_link_basis: value.price_link_basis,
price_link_type: value.price_link_type,
price: Some(value.price),
tax_lot_method: value.tax_lot_method,
order_leg_collection: Some(
value
.order_leg_collection
.into_iter()
.map(Into::into)
.collect(),
),
activation_price: value.activation_price,
special_instruction: value.special_instruction,
order_strategy_type: value.order_strategy_type,
order_id: Some(value.order_id),
cancelable: Some(value.cancelable),
editable: Some(value.editable),
status: Some(value.status),
entered_time: Some(value.entered_time),
close_time: value.close_time,
account_number: Some(value.account_number),
order_activity_collection: value.order_activity_collection,
replacing_order_collection: value.replacing_order_collection,
child_order_strategies: value
.child_order_strategies
.map(|orders| orders.into_iter().map(Into::into).collect()),
status_description: value.status_description,
}
}
}
impl OrderRequest {
pub fn market(
symbol: InstrumentRequest,
instruction: Instruction,
quantity: f64,
) -> Result<Self, Error> {
let order_leg_collection = vec![OrderLegCollectionRequest {
instruction,
quantity,
instrument: symbol,
}];
OrderRequestBuilder::default()
.order_type(OrderTypeRequest::Market)
.session(Session::Normal)
.duration(Duration::Day)
.order_strategy_type(OrderStrategyType::Single)
.order_leg_collection(order_leg_collection)
.build()
.map_err(Error::OrderRequestBuild)
}
pub fn limit(
symbol: InstrumentRequest,
instruction: Instruction,
quantity: f64,
price: f64,
) -> Result<Self, Error> {
let order_leg_collection = vec![OrderLegCollectionRequest {
instruction,
quantity,
instrument: symbol,
}];
OrderRequestBuilder::default()
.complex_order_strategy_type(ComplexOrderStrategyType::None)
.order_type(OrderTypeRequest::Limit)
.session(Session::Normal)
.price(price)
.duration(Duration::Day)
.order_strategy_type(OrderStrategyType::Single)
.order_leg_collection(order_leg_collection)
.build()
.map_err(Error::OrderRequestBuild)
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OrderTypeRequest {
Market,
#[default]
Limit,
Stop,
StopLimit,
TrailingStop,
Cabinet,
NonMarketable,
MarketOnClose,
Exercise,
TrailingStopLimit,
NetDebit,
NetCredit,
NetZero,
LimitOnClose,
}
impl From<OrderType> for OrderTypeRequest {
fn from(value: OrderType) -> Self {
match value {
OrderType::Market => OrderTypeRequest::Market,
OrderType::Limit => OrderTypeRequest::Limit,
OrderType::Stop => OrderTypeRequest::Stop,
OrderType::StopLimit => OrderTypeRequest::StopLimit,
OrderType::TrailingStop => OrderTypeRequest::TrailingStop,
OrderType::Cabinet => OrderTypeRequest::Cabinet,
OrderType::NonMarketable => OrderTypeRequest::NonMarketable,
OrderType::MarketOnClose => OrderTypeRequest::MarketOnClose,
OrderType::Exercise => OrderTypeRequest::Exercise,
OrderType::TrailingStopLimit => OrderTypeRequest::TrailingStopLimit,
OrderType::NetDebit => OrderTypeRequest::NetDebit,
OrderType::NetCredit => OrderTypeRequest::NetCredit,
OrderType::NetZero => OrderTypeRequest::NetZero,
OrderType::LimitOnClose => OrderTypeRequest::LimitOnClose,
OrderType::Unknown => panic!("Unknown"),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderLegCollectionRequest {
pub instrument: InstrumentRequest,
pub instruction: Instruction,
pub quantity: f64,
}
impl From<OrderLegCollection> for OrderLegCollectionRequest {
fn from(value: OrderLegCollection) -> Self {
Self {
instrument: value.instrument.into(),
instruction: value.instruction,
quantity: value.quantity,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "assetType", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum InstrumentRequest {
Equity { symbol: String },
Option { symbol: String },
}
impl From<AccountsInstrument> for InstrumentRequest {
fn from(value: AccountsInstrument) -> Self {
match value {
AccountsInstrument::CashEquivalent(x) => Self::Equity {
symbol: x.accounts_base_instrument.symbol,
},
AccountsInstrument::Equity(x) => Self::Equity {
symbol: x.accounts_base_instrument.symbol,
},
AccountsInstrument::FixedIncome(x) => Self::Equity {
symbol: x.accounts_base_instrument.symbol,
},
AccountsInstrument::MutualFund(x) => Self::Equity {
symbol: x.accounts_base_instrument.symbol,
},
AccountsInstrument::Option(x) => Self::Option {
symbol: x.accounts_base_instrument.symbol,
},
AccountsInstrument::Index(x) => Self::Equity {
symbol: x.accounts_base_instrument.symbol,
},
AccountsInstrument::Currency(x) => Self::Equity {
symbol: x.accounts_base_instrument.symbol,
},
AccountsInstrument::CollectiveInvestment(x) => Self::Equity {
symbol: x.accounts_base_instrument.symbol,
},
}
}
}
impl From<InstrumentResponse> for InstrumentRequest {
fn from(value: InstrumentResponse) -> Self {
match value.asset_type {
InstrumentAssetType::Bond
| InstrumentAssetType::Equity
| InstrumentAssetType::Etf
| InstrumentAssetType::Extended
| InstrumentAssetType::Forex
| InstrumentAssetType::Future
| InstrumentAssetType::Fundamental
| InstrumentAssetType::Index
| InstrumentAssetType::Indicator
| InstrumentAssetType::MutualFund
| InstrumentAssetType::Unknown => Self::Equity {
symbol: value.symbol,
},
InstrumentAssetType::FutureOption | InstrumentAssetType::Option => Self::Option {
symbol: value.symbol,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_json_diff::{CompareMode, Config, NumericMode, assert_json_matches};
use serde_json::json;
#[test]
fn test_de() {
let json = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/model/Trader/OrderRequest.json"
));
let val = serde_json::from_str::<OrderRequest>(json);
println!("{val:?}");
assert!(val.is_ok());
}
#[test]
fn test_market() {
let expected = json!({
"orderType": "MARKET",
"session": "NORMAL",
"duration": "DAY",
"orderStrategyType": "SINGLE",
"orderLegCollection": [
{
"instruction": "BUY",
"quantity": 15,
"instrument": {
"symbol": "XYZ",
"assetType": "EQUITY"
}
}
]
});
let symbol = InstrumentRequest::Equity {
symbol: "XYZ".to_string(),
};
let order_req = OrderRequest::market(symbol, Instruction::Buy, 15.0).unwrap();
let order_req = serde_json::to_value(order_req).unwrap();
assert_json_matches!(
order_req,
expected,
Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat)
);
}
#[test]
fn test_limit() {
let expected = json!({
"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"
}
}
]
});
let symbol = InstrumentRequest::Option {
symbol: "XYZ 240315C00500000".to_string(),
};
let order_req = OrderRequest::limit(symbol, Instruction::BuyToOpen, 10.0, 6.45).unwrap();
let order_req = serde_json::to_value(order_req).unwrap();
assert_json_matches!(
order_req,
expected,
Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat)
);
}
#[test]
fn test_vertical_call_spread() {
let expected = json!({
"orderType": "NET_DEBIT",
"session": "NORMAL",
"price": 0.1,
"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"
}
}
]
});
let symbol1 = InstrumentRequest::Option {
symbol: "XYZ 240315P00045000".to_string(),
};
let symbol2 = InstrumentRequest::Option {
symbol: "XYZ 240315P00043000".to_string(),
};
let order_req = OrderRequestBuilder::default()
.order_type(OrderTypeRequest::NetDebit)
.session(Session::Normal)
.duration(Duration::Day)
.price(0.1)
.order_leg_collection(vec![
OrderLegCollectionRequest {
instruction: Instruction::BuyToOpen,
quantity: 2.0,
instrument: symbol1,
},
OrderLegCollectionRequest {
instruction: Instruction::SellToOpen,
quantity: 2.0,
instrument: symbol2,
},
])
.build()
.unwrap();
let order_req = serde_json::to_value(order_req).unwrap();
assert_json_matches!(
order_req,
expected,
Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat)
);
}
#[test]
fn test_one_triggers_another() {
let expected = json!({
"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"
}
}
]
}
]
});
let symbol = InstrumentRequest::Equity {
symbol: "XYZ".to_string(),
};
let child_order_req = OrderRequestBuilder::default()
.order_type(OrderTypeRequest::Limit)
.session(Session::Normal)
.duration(Duration::Day)
.price(42.03)
.order_leg_collection(vec![OrderLegCollectionRequest {
instruction: Instruction::Sell,
quantity: 10.0,
instrument: symbol.clone(),
}])
.build()
.unwrap();
let order_req = OrderRequestBuilder::default()
.order_type(OrderTypeRequest::Limit)
.session(Session::Normal)
.duration(Duration::Day)
.price(34.97)
.order_strategy_type(OrderStrategyType::Trigger)
.order_leg_collection(vec![OrderLegCollectionRequest {
instruction: Instruction::Buy,
quantity: 10.0,
instrument: symbol,
}])
.child_order_strategies(vec![child_order_req])
.build()
.unwrap();
let order_req = serde_json::to_value(order_req).unwrap();
assert_json_matches!(
order_req,
expected,
Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat)
);
}
#[test]
fn test_one_cancels_another() {
let expected = json!({
"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.0,
"stopPrice": 37.03,
"duration": "DAY",
"orderStrategyType": "SINGLE",
"orderLegCollection": [
{
"instruction": "SELL",
"quantity": 2,
"instrument": {
"symbol": "XYZ",
"assetType": "EQUITY"
}
}
]
}
]
});
let symbol = InstrumentRequest::Equity {
symbol: "XYZ".to_string(),
};
let child_order_req1 = OrderRequestBuilder::default()
.order_type(OrderTypeRequest::Limit)
.session(Session::Normal)
.duration(Duration::Day)
.price(45.97)
.order_leg_collection(vec![OrderLegCollectionRequest {
instruction: Instruction::Sell,
quantity: 2.0,
instrument: symbol.clone(),
}])
.build()
.unwrap();
let child_order_req2 = OrderRequestBuilder::default()
.order_type(OrderTypeRequest::StopLimit)
.session(Session::Normal)
.duration(Duration::Day)
.price(37.00)
.stop_price(37.03)
.order_leg_collection(vec![OrderLegCollectionRequest {
instruction: Instruction::Sell,
quantity: 2.0,
instrument: symbol.clone(),
}])
.build()
.unwrap();
let order_req = OrderRequestBuilder::default()
.order_strategy_type(OrderStrategyType::Oco)
.child_order_strategies(vec![child_order_req1, child_order_req2])
.build()
.unwrap();
let order_req = serde_json::to_value(order_req).unwrap();
assert_json_matches!(
order_req,
expected,
Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat)
);
}
#[test]
#[allow(clippy::too_many_lines)]
fn test_one_triggers_a_one_cancels_another() {
let expected = json!({
"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"
}
}
]
}
]
}
]
});
let symbol = InstrumentRequest::Equity {
symbol: "XYZ".to_string(),
};
let child_child_order_req1 = OrderRequestBuilder::default()
.order_type(OrderTypeRequest::Limit)
.session(Session::Normal)
.duration(Duration::GoodTillCancel)
.price(15.27)
.order_leg_collection(vec![OrderLegCollectionRequest {
instruction: Instruction::Sell,
quantity: 5.0,
instrument: symbol.clone(),
}])
.build()
.unwrap();
let child_child_order_req2 = OrderRequestBuilder::default()
.order_type(OrderTypeRequest::Stop)
.session(Session::Normal)
.duration(Duration::GoodTillCancel)
.stop_price(11.27)
.order_leg_collection(vec![OrderLegCollectionRequest {
instruction: Instruction::Sell,
quantity: 5.0,
instrument: symbol.clone(),
}])
.build()
.unwrap();
let child_order_req = OrderRequestBuilder::default()
.order_strategy_type(OrderStrategyType::Oco)
.child_order_strategies(vec![child_child_order_req1, child_child_order_req2])
.build()
.unwrap();
let order_req = OrderRequestBuilder::default()
.order_strategy_type(OrderStrategyType::Trigger)
.session(Session::Normal)
.duration(Duration::Day)
.order_type(OrderTypeRequest::Limit)
.price(14.97)
.order_leg_collection(vec![OrderLegCollectionRequest {
instruction: Instruction::Buy,
quantity: 5.0,
instrument: symbol.clone(),
}])
.child_order_strategies(vec![child_order_req])
.build()
.unwrap();
let order_req = serde_json::to_value(order_req).unwrap();
assert_json_matches!(
order_req,
expected,
Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat)
);
}
#[test]
fn test_sell_trailing_stop() {
let expected = json!({
"complexOrderStrategyType": "NONE",
"orderType": "TRAILING_STOP",
"session": "NORMAL",
"stopPriceLinkBasis": "BID",
"stopPriceLinkType": "VALUE",
"stopPriceOffset": 10,
"duration": "DAY",
"orderStrategyType": "SINGLE",
"orderLegCollection": [
{
"instruction": "SELL",
"quantity": 10,
"instrument": {
"symbol": "XYZ",
"assetType": "EQUITY"
}
}
]
});
let symbol = InstrumentRequest::Equity {
symbol: "XYZ".to_string(),
};
let order_req = OrderRequestBuilder::default()
.complex_order_strategy_type(ComplexOrderStrategyType::None)
.order_type(OrderTypeRequest::TrailingStop)
.session(Session::Normal)
.duration(Duration::Day)
.stop_price_link_basis(StopPriceLinkBasis::Bid)
.stop_price_link_type(StopPriceLinkType::Value)
.stop_price_offset(10.0)
.price(14.97)
.order_leg_collection(vec![OrderLegCollectionRequest {
instruction: Instruction::Sell,
quantity: 10.0,
instrument: symbol.clone(),
}])
.build()
.unwrap();
let order_req = serde_json::to_value(order_req).unwrap();
assert_json_matches!(
order_req,
expected,
Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat)
);
}
}