rithmic_rs/api/commands/
triggers.rs1use crate::{
5 error::RithmicError,
6 types::{OrderCondition, OrderPriceField},
7};
8
9#[derive(Debug, Clone, PartialEq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30#[non_exhaustive]
31#[must_use = "a trailing stop does nothing until attached to an order"]
32pub struct TrailingStop {
33 pub trail_by_ticks: i32,
35 pub trail_by_price_id: i32,
37}
38
39impl TrailingStop {
40 #[allow(clippy::new_without_default)]
42 pub fn new() -> Self {
43 Self {
44 trail_by_ticks: 0,
45 trail_by_price_id: 0,
46 }
47 }
48
49 pub fn trail_by_ticks(mut self, trail_by_ticks: i32) -> Self {
51 self.trail_by_ticks = trail_by_ticks;
52 self
53 }
54
55 pub fn trail_by_price_id(mut self, trail_by_price_id: i32) -> Self {
57 self.trail_by_price_id = trail_by_price_id;
58 self
59 }
60
61 pub fn build(self) -> Result<Self, RithmicError> {
63 if self.trail_by_ticks < 1 {
64 return Err(RithmicError::InvalidArgument(
65 "trail_by_ticks must be at least 1".to_string(),
66 ));
67 }
68 if self.trail_by_price_id < 1 {
69 return Err(RithmicError::InvalidArgument(
70 "trail_by_price_id must be at least 1".to_string(),
71 ));
72 }
73 Ok(self)
74 }
75}
76
77#[derive(Debug, Clone, PartialEq)]
98#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
99#[non_exhaustive]
100#[must_use = "a trigger does nothing until attached to an order"]
101pub struct RithmicIfTouchedTrigger {
102 pub symbol: String,
104 pub exchange: String,
106 pub condition: OrderCondition,
108 pub price_field: OrderPriceField,
110 pub price: Option<f64>,
112}
113
114impl RithmicIfTouchedTrigger {
115 #[allow(clippy::new_without_default)]
117 pub fn new() -> Self {
118 Self {
119 symbol: String::new(),
120 exchange: String::new(),
121 condition: OrderCondition::GreaterThanEqualTo,
122 price_field: OrderPriceField::TradePrice,
123 price: None,
124 }
125 }
126
127 pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
129 self.symbol = symbol.into();
130 self
131 }
132
133 pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
135 self.exchange = exchange.into();
136 self
137 }
138
139 pub fn condition(mut self, condition: OrderCondition) -> Self {
141 self.condition = condition;
142 self
143 }
144
145 pub fn price_field(mut self, price_field: OrderPriceField) -> Self {
147 self.price_field = price_field;
148 self
149 }
150
151 pub fn price(mut self, price: f64) -> Self {
153 self.price = Some(price);
154 self
155 }
156
157 pub fn build(self) -> Result<Self, RithmicError> {
159 if self.symbol.is_empty() {
160 return Err(RithmicError::InvalidArgument(
161 "an if-touched trigger requires a symbol".to_string(),
162 ));
163 }
164
165 if self.exchange.is_empty() {
166 return Err(RithmicError::InvalidArgument(
167 "an if-touched trigger requires an exchange".to_string(),
168 ));
169 }
170
171 if self.price.is_none() {
172 return Err(RithmicError::InvalidArgument(
173 "an if-touched trigger requires a price; unset would otherwise \
174 release the order immediately under the default condition"
175 .to_string(),
176 ));
177 }
178 Ok(self)
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn a_trailing_stop_requires_both_fields() {
188 assert!(TrailingStop::new().build().is_err());
189 assert!(TrailingStop::new().trail_by_ticks(20).build().is_err());
190 assert!(TrailingStop::new().trail_by_price_id(1).build().is_err());
191 assert!(
192 TrailingStop::new()
193 .trail_by_ticks(20)
194 .trail_by_price_id(1)
195 .build()
196 .is_ok()
197 );
198 }
199
200 #[test]
201 fn an_if_touched_trigger_requires_symbol_exchange_and_price() {
202 let full = RithmicIfTouchedTrigger::new()
203 .symbol("NQM6")
204 .exchange("CME")
205 .price(18250.5);
206 assert!(full.clone().build().is_ok());
207
208 assert!(full.clone().symbol("").build().is_err());
209 assert!(full.exchange("").build().is_err());
210
211 let err = RithmicIfTouchedTrigger::new()
212 .symbol("NQM6")
213 .exchange("CME")
214 .build()
215 .unwrap_err()
216 .to_string();
217 assert!(err.contains("requires a price"), "{err}");
218 }
219}