use super::triggers::{RithmicIfTouchedTrigger, TrailingStop};
use super::validate_instrument;
use crate::{
error::RithmicError,
types::{ManualOrAutoEntry, OrderSide, OrderType, TimeInForce},
};
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
#[must_use = "an order does nothing until passed to a plant handle"]
pub struct RithmicOrder {
pub symbol: String,
pub exchange: String,
pub quantity: i32,
pub price: Option<f64>,
pub transaction_type: OrderSide,
pub price_type: OrderType,
pub user_tag: String,
pub duration: TimeInForce,
pub trigger_price: Option<f64>,
pub trailing_stop: Option<TrailingStop>,
pub trade_route: Option<String>,
pub manual_or_auto: ManualOrAutoEntry,
pub window_name: Option<String>,
pub release_at_ssboe: Option<i32>,
pub release_at_usecs: Option<i32>,
pub cancel_at_ssboe: Option<i32>,
pub cancel_at_usecs: Option<i32>,
pub cancel_after_secs: Option<i32>,
pub if_touched: Option<RithmicIfTouchedTrigger>,
}
impl RithmicOrder {
pub fn new() -> Self {
Self::default()
}
pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
self.symbol = symbol.into();
self
}
pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
self.exchange = exchange.into();
self
}
pub fn quantity(mut self, quantity: i32) -> Self {
self.quantity = quantity;
self
}
pub fn transaction_type(mut self, transaction_type: OrderSide) -> Self {
self.transaction_type = transaction_type;
self
}
pub fn price_type(mut self, price_type: OrderType) -> Self {
self.price_type = price_type;
self
}
pub fn price(mut self, price: f64) -> Self {
self.price = Some(price);
self
}
pub fn trigger_price(mut self, trigger_price: f64) -> Self {
self.trigger_price = Some(trigger_price);
self
}
pub fn user_tag(mut self, user_tag: impl Into<String>) -> Self {
self.user_tag = user_tag.into();
self
}
pub fn duration(mut self, duration: TimeInForce) -> Self {
self.duration = duration;
self
}
pub fn trailing_stop(mut self, trailing_stop: TrailingStop) -> Self {
self.trailing_stop = Some(trailing_stop);
self
}
pub fn trailing_stop_by(self, trail_by_ticks: i32, trail_by_price_id: i32) -> Self {
self.trailing_stop(
TrailingStop::new()
.trail_by_ticks(trail_by_ticks)
.trail_by_price_id(trail_by_price_id),
)
}
pub fn trade_route(mut self, trade_route: impl Into<String>) -> Self {
self.trade_route = Some(trade_route.into());
self
}
pub fn manual_or_auto(mut self, manual_or_auto: ManualOrAutoEntry) -> Self {
self.manual_or_auto = manual_or_auto;
self
}
pub fn window_name(mut self, window_name: impl Into<String>) -> Self {
self.window_name = Some(window_name.into());
self
}
pub fn release_at_ssboe(mut self, ssboe: i32) -> Self {
self.release_at_ssboe = Some(ssboe);
self
}
pub fn release_at_usecs(mut self, usecs: i32) -> Self {
self.release_at_usecs = Some(usecs);
self
}
pub fn release_at(self, ssboe: i32, usecs: i32) -> Self {
self.release_at_ssboe(ssboe).release_at_usecs(usecs)
}
pub fn cancel_at_ssboe(mut self, ssboe: i32) -> Self {
self.cancel_at_ssboe = Some(ssboe);
self
}
pub fn cancel_at_usecs(mut self, usecs: i32) -> Self {
self.cancel_at_usecs = Some(usecs);
self
}
pub fn cancel_at(self, ssboe: i32, usecs: i32) -> Self {
self.cancel_at_ssboe(ssboe).cancel_at_usecs(usecs)
}
pub fn cancel_after_secs(mut self, secs: i32) -> Self {
self.cancel_after_secs = Some(secs);
self
}
pub fn if_touched(mut self, if_touched: RithmicIfTouchedTrigger) -> Self {
self.if_touched = Some(if_touched);
self
}
pub fn validate(&self) -> Result<(), RithmicError> {
validate_instrument(&self.symbol, &self.exchange, self.quantity)?;
super::require_prices(self.price_type, self.price, self.trigger_price)
}
pub fn build(self) -> Result<Self, RithmicError> {
self.validate()?;
Ok(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn order() -> RithmicOrder {
RithmicOrder::new()
.symbol("ESH6")
.exchange("CME")
.quantity(1)
.transaction_type(OrderSide::Buy)
.price_type(OrderType::Limit)
}
#[test]
fn a_market_order_validates_without_a_price() {
let order = RithmicOrder {
price_type: OrderType::Market,
..order()
};
assert!(order.validate().is_ok());
}
#[test]
fn a_limit_order_needs_a_price() {
let mut order = RithmicOrder {
price_type: OrderType::Limit,
..order()
};
let err = order.validate().unwrap_err().to_string();
assert!(err.contains("price is required"), "{err}");
order.price = Some(5000.0);
assert!(order.validate().is_ok());
}
#[test]
fn a_stop_market_order_needs_a_trigger_but_no_price() {
let mut order = RithmicOrder {
price_type: OrderType::StopMarket,
..order()
};
let err = order.validate().unwrap_err().to_string();
assert!(err.contains("trigger_price is required"), "{err}");
order.trigger_price = Some(4985.0);
assert!(order.validate().is_ok());
}
#[test]
fn a_stop_limit_order_needs_both() {
let mut order = RithmicOrder {
price_type: OrderType::StopLimit,
price: Some(4980.0),
..order()
};
assert!(order.validate().is_err());
order.trigger_price = Some(4985.0);
assert!(order.validate().is_ok());
}
#[test]
fn the_error_names_the_order_type() {
let order = RithmicOrder {
price_type: OrderType::LimitIfTouched,
..order()
};
let err = order.validate().unwrap_err().to_string();
assert!(err.contains("LIMIT_IF_TOUCHED"), "{err}");
}
#[test]
fn the_paired_setters_assign_their_arguments_in_order() {
let order = order()
.price(4980.0)
.trailing_stop_by(20, 1)
.release_at(35900, 500)
.cancel_at(36000, 250)
.build()
.unwrap();
let trailing = order.trailing_stop.unwrap();
assert_eq!(trailing.trail_by_ticks, 20);
assert_eq!(trailing.trail_by_price_id, 1);
assert_eq!(order.release_at_ssboe, Some(35900));
assert_eq!(order.release_at_usecs, Some(500));
assert_eq!(order.cancel_at_ssboe, Some(36000));
assert_eq!(order.cancel_at_usecs, Some(250));
}
#[test]
fn an_order_requires_its_identity() {
assert!(order().symbol("").price(5000.0).build().is_err());
assert!(order().exchange("").price(5000.0).build().is_err());
assert!(order().quantity(0).price(5000.0).build().is_err());
assert!(order().price(5000.0).build().is_ok());
}
}