Skip to main content

longbridge/trade/requests/
replace_order.rs

1use rust_decimal::Decimal;
2use serde::Serialize;
3use time::OffsetDateTime;
4
5use crate::trade::{AttachedOrderType, OrderType, OutsideRTH, TimeInForceType};
6
7/// Options for replace order request
8#[derive(Debug, Serialize, Clone)]
9pub struct ReplaceOrderOptions {
10    order_id: String,
11    quantity: Decimal,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    price: Option<Decimal>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    trigger_price: Option<Decimal>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    limit_offset: Option<Decimal>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    trailing_amount: Option<Decimal>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    trailing_percent: Option<Decimal>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    limit_depth_level: Option<i32>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    trigger_count: Option<i32>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    monitor_price: Option<Decimal>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    remark: Option<String>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    delayed_until: Option<i64>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    attached_params: Option<ReplaceAttachedParams>,
34}
35
36impl ReplaceOrderOptions {
37    /// Create a new `ReplaceOrderOptions`
38    #[inline]
39    pub fn new(order_id: impl Into<String>, quantity: Decimal) -> Self {
40        Self {
41            order_id: order_id.into(),
42            quantity,
43            price: None,
44            trigger_price: None,
45            limit_offset: None,
46            trailing_amount: None,
47            trailing_percent: None,
48            limit_depth_level: None,
49            trigger_count: None,
50            monitor_price: None,
51            remark: None,
52            delayed_until: None,
53            attached_params: None,
54        }
55    }
56
57    /// Set the price
58    #[inline]
59    #[must_use]
60    pub fn price(self, price: Decimal) -> Self {
61        Self {
62            price: Some(price),
63            ..self
64        }
65    }
66
67    /// Set the trigger price
68    #[inline]
69    #[must_use]
70    pub fn trigger_price(self, trigger_price: Decimal) -> Self {
71        Self {
72            trigger_price: Some(trigger_price),
73            ..self
74        }
75    }
76
77    /// Set the limit offset
78    #[inline]
79    #[must_use]
80    pub fn limit_offset(self, limit_offset: Decimal) -> Self {
81        Self {
82            limit_offset: Some(limit_offset),
83            ..self
84        }
85    }
86
87    /// Set the trailing amount
88    #[inline]
89    #[must_use]
90    pub fn trailing_amount(self, trailing_amount: Decimal) -> Self {
91        Self {
92            trailing_amount: Some(trailing_amount),
93            ..self
94        }
95    }
96
97    /// Set the trailing percent
98    #[inline]
99    #[must_use]
100    pub fn trailing_percent(self, trailing_percent: Decimal) -> Self {
101        Self {
102            trailing_percent: Some(trailing_percent),
103            ..self
104        }
105    }
106
107    /// Set the limit depth level
108    #[inline]
109    #[must_use]
110    pub fn limit_depth_level(self, limit_depth_level: i32) -> Self {
111        Self {
112            limit_depth_level: Some(limit_depth_level),
113            ..self
114        }
115    }
116
117    /// Set the trigger count
118    #[inline]
119    #[must_use]
120    pub fn trigger_count(self, trigger_count: i32) -> Self {
121        Self {
122            trigger_count: Some(trigger_count),
123            ..self
124        }
125    }
126
127    /// Set the monitor price
128    #[inline]
129    #[must_use]
130    pub fn monitor_price(self, monitor_price: Decimal) -> Self {
131        Self {
132            monitor_price: Some(monitor_price),
133            ..self
134        }
135    }
136
137    /// Set the remark
138    #[inline]
139    #[must_use]
140    pub fn remark(self, remark: impl Into<String>) -> Self {
141        Self {
142            remark: Some(remark.into()),
143            ..self
144        }
145    }
146
147    /// Set a new time-condition submit time.
148    ///
149    /// Must be later than now and no more than 90 days ahead. Leave unset to
150    /// keep the order's current time condition. If combined with
151    /// [`clear_delayed_until`](Self::clear_delayed_until), the last call wins.
152    #[inline]
153    #[must_use]
154    pub fn delayed_until(self, delayed_until: OffsetDateTime) -> Self {
155        Self {
156            delayed_until: Some(super::delayed_until_timestamp(delayed_until)),
157            ..self
158        }
159    }
160
161    /// Clear the time condition, turning the order into a regular order.
162    ///
163    /// If combined with [`delayed_until`](Self::delayed_until), the last call
164    /// wins.
165    #[inline]
166    #[must_use]
167    pub fn clear_delayed_until(self) -> Self {
168        Self {
169            delayed_until: Some(1),
170            ..self
171        }
172    }
173
174    /// Set attached order parameters
175    pub fn attached_params(self, params: ReplaceAttachedParams) -> Self {
176        Self {
177            attached_params: Some(params),
178            ..self
179        }
180    }
181}
182
183/// Attached order parameters for replace order
184#[derive(Debug, Serialize, Clone)]
185pub struct ReplaceAttachedParams {
186    attached_order_type: AttachedOrderType,
187    #[serde(skip_serializing_if = "Option::is_none")]
188    profit_taker_price: Option<Decimal>,
189    #[serde(skip_serializing_if = "Option::is_none")]
190    stop_loss_price: Option<Decimal>,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    time_in_force: Option<TimeInForceType>,
193    #[serde(skip_serializing_if = "Option::is_none")]
194    expire_time: Option<i64>,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    activate_order_type: Option<OrderType>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    profit_taker_submit_price: Option<Decimal>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    stop_loss_submit_price: Option<Decimal>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    activate_rth: Option<OutsideRTH>,
203    #[serde(skip_serializing_if = "Option::is_none")]
204    profit_taker_id: Option<i64>,
205    #[serde(skip_serializing_if = "Option::is_none")]
206    stop_loss_id: Option<i64>,
207    #[serde(skip_serializing_if = "Option::is_none")]
208    cancel_all_attached: Option<bool>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    main_id: Option<i64>,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    quantity: Option<Decimal>,
213    #[serde(skip_serializing_if = "Option::is_none")]
214    market_price: Option<Decimal>,
215    #[serde(skip_serializing_if = "Option::is_none")]
216    profit_taker_depth_level: Option<i32>,
217    #[serde(skip_serializing_if = "Option::is_none")]
218    stop_loss_depth_level: Option<i32>,
219}
220
221impl ReplaceAttachedParams {
222    /// Create new ReplaceAttachedParams
223    pub fn new(attached_order_type: AttachedOrderType) -> Self {
224        Self {
225            attached_order_type,
226            profit_taker_price: None,
227            stop_loss_price: None,
228            time_in_force: None,
229            expire_time: None,
230            activate_order_type: None,
231            profit_taker_submit_price: None,
232            stop_loss_submit_price: None,
233            activate_rth: None,
234            profit_taker_id: None,
235            stop_loss_id: None,
236            cancel_all_attached: None,
237            main_id: None,
238            quantity: None,
239            market_price: None,
240            profit_taker_depth_level: None,
241            stop_loss_depth_level: None,
242        }
243    }
244    /// Set the take-profit trigger price
245    pub fn profit_taker_price(self, v: Decimal) -> Self {
246        Self {
247            profit_taker_price: Some(v),
248            ..self
249        }
250    }
251    /// Set the stop-loss trigger price
252    pub fn stop_loss_price(self, v: Decimal) -> Self {
253        Self {
254            stop_loss_price: Some(v),
255            ..self
256        }
257    }
258    /// Set the time in force type
259    pub fn time_in_force(self, v: TimeInForceType) -> Self {
260        Self {
261            time_in_force: Some(v),
262            ..self
263        }
264    }
265    /// Set the expiry time (unix timestamp seconds)
266    pub fn expire_time(self, v: i64) -> Self {
267        Self {
268            expire_time: Some(v),
269            ..self
270        }
271    }
272    /// Set the order type to submit after trigger
273    pub fn activate_order_type(self, v: OrderType) -> Self {
274        Self {
275            activate_order_type: Some(v),
276            ..self
277        }
278    }
279    /// Set the take-profit limit price
280    pub fn profit_taker_submit_price(self, v: Decimal) -> Self {
281        Self {
282            profit_taker_submit_price: Some(v),
283            ..self
284        }
285    }
286    /// Set the stop-loss limit price
287    pub fn stop_loss_submit_price(self, v: Decimal) -> Self {
288        Self {
289            stop_loss_submit_price: Some(v),
290            ..self
291        }
292    }
293    /// Set the RTH setting for the activated order
294    pub fn activate_rth(self, v: OutsideRTH) -> Self {
295        Self {
296            activate_rth: Some(v),
297            ..self
298        }
299    }
300
301    /// Set the bid/ask depth level for the take-profit limit order submitted
302    /// price, valid when `activate_order_type` is `LIT`
303    pub fn profit_taker_depth_level(self, v: i32) -> Self {
304        Self {
305            profit_taker_depth_level: Some(v),
306            ..self
307        }
308    }
309
310    /// Set the bid/ask depth level for the stop-loss limit order submitted
311    /// price, valid when `activate_order_type` is `LIT`
312    pub fn stop_loss_depth_level(self, v: i32) -> Self {
313        Self {
314            stop_loss_depth_level: Some(v),
315            ..self
316        }
317    }
318    /// Set the take-profit order ID (for modifying existing attached order)
319    pub fn profit_taker_id(self, v: i64) -> Self {
320        Self {
321            profit_taker_id: Some(v),
322            ..self
323        }
324    }
325    /// Set the stop-loss order ID (for modifying existing attached order)
326    pub fn stop_loss_id(self, v: i64) -> Self {
327        Self {
328            stop_loss_id: Some(v),
329            ..self
330        }
331    }
332    /// Cancel all attached orders
333    pub fn cancel_all_attached(self) -> Self {
334        Self {
335            cancel_all_attached: Some(true),
336            ..self
337        }
338    }
339    /// Set the main order ID
340    pub fn main_id(self, v: i64) -> Self {
341        Self {
342            main_id: Some(v),
343            ..self
344        }
345    }
346    /// Set the quantity
347    pub fn quantity(self, v: Decimal) -> Self {
348        Self {
349            quantity: Some(v),
350            ..self
351        }
352    }
353    /// Set the market price
354    pub fn market_price(self, v: Decimal) -> Self {
355        Self {
356            market_price: Some(v),
357            ..self
358        }
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use time::macros::datetime;
365
366    use super::*;
367
368    #[test]
369    fn delayed_until() {
370        let opts = ReplaceOrderOptions::new("1", Decimal::from(100));
371        assert!(
372            serde_json::to_value(&opts)
373                .unwrap()
374                .get("delayed_until")
375                .is_none()
376        );
377
378        let opts = opts.delayed_until(datetime!(2026-01-01 00:00 UTC));
379        assert_eq!(
380            serde_json::to_value(&opts).unwrap()["delayed_until"],
381            serde_json::json!(1767225600)
382        );
383
384        let opts = opts.delayed_until(OffsetDateTime::UNIX_EPOCH + time::Duration::SECOND);
385        assert_eq!(
386            serde_json::to_value(&opts).unwrap()["delayed_until"],
387            serde_json::json!(2),
388            "a time that maps to the clear sentinel must not be sent as 1"
389        );
390
391        let opts = opts.clear_delayed_until();
392        assert_eq!(
393            serde_json::to_value(&opts).unwrap()["delayed_until"],
394            serde_json::json!(1)
395        );
396
397        // The last of `delayed_until` / `clear_delayed_until` wins.
398        let opts = opts.delayed_until(datetime!(2026-01-02 00:00 UTC));
399        assert_eq!(
400            serde_json::to_value(&opts).unwrap()["delayed_until"],
401            serde_json::json!(1767312000)
402        );
403    }
404
405    #[test]
406    fn attached_params_depth_level() {
407        let params = ReplaceAttachedParams::new(AttachedOrderType::Bracket);
408        let value = serde_json::to_value(&params).unwrap();
409        assert!(value.get("profit_taker_depth_level").is_none());
410        assert!(value.get("stop_loss_depth_level").is_none());
411
412        let params = params.profit_taker_depth_level(2).stop_loss_depth_level(5);
413        let value = serde_json::to_value(&params).unwrap();
414        assert_eq!(value["profit_taker_depth_level"], serde_json::json!(2));
415        assert_eq!(value["stop_loss_depth_level"], serde_json::json!(5));
416    }
417}