Skip to main content

rithmic_rs/api/commands/
modify.rs

1//! Modifying a working order: its terms, and the tag it reports under.
2
3use super::triggers::RithmicIfTouchedTrigger;
4use super::validate_instrument;
5
6use crate::{
7    error::RithmicError,
8    types::{ManualOrAutoEntry, OrderType},
9};
10
11/// Modify an existing order's price, quantity, or type.
12///
13/// # Example
14///
15/// ```
16/// use rithmic_rs::{OrderType, RithmicModifyOrder};
17/// # fn main() -> Result<(), rithmic_rs::RithmicError> {
18/// // "123456" is the basket_id from the order notification.
19/// let modification = RithmicModifyOrder::new()
20///     .id("123456")
21///     .symbol("ESH6")
22///     .exchange("CME")
23///     .quantity(2)
24///     .price(5005.0)
25///     .price_type(OrderType::Limit)
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 modification does nothing until passed to a plant handle"]
34pub struct RithmicModifyOrder {
35    /// The `basket_id` from the order notification
36    pub id: String,
37    /// Exchange code
38    pub exchange: String,
39    /// Trading symbol
40    pub symbol: String,
41    /// New quantity
42    pub quantity: i32,
43    /// New price, omitted from the request when unset. A modify restates the
44    /// order, so set this to the order's current price when only the quantity
45    /// is changing.
46    pub price: Option<f64>,
47    /// Order type
48    pub price_type: OrderType,
49    /// Trigger price. Left unset, the four triggering price types — the stop and
50    /// if-touched pairs — send `price` in its place.
51    pub trigger_price: Option<f64>,
52    /// Whether the modification was made by a human or automatically.
53    pub manual_or_auto: ManualOrAutoEntry,
54    /// Originating window name reported to Rithmic.
55    pub window_name: Option<String>,
56    /// Ticks to trail behind the market price.
57    ///
58    /// A bare distance, not a [`TrailingStop`](crate::TrailingStop) — a modify
59    /// takes no price-id.
60    pub trail_by_ticks: Option<i32>,
61    /// Conditional trigger on the resulting order.
62    pub if_touched: Option<RithmicIfTouchedTrigger>,
63}
64
65impl RithmicModifyOrder {
66    /// Start from the defaults.
67    ///
68    /// A modify restates the order rather than patching it, so every field that
69    /// describes the resulting order has to be set.
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    /// The `basket_id` of the order being modified.
75    pub fn id(mut self, id: impl Into<String>) -> Self {
76        self.id = id.into();
77        self
78    }
79
80    /// Instrument symbol.
81    pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
82        self.symbol = symbol.into();
83        self
84    }
85
86    /// Exchange the instrument trades on.
87    pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
88        self.exchange = exchange.into();
89        self
90    }
91
92    /// The order's size after the modification.
93    pub fn quantity(mut self, quantity: i32) -> Self {
94        self.quantity = quantity;
95        self
96    }
97
98    /// The order's price after the modification.
99    pub fn price(mut self, price: f64) -> Self {
100        self.price = Some(price);
101        self
102    }
103
104    /// The order's type after the modification.
105    pub fn price_type(mut self, price_type: OrderType) -> Self {
106        self.price_type = price_type;
107        self
108    }
109
110    /// Trigger price distinct from the limit price. Left unset, the triggering
111    /// price types send `price` in its place.
112    pub fn trigger_price(mut self, trigger_price: f64) -> Self {
113        self.trigger_price = Some(trigger_price);
114        self
115    }
116
117    /// Whether this was done by a human or automatically.
118    pub fn manual_or_auto(mut self, manual_or_auto: ManualOrAutoEntry) -> Self {
119        self.manual_or_auto = manual_or_auto;
120        self
121    }
122
123    /// Window name to report this modification under.
124    pub fn window_name(mut self, window_name: impl Into<String>) -> Self {
125        self.window_name = Some(window_name.into());
126        self
127    }
128
129    /// Trail the resulting order this many ticks behind the market price.
130    pub fn trail_by_ticks(mut self, trail_by_ticks: i32) -> Self {
131        self.trail_by_ticks = Some(trail_by_ticks);
132        self
133    }
134
135    /// Attach a conditional trigger to the resulting order.
136    pub fn if_touched(mut self, if_touched: RithmicIfTouchedTrigger) -> Self {
137        self.if_touched = Some(if_touched);
138        self
139    }
140
141    /// Check the modification carries the prices its [`Self::price_type`]
142    /// requires: `Limit`, `StopLimit` and `LimitIfTouched` need [`Self::price`];
143    /// `StopMarket`, `StopLimit`, `MarketIfTouched` and `LimitIfTouched` need a
144    /// trigger, which is [`Self::trigger_price`] or the [`Self::price`] that
145    /// stands in for it. `Market` needs neither.
146    pub fn validate(&self) -> Result<(), RithmicError> {
147        if self.id.is_empty() {
148            return Err(RithmicError::InvalidArgument(
149                "a modify requires the basket_id of the order it restates".to_string(),
150            ));
151        }
152
153        validate_instrument(&self.symbol, &self.exchange, self.quantity)?;
154
155        let (needs_price, needs_trigger) = super::price_requirements(self.price_type);
156
157        let order_type = self.price_type.as_str_name();
158
159        if needs_price && self.price.is_none() {
160            return Err(RithmicError::InvalidArgument(format!(
161                "price is required for a {order_type} order"
162            )));
163        }
164
165        if needs_trigger && self.trigger_price.is_none() && self.price.is_none() {
166            return Err(RithmicError::InvalidArgument(format!(
167                "trigger_price, or a price to stand in for it, is required for a {order_type} order"
168            )));
169        }
170
171        Ok(())
172    }
173
174    /// Requires the basket_id, the instrument, and the prices the price type
175    /// needs.
176    pub fn build(self) -> Result<Self, RithmicError> {
177        self.validate()?;
178        Ok(self)
179    }
180}
181
182/// Change the `user_tag` reported on an order's subsequent notifications.
183///
184/// # Example
185///
186/// ```
187/// use rithmic_rs::RithmicModifyOrderReferenceData;
188/// # fn main() -> Result<(), rithmic_rs::RithmicError> {
189/// let command = RithmicModifyOrderReferenceData::new()
190///     .basket_id("123456")
191///     .user_tag("new-tag")
192///     .build()?;
193/// # Ok(())
194/// # }
195/// ```
196#[derive(Debug, Clone, Default, PartialEq)]
197#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
198#[non_exhaustive]
199#[must_use = "a command does nothing until passed to a plant handle"]
200pub struct RithmicModifyOrderReferenceData {
201    /// The `basket_id` from the order notification.
202    pub basket_id: String,
203    /// The new tag. Empty is how a tag is cleared, so it is sent as given.
204    pub user_tag: String,
205}
206
207impl RithmicModifyOrderReferenceData {
208    /// Start from the defaults.
209    pub fn new() -> Self {
210        Self::default()
211    }
212
213    /// The `basket_id` of the order to retag.
214    pub fn basket_id(mut self, basket_id: impl Into<String>) -> Self {
215        self.basket_id = basket_id.into();
216        self
217    }
218
219    /// The new tag. Empty clears the tag.
220    pub fn user_tag(mut self, user_tag: impl Into<String>) -> Self {
221        self.user_tag = user_tag.into();
222        self
223    }
224
225    /// Requires the basket_id; the tag itself may be empty.
226    pub fn validate(&self) -> Result<(), RithmicError> {
227        if self.basket_id.is_empty() {
228            return Err(RithmicError::InvalidArgument(
229                "a retag requires the basket_id of the order it retags".to_string(),
230            ));
231        }
232        Ok(())
233    }
234
235    /// Requires the basket_id; the tag itself may be empty.
236    pub fn build(self) -> Result<Self, RithmicError> {
237        self.validate()?;
238        Ok(self)
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    fn modify(price_type: OrderType) -> RithmicModifyOrder {
247        RithmicModifyOrder::new()
248            .id("b")
249            .symbol("ESM6")
250            .exchange("CME")
251            .quantity(1)
252            .price_type(price_type)
253    }
254
255    /// The table is `RithmicOrder::validate`'s, except that a triggering type
256    /// accepts `price` standing in for the trigger — a modify restates the
257    /// order, and moving a stop by its price alone predates `trigger_price`.
258    #[test]
259    fn a_modify_requires_the_prices_its_type_needs() {
260        assert!(modify(OrderType::Market).build().is_ok());
261
262        assert!(modify(OrderType::Limit).build().is_err());
263        assert!(modify(OrderType::Limit).price(5000.0).build().is_ok());
264
265        // Neither a trigger nor a price to stand in for it.
266        assert!(modify(OrderType::StopMarket).build().is_err());
267        assert!(modify(OrderType::StopMarket).price(5000.0).build().is_ok());
268        assert!(
269            modify(OrderType::StopMarket)
270                .trigger_price(5000.0)
271                .build()
272                .is_ok()
273        );
274
275        // The limit price cannot be stood in for.
276        assert!(
277            modify(OrderType::StopLimit)
278                .trigger_price(4999.0)
279                .build()
280                .is_err()
281        );
282        assert!(modify(OrderType::StopLimit).price(5000.0).build().is_ok());
283
284        assert!(modify(OrderType::MarketIfTouched).build().is_err());
285        assert!(
286            modify(OrderType::LimitIfTouched)
287                .price(5000.0)
288                .build()
289                .is_ok()
290        );
291    }
292    #[test]
293    fn a_modify_requires_the_basket_id_and_instrument() {
294        assert!(modify(OrderType::Market).id("").build().is_err());
295        assert!(modify(OrderType::Market).symbol("").build().is_err());
296        assert!(modify(OrderType::Market).quantity(0).build().is_err());
297    }
298
299    #[test]
300    fn a_retag_requires_the_basket_id_but_takes_an_empty_tag() {
301        assert!(RithmicModifyOrderReferenceData::new().build().is_err());
302        assert!(
303            RithmicModifyOrderReferenceData::new()
304                .basket_id("b")
305                .user_tag("")
306                .build()
307                .is_ok()
308        );
309    }
310}