Skip to main content

longbridge/grid/
types.rs

1//! Grid trading types
2
3use num_enum::{FromPrimitive, IntoPrimitive};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use time::OffsetDateTime;
7
8use crate::serde_utils;
9
10/// Serde helper for response numeric fields: they arrive as strings and an
11/// empty string means "no value". Deserializes to `Option<Decimal>` (empty →
12/// `None`) and serializes back to a string (`None` → `""`) to preserve the
13/// exact wire format.
14mod opt_decimal_string {
15    use rust_decimal::Decimal;
16    use serde::{Deserialize, Deserializer, Serializer};
17
18    pub(crate) fn serialize<S: Serializer>(
19        value: &Option<Decimal>,
20        serializer: S,
21    ) -> Result<S::Ok, S::Error> {
22        match value {
23            Some(v) => serializer.serialize_str(&v.to_string()),
24            None => serializer.serialize_str(""),
25        }
26    }
27
28    pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
29        deserializer: D,
30    ) -> Result<Option<Decimal>, D::Error> {
31        let s = String::deserialize(deserializer)?;
32        if s.is_empty() {
33            Ok(None)
34        } else {
35            s.parse::<Decimal>()
36                .map(Some)
37                .map_err(serde::de::Error::custom)
38        }
39    }
40}
41
42/// How grid trigger thresholds are interpreted (wire: `i32`).
43#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, IntoPrimitive)]
44#[repr(i32)]
45pub enum TriggerPriceType {
46    /// Trigger by absolute price spread
47    Spread = 1,
48    /// Trigger by percent
49    Percent = 2,
50    /// Unknown value, preserved verbatim
51    #[num_enum(catch_all)]
52    Unknown(i32),
53}
54
55impl Default for TriggerPriceType {
56    fn default() -> Self {
57        Self::Unknown(0)
58    }
59}
60
61impl Serialize for TriggerPriceType {
62    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
63        i32::from(*self).serialize(serializer)
64    }
65}
66
67impl<'de> Deserialize<'de> for TriggerPriceType {
68    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
69        Ok(Self::from(i32::deserialize(deserializer)?))
70    }
71}
72
73/// Time in force for a grid order (wire: `i32`).
74#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, IntoPrimitive)]
75#[repr(i32)]
76pub enum GridTimeInForce {
77    /// Day order
78    Day = 0,
79    /// Good-til-canceled
80    GoodTilCanceled = 1,
81    /// Good-til-date
82    GoodTilDate = 6,
83    /// Unknown value, preserved verbatim
84    #[num_enum(catch_all)]
85    Unknown(i32),
86}
87
88impl Default for GridTimeInForce {
89    fn default() -> Self {
90        Self::Day
91    }
92}
93
94impl Serialize for GridTimeInForce {
95    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
96        i32::from(*self).serialize(serializer)
97    }
98}
99
100impl<'de> Deserialize<'de> for GridTimeInForce {
101    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
102        Ok(Self::from(i32::deserialize(deserializer)?))
103    }
104}
105
106/// Action taken when a grid boundary is reached (wire: `i32`).
107#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, FromPrimitive, IntoPrimitive)]
108#[repr(i32)]
109pub enum GridLimitEvent {
110    /// Ignore — keep the grid running
111    Ignore = 1,
112    /// Close the position at the last price
113    CloseAtLast = 2,
114    /// Unknown value, preserved verbatim
115    #[num_enum(catch_all)]
116    Unknown(i32),
117}
118
119impl Default for GridLimitEvent {
120    fn default() -> Self {
121        Self::Unknown(0)
122    }
123}
124
125impl Serialize for GridLimitEvent {
126    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
127        i32::from(*self).serialize(serializer)
128    }
129}
130
131impl<'de> Deserialize<'de> for GridLimitEvent {
132    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
133        Ok(Self::from(i32::deserialize(deserializer)?))
134    }
135}
136
137/// Grid trading rule — parameters for submit / replace.
138///
139/// Mirrors the `GridTradingRule` message in the gridtrading proto. Prices and
140/// quantities are decimals serialized as strings; enum-like fields are raw
141/// integers whose code tables are documented inline.
142#[derive(Debug, Clone, Default, Serialize, Deserialize)]
143pub struct GridTradeRule {
144    /// Base price the grid is anchored to
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub submitted_base_price: Option<Decimal>,
147    /// Upper price bound
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub upper_limit_price: Option<Decimal>,
150    /// Lower price bound
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub lower_limit_price: Option<Decimal>,
153    /// Trigger price type (only `1` / `2` allowed)
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub trigger_price_type: Option<TriggerPriceType>,
156    /// Upward trigger spread (absolute)
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub trigger_spread_up: Option<Decimal>,
159    /// Downward trigger spread (absolute)
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub trigger_spread_down: Option<Decimal>,
162    /// Upward trigger percent
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub trigger_percent_up: Option<Decimal>,
165    /// Downward trigger percent
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub trigger_percent_down: Option<Decimal>,
168    /// Whether a single grid level may trigger multiple times
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub multiple_trigger: Option<bool>,
171    /// Time in force (`0` = Day, `1` = GTC, `6` = GTD)
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub time_in_force: Option<GridTimeInForce>,
174    /// Quantity handled when the upper bound is reached
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub upper_limit_quantity: Option<Decimal>,
177    /// Quantity handled when the lower bound is reached
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub lower_limit_quantity: Option<Decimal>,
180    /// Expiry time (unix seconds), used with GTD
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub expire_time: Option<i64>,
183    /// Action when the upper bound is reached (only `1` / `2` allowed)
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub upper_limit_event: Option<GridLimitEvent>,
186    /// Action when the lower bound is reached (only `1` / `2` allowed)
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub lower_limit_event: Option<GridLimitEvent>,
189    /// Sell-side order-book depth (-5..5, `0` = use `grid_order_type_up`)
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub trigger_sell_depth: Option<i32>,
192    /// Buy-side order-book depth (-5..5, `0` = use `grid_order_type_down`)
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub trigger_buy_depth: Option<i32>,
195    /// Quantity per trigger
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub trigger_quantity: Option<Decimal>,
198    /// Whether short selling is allowed
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub support_shortsell: Option<bool>,
201    /// Regular trading hours flag (`0` / `1` / `2`)
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub rth: Option<i32>,
204    /// Sell-side order type when depth is `0` (`GMO` / `GLO` / `GTG`)
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub grid_order_type_up: Option<String>,
207    /// Buy-side order type when depth is `0` (`GMO` / `GLO` / `GTG`)
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub grid_order_type_down: Option<String>,
210}
211
212/// How a grid's up/down trigger thresholds are expressed. Percent and spread
213/// are mutually exclusive; modeling them as an enum makes the choice explicit
214/// (instead of four independent optional fields).
215#[derive(Debug, Clone, Copy)]
216pub enum GridTrigger {
217    /// Trigger by percent (`up`, `down`)
218    Percent {
219        /// Upward trigger percent
220        up: Decimal,
221        /// Downward trigger percent
222        down: Decimal,
223    },
224    /// Trigger by absolute price spread (`up`, `down`)
225    Spread {
226        /// Upward trigger spread
227        up: Decimal,
228        /// Downward trigger spread
229        down: Decimal,
230    },
231}
232
233impl GridTradeRule {
234    /// Create a rule with the fields a valid grid order requires. The gateway
235    /// still validates business rules, but this makes the minimum field set
236    /// visible in the type signature instead of leaving all fields optional.
237    #[allow(clippy::too_many_arguments)]
238    pub fn new(
239        base_price: Decimal,
240        upper_price: Decimal,
241        lower_price: Decimal,
242        trigger: GridTrigger,
243        quantity: Decimal,
244        upper_quantity: Decimal,
245        lower_quantity: Decimal,
246        time_in_force: GridTimeInForce,
247    ) -> Self {
248        let mut rule = GridTradeRule {
249            submitted_base_price: Some(base_price),
250            upper_limit_price: Some(upper_price),
251            lower_limit_price: Some(lower_price),
252            trigger_quantity: Some(quantity),
253            upper_limit_quantity: Some(upper_quantity),
254            lower_limit_quantity: Some(lower_quantity),
255            time_in_force: Some(time_in_force),
256            ..Default::default()
257        };
258        match trigger {
259            GridTrigger::Percent { up, down } => {
260                rule.trigger_price_type = Some(TriggerPriceType::Percent);
261                rule.trigger_percent_up = Some(up);
262                rule.trigger_percent_down = Some(down);
263            }
264            GridTrigger::Spread { up, down } => {
265                rule.trigger_price_type = Some(TriggerPriceType::Spread);
266                rule.trigger_spread_up = Some(up);
267                rule.trigger_spread_down = Some(down);
268            }
269        }
270        rule
271    }
272
273    /// Set the actions taken at the upper / lower bounds.
274    #[must_use]
275    pub fn limit_events(mut self, upper: GridLimitEvent, lower: GridLimitEvent) -> Self {
276        self.upper_limit_event = Some(upper);
277        self.lower_limit_event = Some(lower);
278        self
279    }
280
281    /// Set the sell / buy order-book depths (`0` = use the order type).
282    #[must_use]
283    pub fn depths(mut self, sell: i32, buy: i32) -> Self {
284        self.trigger_sell_depth = Some(sell);
285        self.trigger_buy_depth = Some(buy);
286        self
287    }
288
289    /// Set the sell / buy order types (`GMO` / `GLO` / `GTG`).
290    #[must_use]
291    pub fn order_types(mut self, up: impl Into<String>, down: impl Into<String>) -> Self {
292        self.grid_order_type_up = Some(up.into());
293        self.grid_order_type_down = Some(down.into());
294        self
295    }
296
297    /// Allow a single grid level to trigger multiple times.
298    #[must_use]
299    pub fn multiple_trigger(mut self, value: bool) -> Self {
300        self.multiple_trigger = Some(value);
301        self
302    }
303
304    /// Allow short selling.
305    #[must_use]
306    pub fn support_shortsell(mut self, value: bool) -> Self {
307        self.support_shortsell = Some(value);
308        self
309    }
310
311    /// Set the regular-trading-hours flag (`0` / `1` / `2`).
312    #[must_use]
313    pub fn rth(mut self, value: i32) -> Self {
314        self.rth = Some(value);
315        self
316    }
317
318    /// Set the expiry time (unix seconds), used with a GTD time-in-force.
319    #[must_use]
320    pub fn expire_time(mut self, unix_seconds: i64) -> Self {
321        self.expire_time = Some(unix_seconds);
322        self
323    }
324}
325
326/// A grid trading order (element of the list / by-ids responses).
327///
328/// Fields reflect the gateway JSON; the security is exposed via `symbol`
329/// (`700.HK`). Numeric values are returned as strings; unknown fields are
330/// ignored (`#[serde(default)]`).
331#[derive(Debug, Clone, Default, Serialize, Deserialize)]
332#[serde(default)]
333pub struct GridOrder {
334    /// Grid master order ID
335    pub order_id: String,
336    /// Security symbol (e.g. `700.HK`)
337    pub symbol: String,
338    /// Stock name
339    pub stock_name: String,
340    /// Market
341    pub market: String,
342    /// Order status
343    pub status: String,
344    /// Grid running status
345    pub grid_status: String,
346    /// Submitted base price
347    #[serde(with = "opt_decimal_string")]
348    pub submitted_base_price: Option<Decimal>,
349    /// Current base price
350    #[serde(with = "opt_decimal_string")]
351    pub current_base_price: Option<Decimal>,
352    /// Base price before the last trigger
353    #[serde(with = "opt_decimal_string")]
354    pub pre_trigger_base_price: Option<Decimal>,
355    /// Base price after the last trigger
356    #[serde(with = "opt_decimal_string")]
357    pub post_trigger_base_price: Option<Decimal>,
358    /// Upper price bound
359    #[serde(with = "opt_decimal_string")]
360    pub upper_limit_price: Option<Decimal>,
361    /// Lower price bound
362    #[serde(with = "opt_decimal_string")]
363    pub lower_limit_price: Option<Decimal>,
364    /// Trigger price type (`1` = spread, `2` = percent)
365    pub trigger_price_type: TriggerPriceType,
366    /// Upward trigger spread
367    #[serde(with = "opt_decimal_string")]
368    pub trigger_spread_up: Option<Decimal>,
369    /// Downward trigger spread
370    #[serde(with = "opt_decimal_string")]
371    pub trigger_spread_down: Option<Decimal>,
372    /// Upward trigger percent
373    #[serde(with = "opt_decimal_string")]
374    pub trigger_percent_up: Option<Decimal>,
375    /// Downward trigger percent
376    #[serde(with = "opt_decimal_string")]
377    pub trigger_percent_down: Option<Decimal>,
378    /// Pullback percent
379    #[serde(with = "opt_decimal_string")]
380    pub pullback_percent: Option<Decimal>,
381    /// Pullback spread
382    #[serde(with = "opt_decimal_string")]
383    pub pullback_spread: Option<Decimal>,
384    /// Rebound percent
385    #[serde(with = "opt_decimal_string")]
386    pub rebound_percent: Option<Decimal>,
387    /// Rebound spread
388    #[serde(with = "opt_decimal_string")]
389    pub rebound_spread: Option<Decimal>,
390    /// Sell-side execution order type (e.g. `MO`)
391    pub trigger_sell_order_type: String,
392    /// Buy-side execution order type (e.g. `MO`)
393    pub trigger_buy_order_type: String,
394    /// Sell-side order-book depth
395    pub trigger_sell_depth: i32,
396    /// Buy-side order-book depth
397    pub trigger_buy_depth: i32,
398    /// Quantity per trigger
399    #[serde(with = "opt_decimal_string")]
400    pub trigger_quantity: Option<Decimal>,
401    /// Quantity per sell trigger
402    #[serde(with = "opt_decimal_string")]
403    pub trigger_sell_quantity: Option<Decimal>,
404    /// Quantity per buy trigger
405    #[serde(with = "opt_decimal_string")]
406    pub trigger_buy_quantity: Option<Decimal>,
407    /// Quantity handled at the upper bound
408    #[serde(with = "opt_decimal_string")]
409    pub upper_limit_quantity: Option<Decimal>,
410    /// Quantity handled at the lower bound
411    #[serde(with = "opt_decimal_string")]
412    pub lower_limit_quantity: Option<Decimal>,
413    /// Action at the upper bound
414    pub upper_limit_event: GridLimitEvent,
415    /// Action at the lower bound
416    pub lower_limit_event: GridLimitEvent,
417    /// Whether a single grid level may trigger multiple times
418    pub multiple_trigger: bool,
419    /// Number of times the grid has triggered
420    pub trigger_times: i32,
421    /// Accumulated bought quantity
422    #[serde(with = "opt_decimal_string")]
423    pub total_buy_quantity: Option<Decimal>,
424    /// Accumulated sold quantity
425    #[serde(with = "opt_decimal_string")]
426    pub total_sell_quantity: Option<Decimal>,
427    /// Accumulated profit balance
428    #[serde(with = "opt_decimal_string")]
429    pub total_profit_balance: Option<Decimal>,
430    /// Settlement currency
431    pub settlement_currency: String,
432    /// Time in force (`0` = Day, `1` = GTC, `6` = GTD)
433    pub time_in_force: GridTimeInForce,
434    /// Expiry date (`YYYY-MM-DD`, GTD)
435    pub gtd: String,
436    /// Created time (RFC3339)
437    #[serde(
438        deserialize_with = "serde_utils::timestamp_opt::deserialize",
439        serialize_with = "serde_utils::rfc3339_opt::serialize"
440    )]
441    pub created_at: Option<OffsetDateTime>,
442    /// Regular trading hours flag
443    pub rth: i32,
444    /// Whether short selling is allowed
445    pub support_shortsell: bool,
446    /// Sell-side grid order type (`GMO` / `GLO` / `GTG`)
447    pub grid_order_type_up: String,
448    /// Buy-side grid order type (`GMO` / `GLO` / `GTG`)
449    pub grid_order_type_down: String,
450}
451
452/// A triggered sub-order carried in the grid order detail.
453#[derive(Debug, Clone, Default, Serialize, Deserialize)]
454#[serde(default)]
455pub struct GridOrderSubOrder {
456    /// Sub-order ID
457    pub id: String,
458    /// Order price
459    #[serde(with = "opt_decimal_string")]
460    pub price: Option<Decimal>,
461    /// Order type
462    pub order_type: String,
463    /// Order quantity
464    #[serde(with = "opt_decimal_string")]
465    pub quantity: Option<Decimal>,
466    /// Executed quantity
467    #[serde(with = "opt_decimal_string")]
468    pub executed_qty: Option<Decimal>,
469    /// Buy / sell direction
470    pub action: i32,
471    /// Order status
472    pub status: String,
473    /// Submitted time (RFC3339)
474    #[serde(
475        deserialize_with = "serde_utils::timestamp_opt::deserialize",
476        serialize_with = "serde_utils::rfc3339_opt::serialize"
477    )]
478    pub submitted_at: Option<OffsetDateTime>,
479    /// Regular trading hours flag
480    pub rth: i32,
481}
482
483/// A grid order lifecycle-history entry carried in the grid order detail.
484#[derive(Debug, Clone, Default, Serialize, Deserialize)]
485#[serde(default)]
486pub struct GridOrderHistory {
487    /// History entry ID (paging cursor)
488    pub history_id: String,
489    /// Created time (RFC3339)
490    #[serde(
491        deserialize_with = "serde_utils::timestamp_opt::deserialize",
492        serialize_with = "serde_utils::rfc3339_opt::serialize"
493    )]
494    pub created_at: Option<OffsetDateTime>,
495    /// Status at this point
496    pub status: String,
497    /// Suspend reason, if any
498    pub suspend_reason: String,
499    /// Additional reason detail, if any
500    pub reason: String,
501}
502
503/// Detail of a grid trading order.
504#[derive(Debug, Clone, Default, Serialize, Deserialize)]
505#[serde(default)]
506pub struct GridOrderDetail {
507    /// Grid master order ID
508    pub order_id: String,
509    /// Security symbol (e.g. `700.HK`)
510    pub symbol: String,
511    /// Stock name
512    pub stock_name: String,
513    /// Order status
514    pub status: String,
515    /// Grid running status
516    pub grid_status: String,
517    /// Suspend reason, if any
518    pub suspend_reason: String,
519    /// Sleeping reason, if any
520    pub sleeping_reason: String,
521    /// Submitted base price
522    #[serde(with = "opt_decimal_string")]
523    pub submitted_base_price: Option<Decimal>,
524    /// Current base price
525    #[serde(with = "opt_decimal_string")]
526    pub current_base_price: Option<Decimal>,
527    /// Upper price bound
528    #[serde(with = "opt_decimal_string")]
529    pub upper_limit_price: Option<Decimal>,
530    /// Lower price bound
531    #[serde(with = "opt_decimal_string")]
532    pub lower_limit_price: Option<Decimal>,
533    /// Trigger price type (`1` = spread, `2` = percent)
534    pub trigger_price_type: TriggerPriceType,
535    /// Upward trigger spread
536    #[serde(with = "opt_decimal_string")]
537    pub trigger_spread_up: Option<Decimal>,
538    /// Downward trigger spread
539    #[serde(with = "opt_decimal_string")]
540    pub trigger_spread_down: Option<Decimal>,
541    /// Upward trigger percent
542    #[serde(with = "opt_decimal_string")]
543    pub trigger_percent_up: Option<Decimal>,
544    /// Downward trigger percent
545    #[serde(with = "opt_decimal_string")]
546    pub trigger_percent_down: Option<Decimal>,
547    /// Pullback percent
548    #[serde(with = "opt_decimal_string")]
549    pub pullback_percent: Option<Decimal>,
550    /// Pullback spread
551    #[serde(with = "opt_decimal_string")]
552    pub pullback_spread: Option<Decimal>,
553    /// Rebound percent
554    #[serde(with = "opt_decimal_string")]
555    pub rebound_percent: Option<Decimal>,
556    /// Rebound spread
557    #[serde(with = "opt_decimal_string")]
558    pub rebound_spread: Option<Decimal>,
559    /// Whether a single grid level may trigger multiple times
560    pub multiple_trigger: bool,
561    /// Time in force (`0` = Day, `1` = GTC, `6` = GTD)
562    pub time_in_force: GridTimeInForce,
563    /// Quantity per trigger
564    #[serde(with = "opt_decimal_string")]
565    pub trigger_quantity: Option<Decimal>,
566    /// Quantity per sell trigger
567    #[serde(with = "opt_decimal_string")]
568    pub trigger_sell_quantity: Option<Decimal>,
569    /// Quantity per buy trigger
570    #[serde(with = "opt_decimal_string")]
571    pub trigger_buy_quantity: Option<Decimal>,
572    /// Quantity handled at the upper bound
573    #[serde(with = "opt_decimal_string")]
574    pub upper_limit_quantity: Option<Decimal>,
575    /// Quantity handled at the lower bound
576    #[serde(with = "opt_decimal_string")]
577    pub lower_limit_quantity: Option<Decimal>,
578    /// Action at the upper bound
579    pub upper_limit_event: GridLimitEvent,
580    /// Action at the lower bound
581    pub lower_limit_event: GridLimitEvent,
582    /// Sell-side order-book depth
583    pub trigger_sell_depth: i32,
584    /// Buy-side order-book depth
585    pub trigger_buy_depth: i32,
586    /// Created time (RFC3339)
587    #[serde(
588        deserialize_with = "serde_utils::timestamp_opt::deserialize",
589        serialize_with = "serde_utils::rfc3339_opt::serialize"
590    )]
591    pub created_at: Option<OffsetDateTime>,
592    /// Last updated time (RFC3339)
593    #[serde(
594        deserialize_with = "serde_utils::timestamp_opt::deserialize",
595        serialize_with = "serde_utils::rfc3339_opt::serialize"
596    )]
597    pub updated_at: Option<OffsetDateTime>,
598    /// Settlement currency
599    pub settlement_currency: String,
600    /// Expiry time (RFC3339)
601    #[serde(
602        deserialize_with = "serde_utils::timestamp_opt::deserialize",
603        serialize_with = "serde_utils::rfc3339_opt::serialize"
604    )]
605    pub expire_time: Option<OffsetDateTime>,
606    /// Expiry date (`YYYY-MM-DD`, GTD)
607    pub gtd: String,
608    /// Triggered sub-orders
609    pub grid_sub_orders: Vec<GridOrderSubOrder>,
610    /// Whether there are more sub-orders to page
611    pub sub_has_more: bool,
612    /// Lifecycle history entries
613    pub grid_order_history: Vec<GridOrderHistory>,
614    /// Whether there are more history entries to page
615    pub history_has_more: bool,
616    /// Whether short selling is allowed
617    pub support_shortsell: bool,
618    /// Regular trading hours flag
619    pub rth: i32,
620    /// Sell-side grid order type (`GMO` / `GLO` / `GTG`)
621    pub grid_order_type_up: String,
622    /// Buy-side grid order type (`GMO` / `GLO` / `GTG`)
623    pub grid_order_type_down: String,
624}
625
626/// A grid trigger-history entry (one triggered order).
627#[derive(Debug, Clone, Default, Serialize, Deserialize)]
628#[serde(default)]
629pub struct TriggerOrder {
630    /// Triggered order ID
631    pub id: String,
632    /// Order status
633    pub status: String,
634    /// Stock name
635    pub name: String,
636    /// Security symbol (e.g. `700.HK`)
637    pub symbol: String,
638    /// Order price
639    #[serde(with = "opt_decimal_string")]
640    pub price: Option<Decimal>,
641    /// Order quantity
642    #[serde(with = "opt_decimal_string")]
643    pub quantity: Option<Decimal>,
644    /// Executed average price
645    #[serde(with = "opt_decimal_string")]
646    pub executed_price: Option<Decimal>,
647    /// Executed total quantity
648    #[serde(with = "opt_decimal_string")]
649    pub executed_qty: Option<Decimal>,
650    /// Submitted time (RFC3339)
651    #[serde(
652        deserialize_with = "serde_utils::timestamp_opt::deserialize",
653        serialize_with = "serde_utils::rfc3339_opt::serialize"
654    )]
655    pub submitted_at: Option<OffsetDateTime>,
656    /// Buy / sell direction
657    pub action: i32,
658    /// Order type
659    pub order_type: String,
660    /// Trigger price
661    #[serde(with = "opt_decimal_string")]
662    pub trigger_price: Option<Decimal>,
663    /// Rejection reason, if any
664    pub msg: String,
665    /// Settlement currency
666    pub currency: String,
667    /// Latest quote price
668    #[serde(with = "opt_decimal_string")]
669    pub last_done: Option<Decimal>,
670    /// Last updated time (RFC3339)
671    #[serde(
672        deserialize_with = "serde_utils::timestamp_opt::deserialize",
673        serialize_with = "serde_utils::rfc3339_opt::serialize"
674    )]
675    pub updated_at: Option<OffsetDateTime>,
676    /// Time in force (`0` = Day, `1` = GTC, `6` = GTD)
677    pub time_in_force: GridTimeInForce,
678    /// Expiry date (`YYYY-MM-DD`, GTD)
679    pub gtd: String,
680    /// Trigger time (RFC3339)
681    #[serde(
682        deserialize_with = "serde_utils::timestamp_opt::deserialize",
683        serialize_with = "serde_utils::rfc3339_opt::serialize"
684    )]
685    pub trigger_at: Option<OffsetDateTime>,
686    /// Conditional trigger status
687    pub trigger_status: i32,
688}
689
690/// A price-step (bid-size) rule entry from the symbol-info response.
691#[derive(Debug, Clone, Default, Serialize, Deserialize)]
692#[serde(default)]
693pub struct GridBidSize {
694    /// Range start price (inclusive)
695    #[serde(with = "opt_decimal_string")]
696    pub str_proceed: Option<Decimal>,
697    /// Range end price
698    #[serde(with = "opt_decimal_string")]
699    pub end_proceed: Option<Decimal>,
700    /// Price step within the range
701    #[serde(with = "opt_decimal_string")]
702    pub bid_size: Option<Decimal>,
703}
704
705/// Channel / authorization info nested in the symbol-info response, holding the
706/// fields the grid order window needs.
707#[derive(Debug, Clone, Default, Serialize, Deserialize)]
708#[serde(default)]
709pub struct GridChannelInfo {
710    /// Whether the strategy compliance authorization has been granted
711    pub strategy_granted: bool,
712    /// Whether the RTH toggle is supported
713    pub support_rth: bool,
714    /// Trading currency
715    pub currency: String,
716    /// Supported settlement currencies
717    pub settlement_currency: Vec<String>,
718}
719
720/// Security (symbol) info (`/v1/orders/info`) used to build a grid order.
721///
722/// Returns the target security's name, latest price, lot sizes, price-step
723/// rules and channel / authorization info needed by the grid order window. The
724/// endpoint takes a `symbol` query parameter (e.g. `700.HK`).
725#[derive(Debug, Clone, Default, Serialize, Deserialize)]
726#[serde(default)]
727pub struct GridSymbolInfo {
728    /// Security name
729    pub name: String,
730    /// Latest quote price
731    #[serde(with = "opt_decimal_string")]
732    pub last_done: Option<Decimal>,
733    /// Board lot size
734    #[serde(with = "opt_decimal_string")]
735    pub lot_size: Option<Decimal>,
736    /// Buy-side board lot size
737    #[serde(with = "opt_decimal_string")]
738    pub buy_lot_size: Option<Decimal>,
739    /// Sell-side board lot size
740    #[serde(with = "opt_decimal_string")]
741    pub sell_lot_size: Option<Decimal>,
742    /// Price-step (bid-size) rule table
743    pub bid_sizes: Vec<GridBidSize>,
744    /// Channel / authorization info (strategy grant, RTH, currencies).
745    ///
746    /// Uses `alias` (not `rename`) so deserialization still accepts the
747    /// server's `channel_infos` key, while serialization emits the clean
748    /// `channel_info` name for downstream consumers (e.g. CLI `--format json`).
749    #[serde(alias = "channel_infos")]
750    pub channel_info: GridChannelInfo,
751}