Skip to content

Commit 0cec51e

Browse files
committed
feat: Add RithmicOrder API with trigger prices and trailing stops
Introduces a new RithmicOrder struct for placing standalone orders with advanced features. Adds unsubscribe methods for ticker plant. New features: - RithmicOrder struct with trigger_price and trailing_stop support - TrailingStop configuration for trailing stop orders - place_order(RithmicOrder) method on RithmicOrderPlantHandle - unsubscribe() and unsubscribe_order_book() on RithmicTickerPlantHandle Deprecates place_new_order() in favor of the new place_order() API.
1 parent 2c4929b commit 0cec51e

File tree

7 files changed

+328
-4
lines changed

7 files changed

+328
-4
lines changed

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
#### New Order API
13+
- **`RithmicOrder`**: New struct for placing standalone orders with advanced features
14+
- Supports trigger prices for stop orders (StopLimit, StopMarket)
15+
- Supports trailing stops via `TrailingStop` configuration
16+
- Ergonomic API using `Default` trait for optional fields
17+
- Comprehensive documentation with examples
18+
- **`TrailingStop`**: Configuration struct for trailing stop orders
19+
- `trail_by_ticks`: Number of ticks to trail behind market price
20+
- **`place_order(RithmicOrder)`**: New method on `RithmicOrderPlantHandle`
21+
- Preferred method for placing standalone orders
22+
- Supports all order types including stop orders and trailing stops
23+
24+
#### Ticker Plant Unsubscribe Methods
25+
- **`unsubscribe(symbol, exchange)`**: Unsubscribe from market data for a symbol
26+
- **`unsubscribe_order_book(symbol, exchange)`**: Unsubscribe from order book depth-by-order updates
27+
28+
### Deprecated
29+
- **`place_new_order()`**: Use `place_order(RithmicOrder)` instead
30+
- The new API supports trigger prices and trailing stops
31+
- Marked with `#[deprecated(since = "0.7.2")]`
32+
1033
## [0.7.1] - 2026-01-23
1134

1235
### Added

src/api.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ pub(crate) mod sender_api;
2121

2222
// Re-export commonly used types
2323
pub use receiver_api::RithmicResponse;
24+
2425
pub use rithmic_command_types::{
25-
RithmicBracketOrder, RithmicCancelOrder, RithmicModifyOrder, RithmicOcoOrderLeg,
26+
RithmicBracketOrder, RithmicCancelOrder, RithmicModifyOrder, RithmicOcoOrderLeg, RithmicOrder,
27+
TrailingStop,
2628
};
2729

2830
// Re-export bracket order enums

src/api/rithmic_command_types.rs

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::rti::{request_bracket_order, request_modify_order, request_oco_order};
1+
use crate::rti::{
2+
request_bracket_order, request_modify_order, request_new_order, request_oco_order,
3+
};
24

