Skip to main content

longbridge/trade/requests/
submit_order.rs

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