longbridge/trade/requests/
submit_order.rs1use 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#[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 pub fn limit_depth_level(self, level: i32) -> Self {
156 Self {
157 limit_depth_level: Some(level),
158 ..self
159 }
160 }
161
162 pub fn trigger_count(self, count: i32) -> Self {
164 Self {
165 trigger_count: Some(count),
166 ..self
167 }
168 }
169
170 pub fn monitor_price(self, price: Decimal) -> Self {
172 Self {
173 monitor_price: Some(price),
174 ..self
175 }
176 }
177
178 #[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 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 #[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 pub fn attached_params(self, params: SubmitAttachedParams) -> Self {
215 Self {
216 attached_params: Some(params),
217 ..self
218 }
219 }
220}
221
222#[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 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 pub fn profit_taker_price(self, v: Decimal) -> Self {
267 Self {
268 profit_taker_price: Some(v),
269 ..self
270 }
271 }
272 pub fn stop_loss_price(self, v: Decimal) -> Self {
274 Self {
275 stop_loss_price: Some(v),
276 ..self
277 }
278 }
279 pub fn time_in_force(self, v: TimeInForceType) -> Self {
281 Self {
282 time_in_force: Some(v),
283 ..self
284 }
285 }
286 pub fn expire_time(self, v: i64) -> Self {
288 Self {
289 expire_time: Some(v),
290 ..self
291 }
292 }
293 pub fn activate_order_type(self, v: OrderType) -> Self {
295 Self {
296 activate_order_type: Some(v),
297 ..self
298 }
299 }
300 pub fn profit_taker_submit_price(self, v: Decimal) -> Self {
302 Self {
303 profit_taker_submit_price: Some(v),
304 ..self
305 }
306 }
307 pub fn stop_loss_submit_price(self, v: Decimal) -> Self {
309 Self {
310 stop_loss_submit_price: Some(v),
311 ..self
312 }
313 }
314 pub fn activate_rth(self, v: OutsideRTH) -> Self {
316 Self {
317 activate_rth: Some(v),
318 ..self
319 }
320 }
321
322 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 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(¶ms).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(¶ms).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}