use crate::{
error::RithmicError,
types::{OrderCondition, OrderPriceField},
};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
#[must_use = "a trailing stop does nothing until attached to an order"]
pub struct TrailingStop {
pub trail_by_ticks: i32,
pub trail_by_price_id: i32,
}
impl TrailingStop {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
trail_by_ticks: 0,
trail_by_price_id: 0,
}
}
pub fn trail_by_ticks(mut self, trail_by_ticks: i32) -> Self {
self.trail_by_ticks = trail_by_ticks;
self
}
pub fn trail_by_price_id(mut self, trail_by_price_id: i32) -> Self {
self.trail_by_price_id = trail_by_price_id;
self
}
pub fn build(self) -> Result<Self, RithmicError> {
if self.trail_by_ticks < 1 {
return Err(RithmicError::InvalidArgument(
"trail_by_ticks must be at least 1".to_string(),
));
}
if self.trail_by_price_id < 1 {
return Err(RithmicError::InvalidArgument(
"trail_by_price_id must be at least 1".to_string(),
));
}
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
#[must_use = "a trigger does nothing until attached to an order"]
pub struct RithmicIfTouchedTrigger {
pub symbol: String,
pub exchange: String,
pub condition: OrderCondition,
pub price_field: OrderPriceField,
pub price: Option<f64>,
}
impl RithmicIfTouchedTrigger {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
symbol: String::new(),
exchange: String::new(),
condition: OrderCondition::GreaterThanEqualTo,
price_field: OrderPriceField::TradePrice,
price: None,
}
}
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 condition(mut self, condition: OrderCondition) -> Self {
self.condition = condition;
self
}
pub fn price_field(mut self, price_field: OrderPriceField) -> Self {
self.price_field = price_field;
self
}
pub fn price(mut self, price: f64) -> Self {
self.price = Some(price);
self
}
pub fn build(self) -> Result<Self, RithmicError> {
if self.symbol.is_empty() {
return Err(RithmicError::InvalidArgument(
"an if-touched trigger requires a symbol".to_string(),
));
}
if self.exchange.is_empty() {
return Err(RithmicError::InvalidArgument(
"an if-touched trigger requires an exchange".to_string(),
));
}
if self.price.is_none() {
return Err(RithmicError::InvalidArgument(
"an if-touched trigger requires a price; unset would otherwise \
release the order immediately under the default condition"
.to_string(),
));
}
Ok(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_trailing_stop_requires_both_fields() {
assert!(TrailingStop::new().build().is_err());
assert!(TrailingStop::new().trail_by_ticks(20).build().is_err());
assert!(TrailingStop::new().trail_by_price_id(1).build().is_err());
assert!(
TrailingStop::new()
.trail_by_ticks(20)
.trail_by_price_id(1)
.build()
.is_ok()
);
}
#[test]
fn an_if_touched_trigger_requires_symbol_exchange_and_price() {
let full = RithmicIfTouchedTrigger::new()
.symbol("NQM6")
.exchange("CME")
.price(18250.5);
assert!(full.clone().build().is_ok());
assert!(full.clone().symbol("").build().is_err());
assert!(full.exchange("").build().is_err());
let err = RithmicIfTouchedTrigger::new()
.symbol("NQM6")
.exchange("CME")
.build()
.unwrap_err()
.to_string();
assert!(err.contains("requires a price"), "{err}");
}
}