35
/// One leg of an OCO (One-Cancels-Other) order pair.
46
///
@@ -152,3 +154,125 @@ pub struct RithmicCancelOrder {
152154
/// The `basket_id` from the order notification
153155
pub id: String,
154156
}
157+
158+
/// Configuration for trailing stop orders.
159+
///
160+
/// When a trailing stop is configured, the stop price follows the market
161+
/// by the specified number of ticks.
162+
///
163+
/// # Example
164+
///
165+
/// ```ignore
166+
/// use rithmic_rs::TrailingStop;
167+
///
168+
/// let trailing = TrailingStop { trail_by_ticks: 20 };
169+
/// ```
170+
#[derive(Debug, Clone, Default)]
171+
pub struct TrailingStop {
172+
/// Number of ticks to trail behind the market price
173+
pub trail_by_ticks: i32,
174+
}
175+
176+
/// A standalone order (not a bracket order).
177+
///
178+
/// Use this struct with `RithmicOrderPlantHandle::place_order()` to submit
179+
/// orders with advanced features like trigger prices and trailing stops.
180+
///
181+
/// For orders with automatic profit targets and stop losses, use
182+
/// [`RithmicBracketOrder`] instead.
183+
///
184+
/// # Example: Simple Limit Order
185+
///
186+
/// ```ignore
187+
/// use rithmic_rs::{RithmicOrder, NewOrderTransactionType, NewOrderPriceType};
188+
///
189+
/// let order = RithmicOrder {
190+
/// symbol: "ESM5".to_string(),
191+
/// exchange: "CME".to_string(),
192+
/// quantity: 1,
193+
/// price: 5000.0,
194+
/// transaction_type: NewOrderTransactionType::Buy,
195+
/// price_type: NewOrderPriceType::Limit,
196+
/// user_tag: "my-order-1".to_string(),
197+
/// ..Default::default()
198+
/// };
199+
/// handle.place_order(order).await?;
200+
/// ```
201+
///
202+
/// # Example: Stop-Limit Order with Trigger Price
203+
///
204+
/// ```ignore
205+
/// use rithmic_rs::{RithmicOrder, NewOrderTransactionType, NewOrderPriceType};
206+
///
207+
/// let order = RithmicOrder {
208+
/// symbol: "ESM5".to_string(),
209+
/// exchange: "CME".to_string(),
210+
/// quantity: 1,
211+
/// price: 4980.0, // Limit price after trigger
212+
/// trigger_price: Some(4985.0), // Stop triggers at this price
213+
/// transaction_type: NewOrderTransactionType::Sell,
214+
/// price_type: NewOrderPriceType::StopLimit,
215+
/// user_tag: "stop-order".to_string(),
216+
/// ..Default::default()
217+
/// };
218+
/// ```
219+
///
220+
/// # Example: Trailing Stop Order
221+
///
222+
/// ```ignore
223+
/// use rithmic_rs::{RithmicOrder, NewOrderTransactionType, NewOrderPriceType, TrailingStop};
224+
///
225+
/// let order = RithmicOrder {
226+
/// symbol: "ESM5".to_string(),
227+
/// exchange: "CME".to_string(),
228+
/// quantity: 1,
229+
/// price: 0.0, // Not used for trailing stops
230+
/// transaction_type: NewOrderTransactionType::Sell,
231+
/// price_type: NewOrderPriceType::StopMarket,
232+
/// trailing_stop: Some(TrailingStop { trail_by_ticks: 20 }),
233+
/// user_tag: "trailing-stop".to_string(),
234+
/// ..Default::default()
235+
/// };
236+
/// ```
237+
#[derive(Debug, Clone)]
238+
pub struct RithmicOrder {
239+
/// Trading symbol (e.g., "ESM5")
240+
pub symbol: String,
241+
/// Exchange code (e.g., "CME")
242+
pub exchange: String,
243+
/// Number of contracts
244+
pub quantity: i32,
245+
/// Order price (ignored for market orders)
246+
pub price: f64,
247+
/// Buy or Sell
248+
pub transaction_type: request_new_order::TransactionType,
249+
/// Order type (Limit, Market, StopLimit, StopMarket, etc.)
250+
pub price_type: request_new_order::PriceType,
251+
/// Your identifier for tracking this order
252+
pub user_tag: String,
253+
/// Order duration (defaults to Day if None)
254+
pub duration: Option<request_new_order::Duration>,
255+
/// Trigger price for stop orders (StopLimit, StopMarket, etc.)
256+
///
257+
/// Required for stop orders; ignored for limit/market orders.
258+
pub trigger_price: Option<f64>,
259+
/// Trailing stop configuration
260+
pub trailing_stop: Option<TrailingStop>,
261+
}
262+
263+
impl Default for RithmicOrder {
264+
fn default() -> Self {
265+
Self {
266+
symbol: String::new(),
267+
exchange: String::new(),
268+
quantity: 0,
269+
price: 0.0,
270+
transaction_type: request_new_order::TransactionType::Buy,
271+
price_type: request_new_order::PriceType::Limit,
272+
user_tag: String::new(),
273+
duration: None,
274+
trigger_price: None,
275+
trailing_stop: None,
276+
}
277+
}
278+
}

