Skip to main content

rithmic_rs/api/commands/
oco.rs

1//! OCO (One-Cancels-Other) groups and the legs they hold.
2
3use super::triggers::TrailingStop;
4use super::validate_instrument;
5
6use crate::{
7    error::RithmicError,
8    types::{ManualOrAutoEntry, OrderSide, OrderType, TimeInForce},
9};
10
11/// One leg of an OCO (One-Cancels-Other) order group.
12///
13/// # Example
14///
15/// ```
16/// use rithmic_rs::{OrderSide, OrderType, RithmicOcoOrderLeg};
17/// # fn main() -> Result<(), rithmic_rs::RithmicError> {
18/// let take_profit = RithmicOcoOrderLeg::new()
19///     .symbol("ESH6")
20///     .exchange("CME")
21///     .quantity(1)
22///     .transaction_type(OrderSide::Sell)
23///     .price_type(OrderType::Limit)
24///     .price(5020.0)
25///     .user_tag("take-profit")
26///     .build()?;
27/// # Ok(())
28/// # }
29/// ```
30#[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    /// Trading symbol (e.g., "ESH6")
36    pub symbol: String,
37    /// Exchange code (e.g., "CME")
38    pub exchange: String,
39    /// Number of contracts
40    pub quantity: i32,
41    /// Leg price. A market leg does not need one.
42    pub price: Option<f64>,
43    /// Trigger price. Only a stop leg needs one.
44    pub trigger_price: Option<f64>,
45    /// Buy or Sell
46    pub transaction_type: OrderSide,
47    /// Order duration
48    pub duration: TimeInForce,
49    /// Order type. Template 328 declares no if-touched price type, so
50    /// [`OrderType::MarketIfTouched`] and [`OrderType::LimitIfTouched`] are
51    /// rejected on an OCO leg.
52    pub price_type: OrderType,
53    /// Your identifier for this order
54    pub user_tag: String,
55    /// Optional trailing stop configuration for this leg
56    pub trailing_stop: Option<TrailingStop>,
57    /// Route to send on. `None` uses the route the server published for this
58    /// leg's exchange.
59    pub trade_route: Option<String>,
60    /// Whether the leg was placed by a human or automatically.
61    pub manual_or_auto: ManualOrAutoEntry,
62    /// Originating window name reported to Rithmic. `window_name` is repeated
63    /// on `RequestOcoOrder`, so it is per-leg like the other leg fields.
64    pub window_name: Option<String>,
65}
66
67impl RithmicOcoOrderLeg {
68    /// Start from the defaults.
69    pub fn new() -> Self {
70        Self::default()
71    }
72
73    /// Instrument symbol.
74    pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
75        self.symbol = symbol.into();
76        self
77    }
78
79    /// Exchange the instrument trades on.
80    pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
81        self.exchange = exchange.into();
82        self
83    }
84
85    /// Number of contracts on this leg.
86    pub fn quantity(mut self, quantity: i32) -> Self {
87        self.quantity = quantity;
88        self
89    }
90
91    /// Buy or sell.
92    pub fn transaction_type(mut self, transaction_type: OrderSide) -> Self {
93        self.transaction_type = transaction_type;
94        self
95    }
96
97    /// Market, limit, or stop.
98    pub fn price_type(mut self, price_type: OrderType) -> Self {
99        self.price_type = price_type;
100        self
101    }
102
103    /// Leg price.
104    pub fn price(mut self, price: f64) -> Self {
105        self.price = Some(price);
106        self
107    }
108
109    /// Trigger price for stop order types.
110    pub fn trigger_price(mut self, trigger_price: f64) -> Self {
111        self.trigger_price = Some(trigger_price);
112        self
113    }
114
115    /// How long the leg stays working.
116    pub fn duration(mut self, duration: TimeInForce) -> Self {
117        self.duration = duration;
118        self
119    }
120
121    /// Your identifier for this leg.
122    pub fn user_tag(mut self, user_tag: impl Into<String>) -> Self {
123        self.user_tag = user_tag.into();
124        self
125    }
126
127    /// Trailing stop configuration for this leg.
128    pub fn trailing_stop(mut self, trailing_stop: TrailingStop) -> Self {
129        self.trailing_stop = Some(trailing_stop);
130        self
131    }
132
133    /// Trail by `trail_by_ticks` against Rithmic's `trail_by_price_id`.
134    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    /// Route to send on, overriding the route published for the exchange.
143    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    /// Whether this was done by a human or automatically.
149    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    /// Window name to report this leg under.
155    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    /// Requires a symbol, an exchange, a positive quantity, and the prices
161    /// the [`Self::price_type`] needs. An OCO leg cannot be if-touched. An
162    /// embedded trailing stop is not re-validated.
163    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    /// Requires an instrument and the prices the price type needs.
180    pub fn build(self) -> Result<Self, RithmicError> {
181        self.validate()?;
182        Ok(self)
183    }
184}
185
186/// A group of OCO legs: when one fills, the others are cancelled.
187///
188/// # Example
189///
190/// ```
191/// use rithmic_rs::{OrderSide, OrderType, RithmicOcoOrder, RithmicOcoOrderLeg};
192/// # fn main() -> Result<(), rithmic_rs::RithmicError> {
193/// let take_profit = RithmicOcoOrderLeg::new()
194///     .symbol("ESH6")
195///     .exchange("CME")
196///     .quantity(1)
197///     .transaction_type(OrderSide::Sell)
198///     .price_type(OrderType::Limit)
199///     .price(5020.0)
200///     .build()?;
201/// let stop_loss = RithmicOcoOrderLeg::new()
202///     .symbol("ESH6")
203///     .exchange("CME")
204///     .quantity(1)
205///     .transaction_type(OrderSide::Sell)
206///     .price_type(OrderType::StopMarket)
207///     .trigger_price(4980.0)
208///     .build()?;
209///
210/// let order = RithmicOcoOrder::new().legs([take_profit, stop_loss]).build()?;
211/// # Ok(())
212/// # }
213/// ```
214#[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    /// The legs of the group, in the order they are sent.
220    pub legs: Vec<RithmicOcoOrderLeg>,
221    /// Cancel the group at this second-since-beginning-of-epoch value.
222    pub cancel_at_ssboe: Option<i32>,
223    /// Microsecond component for `cancel_at_ssboe`.
224    pub cancel_at_usecs: Option<i32>,
225    /// Cancel the group after this many seconds.
226    pub cancel_after_secs: Option<i32>,
227}
228
229impl RithmicOcoOrder {
230    /// Start from the defaults, with no legs.
231    pub fn new() -> Self {
232        Self::default()
233    }
234
235    /// Append one more leg.
236    pub fn leg(mut self, leg: RithmicOcoOrderLeg) -> Self {
237        self.legs.push(leg);
238        self
239    }
240
241    /// Append several more legs.
242    pub fn legs(mut self, legs: impl IntoIterator<Item = RithmicOcoOrderLeg>) -> Self {
243        self.legs.extend(legs);
244        self
245    }
246
247    /// Cancel the group at this second-since-beginning-of-epoch value.
248    pub fn cancel_at_ssboe(mut self, ssboe: i32) -> Self {
249        self.cancel_at_ssboe = Some(ssboe);
250        self
251    }
252
253    /// Microsecond component of the cancel time.
254    pub fn cancel_at_usecs(mut self, usecs: i32) -> Self {
255        self.cancel_at_usecs = Some(usecs);
256        self
257    }
258
259    /// Set both halves of the cancel time.
260    pub fn cancel_at(self, ssboe: i32, usecs: i32) -> Self {
261        self.cancel_at_ssboe(ssboe).cancel_at_usecs(usecs)
262    }
263
264    /// Cancel the group after this many seconds.
265    pub fn cancel_after_secs(mut self, secs: i32) -> Self {
266        self.cancel_after_secs = Some(secs);
267        self
268    }
269
270    /// Check every leg validates.
271    pub fn validate(&self) -> Result<(), RithmicError> {
272        for leg in &self.legs {
273            leg.validate()?;
274        }
275
276        Ok(())
277    }
278
279    /// Validate and return the group. The leg count is not checked here; the
280    /// handle refuses a group shorter than two legs.
281    pub fn build(self) -> Result<Self, RithmicError> {
282        self.validate()?;
283        Ok(self)
284    }
285
286    /// Copy out the group-level timing. The plant hands the legs to the route
287    /// cache before it reaches the sender, so the timing has to travel
288    /// separately.
289    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/// The group-level cancel timing on an OCO order, carried on its own so the
299/// three same-typed fields cannot be swapped at a call site.
300#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
301pub(crate) struct OcoCancelTiming {
302    /// Cancel the group at this second-since-beginning-of-epoch value.
303    pub(crate) cancel_at_ssboe: Option<i32>,
304    /// Microsecond component for `cancel_at_ssboe`.
305    pub(crate) cancel_at_usecs: Option<i32>,
306    /// Cancel the group after this many seconds.
307    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    /// The one place a crate-owned enum is wider than the message it targets.
335    #[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    /// The group checks its legs, not how many of them there are.
349    #[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}