use crate::accounts::AccountNumber;
use crate::types::instrument::InstrumentType;
use crate::types::wire::wire_enum;
use chrono::{DateTime, FixedOffset, NaiveDate};
use derive_builder::Builder;
use pretty_simple_display::{DebugPretty, DisplaySimple};
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum PriceEffect {
Debit,
Credit,
None,
}
impl fmt::Display for PriceEffect {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PriceEffect::Debit => write!(f, "Debit"),
PriceEffect::Credit => write!(f, "Credit"),
PriceEffect::None => write!(f, "None"),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum Action {
#[serde(rename = "Buy to Open")]
BuyToOpen,
#[serde(rename = "Sell to Open")]
SellToOpen,
#[serde(rename = "Buy to Close")]
BuyToClose,
#[serde(rename = "Sell to Close")]
SellToClose,
Sell,
Buy,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum OrderType {
Limit,
Market,
#[serde(rename = "Marketable Limit")]
MarketableLimit,
Stop,
#[serde(rename = "Stop Limit")]
StopLimit,
#[serde(rename = "Notional Market")]
NotionalMarket,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum TimeInForce {
#[serde(rename = "Day")]
Day,
#[serde(rename = "GTC")]
Gtc,
#[serde(rename = "GTD")]
Gtd,
#[serde(rename = "Ext")]
Ext,
#[serde(rename = "GTC Ext")]
GTCExt,
#[serde(rename = "IOC")]
Ioc,
}
wire_enum! {
OrderStatus {
Received => "Received",
Routed => "Routed",
InFlight => "In Flight",
Live => "Live",
CancelRequested => "Cancel Requested",
ReplaceRequested => "Replace Requested",
Contingent => "Contingent",
Filled => "Filled",
Cancelled => "Cancelled",
Expired => "Expired",
Rejected => "Rejected",
Removed => "Removed",
PartiallyRemoved => "Partially Removed",
}
}
impl OrderStatus {
pub fn is_terminal(&self) -> bool {
matches!(
self,
OrderStatus::Filled
| OrderStatus::Cancelled
| OrderStatus::Expired
| OrderStatus::Rejected
| OrderStatus::Removed
)
}
}
#[derive(
DebugPretty, DisplaySimple, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
#[serde(transparent)]
pub struct Symbol(pub String);
impl<T: AsRef<str>> From<T> for Symbol {
fn from(value: T) -> Self {
Self(value.as_ref().to_owned())
}
}
pub trait AsSymbol {
fn as_symbol(&self) -> Symbol;
}
impl<T: AsRef<str>> AsSymbol for T {
fn as_symbol(&self) -> Symbol {
Symbol(self.as_ref().to_owned())
}
}
impl AsSymbol for Symbol {
fn as_symbol(&self) -> Symbol {
self.clone()
}
}
impl AsSymbol for &Symbol {
fn as_symbol(&self) -> Symbol {
(*self).clone()
}
}
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(transparent)]
pub struct OrderId(pub u64);
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LiveOrderRecord {
pub id: OrderId,
pub account_number: AccountNumber,
pub time_in_force: TimeInForce,
pub order_type: OrderType,
#[serde(with = "crate::types::wire::decimal")]
pub size: Decimal,
pub underlying_symbol: Symbol,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub price: Option<Decimal>,
#[serde(default)]
pub price_effect: Option<PriceEffect>,
pub status: OrderStatus,
pub cancellable: bool,
pub editable: bool,
pub edited: bool,
#[serde(default)]
pub legs: Vec<LiveOrderLeg>,
#[serde(default)]
pub underlying_instrument_type: Option<InstrumentType>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub value: Option<Decimal>,
#[serde(default)]
pub value_effect: Option<PriceEffect>,
#[serde(default, with = "crate::types::wire::loose_string_option")]
pub leg_count: Option<String>,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub reject_reason: Option<String>,
#[serde(default)]
pub contingent_status: Option<String>,
#[serde(default)]
pub stop_trigger: Option<String>,
#[serde(default, with = "crate::types::wire::date_option")]
pub gtc_date: Option<NaiveDate>,
#[serde(default)]
pub complex_order_id: Option<String>,
#[serde(default)]
pub complex_order_tag: Option<String>,
#[serde(default)]
pub replaces_order_id: Option<String>,
#[serde(default)]
pub replacing_order_id: Option<String>,
#[serde(default)]
pub external_identifier: Option<String>,
#[serde(default)]
pub global_request_id: Option<String>,
#[serde(default)]
pub preflight_id: Option<String>,
#[serde(default, with = "crate::types::wire::loose_string_option")]
pub user_id: Option<String>,
#[serde(default)]
pub username: Option<String>,
#[serde(default, with = "crate::types::wire::loose_string_option")]
pub cancel_user_id: Option<String>,
#[serde(default)]
pub cancel_username: Option<String>,
#[serde(default, with = "crate::types::wire::datetime_option")]
pub received_at: Option<DateTime<FixedOffset>>,
#[serde(default, with = "crate::types::wire::datetime_option")]
pub in_flight_at: Option<DateTime<FixedOffset>>,
#[serde(default, with = "crate::types::wire::datetime_option")]
pub live_at: Option<DateTime<FixedOffset>>,
#[serde(default, with = "crate::types::wire::datetime_option")]
pub terminal_at: Option<DateTime<FixedOffset>>,
#[serde(default, with = "crate::types::wire::datetime_option")]
pub cancelled_at: Option<DateTime<FixedOffset>>,
#[serde(default, with = "crate::types::wire::loose_string_option")]
pub updated_at: Option<String>,
}
#[allow(dead_code)]
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LiveOrderLeg {
pub instrument_type: InstrumentType,
pub symbol: Symbol,
#[serde(with = "crate::types::wire::decimal")]
pub quantity: Decimal,
#[serde(with = "crate::types::wire::decimal")]
pub remaining_quantity: Decimal,
pub action: Action,
#[serde(default)]
pub fills: Vec<OrderFill>,
}
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct OrderFill {
#[serde(default, with = "crate::types::wire::decimal_option")]
pub quantity: Option<Decimal>,
#[serde(default, with = "crate::types::wire::decimal_option")]
pub fill_price: Option<Decimal>,
#[serde(default, with = "crate::types::wire::datetime_option")]
pub filled_at: Option<DateTime<FixedOffset>>,
#[serde(default)]
pub destination_venue: Option<String>,
#[serde(default)]
pub fill_id: Option<String>,
#[serde(default)]
pub ext_exec_id: Option<String>,
#[serde(default)]
pub ext_group_fill_id: Option<String>,
}
#[derive(Builder, Serialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
#[builder(setter(into), build_fn(validate = "OrderBuilder::validate_order"))]
pub struct Order {
time_in_force: TimeInForce,
order_type: OrderType,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
price: Decimal,
price_effect: PriceEffect,
legs: Vec<OrderLeg>,
}
#[derive(Builder, Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
#[builder(setter(into), build_fn(validate = "OrderLegBuilder::validate_leg"))]
pub struct OrderLeg {
instrument_type: InstrumentType,
symbol: Symbol,
#[serde(with = "crate::types::wire::decimal")]
quantity: Decimal,
action: Action,
}
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct OrderPlacedResult {
pub order: LiveOrderRecord,
pub warnings: Vec<Warning>,
pub buying_power_effect: BuyingPowerEffect,
pub fee_calculation: FeeCalculation,
}
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DryRunResult {
pub order: DryRunRecord,
pub warnings: Vec<Warning>,
pub buying_power_effect: BuyingPowerEffect,
pub fee_calculation: FeeCalculation,
}
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DryRunRecord {
pub account_number: AccountNumber,
pub time_in_force: TimeInForce,
pub order_type: OrderType,
#[serde(with = "crate::types::wire::decimal")]
pub size: Decimal,
pub underlying_symbol: Symbol,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub price: Decimal,
pub price_effect: PriceEffect,
pub status: OrderStatus,
pub cancellable: bool,
pub editable: bool,
pub edited: bool,
pub legs: Vec<OrderLeg>,
}
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct BuyingPowerEffect {
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub change_in_margin_requirement: Decimal,
pub change_in_margin_requirement_effect: PriceEffect,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub change_in_buying_power: Decimal,
pub change_in_buying_power_effect: PriceEffect,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub current_buying_power: Decimal,
pub current_buying_power_effect: PriceEffect,
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub impact: Decimal,
pub effect: PriceEffect,
}
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct FeeCalculation {
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
pub total_fees: Decimal,
pub total_fees_effect: PriceEffect,
}
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Warning {
pub code: Option<String>,
#[serde(default)]
pub message: String,
#[serde(flatten)]
pub details: std::collections::BTreeMap<String, serde_json::Value>,
}
impl Warning {
pub fn has_details(&self) -> bool {
!self.details.is_empty()
}
}
#[cfg(test)]
mod warning_tests {
use super::*;
const WARNING: &str = r#"{
"code": "tif_next_valid_sesssion",
"message": "Your order will be placed at the next valid session.",
"preflight-id": "9f3c",
"buying-power-required": "1250.00"
}"#;
#[test]
fn a_warning_keeps_its_code_and_message() {
let warning: Warning = serde_json::from_str(WARNING).expect("warnings must parse");
assert_eq!(warning.code.as_deref(), Some("tif_next_valid_sesssion"));
assert_eq!(
warning.message,
"Your order will be placed at the next valid session."
);
}
#[test]
fn undocumented_keys_are_preserved_rather_than_discarded() {
let warning: Warning = serde_json::from_str(WARNING).expect("warnings must parse");
assert!(warning.has_details());
assert_eq!(
warning.details.get("preflight-id").and_then(|v| v.as_str()),
Some("9f3c")
);
assert_eq!(
warning
.details
.get("buying-power-required")
.and_then(|v| v.as_str()),
Some("1250.00")
);
}
#[test]
fn a_warning_without_a_message_is_not_fatal() {
let warning: Warning =
serde_json::from_str(r#"{"code":"odd","note":"venue changed shape"}"#)
.expect("a missing message must not fail the dry run");
assert_eq!(warning.code.as_deref(), Some("odd"));
assert!(warning.message.is_empty());
assert_eq!(
warning.details.get("note").and_then(|v| v.as_str()),
Some("venue changed shape")
);
}
#[test]
fn a_dry_run_response_surfaces_its_warnings() {
let body = format!(r#"{{"warnings":[{WARNING}]}}"#);
#[derive(serde::Deserialize)]
struct JustWarnings {
warnings: Vec<Warning>,
}
let parsed: JustWarnings = serde_json::from_str(&body).expect("the list must parse");
assert_eq!(parsed.warnings.len(), 1);
assert!(
!parsed.warnings[0].message.is_empty(),
"a caller must be able to read the warning before risking money"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::Decimal;
use std::str::FromStr;
#[test]
fn test_price_effect_display() {
assert_eq!(format!("{}", PriceEffect::Debit), "Debit");
assert_eq!(format!("{}", PriceEffect::Credit), "Credit");
assert_eq!(format!("{}", PriceEffect::None), "None");
}
#[test]
fn test_order_status_display() {
assert_eq!(format!("{}", OrderStatus::Received), "Received");
assert_eq!(format!("{}", OrderStatus::Live), "Live");
assert_eq!(format!("{}", OrderStatus::Filled), "Filled");
assert_eq!(format!("{}", OrderStatus::Cancelled), "Cancelled");
assert_eq!(format!("{}", OrderStatus::InFlight), "In Flight");
assert_eq!(
format!("{}", OrderStatus::CancelRequested),
"Cancel Requested"
);
assert_eq!(
format!("{}", OrderStatus::ReplaceRequested),
"Replace Requested"
);
assert_eq!(
format!("{}", OrderStatus::PartiallyRemoved),
"Partially Removed"
);
}
#[test]
fn test_symbol_from_string() {
let symbol = Symbol::from("AAPL");
assert_eq!(symbol.0, "AAPL");
let symbol = Symbol::from(String::from("MSFT"));
assert_eq!(symbol.0, "MSFT");
}
#[test]
fn test_symbol_as_symbol_trait() {
let symbol_str = "TSLA";
let symbol = symbol_str.as_symbol();
assert_eq!(symbol.0, "TSLA");
let symbol_string = String::from("GOOGL");
let symbol = symbol_string.as_symbol();
assert_eq!(symbol.0, "GOOGL");
let symbol_obj = Symbol::from("NVDA");
let symbol = symbol_obj.as_symbol();
assert_eq!(symbol.0, "NVDA");
let symbol_ref = &Symbol::from("AMD");
let symbol = symbol_ref.as_symbol();
assert_eq!(symbol.0, "AMD");
}
#[test]
fn test_order_id() {
let order_id = OrderId(12345);
assert_eq!(order_id.0, 12345);
}
#[test]
fn test_order_builder() {
let leg = OrderLegBuilder::default()
.instrument_type(InstrumentType::Equity)
.symbol("AAPL")
.quantity(Decimal::from(1))
.action(Action::BuyToOpen)
.build()
.unwrap();
let order = OrderBuilder::default()
.time_in_force(TimeInForce::Day)
.order_type(OrderType::Limit)
.price(Decimal::from_str("150.50").unwrap())
.price_effect(PriceEffect::Debit)
.legs(vec![leg])
.build()
.unwrap();
let serialized = serde_json::to_string(&order).unwrap();
assert!(serialized.contains("Day"));
assert!(serialized.contains("Limit"));
assert!(serialized.contains("150.50"));
assert!(serialized.contains("Debit"));
}
#[test]
fn test_order_leg_builder() {
let order_leg = OrderLegBuilder::default()
.instrument_type(InstrumentType::Equity)
.symbol(Symbol::from("AAPL"))
.quantity(Decimal::from(100))
.action(Action::Buy)
.build()
.unwrap();
let serialized = serde_json::to_string(&order_leg).unwrap();
assert!(serialized.contains("Equity"));
assert!(serialized.contains("AAPL"));
assert!(serialized.contains("100"));
assert!(serialized.contains("Buy"));
}
#[test]
fn test_enum_serialization() {
let action = Action::BuyToOpen;
let serialized = serde_json::to_string(&action).unwrap();
assert_eq!(serialized, "\"Buy to Open\"");
let action = Action::SellToClose;
let serialized = serde_json::to_string(&action).unwrap();
assert_eq!(serialized, "\"Sell to Close\"");
let order_type = OrderType::MarketableLimit;
let serialized = serde_json::to_string(&order_type).unwrap();
assert_eq!(serialized, "\"Marketable Limit\"");
let order_type = OrderType::StopLimit;
let serialized = serde_json::to_string(&order_type).unwrap();
assert_eq!(serialized, "\"Stop Limit\"");
let tif = TimeInForce::Gtc;
let serialized = serde_json::to_string(&tif).unwrap();
assert_eq!(serialized, "\"GTC\"");
let tif = TimeInForce::GTCExt;
let serialized = serde_json::to_string(&tif).unwrap();
assert_eq!(serialized, "\"GTC Ext\"");
}
#[test]
fn test_enum_deserialization() {
let action: Action = serde_json::from_str("\"Buy to Open\"").unwrap();
matches!(action, Action::BuyToOpen);
let action: Action = serde_json::from_str("\"Sell to Close\"").unwrap();
matches!(action, Action::SellToClose);
let status: OrderStatus = serde_json::from_str("\"In Flight\"").unwrap();
matches!(status, OrderStatus::InFlight);
let status: OrderStatus = serde_json::from_str("\"Cancel Requested\"").unwrap();
matches!(status, OrderStatus::CancelRequested);
}
#[test]
fn test_symbol_clone_and_eq() {
let symbol1 = Symbol::from("AAPL");
let symbol2 = symbol1.clone();
assert_eq!(symbol1, symbol2);
let symbol3 = Symbol::from("MSFT");
assert_ne!(symbol1, symbol3);
}
#[test]
fn test_symbol_ordering() {
let symbol1 = Symbol::from("AAPL");
let symbol2 = Symbol::from("MSFT");
let symbol3 = Symbol::from("AAPL");
assert!(symbol1 < symbol2);
assert!(symbol1 <= symbol3);
assert!(symbol2 > symbol1);
assert_eq!(symbol1, symbol3);
}
#[test]
fn test_price_effect_copies_and_compares() {
let effect1 = PriceEffect::Debit;
let effect2 = effect1;
assert_eq!(effect1, effect2);
assert_ne!(effect1, PriceEffect::Credit);
}
#[test]
fn test_all_enum_variants_exist() {
let _actions = [
Action::BuyToOpen,
Action::SellToOpen,
Action::BuyToClose,
Action::SellToClose,
Action::Sell,
Action::Buy,
];
let _order_types = [
OrderType::Limit,
OrderType::Market,
OrderType::MarketableLimit,
OrderType::Stop,
OrderType::StopLimit,
OrderType::NotionalMarket,
];
let _time_in_forces = [
TimeInForce::Day,
TimeInForce::Gtc,
TimeInForce::Gtd,
TimeInForce::Ext,
TimeInForce::GTCExt,
TimeInForce::Ioc,
];
let _statuses = [
OrderStatus::Received,
OrderStatus::Routed,
OrderStatus::InFlight,
OrderStatus::Live,
OrderStatus::CancelRequested,
OrderStatus::ReplaceRequested,
OrderStatus::Contingent,
OrderStatus::Filled,
OrderStatus::Cancelled,
OrderStatus::Expired,
OrderStatus::Rejected,
OrderStatus::Removed,
OrderStatus::PartiallyRemoved,
];
}
}
impl Order {
pub fn legs(&self) -> &[OrderLeg] {
&self.legs
}
}
impl OrderLeg {
pub fn instrument_type(&self) -> &InstrumentType {
&self.instrument_type
}
pub fn symbol(&self) -> &Symbol {
&self.symbol
}
pub fn quantity(&self) -> Decimal {
self.quantity
}
pub fn action(&self) -> Action {
self.action
}
}
impl OrderLegBuilder {
fn validate_leg(&self) -> Result<(), String> {
if let Some(quantity) = self.quantity
&& quantity <= Decimal::ZERO
{
return Err(format!(
"order leg quantity must be greater than zero, got {quantity}; \
use the action field to express direction"
));
}
if let Some(symbol) = &self.symbol
&& symbol.0.trim().is_empty()
{
return Err("order leg symbol must not be empty".to_string());
}
Ok(())
}
}
impl OrderBuilder {
fn validate_order(&self) -> Result<(), String> {
if let Some(legs) = &self.legs
&& legs.is_empty()
{
return Err("an order must have at least one leg".to_string());
}
let Some(order_type) = &self.order_type else {
return Ok(());
};
let needs_positive_price = match order_type {
OrderType::Limit | OrderType::StopLimit | OrderType::MarketableLimit => true,
OrderType::Stop => true,
OrderType::NotionalMarket => true,
OrderType::Market => false,
};
if let Some(price) = self.price {
if needs_positive_price && price <= Decimal::ZERO {
return Err(format!(
"a {order_type:?} order needs a price greater than zero, got {price}"
));
}
if !needs_positive_price && price != Decimal::ZERO {
return Err(format!(
"a {order_type:?} order carries no price, so price must be zero, got \
{price}; use Limit to bound the fill"
));
}
}
Ok(())
}
}
#[cfg(test)]
mod builder_validation_tests {
use super::*;
use std::str::FromStr;
fn leg() -> OrderLeg {
OrderLegBuilder::default()
.instrument_type(InstrumentType::Equity)
.symbol("AAPL")
.quantity(Decimal::from(1))
.action(Action::BuyToOpen)
.build()
.expect("a one-share buy is valid")
}
#[test]
fn a_leg_needs_a_positive_quantity() {
for quantity in ["0", "-1", "-0.5"] {
let error = OrderLegBuilder::default()
.instrument_type(InstrumentType::Equity)
.symbol("AAPL")
.quantity(Decimal::from_str(quantity).unwrap())
.action(Action::BuyToOpen)
.build()
.expect_err("a non-positive quantity must not build");
assert!(
error.to_string().contains("greater than zero"),
"the error should say what is wrong: {error}"
);
}
}
#[test]
fn a_fractional_quantity_is_allowed() {
OrderLegBuilder::default()
.instrument_type(InstrumentType::Cryptocurrency)
.symbol("BTC/USD")
.quantity(Decimal::from_str("0.0001").unwrap())
.action(Action::BuyToOpen)
.build()
.expect("a fractional crypto quantity is valid");
}
#[test]
fn a_leg_needs_a_symbol() {
let error = OrderLegBuilder::default()
.instrument_type(InstrumentType::Equity)
.symbol(" ")
.quantity(Decimal::from(1))
.action(Action::BuyToOpen)
.build()
.expect_err("a blank symbol must not build");
assert!(error.to_string().contains("symbol"), "{error}");
}
#[test]
fn an_order_needs_at_least_one_leg() {
let error = OrderBuilder::default()
.time_in_force(TimeInForce::Day)
.order_type(OrderType::Market)
.price(Decimal::ZERO)
.price_effect(PriceEffect::None)
.legs(Vec::<OrderLeg>::new())
.build()
.expect_err("an order with no legs does nothing");
assert!(error.to_string().contains("at least one leg"), "{error}");
}
#[test]
fn a_limit_order_needs_a_working_price() {
let error = OrderBuilder::default()
.time_in_force(TimeInForce::Day)
.order_type(OrderType::Limit)
.price(Decimal::ZERO)
.price_effect(PriceEffect::Debit)
.legs(vec![leg()])
.build()
.expect_err("a limit order at zero is not a price");
assert!(error.to_string().contains("greater than zero"), "{error}");
}
#[test]
fn a_market_order_takes_no_price() {
let error = OrderBuilder::default()
.time_in_force(TimeInForce::Day)
.order_type(OrderType::Market)
.price(Decimal::from(100))
.price_effect(PriceEffect::Debit)
.legs(vec![leg()])
.build()
.expect_err("a market order with a price must not build");
assert!(error.to_string().contains("price must be zero"), "{error}");
}
#[test]
fn a_well_formed_order_still_builds() {
OrderBuilder::default()
.time_in_force(TimeInForce::Day)
.order_type(OrderType::Limit)
.price(Decimal::from_str("1.25").unwrap())
.price_effect(PriceEffect::Debit)
.legs(vec![leg()])
.build()
.expect("a limit order with a price and a leg is valid");
}
}
#[cfg(test)]
mod order_type_price_tests {
use super::*;
use std::str::FromStr;
fn leg() -> OrderLeg {
OrderLegBuilder::default()
.instrument_type(InstrumentType::Equity)
.symbol("AAPL")
.quantity(Decimal::from(1))
.action(Action::BuyToOpen)
.build()
.expect("a one-share buy is valid")
}
fn build_with(order_type: OrderType, price: &str) -> Result<Order, OrderBuilderError> {
OrderBuilder::default()
.time_in_force(TimeInForce::Day)
.order_type(order_type)
.price(Decimal::from_str(price).unwrap())
.price_effect(PriceEffect::Debit)
.legs(vec![leg()])
.build()
}
#[test]
fn every_priced_order_type_rejects_a_non_positive_price() {
for order_type in [
OrderType::Limit,
OrderType::StopLimit,
OrderType::MarketableLimit,
OrderType::Stop,
OrderType::NotionalMarket,
] {
for price in ["0", "-1"] {
let error =
build_with(order_type, price).expect_err("a non-positive price must not build");
assert!(
error.to_string().contains("greater than zero"),
"{order_type:?} at {price} should be rejected: {error}"
);
}
build_with(order_type, "1.25")
.unwrap_or_else(|e| panic!("{order_type:?} at 1.25 should build: {e}"));
}
}
#[test]
fn a_market_order_is_the_only_one_that_takes_no_price() {
build_with(OrderType::Market, "0").expect("a market order with no price builds");
let error =
build_with(OrderType::Market, "100").expect_err("a market order with a price must not");
assert!(error.to_string().contains("price must be zero"), "{error}");
}
}
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct OrderAmendment {
pub order_type: OrderType,
pub time_in_force: TimeInForce,
#[serde(with = "crate::types::wire::decimal")]
pub stop_trigger: Decimal,
pub price_effect: PriceEffect,
pub value_effect: PriceEffect,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::wire::decimal_option"
)]
pub price: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::wire::decimal_option"
)]
pub value: Option<Decimal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::wire::date_option"
)]
pub gtc_date: Option<NaiveDate>,
}
impl OrderAmendment {
pub fn new(
order_type: OrderType,
time_in_force: TimeInForce,
stop_trigger: Decimal,
price_effect: PriceEffect,
value_effect: PriceEffect,
) -> Self {
Self {
order_type,
time_in_force,
stop_trigger,
price_effect,
value_effect,
price: None,
value: None,
gtc_date: None,
}
}
#[must_use]
pub fn with_price(mut self, price: Decimal) -> Self {
self.price = Some(price);
self
}
#[must_use]
pub fn with_value(mut self, value: Decimal) -> Self {
self.value = Some(value);
self
}
#[must_use]
pub fn with_gtc_date(mut self, gtc_date: NaiveDate) -> Self {
self.gtc_date = Some(gtc_date);
self
}
pub(crate) fn validate(&self) -> crate::TastyResult<()> {
let is_gtd = matches!(self.time_in_force, TimeInForce::Gtd);
if self.gtc_date.is_some() && !is_gtd {
return Err(crate::TastyTradeError::Precondition(format!(
"a good-til-date expiry only applies to a GTD order, and this one \
is {:?}",
self.time_in_force
)));
}
if is_gtd && self.gtc_date.is_none() {
return Err(crate::TastyTradeError::Precondition(
"a GTD order needs the date it expires on".to_string(),
));
}
if matches!(self.order_type, OrderType::Limit | OrderType::StopLimit)
&& self.price.is_none()
{
return Err(crate::TastyTradeError::Precondition(format!(
"a {:?} order needs a price",
self.order_type
)));
}
Ok(())
}
}