1use super::triggers::TrailingStop;
4use super::validate_instrument;
5
6use crate::{
7 error::RithmicError,
8 types::{ManualOrAutoEntry, OrderSide, OrderType, TimeInForce},
9};
10
11#[derive(Debug, Clone, Default, PartialEq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32#[non_exhaustive]
33#[must_use = "a leg does nothing until added to an OCO group"]
34pub struct RithmicOcoOrderLeg {
35 pub symbol: String,
37 pub exchange: String,
39 pub quantity: i32,
41 pub price: Option<f64>,
43 pub trigger_price: Option<f64>,
45 pub transaction_type: OrderSide,
47 pub duration: TimeInForce,
49 pub price_type: OrderType,
53 pub user_tag: String,
55 pub trailing_stop: Option<TrailingStop>,
57 pub trade_route: Option<String>,
60 pub manual_or_auto: ManualOrAutoEntry,
62 pub window_name: Option<String>,
65}
66
67impl RithmicOcoOrderLeg {
68 pub fn new() -> Self {
70 Self::default()
71 }
72
73 pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
75 self.symbol = symbol.into();
76 self
77 }
78
79 pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
81 self.exchange = exchange.into();
82 self
83 }
84
85 pub fn quantity(mut self, quantity: i32) -> Self {
87 self.quantity = quantity;
88 self
89 }
90
91 pub fn transaction_type(mut self, transaction_type: OrderSide) -> Self {
93 self.transaction_type = transaction_type;
94 self
95 }
96
97 pub fn price_type(mut self, price_type: OrderType) -> Self {
99 self.price_type = price_type;
100 self
101 }
102
103 pub fn price(mut self, price: f64) -> Self {
105 self.price = Some(price);
106 self
107 }
108
109 pub fn trigger_price(mut self, trigger_price: f64) -> Self {
111 self.trigger_price = Some(trigger_price);
112 self
113 }
114
115 pub fn duration(mut self, duration: TimeInForce) -> Self {
117 self.duration = duration;
118 self
119 }
120
121 pub fn user_tag(mut self, user_tag: impl Into<String>) -> Self {
123 self.user_tag = user_tag.into();
124 self
125 }
126
127 pub fn trailing_stop(mut self, trailing_stop: TrailingStop) -> Self {
129 self.trailing_stop = Some(trailing_stop);
130 self
131 }
132
133 pub fn trailing_stop_by(self, trail_by_ticks: i32, trail_by_price_id: i32) -> Self {
135 self.trailing_stop(
136 TrailingStop::new()
137 .trail_by_ticks(trail_by_ticks)
138 .trail_by_price_id(trail_by_price_id),
139 )
140 }
141
142 pub fn trade_route(mut self, trade_route: impl Into<String>) -> Self {
144 self.trade_route = Some(trade_route.into());
145 self
146 }
147
148 pub fn manual_or_auto(mut self, manual_or_auto: ManualOrAutoEntry) -> Self {
150 self.manual_or_auto = manual_or_auto;
151 self
152 }
153
154 pub fn window_name(mut self, window_name: impl Into<String>) -> Self {
156 self.window_name = Some(window_name.into());
157 self
158 }
159
160 pub fn validate(&self) -> Result<(), RithmicError> {
164 validate_instrument(&self.symbol, &self.exchange, self.quantity)?;
165
166 if matches!(
167 self.price_type,
168 OrderType::MarketIfTouched | OrderType::LimitIfTouched
169 ) {
170 return Err(RithmicError::InvalidArgument(format!(
171 "price_type {} is not available on an OCO leg",
172 self.price_type.as_str_name()
173 )));
174 }
175
176 super::require_prices(self.price_type, self.price, self.trigger_price)
177 }
178
179 pub fn build(self) -> Result<Self, RithmicError> {
181 self.validate()?;
182 Ok(self)
183 }
184}
185
186#[derive(Debug, Clone, Default, PartialEq)]
215#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
216#[non_exhaustive]
217#[must_use = "an order does nothing until passed to a plant handle"]
218pub struct RithmicOcoOrder {
219 pub legs: Vec<RithmicOcoOrderLeg>,
221 pub cancel_at_ssboe: Option<i32>,
223 pub cancel_at_usecs: Option<i32>,
225 pub cancel_after_secs: Option<i32>,
227}
228
229impl RithmicOcoOrder {
230 pub fn new() -> Self {
232 Self::default()
233 }
234
235 pub fn leg(mut self, leg: RithmicOcoOrderLeg) -> Self {
237 self.legs.push(leg);
238 self
239 }
240
241 pub fn legs(mut self, legs: impl IntoIterator<Item = RithmicOcoOrderLeg>) -> Self {
243 self.legs.extend(legs);
244 self
245 }
246
247 pub fn cancel_at_ssboe(mut self, ssboe: i32) -> Self {
249 self.cancel_at_ssboe = Some(ssboe);
250 self
251 }
252
253 pub fn cancel_at_usecs(mut self, usecs: i32) -> Self {
255 self.cancel_at_usecs = Some(usecs);
256 self
257 }
258
259 pub fn cancel_at(self, ssboe: i32, usecs: i32) -> Self {
261 self.cancel_at_ssboe(ssboe).cancel_at_usecs(usecs)
262 }
263
264 pub fn cancel_after_secs(mut self, secs: i32) -> Self {
266 self.cancel_after_secs = Some(secs);
267 self
268 }
269
270 pub fn validate(&self) -> Result<(), RithmicError> {
272 for leg in &self.legs {
273 leg.validate()?;
274 }
275
276 Ok(())
277 }
278
279 pub fn build(self) -> Result<Self, RithmicError> {
282 self.validate()?;
283 Ok(self)
284 }
285
286 pub(crate) fn cancel_timing(&self) -> OcoCancelTiming {
290 OcoCancelTiming {
291 cancel_at_ssboe: self.cancel_at_ssboe,
292 cancel_at_usecs: self.cancel_at_usecs,
293 cancel_after_secs: self.cancel_after_secs,
294 }
295 }
296}
297
298#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
301pub(crate) struct OcoCancelTiming {
302 pub(crate) cancel_at_ssboe: Option<i32>,
304 pub(crate) cancel_at_usecs: Option<i32>,
306 pub(crate) cancel_after_secs: Option<i32>,
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 fn leg(price_type: OrderType) -> RithmicOcoOrderLeg {
315 RithmicOcoOrderLeg {
316 symbol: "ESM6".to_string(),
317 exchange: "CME".to_string(),
318 quantity: 1,
319 price_type,
320 ..Default::default()
321 }
322 }
323
324 #[test]
325 fn an_oco_leg_validates_on_the_same_rules() {
326 let mut leg = leg(OrderType::Limit);
327
328 assert!(leg.validate().is_err());
329
330 leg.price = Some(5000.0);
331 assert!(leg.validate().is_ok());
332 }
333
334 #[test]
336 fn an_oco_leg_rejects_the_if_touched_price_types() {
337 let leg = RithmicOcoOrderLeg {
338 price: Some(5000.0),
339 trigger_price: Some(5000.0),
340 ..leg(OrderType::LimitIfTouched)
341 };
342
343 let err = leg.validate().unwrap_err().to_string();
344 assert!(err.contains("LIMIT_IF_TOUCHED"), "{err}");
345 assert!(err.contains("is not available on an OCO leg"), "{err}");
346 }
347
348 #[test]
350 fn an_oco_order_validates_each_leg_but_not_the_count() {
351 let ok = leg(OrderType::Market);
352
353 assert!(RithmicOcoOrder::default().validate().is_ok());
354 assert!(
355 RithmicOcoOrder {
356 legs: vec![ok.clone()],
357 ..Default::default()
358 }
359 .validate()
360 .is_ok()
361 );
362
363 let bad = leg(OrderType::Limit);
364 assert!(
365 RithmicOcoOrder {
366 legs: vec![ok, bad],
367 ..Default::default()
368 }
369 .validate()
370 .is_err()
371 );
372 }
373}