src/api/sender_api.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::rithmic_command_types::{RithmicBracketOrder, RithmicOcoOrderLeg};
1+
use super::rithmic_command_types::{RithmicBracketOrder, RithmicOcoOrderLeg, RithmicOrder};
22
use prost::Message;
33

44
use crate::{
@@ -485,6 +485,49 @@ impl RithmicSenderApi {
485485
self.request_to_buf(req, id)
486486
}
487487

488+
/// Send a new order request using [`RithmicOrder`].
489+
///
490+
/// This is the preferred method for placing orders as it supports
491+
/// advanced features like trigger prices and trailing stops.
492+
///
493+
/// # Arguments
494+
/// * `order` - The order parameters
495+
///
496+
/// # Returns
497+
/// A tuple of (serialized request buffer, request ID)
498+
pub fn request_order(&mut self, order: &RithmicOrder) -> (Vec<u8>, String) {
499+
let id = self.get_next_message_id();
500+
501+
let trade_route = match self.env {
502+
RithmicEnv::Live => TRADE_ROUTE_LIVE,
503+
RithmicEnv::Demo | RithmicEnv::Test => TRADE_ROUTE_DEMO,
504+
};
505+
506+
let req = RequestNewOrder {
507+
template_id: 312,
508+
fcm_id: Some(self.fcm_id.clone()),
509+
ib_id: Some(self.ib_id.clone()),
510+
account_id: Some(self.account_id.clone()),
511+
trade_route: Some(trade_route.into()),
512+
exchange: Some(order.exchange.clone()),
513+
symbol: Some(order.symbol.clone()),
514+
quantity: Some(order.quantity),
515+
price: Some(order.price),
516+
transaction_type: Some(order.transaction_type.into()),
517+
price_type: Some(order.price_type.into()),
518+
manual_or_auto: Some(2),
519+
duration: order.duration.map(|d| d.into()).or(Some(1)),
520+
user_msg: vec![id.clone()],
521+
user_tag: Some(order.user_tag.clone()),
522+
trigger_price: order.trigger_price,
523+
trailing_stop: order.trailing_stop.as_ref().map(|_| true),
524+
trail_by_ticks: order.trailing_stop.as_ref().map(|ts| ts.trail_by_ticks),
525+
..RequestNewOrder::default()
526+
};
527+
528+
self.request_to_buf(req, id)
529+
}
530+
488531
pub fn request_bracket_order(
489532
&mut self,
490533
bracket_order: RithmicBracketOrder,

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ pub use api::{
162162
BracketDuration, BracketPriceType, BracketTransactionType, EasyToBorrowRequest,
163163
ModifyPriceType, NewOrderDuration, NewOrderPriceType, NewOrderTransactionType, OcoDuration,
164164
OcoPriceType, OcoTransactionType, RithmicBracketOrder, RithmicCancelOrder, RithmicModifyOrder,
165-
RithmicOcoOrderLeg, RithmicResponse,
165+
RithmicOcoOrderLeg, RithmicOrder, RithmicResponse, TrailingStop,
166166
};
167167

168168
// Re-export utility types for convenience

src/plants/order_plant.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::{
1212
receiver_api::{RithmicReceiverApi, RithmicResponse},
1313
rithmic_command_types::{
1414
RithmicBracketOrder, RithmicCancelOrder, RithmicModifyOrder, RithmicOcoOrderLeg,
15+
RithmicOrder,
1516
},
1617
sender_api::RithmicSenderApi,
1718
},
@@ -131,6 +132,10 @@ pub(crate) enum OrderPlantCommand {
131132
duration: Option<request_new_order::Duration>,
132133
response_sender: oneshot::Sender<Result<Vec<RithmicResponse>, String>>,
133134
},
135+
PlaceOrder {
136+
order: RithmicOrder,
137+
response_sender: oneshot::Sender<Result<Vec<RithmicResponse>, String>>,
138+
},
134139
PlaceOcoOrder {
135140
order1: RithmicOcoOrderLeg,
136141
order2: RithmicOcoOrderLeg,
@@ -1000,6 +1005,22 @@ impl PlantActor for OrderPlant {
10001005
.await
10011006
.unwrap();
10021007
}
1008+
OrderPlantCommand::PlaceOrder {
1009+
order,
1010+
response_sender,
1011+
} => {
1012+
let (req_buf, id) = self.rithmic_sender_api.request_order(&order);
1013+
1014+
self.request_handler.register_request(RithmicRequest {
1015+
request_id: id,
1016+
responder: response_sender,
1017+
});
1018+
1019+
self.rithmic_sender
1020+
.send(Message::Binary(req_buf.into()))
1021+
.await
1022+
.unwrap();
1023+
}
10031024
OrderPlantCommand::PlaceOcoOrder {
10041025
order1,
10051026
order2,
@@ -1740,6 +1761,10 @@ impl RithmicOrderPlantHandle {
17401761

17411762
/// Place a new single order (without brackets)
17421763
///
1764+
/// # Deprecated
1765+
/// Use [`place_order`](Self::place_order) instead, which accepts a [`RithmicOrder`]
1766+
/// struct and supports trigger prices and trailing stops.
1767+
///
17431768
/// # Arguments
17441769
/// * `exchange` - The exchange code (e.g., "CME")
17451770
/// * `symbol` - The trading symbol (e.g., "ESM1")
@@ -1752,6 +1777,7 @@ impl RithmicOrderPlantHandle {
17521777
///
17531778
/// # Returns
17541779
/// A vector of order placement responses or an error message
1780+
#[deprecated(since = "0.7.2", note = "Use place_order(RithmicOrder) instead")]
17551781
#[allow(clippy::too_many_arguments)]
17561782
pub async fn place_new_order(
17571783
&self,
@@ -1783,6 +1809,51 @@ impl RithmicOrderPlantHandle {
17831809
rx.await.map_err(|_| "Connection closed".to_string())?
17841810
}
17851811

1812+
/// Place a new order using [`RithmicOrder`]
1813+
///
1814+
/// This is the preferred method for placing standalone orders. It supports
1815+
/// all order types including those with trigger prices (stop orders) and
1816+
/// trailing stops.
1817+
///
1818+
/// For orders with automatic profit targets and stop losses, use
1819+
/// [`place_bracket_order`](Self::place_bracket_order) instead.
1820+
///
1821+
/// # Arguments
1822+
/// * `order` - The order parameters
1823+
///
1824+
/// # Returns
1825+
/// A vector of order placement responses or an error message
1826+
///
1827+
/// # Example
1828+
///
1829+
/// ```ignore
1830+
/// use rithmic_rs::{RithmicOrder, NewOrderTransactionType, NewOrderPriceType};
1831+
///
1832+
/// let order = RithmicOrder {
1833+
/// symbol: "ESM5".to_string(),
1834+
/// exchange: "CME".to_string(),
1835+
/// quantity: 1,
1836+
/// price: 5000.0,
1837+
/// transaction_type: NewOrderTransactionType::Buy,
1838+
/// price_type: NewOrderPriceType::Limit,
1839+
/// user_tag: "my-order".to_string(),
1840+
/// ..Default::default()
1841+
/// };
1842+
/// handle.place_order(order).await?;
1843+
/// ```
1844+
pub async fn place_order(&self, order: RithmicOrder) -> Result<Vec<RithmicResponse>, String> {
1845+
let (tx, rx) = oneshot::channel::<Result<Vec<RithmicResponse>, String>>();
1846+
1847+
let command = OrderPlantCommand::PlaceOrder {
1848+
order,
1849+
response_sender: tx,
1850+
};
1851+
1852+
let _ = self.sender.send(command).await;
1853+
1854+
rx.await.map_err(|_| "Connection closed".to_string())?
1855+
}
1856+
17861857
/// Place an OCO (One Cancels Other) order pair
17871858
///
17881859
/// When one order is filled, the other is automatically cancelled.

0 commit comments

Comments
 (0)