Skip to main content

rithmic_rs/api/commands/
order.rs

1//! A standalone order.
2
3use super::triggers::{RithmicIfTouchedTrigger, TrailingStop};
4use super::validate_instrument;
5
6use crate::{
7    error::RithmicError,
8    types::{ManualOrAutoEntry, OrderSide, OrderType, TimeInForce},
9};
10
11/// A standalone order (not a bracket order).
12///
13/// For orders with automatic profit targets and stop losses, use
14/// [`RithmicBracketOrder`](crate::RithmicBracketOrder) instead.
15///
16/// # Example: limit order
17///
18/// ```
19/// use rithmic_rs::{OrderSide, OrderType, RithmicOrder};
20/// # fn main() -> Result<(), rithmic_rs::RithmicError> {
21/// let order = RithmicOrder::new()
22///     .symbol("ESH6")
23///     .exchange("CME")
24///     .quantity(1)
25///     .transaction_type(OrderSide::Buy)
26///     .price_type(OrderType::Limit)
27///     .price(5000.0)
28///     .user_tag("my-order-1")
29///     .build()?;
30/// # Ok(())
31/// # }
32/// ```
33///
34/// # Example: market order
35///
36/// A market order has no price. Leaving `price` unset omits the field rather
37/// than pricing the order at zero.
38///
39/// ```
40/// use rithmic_rs::{OrderSide, OrderType, RithmicOrder};
41/// # fn main() -> Result<(), rithmic_rs::RithmicError> {
42/// let order = RithmicOrder::new()
43///     .symbol("ESH6")
44///     .exchange("CME")
45///     .quantity(1)
46///     .transaction_type(OrderSide::Buy)
47///     .price_type(OrderType::Market)
48///     .user_tag("market-order")
49///     .build()?;
50///
51/// assert_eq!(order.price, None);
52/// # Ok(())
53/// # }
54/// ```
55///
56/// # Example: stop-limit with a trailing stop
57///
58/// ```
59/// use rithmic_rs::{OrderSide, OrderType, RithmicOrder};
60/// # fn main() -> Result<(), rithmic_rs::RithmicError> {
61/// let order = RithmicOrder::new()
62///     .symbol("ESH6")
63///     .exchange("CME")
64///     .quantity(1)
65///     .transaction_type(OrderSide::Sell)
66///     .price_type(OrderType::StopLimit)
67///     .price(4980.0)
68///     .trigger_price(4985.0)
69///     .trailing_stop_by(20, 1)
70///     .build()?;
71/// # Ok(())
72/// # }
73/// ```
74#[derive(Debug, Clone, Default, PartialEq)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
76#[non_exhaustive]
77#[must_use = "an order does nothing until passed to a plant handle"]
78pub struct RithmicOrder {
79    /// Trading symbol (e.g., "ESH6")
80    pub symbol: String,
81    /// Exchange code (e.g., "CME")
82    pub exchange: String,
83    /// Number of contracts
84    pub quantity: i32,
85    /// Order price. A market order does not need one.
86    pub price: Option<f64>,
87    /// Buy or Sell
88    pub transaction_type: OrderSide,
89    /// Order type (Limit, Market, StopLimit, StopMarket, etc.)
90    pub price_type: OrderType,
91    /// Your identifier for tracking this order
92    pub user_tag: String,
93    /// Order duration
94    pub duration: TimeInForce,
95    /// Trigger price. Only a stop or if-touched order needs one.
96    pub trigger_price: Option<f64>,
97    /// Trailing stop configuration
98    pub trailing_stop: Option<TrailingStop>,
99    /// Route to send on. `None` uses the route the server published for `exchange`.
100    pub trade_route: Option<String>,
101    /// Whether the order was placed by a human or automatically.
102    pub manual_or_auto: ManualOrAutoEntry,
103    /// Originating window name reported to Rithmic.
104    pub window_name: Option<String>,
105    /// Release the order at this second-since-beginning-of-epoch value.
106    pub release_at_ssboe: Option<i32>,
107    /// Microsecond component for [`Self::release_at_ssboe`].
108    pub release_at_usecs: Option<i32>,
109    /// Cancel the order at this second-since-beginning-of-epoch value.
110    pub cancel_at_ssboe: Option<i32>,
111    /// Microsecond component for [`Self::cancel_at_ssboe`].
112    pub cancel_at_usecs: Option<i32>,
113    /// Cancel the order after this many seconds.
114    pub cancel_after_secs: Option<i32>,
115    /// Conditional trigger that releases this order once touched.
116    pub if_touched: Option<RithmicIfTouchedTrigger>,
117}
118
119impl RithmicOrder {
120    /// Start from the defaults.
121    pub fn new() -> Self {
122        Self::default()
123    }
124
125    /// Instrument symbol.
126    pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
127        self.symbol = symbol.into();
128        self
129    }
130
131    /// Exchange the instrument trades on.
132    pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
133        self.exchange = exchange.into();
134        self
135    }
136
137    /// Number of contracts.
138    pub fn quantity(mut self, quantity: i32) -> Self {
139        self.quantity = quantity;
140        self
141    }
142
143    /// Buy or sell.
144    pub fn transaction_type(mut self, transaction_type: OrderSide) -> Self {
145        self.transaction_type = transaction_type;
146        self
147    }
148
149    /// Market, limit, stop, or if-touched.
150    pub fn price_type(mut self, price_type: OrderType) -> Self {
151        self.price_type = price_type;
152        self
153    }
154
155    /// Order price.
156    pub fn price(mut self, price: f64) -> Self {
157        self.price = Some(price);
158        self
159    }
160
161    /// Trigger price for stop and if-touched order types.
162    pub fn trigger_price(mut self, trigger_price: f64) -> Self {
163        self.trigger_price = Some(trigger_price);
164        self
165    }
166
167    /// Your identifier for this order.
168    pub fn user_tag(mut self, user_tag: impl Into<String>) -> Self {
169        self.user_tag = user_tag.into();
170        self
171    }
172
173    /// How long the order stays working.
174    pub fn duration(mut self, duration: TimeInForce) -> Self {
175        self.duration = duration;
176        self
177    }
178
179    /// Trailing stop configuration.
180    pub fn trailing_stop(mut self, trailing_stop: TrailingStop) -> Self {
181        self.trailing_stop = Some(trailing_stop);
182        self
183    }
184
185    /// Trail by `trail_by_ticks` against Rithmic's `trail_by_price_id`.
186    pub fn trailing_stop_by(self, trail_by_ticks: i32, trail_by_price_id: i32) -> Self {
187        self.trailing_stop(
188            TrailingStop::new()
189                .trail_by_ticks(trail_by_ticks)
190                .trail_by_price_id(trail_by_price_id),
191        )
192    }
193
194    /// Route to send on, overriding the route published for the exchange.
195    pub fn trade_route(mut self, trade_route: impl Into<String>) -> Self {
196        self.trade_route = Some(trade_route.into());
197        self
198    }
199
200    /// Whether this was done by a human or automatically.
201    pub fn manual_or_auto(mut self, manual_or_auto: ManualOrAutoEntry) -> Self {
202        self.manual_or_auto = manual_or_auto;
203        self
204    }
205
206    /// Window name to report this order under.
207    pub fn window_name(mut self, window_name: impl Into<String>) -> Self {
208        self.window_name = Some(window_name.into());
209        self
210    }
211
212    /// Release the order at this second-since-beginning-of-epoch value.
213    pub fn release_at_ssboe(mut self, ssboe: i32) -> Self {
214        self.release_at_ssboe = Some(ssboe);
215        self
216    }
217
218    /// Microsecond component of the release time.
219    pub fn release_at_usecs(mut self, usecs: i32) -> Self {
220        self.release_at_usecs = Some(usecs);
221        self
222    }
223
224    /// Set both halves of the release time.
225    pub fn release_at(self, ssboe: i32, usecs: i32) -> Self {
226        self.release_at_ssboe(ssboe).release_at_usecs(usecs)
227    }
228
229    /// Cancel the order at this second-since-beginning-of-epoch value.
230    pub fn cancel_at_ssboe(mut self, ssboe: i32) -> Self {
231        self.cancel_at_ssboe = Some(ssboe);
232        self
233    }
234
235    /// Microsecond component of the cancel time.
236    pub fn cancel_at_usecs(mut self, usecs: i32) -> Self {
237        self.cancel_at_usecs = Some(usecs);
238        self
239    }
240
241    /// Set both halves of the cancel time.
242    pub fn cancel_at(self, ssboe: i32, usecs: i32) -> Self {
243        self.cancel_at_ssboe(ssboe).cancel_at_usecs(usecs)
244    }
245
246    /// Cancel the order after this many seconds.
247    pub fn cancel_after_secs(mut self, secs: i32) -> Self {
248        self.cancel_after_secs = Some(secs);
249        self
250    }
251
252    /// Conditional trigger that releases this order once touched.
253    pub fn if_touched(mut self, if_touched: RithmicIfTouchedTrigger) -> Self {
254        self.if_touched = Some(if_touched);
255        self
256    }
257
258    /// Check the order names an instrument (symbol, exchange, a positive
259    /// quantity) and carries the prices its [`Self::price_type`] requires:
260    /// `Limit`, `StopLimit` and `LimitIfTouched` need [`Self::price`];
261    /// `StopMarket`, `StopLimit`, `MarketIfTouched` and `LimitIfTouched` need
262    /// [`Self::trigger_price`]. `Market` needs neither. An embedded
263    /// [`TrailingStop`] or [`RithmicIfTouchedTrigger`] is deliberately not
264    /// re-validated — `build()` on those types is the opt-in strict path.
265    pub fn validate(&self) -> Result<(), RithmicError> {
266        validate_instrument(&self.symbol, &self.exchange, self.quantity)?;
267
268        super::require_prices(self.price_type, self.price, self.trigger_price)
269    }
270
271    /// Requires an instrument and the prices the price type needs.
272    pub fn build(self) -> Result<Self, RithmicError> {
273        self.validate()?;
274        Ok(self)
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    fn order() -> RithmicOrder {
283        RithmicOrder::new()
284            .symbol("ESH6")
285            .exchange("CME")
286            .quantity(1)
287            .transaction_type(OrderSide::Buy)
288            .price_type(OrderType::Limit)
289    }
290
291    #[test]
292    fn a_market_order_validates_without_a_price() {
293        let order = RithmicOrder {
294            price_type: OrderType::Market,
295            ..order()
296        };
297
298        assert!(order.validate().is_ok());
299    }
300
301    #[test]
302    fn a_limit_order_needs_a_price() {
303        let mut order = RithmicOrder {
304            price_type: OrderType::Limit,
305            ..order()
306        };
307
308        let err = order.validate().unwrap_err().to_string();
309        assert!(err.contains("price is required"), "{err}");
310
311        order.price = Some(5000.0);
312        assert!(order.validate().is_ok());
313    }
314
315    #[test]
316    fn a_stop_market_order_needs_a_trigger_but_no_price() {
317        let mut order = RithmicOrder {
318            price_type: OrderType::StopMarket,
319            ..order()
320        };
321
322        let err = order.validate().unwrap_err().to_string();
323        assert!(err.contains("trigger_price is required"), "{err}");
324
325        order.trigger_price = Some(4985.0);
326        assert!(order.validate().is_ok());
327    }
328
329    #[test]
330    fn a_stop_limit_order_needs_both() {
331        let mut order = RithmicOrder {
332            price_type: OrderType::StopLimit,
333            price: Some(4980.0),
334            ..order()
335        };
336
337        assert!(order.validate().is_err());
338
339        order.trigger_price = Some(4985.0);
340        assert!(order.validate().is_ok());
341    }
342
343    /// The message names the protobuf type the caller set, not a Rust-side
344    /// paraphrase, so it lines up with what Rithmic's docs call the order type.
345    #[test]
346    fn the_error_names_the_order_type() {
347        let order = RithmicOrder {
348            price_type: OrderType::LimitIfTouched,
349            ..order()
350        };
351
352        let err = order.validate().unwrap_err().to_string();
353        assert!(err.contains("LIMIT_IF_TOUCHED"), "{err}");
354    }
355
356    /// Only the setters that take more than one argument are worth asserting:
357    /// each pair is same-typed, so a swapped argument compiles and would put the
358    /// microseconds in the seconds field.
359    #[test]
360    fn the_paired_setters_assign_their_arguments_in_order() {
361        let order = order()
362            .price(4980.0)
363            .trailing_stop_by(20, 1)
364            .release_at(35900, 500)
365            .cancel_at(36000, 250)
366            .build()
367            .unwrap();
368
369        let trailing = order.trailing_stop.unwrap();
370        assert_eq!(trailing.trail_by_ticks, 20);
371        assert_eq!(trailing.trail_by_price_id, 1);
372        assert_eq!(order.release_at_ssboe, Some(35900));
373        assert_eq!(order.release_at_usecs, Some(500));
374        assert_eq!(order.cancel_at_ssboe, Some(36000));
375        assert_eq!(order.cancel_at_usecs, Some(250));
376    }
377    #[test]
378    fn an_order_requires_its_identity() {
379        assert!(order().symbol("").price(5000.0).build().is_err());
380        assert!(order().exchange("").price(5000.0).build().is_err());
381        assert!(order().quantity(0).price(5000.0).build().is_err());
382        assert!(order().price(5000.0).build().is_ok());
383    }
384}