use super::triggers::RithmicIfTouchedTrigger;
use super::validate_instrument;
use crate::{
error::RithmicError,
types::{
BracketOperationType, BracketType, 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 RithmicBracketOrder {
pub action: OrderSide,
pub duration: TimeInForce,
pub exchange: String,
pub localid: String,
pub price_type: OrderType,
pub price: Option<f64>,
pub trigger_price: Option<f64>,
pub quantity: i32,
pub symbol: String,
pub bracket_type: Option<BracketType>,
pub target_quantity: Vec<i32>,
pub target_ticks: Vec<i32>,
pub stop_quantity: Vec<i32>,
pub stop_ticks: Vec<i32>,
pub if_touched: Option<RithmicIfTouchedTrigger>,
pub break_even_ticks: Option<i32>,
pub break_even_trigger_ticks: Option<i32>,
pub trailing_stop_trigger_ticks: Option<i32>,
pub trailing_stop_by_last_trade_price: Option<bool>,
pub target_market_order_if_touched: Option<bool>,
pub stop_market_on_reject: Option<bool>,
pub target_market_at_ssboe: Option<i32>,
pub target_market_at_usecs: Option<i32>,
pub stop_market_at_ssboe: Option<i32>,
pub stop_market_at_usecs: Option<i32>,
pub target_market_order_after_secs: Option<i32>,
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 trade_route: Option<String>,
pub manual_or_auto: ManualOrAutoEntry,
pub window_name: Option<String>,
pub operation_type: Option<BracketOperationType>,
}
impl RithmicBracketOrder {
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 action(mut self, action: OrderSide) -> Self {
self.action = action;
self
}
pub fn price_type(mut self, price_type: OrderType) -> Self {
self.price_type = price_type;
self
}
pub fn duration(mut self, duration: TimeInForce) -> Self {
self.duration = duration;
self
}
pub fn localid(mut self, localid: impl Into<String>) -> Self {
self.localid = localid.into();
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 bracket_type(mut self, bracket_type: BracketType) -> Self {
self.bracket_type = Some(bracket_type);
self
}
pub fn target(mut self, ticks: i32) -> Self {
self.target_quantity = vec![self.quantity];
self.target_ticks = vec![ticks];
self
}
pub fn stop(mut self, ticks: i32) -> Self {
self.stop_quantity = vec![self.quantity];
self.stop_ticks = vec![ticks];
self
}
pub fn targets(mut self, legs: impl IntoIterator<Item = (i32, i32)>) -> Self {
let (quantities, ticks): (Vec<i32>, Vec<i32>) = legs.into_iter().unzip();
self.target_quantity = quantities;
self.target_ticks = ticks;
self
}
pub fn stops(mut self, legs: impl IntoIterator<Item = (i32, i32)>) -> Self {
let (quantities, ticks): (Vec<i32>, Vec<i32>) = legs.into_iter().unzip();
self.stop_quantity = quantities;
self.stop_ticks = ticks;
self
}
pub fn if_touched(mut self, if_touched: RithmicIfTouchedTrigger) -> Self {
self.if_touched = Some(if_touched);
self
}
pub fn break_even_ticks(mut self, ticks: i32) -> Self {
self.break_even_ticks = Some(ticks);
self
}
pub fn break_even_trigger_ticks(mut self, ticks: i32) -> Self {
self.break_even_trigger_ticks = Some(ticks);
self
}
pub fn trailing_stop_trigger_ticks(mut self, ticks: i32) -> Self {
self.trailing_stop_trigger_ticks = Some(ticks);
self
}
pub fn trailing_stop_by_last_trade_price(mut self, by_last_trade_price: bool) -> Self {
self.trailing_stop_by_last_trade_price = Some(by_last_trade_price);
self
}
pub fn target_market_order_if_touched(mut self, market_if_touched: bool) -> Self {
self.target_market_order_if_touched = Some(market_if_touched);
self
}
pub fn stop_market_on_reject(mut self, market_on_reject: bool) -> Self {
self.stop_market_on_reject = Some(market_on_reject);
self
}
pub fn target_market_at_ssboe(mut self, ssboe: i32) -> Self {
self.target_market_at_ssboe = Some(ssboe);
self
}
pub fn target_market_at_usecs(mut self, usecs: i32) -> Self {
self.target_market_at_usecs = Some(usecs);
self
}
pub fn target_market_at(self, ssboe: i32, usecs: i32) -> Self {
self.target_market_at_ssboe(ssboe)
.target_market_at_usecs(usecs)
}
pub fn stop_market_at_ssboe(mut self, ssboe: i32) -> Self {
self.stop_market_at_ssboe = Some(ssboe);
self
}
pub fn stop_market_at_usecs(mut self, usecs: i32) -> Self {
self.stop_market_at_usecs = Some(usecs);
self
}
pub fn stop_market_at(self, ssboe: i32, usecs: i32) -> Self {
self.stop_market_at_ssboe(ssboe).stop_market_at_usecs(usecs)
}
pub fn target_market_order_after_secs(mut self, secs: i32) -> Self {
self.target_market_order_after_secs = Some(secs);
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 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 operation_type(mut self, operation_type: BracketOperationType) -> Self {
self.operation_type = Some(operation_type);
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)?;
for (side, quantities, ticks) in [
("target", &self.target_quantity, &self.target_ticks),
("stop", &self.stop_quantity, &self.stop_ticks),
] {
if quantities.len() != ticks.len() {
return Err(RithmicError::InvalidArgument(format!(
"{side} legs are ragged: {} quantities for {} tick distances",
quantities.len(),
ticks.len()
)));
}
if let Some(quantity) = quantities.iter().find(|quantity| **quantity <= 0) {
return Err(RithmicError::InvalidArgument(format!(
"every {side} leg needs a positive quantity, got {quantity} — \
`target(..)`/`stop(..)` size the leg to the quantity set so far"
)));
}
}
if let Some(bracket_type) = self.bracket_type {
let wants = match bracket_type {
BracketType::TargetOnly | BracketType::TargetOnlyStatic => (true, false),
BracketType::StopOnly | BracketType::StopOnlyStatic => (false, true),
BracketType::TargetAndStop | BracketType::TargetAndStopStatic => (true, true),
};
let has = (!self.target_ticks.is_empty(), !self.stop_ticks.is_empty());
if has != wants {
return Err(RithmicError::InvalidArgument(format!(
"bracket_type {} does not match the exit legs: {} target and {} stop",
bracket_type.as_str_name(),
self.target_ticks.len(),
self.stop_ticks.len()
)));
}
}
Ok(())
}
pub fn build(mut self) -> Result<Self, RithmicError> {
self.validate()?;
if self.bracket_type.is_none() {
let has_targets = !self.target_ticks.is_empty();
let has_stops = !self.stop_ticks.is_empty();
self.bracket_type = match (has_targets, has_stops) {
(true, true) => Some(BracketType::TargetAndStopStatic),
(true, false) => Some(BracketType::TargetOnlyStatic),
(false, true) => Some(BracketType::StopOnlyStatic),
(false, false) => None,
};
}
Ok(self)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
#[must_use = "an adjustment does nothing until passed to a plant handle"]
pub struct RithmicBracketLevelAdjustment {
pub id: String,
pub ticks: i32,
pub level: Option<i32>,
}
impl RithmicBracketLevelAdjustment {
pub fn new() -> Self {
Self::default()
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = id.into();
self
}
pub fn ticks(mut self, ticks: i32) -> Self {
self.ticks = ticks;
self
}
pub fn level(mut self, level: i32) -> Self {
self.level = Some(level);
self
}
pub fn validate(&self) -> Result<(), RithmicError> {
if self.id.is_empty() {
return Err(RithmicError::InvalidArgument(
"an adjustment requires the basket_id of the bracket it adjusts".to_string(),
));
}
Ok(())
}
pub fn build(self) -> Result<Self, RithmicError> {
self.validate()?;
Ok(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bracket(quantity: i32, price_type: OrderType) -> RithmicBracketOrder {
RithmicBracketOrder::new()
.symbol("ESH6")
.exchange("CME")
.quantity(quantity)
.action(OrderSide::Buy)
.price_type(price_type)
}
#[test]
fn a_bracket_validates_its_entry_leg() {
let mut order = RithmicBracketOrder {
price_type: OrderType::Limit,
..bracket(1, OrderType::Limit)
};
assert!(order.validate().is_err());
order.price = Some(5000.0);
assert!(order.validate().is_ok());
}
#[test]
fn a_bracket_checks_its_exit_legs_hold_together() {
let ragged = RithmicBracketOrder {
target_quantity: vec![1],
target_ticks: vec![16, 24],
..bracket(1, OrderType::Market)
};
assert!(ragged.validate().is_err());
let zero_sized = RithmicBracketOrder {
stop_quantity: vec![0],
stop_ticks: vec![10],
..bracket(1, OrderType::Market)
};
assert!(zero_sized.validate().is_err());
let mismatched = RithmicBracketOrder {
bracket_type: Some(BracketType::TargetOnly),
stop_quantity: vec![1],
stop_ticks: vec![10],
..bracket(1, OrderType::Market)
};
assert!(mismatched.validate().is_err());
let bare = bracket(1, OrderType::Market);
assert!(bare.validate().is_ok());
let matched = RithmicBracketOrder {
bracket_type: Some(BracketType::StopOnly),
stop_quantity: vec![1],
stop_ticks: vec![10],
..bracket(1, OrderType::Market)
};
assert!(matched.validate().is_ok());
}
#[test]
fn the_bracket_sugar_matches_the_explicit_vectors() {
let sugar = bracket(2, OrderType::Limit)
.price(5000.0)
.target(20)
.stop(10)
.build()
.unwrap();
let explicit = bracket(2, OrderType::Limit)
.price(5000.0)
.targets([(2, 20)])
.stops([(2, 10)])
.build()
.unwrap();
assert_eq!(sugar.target_quantity, explicit.target_quantity);
assert_eq!(sugar.target_ticks, explicit.target_ticks);
assert_eq!(sugar.stop_quantity, explicit.stop_quantity);
assert_eq!(sugar.stop_ticks, explicit.stop_ticks);
assert_eq!(sugar.bracket_type, explicit.bracket_type);
assert_eq!(
sugar.bracket_type,
Some(BracketType::TargetAndStopStatic),
"the simple path must stay byte-identical to what it sent before"
);
}
#[test]
fn the_bracket_sugar_sizes_its_leg_to_the_quantity_set_so_far() {
let after = RithmicBracketOrder::new()
.symbol("ESH6")
.exchange("CME")
.price_type(OrderType::Market)
.quantity(3)
.target(20)
.build()
.unwrap();
assert_eq!(after.target_quantity, vec![3]);
let before = RithmicBracketOrder::new()
.symbol("ESH6")
.exchange("CME")
.price_type(OrderType::Market)
.target(20)
.quantity(3)
.build();
assert!(
before.is_err(),
"quantity set after the leg cannot reach back and resize it, \
so the zero-sized leg fails the build"
);
}
#[test]
fn the_bracket_derives_the_shape_from_the_legs() {
let target_only = bracket(1, OrderType::Market).target(20).build().unwrap();
assert_eq!(
target_only.bracket_type,
Some(BracketType::TargetOnlyStatic)
);
let stop_only = bracket(1, OrderType::Market).stop(10).build().unwrap();
assert_eq!(stop_only.bracket_type, Some(BracketType::StopOnlyStatic));
let explicit = bracket(1, OrderType::Market)
.stop(10)
.bracket_type(BracketType::StopOnly)
.build()
.unwrap();
assert_eq!(explicit.bracket_type, Some(BracketType::StopOnly));
}
#[test]
fn a_bracket_leaves_the_shape_unset_when_there_are_no_exit_legs() {
assert_eq!(
bracket(1, OrderType::Market).build().unwrap().bracket_type,
None
);
}
#[test]
fn an_adjustment_requires_the_basket_id() {
assert!(
RithmicBracketLevelAdjustment::new()
.ticks(10)
.build()
.is_err()
);
assert!(
RithmicBracketLevelAdjustment::new()
.id("123456")
.ticks(10)
.build()
.is_ok()
);
}
#[test]
fn a_bracket_requires_its_identity() {
assert!(bracket(0, OrderType::Market).build().is_err());
assert!(bracket(1, OrderType::Market).symbol("").build().is_err());
assert!(bracket(1, OrderType::Market).build().is_ok());
}
}