use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::{Error, Result};
use crate::trading::enums::{
ActivityCategory, ActivityType, AssetClass, AssetExchange, AssetStatus,
CorporateActionDateType, CorporateActionType, ExerciseStyle, OrderClass, OrderSide, OrderType,
PositionIntent, QueryOrderStatus, TimeInForce,
};
use crate::types::{ContractType, Sort};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum OrderAmount {
Qty(Decimal),
Notional(Decimal),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct StopLimit {
pub stop: Decimal,
pub limit: Decimal,
}
impl StopLimit {
#[must_use]
pub fn new(stop: Decimal, limit: Decimal) -> Self {
Self { stop, limit }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Trail {
Price(Decimal),
Percent(Decimal),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TakeProfitRequest {
#[serde(with = "crate::types::decimal")]
pub limit_price: Decimal,
}
impl TakeProfitRequest {
#[must_use]
pub fn new(limit_price: Decimal) -> Self {
Self { limit_price }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StopLossRequest {
#[serde(with = "crate::types::decimal")]
pub stop_price: Decimal,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub limit_price: Option<Decimal>,
}
impl StopLossRequest {
#[must_use]
pub fn new(stop_price: Decimal) -> Self {
Self {
stop_price,
limit_price: None,
}
}
#[must_use]
pub fn limit_price(mut self, limit_price: Decimal) -> Self {
self.limit_price = Some(limit_price);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OptionLegRequest {
pub symbol: String,
#[serde(with = "crate::types::decimal")]
pub ratio_qty: Decimal,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub side: Option<OrderSide>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub position_intent: Option<PositionIntent>,
}
impl OptionLegRequest {
#[must_use]
pub fn new(symbol: impl Into<String>, ratio_qty: Decimal, side: OrderSide) -> Self {
Self {
symbol: symbol.into(),
ratio_qty,
side: Some(side),
position_intent: None,
}
}
#[must_use]
pub fn with_position_intent(
symbol: impl Into<String>,
ratio_qty: Decimal,
position_intent: PositionIntent,
) -> Self {
Self {
symbol: symbol.into(),
ratio_qty,
side: None,
position_intent: Some(position_intent),
}
}
#[must_use]
pub fn position_intent(mut self, position_intent: PositionIntent) -> Self {
self.position_intent = Some(position_intent);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OrderRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub qty: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub notional: Option<Decimal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub side: Option<OrderSide>,
#[serde(rename = "type")]
pub order_type: OrderType,
pub time_in_force: TimeInForce,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub order_class: Option<OrderClass>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extended_hours: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_order_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub legs: Option<Vec<OptionLegRequest>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub take_profit: Option<TakeProfitRequest>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_loss: Option<StopLossRequest>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub position_intent: Option<PositionIntent>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub limit_price: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub stop_price: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub trail_price: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub trail_percent: Option<Decimal>,
}
impl OrderRequest {
fn base(order_type: OrderType, time_in_force: TimeInForce) -> Self {
Self {
symbol: None,
qty: None,
notional: None,
side: None,
order_type,
time_in_force,
order_class: None,
extended_hours: None,
client_order_id: None,
legs: None,
take_profit: None,
stop_loss: None,
position_intent: None,
limit_price: None,
stop_price: None,
trail_price: None,
trail_percent: None,
}
}
fn single_leg(
order_type: OrderType,
symbol: impl Into<String>,
side: OrderSide,
amount: OrderAmount,
time_in_force: TimeInForce,
) -> Self {
let mut request = Self::base(order_type, time_in_force);
request.symbol = Some(symbol.into());
request.side = Some(side);
match amount {
OrderAmount::Qty(qty) => request.qty = Some(qty),
OrderAmount::Notional(notional) => request.notional = Some(notional),
}
request
}
#[must_use]
pub fn market(
symbol: impl Into<String>,
side: OrderSide,
amount: OrderAmount,
time_in_force: TimeInForce,
) -> Self {
Self::single_leg(OrderType::Market, symbol, side, amount, time_in_force)
}
#[must_use]
pub fn limit(
symbol: impl Into<String>,
side: OrderSide,
amount: OrderAmount,
time_in_force: TimeInForce,
limit_price: Decimal,
) -> Self {
let mut request = Self::single_leg(OrderType::Limit, symbol, side, amount, time_in_force);
request.limit_price = Some(limit_price);
request
}
#[must_use]
pub fn stop(
symbol: impl Into<String>,
side: OrderSide,
amount: OrderAmount,
time_in_force: TimeInForce,
stop_price: Decimal,
) -> Self {
let mut request = Self::single_leg(OrderType::Stop, symbol, side, amount, time_in_force);
request.stop_price = Some(stop_price);
request
}
#[must_use]
pub fn stop_limit(
symbol: impl Into<String>,
side: OrderSide,
amount: OrderAmount,
time_in_force: TimeInForce,
prices: StopLimit,
) -> Self {
let mut request =
Self::single_leg(OrderType::StopLimit, symbol, side, amount, time_in_force);
request.stop_price = Some(prices.stop);
request.limit_price = Some(prices.limit);
request
}
#[must_use]
pub fn trailing_stop(
symbol: impl Into<String>,
side: OrderSide,
amount: OrderAmount,
time_in_force: TimeInForce,
trail: Trail,
) -> Self {
let mut request =
Self::single_leg(OrderType::TrailingStop, symbol, side, amount, time_in_force);
match trail {
Trail::Price(price) => request.trail_price = Some(price),
Trail::Percent(percent) => request.trail_percent = Some(percent),
}
request
}
#[must_use]
pub fn multi_leg(
qty: Decimal,
time_in_force: TimeInForce,
legs: Vec<OptionLegRequest>,
limit_price: Option<Decimal>,
) -> Self {
let order_type = if limit_price.is_some() {
OrderType::Limit
} else {
OrderType::Market
};
let mut request = Self::base(order_type, time_in_force);
request.order_class = Some(OrderClass::Mleg);
request.qty = Some(qty);
request.legs = Some(legs);
request.limit_price = limit_price;
request
}
#[must_use]
pub fn bracket(mut self, take_profit: TakeProfitRequest, stop_loss: StopLossRequest) -> Self {
self.order_class = Some(OrderClass::Bracket);
self.take_profit = Some(take_profit);
self.stop_loss = Some(stop_loss);
self
}
#[must_use]
pub fn oco(mut self, take_profit: TakeProfitRequest, stop_loss: StopLossRequest) -> Self {
self.order_class = Some(OrderClass::Oco);
self.take_profit = Some(take_profit);
self.stop_loss = Some(stop_loss);
self
}
#[must_use]
pub fn oto_take_profit(mut self, take_profit: TakeProfitRequest) -> Self {
self.order_class = Some(OrderClass::Oto);
self.take_profit = Some(take_profit);
self.stop_loss = None;
self
}
#[must_use]
pub fn oto_stop_loss(mut self, stop_loss: StopLossRequest) -> Self {
self.order_class = Some(OrderClass::Oto);
self.stop_loss = Some(stop_loss);
self.take_profit = None;
self
}
#[must_use]
pub fn extended_hours(mut self, extended_hours: bool) -> Self {
self.extended_hours = Some(extended_hours);
self
}
#[must_use]
pub fn client_order_id(mut self, client_order_id: impl Into<String>) -> Self {
self.client_order_id = Some(client_order_id.into());
self
}
#[must_use]
pub fn position_intent(mut self, position_intent: PositionIntent) -> Self {
self.position_intent = Some(position_intent);
self
}
pub fn validate(&self) -> Result<()> {
let invalid = |reason: String| Err(Error::InvalidRequest(reason));
match self.order_class {
Some(OrderClass::Bracket | OrderClass::Oco) => {
let class = self.order_class.as_ref().map_or("", OrderClass::as_str);
if self.take_profit.is_none() {
return invalid(format!("{class} orders require take_profit.limit_price"));
}
if self.stop_loss.is_none() {
return invalid(format!("{class} orders require stop_loss.stop_price"));
}
}
Some(OrderClass::Oto) if self.take_profit.is_none() && self.stop_loss.is_none() => {
return invalid("oto orders require either take_profit or stop_loss".to_owned());
}
Some(OrderClass::Oto) if self.take_profit.is_some() && self.stop_loss.is_some() => {
return invalid(
"oto orders take exactly one exit; use the bracket order class for both"
.to_owned(),
);
}
_ => {}
}
if self.order_class == Some(OrderClass::Mleg) {
if !matches!(self.order_type, OrderType::Market | OrderType::Limit) {
return invalid(
"mleg order class only supports market and limit orders".to_owned(),
);
}
if self.qty.is_none() {
return invalid("qty is required for the mleg order class".to_owned());
}
let legs = self.legs.as_deref().unwrap_or_default();
if legs.is_empty() {
return invalid("legs is required for the mleg order class".to_owned());
}
if legs.len() > 4 {
return invalid("at most 4 legs are allowed for the mleg order class".to_owned());
}
if legs.len() < 2 {
return invalid("at least 2 legs are required for the mleg order class".to_owned());
}
let mut symbols: Vec<&str> = legs.iter().map(|leg| leg.symbol.as_str()).collect();
symbols.sort_unstable();
symbols.dedup();
if symbols.len() != legs.len() {
return invalid("all legs must have unique symbols".to_owned());
}
} else {
if self.symbol.is_none() {
return invalid(
"symbol is required for all order classes other than mleg".to_owned(),
);
}
if self.side.is_none() {
return invalid(
"side is required for all order classes other than mleg".to_owned(),
);
}
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ReplaceOrderRequest {
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub qty: Option<Decimal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time_in_force: Option<TimeInForce>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub limit_price: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub stop_price: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub trail: Option<Decimal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_order_id: Option<String>,
}
impl ReplaceOrderRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn qty(mut self, qty: Decimal) -> Self {
self.qty = Some(qty);
self
}
#[must_use]
pub fn time_in_force(mut self, time_in_force: TimeInForce) -> Self {
self.time_in_force = Some(time_in_force);
self
}
#[must_use]
pub fn limit_price(mut self, limit_price: Decimal) -> Self {
self.limit_price = Some(limit_price);
self
}
#[must_use]
pub fn stop_price(mut self, stop_price: Decimal) -> Self {
self.stop_price = Some(stop_price);
self
}
#[must_use]
pub fn trail(mut self, trail: Decimal) -> Self {
self.trail = Some(trail);
self
}
#[must_use]
pub fn client_order_id(mut self, client_order_id: impl Into<String>) -> Self {
self.client_order_id = Some(client_order_id.into());
self
}
pub fn validate(&self) -> Result<()> {
for (name, value) in [
("qty", self.qty),
("stop_price", self.stop_price),
("trail", self.trail),
] {
if let Some(value) = value
&& value <= Decimal::ZERO
{
return Err(Error::InvalidRequest(format!(
"{name} must be greater than 0"
)));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ClosePositionRequest {
Qty(Decimal),
Percentage(Decimal),
}
impl ClosePositionRequest {
#[must_use]
pub fn to_query(self) -> Vec<(&'static str, String)> {
match self {
Self::Qty(qty) => vec![("qty", qty.to_string())],
Self::Percentage(percentage) => vec![("percentage", percentage.to_string())],
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetOrdersRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<QueryOrderStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub until: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub direction: Option<Sort>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nested: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub side: Option<OrderSide>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "crate::types::serde_util::comma_separated"
)]
pub symbols: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "crate::types::serde_util::comma_separated"
)]
pub asset_class: Option<Vec<AssetClass>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub before_order_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after_order_id: Option<Uuid>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub qty_above: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub qty_below: Option<Decimal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subtag: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetAccountActivitiesRequest {
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "crate::types::serde_util::comma_separated"
)]
pub activity_types: Option<Vec<ActivityType>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub category: Option<ActivityCategory>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub order_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub until: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub direction: Option<Sort>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_size: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_token: Option<String>,
}
impl GetAccountActivitiesRequest {
pub fn validate(&self) -> Result<()> {
if self.category.is_some() && self.activity_types.is_some() {
return Err(Error::InvalidRequest(
"activity_types and category cannot be combined".to_owned(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetOrderByIdRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nested: Option<bool>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetAssetsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<AssetStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asset_class: Option<AssetClass>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exchange: Option<AssetExchange>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attributes: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetCalendarRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end: Option<NaiveDate>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetPortfolioHistoryRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub period: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeframe: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub intraday_reporting: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pnl_reset: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date_end: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extended_hours: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cashflow_types: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateWatchlistRequest {
pub name: String,
pub symbols: Vec<String>,
}
impl CreateWatchlistRequest {
#[must_use]
pub fn new(name: impl Into<String>, symbols: Vec<String>) -> Self {
Self {
name: name.into(),
symbols,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UpdateWatchlistRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbols: Option<Vec<String>>,
}
impl UpdateWatchlistRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn symbols(mut self, symbols: Vec<String>) -> Self {
self.symbols = Some(symbols);
self
}
pub fn validate(&self) -> Result<()> {
if self.name.is_none() && self.symbols.is_none() {
return Err(Error::InvalidRequest(
"one of name or symbols must be defined".to_owned(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetCorporateAnnouncementsRequest {
pub ca_types: Vec<CorporateActionType>,
pub since: NaiveDate,
pub until: NaiveDate,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cusip: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date_type: Option<CorporateActionDateType>,
}
impl GetCorporateAnnouncementsRequest {
#[must_use]
pub fn new(ca_types: Vec<CorporateActionType>, since: NaiveDate, until: NaiveDate) -> Self {
Self {
ca_types,
since,
until,
symbol: None,
cusip: None,
date_type: None,
}
}
#[must_use]
pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
self.symbol = Some(symbol.into());
self
}
#[must_use]
pub fn cusip(mut self, cusip: impl Into<String>) -> Self {
self.cusip = Some(cusip.into());
self
}
#[must_use]
pub fn date_type(mut self, date_type: CorporateActionDateType) -> Self {
self.date_type = Some(date_type);
self
}
#[must_use]
pub fn to_query(&self) -> Vec<(&'static str, String)> {
let mut query: Vec<(&'static str, String)> = self
.ca_types
.iter()
.map(|ca_type| ("ca_types", ca_type.to_string()))
.collect();
query.push(("since", self.since.to_string()));
query.push(("until", self.until.to_string()));
if let Some(symbol) = &self.symbol {
query.push(("symbol", symbol.clone()));
}
if let Some(cusip) = &self.cusip {
query.push(("cusip", cusip.clone()));
}
if let Some(date_type) = &self.date_type {
query.push(("date_type", date_type.to_string()));
}
query
}
pub fn validate(&self) -> Result<()> {
if (self.until - self.since).num_days() > 90 {
return Err(Error::InvalidRequest(
"the date range between since and until must be no more than 90 days".to_owned(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetOptionContractsRequest {
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "crate::types::serde_util::comma_separated"
)]
pub underlying_symbols: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<AssetStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expiration_date: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expiration_date_gte: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expiration_date_lte: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root_symbol: Option<String>,
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub contract_type: Option<ContractType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style: Option<ExerciseStyle>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_deliverables: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ppind: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub strike_price_gte: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub strike_price_lte: Option<Decimal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_token: Option<String>,
}
impl GetOptionContractsRequest {
#[must_use]
pub fn new(underlying_symbols: Vec<String>) -> Self {
Self {
underlying_symbols: Some(underlying_symbols),
status: Some(AssetStatus::Active),
..Self::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn qty(value: i64) -> OrderAmount {
OrderAmount::Qty(Decimal::from(value))
}
fn market() -> OrderRequest {
OrderRequest::market("AAPL", OrderSide::Buy, qty(1), TimeInForce::Day)
}
#[test]
fn market_order_serializes_only_the_fields_that_are_set() {
let json = serde_json::to_value(market()).unwrap();
assert_eq!(
json,
serde_json::json!({
"symbol": "AAPL",
"qty": "1",
"side": "buy",
"type": "market",
"time_in_force": "day",
})
);
}
#[test]
fn notional_orders_send_notional_instead_of_qty() {
let order = OrderRequest::market(
"AAPL",
OrderSide::Buy,
OrderAmount::Notional(Decimal::new(15_050, 2)),
TimeInForce::Day,
);
let json = serde_json::to_value(&order).unwrap();
assert_eq!(json["notional"], "150.50");
assert!(json.get("qty").is_none());
}
#[test]
fn limit_order_carries_its_price() {
let order = OrderRequest::limit(
"AAPL",
OrderSide::Buy,
qty(1),
TimeInForce::Day,
Decimal::new(1835, 1),
);
assert_eq!(order.order_type, OrderType::Limit);
assert_eq!(
serde_json::to_value(&order).unwrap()["limit_price"],
"183.5"
);
}
#[test]
fn trailing_stop_sends_exactly_one_trail_field() {
let by_price = OrderRequest::trailing_stop(
"AAPL",
OrderSide::Sell,
qty(1),
TimeInForce::Day,
Trail::Price(Decimal::from(5)),
);
let by_percent = OrderRequest::trailing_stop(
"AAPL",
OrderSide::Sell,
qty(1),
TimeInForce::Day,
Trail::Percent(Decimal::from(2)),
);
let price_json = serde_json::to_value(&by_price).unwrap();
assert_eq!(price_json["trail_price"], "5");
assert!(price_json.get("trail_percent").is_none());
let percent_json = serde_json::to_value(&by_percent).unwrap();
assert_eq!(percent_json["trail_percent"], "2");
assert!(percent_json.get("trail_price").is_none());
}
#[test]
fn bracket_orders_require_both_exits() {
let take_profit = TakeProfitRequest::new(Decimal::from(200));
let stop_loss = StopLossRequest::new(Decimal::from(150));
assert!(
market()
.bracket(take_profit.clone(), stop_loss)
.validate()
.is_ok()
);
let mut missing_stop = market();
missing_stop.order_class = Some(OrderClass::Bracket);
missing_stop.take_profit = Some(take_profit);
assert!(missing_stop.validate().is_err());
}
#[test]
fn oto_orders_require_at_least_one_exit() {
assert!(
market()
.oto_take_profit(TakeProfitRequest::new(Decimal::from(200)))
.validate()
.is_ok()
);
assert!(
market()
.oto_stop_loss(StopLossRequest::new(Decimal::from(150)))
.validate()
.is_ok()
);
let mut neither = market();
neither.order_class = Some(OrderClass::Oto);
assert!(neither.validate().is_err());
}
#[test]
fn multi_leg_orders_need_between_two_and_four_unique_legs() {
let leg = |symbol: &str| OptionLegRequest::new(symbol, Decimal::from(1), OrderSide::Buy);
let valid = OrderRequest::multi_leg(
Decimal::from(1),
TimeInForce::Day,
vec![leg("AAPL240119C00150000"), leg("AAPL240119P00150000")],
None,
);
assert!(valid.validate().is_ok());
let too_few =
OrderRequest::multi_leg(Decimal::from(1), TimeInForce::Day, vec![leg("A")], None);
assert!(too_few.validate().is_err());
let too_many = OrderRequest::multi_leg(
Decimal::from(1),
TimeInForce::Day,
vec![leg("A"), leg("B"), leg("C"), leg("D"), leg("E")],
None,
);
assert!(too_many.validate().is_err());
let duplicate_symbols = OrderRequest::multi_leg(
Decimal::from(1),
TimeInForce::Day,
vec![leg("A"), leg("A")],
None,
);
assert!(duplicate_symbols.validate().is_err());
}
#[test]
fn multi_leg_orders_reject_unsupported_order_types() {
let mut order = OrderRequest::multi_leg(
Decimal::from(1),
TimeInForce::Day,
vec![
OptionLegRequest::new("A", Decimal::from(1), OrderSide::Buy),
OptionLegRequest::new("B", Decimal::from(1), OrderSide::Sell),
],
None,
);
order.order_type = OrderType::Stop;
assert!(order.validate().is_err());
}
#[test]
fn multi_leg_orders_omit_the_top_level_symbol_and_side() {
let order = OrderRequest::multi_leg(
Decimal::from(1),
TimeInForce::Day,
vec![
OptionLegRequest::new("A", Decimal::from(1), OrderSide::Buy),
OptionLegRequest::new("B", Decimal::from(1), OrderSide::Sell),
],
None,
);
let json = serde_json::to_value(&order).unwrap();
assert!(json.get("symbol").is_none());
assert!(json.get("side").is_none());
assert_eq!(json["order_class"], "mleg");
assert_eq!(json["legs"].as_array().unwrap().len(), 2);
}
#[test]
fn single_leg_orders_require_a_symbol_and_side() {
let mut order = market();
order.symbol = None;
assert!(order.validate().is_err());
let mut order = market();
order.side = None;
assert!(order.validate().is_err());
}
#[test]
fn replace_order_rejects_non_positive_values() {
assert!(
ReplaceOrderRequest::new()
.qty(Decimal::from(1))
.validate()
.is_ok()
);
assert!(
ReplaceOrderRequest::new()
.qty(Decimal::ZERO)
.validate()
.is_err()
);
assert!(
ReplaceOrderRequest::new()
.trail(Decimal::from(-1))
.validate()
.is_err()
);
}
#[test]
fn watchlist_update_requires_at_least_one_field() {
assert!(UpdateWatchlistRequest::new().validate().is_err());
assert!(UpdateWatchlistRequest::new().name("x").validate().is_ok());
assert!(
UpdateWatchlistRequest::new()
.symbols(vec!["AAPL".to_owned()])
.validate()
.is_ok()
);
}
#[test]
fn close_position_renders_one_query_parameter() {
assert_eq!(
ClosePositionRequest::Qty(Decimal::new(15, 1)).to_query(),
vec![("qty", "1.5".to_owned())]
);
assert_eq!(
ClosePositionRequest::Percentage(Decimal::from(50)).to_query(),
vec![("percentage", "50".to_owned())]
);
}
#[test]
fn corporate_announcement_window_is_capped_at_ninety_days() {
let since = NaiveDate::from_ymd_opt(2022, 1, 1).unwrap();
let ok = GetCorporateAnnouncementsRequest::new(
vec![CorporateActionType::Dividend],
since,
NaiveDate::from_ymd_opt(2022, 3, 1).unwrap(),
);
assert!(ok.validate().is_ok());
let too_wide = GetCorporateAnnouncementsRequest::new(
vec![CorporateActionType::Dividend],
since,
NaiveDate::from_ymd_opt(2022, 12, 1).unwrap(),
);
assert!(too_wide.validate().is_err());
}
#[test]
fn option_contracts_request_defaults_to_active() {
let request = GetOptionContractsRequest::new(vec!["AAPL".to_owned()]);
assert_eq!(request.status, Some(AssetStatus::Active));
}
}