Skip to main content

futu_mcp/tool_args/
trd.rs

1//! v1.4.110 P1-1: 拆自 `tool_args.rs` 按 handler 域分组.
2
3#![allow(unused_imports)]
4
5use rmcp::schemars;
6use serde::{Deserialize, Serialize};
7
8use crate::tool_enums;
9use crate::tool_enums::ToolEnum;
10
11use super::*;
12
13#[derive(Debug, Deserialize, schemars::JsonSchema)]
14#[serde(deny_unknown_fields)]
15pub struct TrdAccReq {
16    #[schemars(
17        description = "Trade market — accepts STRING (HK|US|CN|HKCC|FUTURES|SG|AU|JP|MY|CA) OR INT (1=HK, 2=US, 3=CN, 4=HKCC, 5=Futures, 6=SG, 8=AU, 15=JP, 111=MY, 112=CA per Trd_Common.TrdMarket). v1.4.90 P0-E + P1-G 双接 + 5 markets 扩, v1.4.93 C2 加 Futures=5 (期货下单入口)."
18    )]
19    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
20    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
21    pub market: String,
22    #[schemars(
23        description = "Trading account ID (u64). Either `acc_id` OR `card_num` is required. `card_num` accepts the 4-digit suffix shown in the App or the full 16-digit card number."
24    )]
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub acc_id: Option<u64>,
27    #[schemars(
28        description = "App-visible card number. Accepts 4-digit suffix or 16-digit full card number. Either `acc_id` OR `card_num` is required; if both are passed, daemon validates they refer to the same account."
29    )]
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub card_num: Option<String>,
32    #[schemars(description = "Trade environment: real|simulate (default real); alias: trd_env")]
33    // v1.4.83 §5 Phase 3: trd_env alias 对齐 py-futu-api TrdEnv
34    #[serde(default = "default_env", alias = "trd_env")]
35    pub env: String,
36
37    /// v1.4.103 (codex 51 F1 — B5): per-call API key override.
38    ///
39    /// 与 PlaceOrderReq.api_key 同模式 (优先级: tool args > HTTP Bearer > startup
40    /// key). 让 narrow-scope HTTP 客户端能在 read tool 上限定本次 call 的 caller
41    /// key. 留空 / 不传 → 走 HTTP Bearer 或 startup key.
42    #[schemars(
43        description = "Optional per-call API key plaintext. Same priority as PlaceOrderReq.api_key: tool args > HTTP Bearer > startup. Use to scope this call to a specific narrow key."
44    )]
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub api_key: Option<String>,
47
48    /// 货币种类 (HKD|USD|CNH|JPY|SGD|AUD|CAD|MYR|USDT).
49    ///
50    /// 留空时 daemon 按账户所属券商派生默认资金视图币种;显式传入时会
51    /// 校验该账户是否支持对应币种。普通单市场账户可能由 backend 忽略
52    /// 显式币种, 返回其账户基准币种。
53    #[schemars(
54        description = "Optional currency for fund response unit (HKD|USD|CNH|JPY|SGD|AUD|CAD|MYR|USDT). If omitted, daemon uses the broker/account default view currency. Explicit values are validated against the account; single-market accounts may ignore explicit currency and return their base currency."
55    )]
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub currency: Option<String>,
58}
59
60#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
61#[serde(deny_unknown_fields)]
62pub struct PlaceOrderReq {
63    #[schemars(
64        description = "Trade market — accepts STRING (HK|US|CN|HKCC|FUTURES|SG|AU|JP|MY|CA) OR INT (1=HK, 2=US, 3=CN, 4=HKCC, 5=Futures, 6=SG, 8=AU, 15=JP, 111=MY, 112=CA per Trd_Common.TrdMarket). v1.4.90 P0-E + P1-G 双接 + 5 markets 扩, v1.4.93 C2 加 Futures=5 (期货下单入口)."
65    )]
66    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
67    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
68    pub market: String,
69    #[schemars(
70        description = "Trading account ID (u64). Either `acc_id` OR `card_num` is required. ⚠️ Call `futu_list_accounts` first to discover acc_id — gateway does NOT infer a default. v1.4.105 D12: alternatively pass `card_num` (App 显示的 4 位末尾或 16 位完整) and daemon resolves it via GetAccList."
71    )]
72    // v1.4.105 D12: acc_id 改 default=0, 让 user 可改传 card_num. handler 端
73    // 如果 acc_id=0 且 card_num=None 仍 reject (二选一必填).
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub acc_id: Option<u64>,
76    #[schemars(
77        description = "v1.4.105 D12: card number (App 显示卡号). Accepts 4-digit suffix (e.g. `<card-suffix>`, App 内显示如 \"Margin Composite Account (`<card-suffix>`)\") OR 16-digit full (e.g. `<full-card-num>`). 示例为 synthetic placeholder, 不是真实账户信息. Daemon resolves via GetAccList → matched acc_id. **Either `acc_id` OR `card_num` required**; if both passed, daemon validates resolution matches acc_id (mismatch = 400 reject)."
78    )]
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub card_num: Option<String>,
81    #[schemars(
82        description = "Trade environment: real|simulate. Defaults to simulate for safety. Alias: trd_env"
83    )]
84    // v1.4.83 §5 Phase 3: trd_env alias 对齐 py-futu-api TrdEnv
85    #[serde(default = "default_env_simulate", alias = "trd_env")]
86    pub env: String,
87    #[schemars(description = "Order side: BUY|SELL|SELL_SHORT|BUY_BACK. Alias: trd_side")]
88    // v1.4.83 §5 Phase 3: trd_side alias 对齐 py-futu-api TrdSide
89    #[serde(alias = "trd_side")]
90    pub side: String,
91    #[schemars(
92        description = "Order type — accepts STRING enum OR INT (Trd_Common.OrderType). v1.4.90 P0-E 双接覆盖 v1.4.53 全 17 variant: \
93         NORMAL=1 (limit) | MARKET=2 | ABSOLUTE_LIMIT=5 | AUCTION=6 | AUCTION_LIMIT=7 | SPECIAL_LIMIT=8 | SPECIAL_LIMIT_ALL=9 | \
94         v1.4.53 条件单: STOP=10 (止损市价) | STOP_LIMIT=11 (止损限价) | MIT=12 (止盈触及市价) | LIT=13 (止盈触及限价) | TRAILING_STOP=14 (跟踪止损市价) | \
95         TRAILING_STOP_LIMIT=15 (跟踪止损限价) | TWAP_MARKET=16 | TWAP_LIMIT=17 | VWAP_MARKET=18 | VWAP_LIMIT=19. 条件单须搭配 `stop_price` / `trail_type` / `trail_value` / `trail_spread` 字段。alias: LIMIT → NORMAL."
96    )]
97    // v1.4.90 P0-E: int OR string 双接, normalize 到 canonical proto string
98    // (NORMAL/STOP/MIT/...). 老 6 variant alias 保留 backward-compat.
99    #[serde(
100        default = "default_order_type",
101        deserialize_with = "tool_enums::deser_order_type_as_string"
102    )]
103    pub order_type: String,
104    #[schemars(description = "Security code WITHOUT market prefix, e.g. 00700 / AAPL / 600519")]
105    pub code: String,
106    #[schemars(description = "Order quantity (shares / contracts)")]
107    pub qty: f64,
108    #[schemars(description = "Limit price (required for NORMAL; optional for MARKET)")]
109    pub price: Option<f64>,
110    #[schemars(
111        description = "Optional per-call API key override (plaintext). When set, this key is used for scope/limits/audit instead of the process-wide FUTU_MCP_API_KEY. Useful for multi-tenant scenarios where different calls should be billed / scoped to different keys."
112    )]
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub api_key: Option<String>,
115    #[schemars(
116        description = "v1.4.39: Optional idempotency key. When set, retries with the same key within 90-second TTL return the cached response WITHOUT placing a duplicate order. Example: generate a UUID per logical order intent; if agent retry fires, pass the same key. Without this field, each call places a separate order (backward-compatible with v1.4.38)."
117    )]
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub idempotency_key: Option<String>,
120    // ===== v1.4.53 F1 条件单字段 =====
121    #[schemars(
122        description = "v1.4.53 条件单: stop / take-profit trigger price (aka aux_price). Required for STOP / STOP_LIMIT / MIT (market-if-touched) / LIT (limit-if-touched). For MIT/LIT it's the take-profit trigger."
123    )]
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub stop_price: Option<f64>,
126    #[schemars(
127        description = "v1.4.53 trailing stop: 1=Ratio (percentage) / 2=Amount (absolute value). Only for TRAILING_STOP / TRAILING_STOP_LIMIT order types."
128    )]
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub trail_type: Option<i32>,
131    #[schemars(
132        description = "v1.4.53 trailing stop: trail percentage (if trail_type=1) or amount (if trail_type=2)."
133    )]
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub trail_value: Option<f64>,
136    #[schemars(
137        description = "v1.4.53 trailing stop limit: price spread for TRAILING_STOP_LIMIT (limit offset from trigger)."
138    )]
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub trail_spread: Option<f64>,
141}
142
143#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
144#[serde(deny_unknown_fields)]
145pub struct ModifyOrderReq {
146    #[schemars(
147        description = "Trade market — accepts STRING (HK|US|CN|HKCC|FUTURES|SG|AU|JP|MY|CA) OR INT (1=HK, 2=US, 3=CN, 4=HKCC, 5=Futures, 6=SG, 8=AU, 15=JP, 111=MY, 112=CA per Trd_Common.TrdMarket). v1.4.90 P0-E + P1-G 双接 + 5 markets 扩, v1.4.93 C2 加 Futures=5 (期货下单入口)."
148    )]
149    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
150    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
151    pub market: String,
152    #[schemars(
153        description = "Trading account ID (u64). Either `acc_id` OR `card_num` is required. v1.4.105 D12: alternative is `card_num`."
154    )]
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub acc_id: Option<u64>,
157    #[schemars(
158        description = "v1.4.105 D12: card number (4-digit suffix or 16-digit full). See PlaceOrderReq.card_num for semantics."
159    )]
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub card_num: Option<String>,
162    #[schemars(
163        description = "Trade environment: real|simulate (default simulate); alias: trd_env"
164    )]
165    // v1.4.83 §5 Phase 3
166    #[serde(default = "default_env_simulate", alias = "trd_env")]
167    pub env: String,
168    #[schemars(
169        description = "Order ID to modify. Accepts numeric orderID (integer or integer string) OR backend orderIDEx string such as FU.../FH...; string recommended for JS clients since u64 > 2^53 loses precision as JSON number."
170    )]
171    // v1.4.110: 双接 numeric orderID + FU/FH orderIDEx.
172    #[serde(deserialize_with = "deser_order_id_raw_from_int_or_str")]
173    pub order_id: String,
174    #[schemars(
175        description = "Modify op: NORMAL (change qty/price) | CANCEL | DISABLE | ENABLE | DELETE"
176    )]
177    #[serde(default = "default_modify_op")]
178    pub op: String,
179    #[schemars(description = "New quantity (for NORMAL op)")]
180    pub qty: Option<f64>,
181    #[schemars(description = "New price (for NORMAL op)")]
182    pub price: Option<f64>,
183    #[schemars(description = "Optional per-call API key override. See PlaceOrderReq.api_key.")]
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub api_key: Option<String>,
186    #[schemars(
187        description = "v1.4.39: Optional idempotency key (90s TTL). See PlaceOrderReq.idempotency_key."
188    )]
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    pub idempotency_key: Option<String>,
191}
192
193#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
194#[serde(deny_unknown_fields)]
195pub struct CancelOrderReq {
196    #[schemars(
197        description = "Trade market — accepts STRING (HK|US|CN|HKCC|FUTURES|SG|AU|JP|MY|CA) OR INT (1=HK, 2=US, 3=CN, 4=HKCC, 5=Futures, 6=SG, 8=AU, 15=JP, 111=MY, 112=CA per Trd_Common.TrdMarket). v1.4.90 P0-E + P1-G 双接 + 5 markets 扩, v1.4.93 C2 加 Futures=5 (期货下单入口)."
198    )]
199    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
200    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
201    pub market: String,
202    #[schemars(
203        description = "Trading account ID (u64). Either `acc_id` OR `card_num` is required. v1.4.105 D12: alternative is `card_num`."
204    )]
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    pub acc_id: Option<u64>,
207    #[schemars(
208        description = "v1.4.105 D12: card number (4-digit suffix or 16-digit full). See PlaceOrderReq.card_num for semantics."
209    )]
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub card_num: Option<String>,
212    #[schemars(
213        description = "Trade environment: real|simulate (default simulate); alias: trd_env"
214    )]
215    // v1.4.83 §5 Phase 3
216    #[serde(default = "default_env_simulate", alias = "trd_env")]
217    pub env: String,
218    #[schemars(
219        description = "Order ID to cancel. Accepts numeric orderID (integer or integer string) OR backend orderIDEx string such as FU.../FH...; string recommended for JS clients since u64 > 2^53 loses precision as JSON number."
220    )]
221    // v1.4.110: 双接 numeric orderID + FU/FH orderIDEx.
222    #[serde(deserialize_with = "deser_order_id_raw_from_int_or_str")]
223    pub order_id: String,
224    #[schemars(description = "Optional per-call API key override. See PlaceOrderReq.api_key.")]
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub api_key: Option<String>,
227    #[schemars(
228        description = "v1.4.39: Optional idempotency key (90s TTL). See PlaceOrderReq.idempotency_key."
229    )]
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub idempotency_key: Option<String>,
232}
233
234#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
235#[serde(deny_unknown_fields)]
236pub struct ReconfirmOrderReq {
237    #[schemars(
238        description = "Trade market — accepts STRING (HK|US|CN|HKCC|FUTURES|SG|AU|JP|MY|CA) OR INT (1=HK, 2=US, 3=CN, 4=HKCC, 5=Futures, 6=SG, 8=AU, 15=JP, 111=MY, 112=CA per Trd_Common.TrdMarket)."
239    )]
240    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
241    pub market: String,
242    #[schemars(
243        description = "Trading account ID (u64). Either `acc_id` OR `card_num` is required."
244    )]
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub acc_id: Option<u64>,
247    #[schemars(
248        description = "Card number (4-digit suffix or 16-digit full). Either `acc_id` OR `card_num` is required."
249    )]
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub card_num: Option<String>,
252    #[schemars(
253        description = "Trade environment: real|simulate (default simulate); alias: trd_env"
254    )]
255    #[serde(default = "default_env_simulate", alias = "trd_env")]
256    pub env: String,
257    #[schemars(
258        description = "FTAPI numeric order_id to reconfirm. Accepts JSON number or integer string; orderIDEx strings are not supported by Trd_ReconfirmOrder."
259    )]
260    #[serde(deserialize_with = "deser_order_id_raw_from_int_or_str")]
261    pub order_id: String,
262    #[schemars(description = "Reconfirm reason int per Trd_Common.ReconfirmOrderReason.")]
263    pub reason: i32,
264    #[schemars(description = "Optional per-call API key override. See PlaceOrderReq.api_key.")]
265    #[serde(default, skip_serializing_if = "Option::is_none")]
266    pub api_key: Option<String>,
267}
268
269#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
270#[serde(deny_unknown_fields)]
271pub struct UnlockTradeReq {
272    /// true = 解锁(默认),false = 重新锁住交易 cipher(防御用)
273    #[schemars(
274        description = "true to unlock trading (default); false to lock trading cipher back (defensive). Lock does not require a password."
275    )]
276    #[serde(default = "default_true")]
277    pub unlock: bool,
278    /// v1.4.31: OTP / 令牌动态密码。仅在首次调用返回 `need_otp=true` 或
279    /// `err_code=-8` 时需要传;普通账号(无 2FA)留空即可。
280    #[schemars(
281        description = "OTP / 2FA token (plaintext). Only required when a previous unlock call returned need_otp=true or err_code=-8 (TRADE_AUTH_NEED_AUTH_TOKEN). Leave empty for accounts without 2FA. Alias: token / one_time_password"
282    )]
283    // v1.4.84 §5 B1
284    #[serde(
285        default,
286        alias = "token",
287        alias = "one_time_password",
288        skip_serializing_if = "Option::is_none"
289    )]
290    pub otp: Option<String>,
291    /// v1.4.33: 只解锁该券商下的账户(对齐 C++ OpenD per-broker unlock 语义)。
292    /// SecurityFirm enum (i32):
293    ///   1=FutuSecurities (HK), 2=FutuInc (US/MooMoo), 3=FutuSG,
294    ///   4=FutuAU, 5=FutuCA, 6=FutuMY, 7=FutuJP.
295    /// 留空 = 解锁所有 broker(v1.4.31 行为,向后兼容)。
296    #[schemars(
297        description = "Optional. Restrict unlock to a single security firm (broker). SecurityFirm enum (i32): 1=FutuHK, 2=FutuUS/MooMoo, 3=FutuSG, 4=FutuAU, 5=FutuCA, 6=FutuMY, 7=FutuJP. If omitted, unlocks all brokers in parallel (backward-compatible default). Alias: broker / security_firm_id"
298    )]
299    // v1.4.84 §5 B1
300    #[serde(
301        default,
302        alias = "broker",
303        alias = "security_firm_id",
304        skip_serializing_if = "Option::is_none"
305    )]
306    pub security_firm: Option<i32>,
307    /// v1.4.34: 只解锁指定 acc_id 列表(正整数;空 / omitted = 不 per-account filter)。
308    /// 和 security_firm 同时传时取交集(账户必须同时满足)。解决同 broker 内
309    /// 影子账户拖垮主账户的场景——LLM 显式传主账户 acc_id 可以避免影子
310    /// 账户进 unlock 请求。
311    #[schemars(
312        description = "Optional. Array of positive non-zero u64 acc_ids to unlock (empty / omitted = no per-account filter, use security_firm rule or unlock all). Intersects with security_firm: account must satisfy BOTH. Use when you need to exclude a shadow sub-account that shares a broker with the main account — pass only the main acc_id here. Alias: account_ids / accounts"
313    )]
314    // v1.4.84 §5 B1
315    #[serde(
316        default,
317        alias = "account_ids",
318        alias = "accounts",
319        skip_serializing_if = "Option::is_none"
320    )]
321    pub acc_ids: Option<Vec<u64>>,
322}
323
324#[derive(Debug, Deserialize, schemars::JsonSchema)]
325#[serde(deny_unknown_fields)]
326pub struct MaxTrdQtysReq {
327    #[schemars(
328        description = "Trade market — accepts STRING (HK|US|CN|HKCC|FUTURES|SG|AU|JP|MY|CA) OR INT (1=HK, 2=US, 3=CN, 4=HKCC, 5=Futures, 6=SG, 8=AU, 15=JP, 111=MY, 112=CA per Trd_Common.TrdMarket). v1.4.90 P0-E + P1-G 双接 + 5 markets 扩, v1.4.93 C2 加 Futures=5 (期货下单入口)."
329    )]
330    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
331    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
332    pub market: String,
333    #[schemars(
334        description = "Trading account ID (u64). ⚠️ Call `futu_list_accounts` first to discover — gateway does NOT infer a default; omitting or inventing an id fails."
335    )]
336    pub acc_id: u64,
337    #[schemars(description = "Trade environment: real|simulate (default real); alias: trd_env")]
338    // v1.4.84 §5 B1
339    #[serde(default = "default_env", alias = "trd_env")]
340    pub env: String,
341    // v1.4.41 (P2.6): description 强调 INTEGER 避免 LLM 传 "NORMAL" 撞 -32602.
342    // v1.4.42 (P3.3 修): 加 deserialize_with 接 int OR string —— LLM agent 传
343    // "NORMAL" / "MARKET" 等字符串也能工作(跟 PlaceOrderReq / ModifyOrderReq
344    // 的 string enum 风格统一)。backward-compat:旧客户端传 int 继续能用。
345    #[schemars(
346        description = "Order type. Accepts both INTEGER (1=NORMAL/limit, 2=MARKET, 5=AUCTION, 6=ABSOLUTE_LIMIT, 7=SPECIAL_LIMIT) and STRING enum (NORMAL|MARKET|AUCTION|ABSOLUTE_LIMIT|SPECIAL_LIMIT). Example: 1 or \"NORMAL\"."
347    )]
348    #[serde(deserialize_with = "deser_int_or_order_type_str")]
349    pub order_type: i32,
350    #[schemars(
351        description = "Security code WITHOUT market prefix (e.g. 00700 / AAPL); alias: symbol / stock"
352    )]
353    // v1.4.84 §5 B1
354    #[serde(alias = "symbol", alias = "stock")]
355    pub code: String,
356    #[schemars(description = "Limit price (pass 0.0 for market orders)")]
357    pub price: f64,
358    #[schemars(description = "Existing order_id (for modify-order max-qty calc, optional)")]
359    #[serde(default)]
360    pub order_id: Option<u64>,
361}
362
363#[derive(Debug, Deserialize, schemars::JsonSchema)]
364#[serde(deny_unknown_fields)]
365pub struct OrderFeeReq {
366    #[schemars(
367        description = "Trade market — accepts STRING (HK|US|CN|HKCC|FUTURES|SG|AU|JP|MY|CA) OR INT (1=HK, 2=US, 3=CN, 4=HKCC, 5=Futures, 6=SG, 8=AU, 15=JP, 111=MY, 112=CA per Trd_Common.TrdMarket). v1.4.90 P0-E + P1-G 双接 + 5 markets 扩, v1.4.93 C2 加 Futures=5 (期货下单入口)."
368    )]
369    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
370    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
371    pub market: String,
372    #[schemars(
373        description = "Trading account ID (u64). ⚠️ Call `futu_list_accounts` first to discover — gateway does NOT infer a default; omitting or inventing an id fails."
374    )]
375    pub acc_id: u64,
376    #[schemars(description = "Trade environment: real|simulate (default real); alias: trd_env")]
377    // v1.4.84 §5 B1
378    #[serde(default = "default_env", alias = "trd_env")]
379    pub env: String,
380    #[schemars(
381        description = "Order_id_ex list (strings) — returned by place_order response; alias: order_ids_ex / order_ids"
382    )]
383    // v1.4.84 §5 B1
384    #[serde(alias = "order_ids_ex", alias = "order_ids")]
385    pub order_id_ex_list: Vec<String>,
386}
387
388#[derive(Debug, Deserialize, schemars::JsonSchema)]
389#[serde(deny_unknown_fields)]
390pub struct MarginRatioReq {
391    #[schemars(
392        description = "Trade market — accepts STRING (HK|US|CN|HKCC|FUTURES|SG|AU|JP|MY|CA) OR INT (1=HK, 2=US, 3=CN, 4=HKCC, 5=Futures, 6=SG, 8=AU, 15=JP, 111=MY, 112=CA per Trd_Common.TrdMarket). v1.4.90 P0-E + P1-G 双接 + 5 markets 扩, v1.4.93 C2 加 Futures=5 (期货下单入口)."
393    )]
394    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
395    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
396    pub market: String,
397    #[schemars(
398        description = "Trading account ID (u64). ⚠️ Call `futu_list_accounts` first to discover — gateway does NOT infer a default; omitting or inventing an id fails."
399    )]
400    pub acc_id: u64,
401    #[schemars(description = "Trade environment: real|simulate (default real); alias: trd_env")]
402    // v1.4.84 §5 B1
403    #[serde(default = "default_env", alias = "trd_env")]
404    pub env: String,
405    #[schemars(
406        description = "Symbols in MARKET.CODE format (e.g. HK.00700, US.AAPL); alias: symbols / code_list / symbol_list"
407    )]
408    // v1.4.84 §5 B1
409    #[serde(alias = "symbols", alias = "code_list", alias = "symbol_list")]
410    pub codes: Vec<String>,
411}
412
413#[derive(Debug, Deserialize, schemars::JsonSchema)]
414#[serde(deny_unknown_fields)]
415pub struct HistoryQueryReq {
416    #[schemars(
417        description = "Trade market — accepts STRING (HK|US|CN|HKCC|FUTURES|SG|AU|JP|MY|CA) OR INT (1=HK, 2=US, 3=CN, 4=HKCC, 5=Futures, 6=SG, 8=AU, 15=JP, 111=MY, 112=CA per Trd_Common.TrdMarket). v1.4.90 P0-E + P1-G 双接 + 5 markets 扩, v1.4.93 C2 加 Futures=5 (期货下单入口)."
418    )]
419    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
420    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
421    pub market: String,
422    #[schemars(
423        description = "Trading account ID (u64). ⚠️ Call `futu_list_accounts` first to discover — gateway does NOT infer a default; omitting or inventing an id fails."
424    )]
425    pub acc_id: u64,
426    #[schemars(description = "Trade environment: real|simulate (default real); alias: trd_env")]
427    // v1.4.83 §5 Phase 3: trd_env alias 对齐 py-futu-api TrdEnv
428    #[serde(default = "default_env", alias = "trd_env")]
429    pub env: String,
430    #[schemars(
431        description = "Filter by codes (empty = all). Each item is bare code without market prefix. \
432                       Alias: symbols / symbol_list"
433    )]
434    // v1.4.83 §5 Phase 3: 接受 symbols / symbol_list alias
435    #[serde(default, alias = "symbols", alias = "symbol_list")]
436    pub code_list: Vec<String>,
437    #[schemars(
438        description = "Begin time 'yyyy-MM-dd HH:mm:ss' (optional); alias: begin / start_time / from"
439    )]
440    #[serde(default, alias = "begin", alias = "start_time", alias = "from")]
441    pub begin_time: Option<String>,
442    #[schemars(description = "End time 'yyyy-MM-dd HH:mm:ss' (optional); alias: end / to")]
443    #[serde(default, alias = "end", alias = "to")]
444    pub end_time: Option<String>,
445}
446
447#[derive(Debug, Deserialize, schemars::JsonSchema)]
448#[serde(deny_unknown_fields)]
449pub struct CapitalFlowReq {
450    #[schemars(
451        description = "Security symbol in MARKET.CODE format (e.g. HK.00700); alias: code / stock"
452    )]
453    // v1.4.83 §5 Phase 3
454    #[serde(alias = "code", alias = "stock")]
455    pub symbol: String,
456    #[schemars(description = "Period type: 1=INTRADAY 2=DAY 3=WEEK 4=MONTH (default 1)")]
457    #[serde(default)]
458    pub period_type: Option<i32>,
459    #[schemars(
460        description = "Begin time 'yyyy-MM-dd' (optional, DAY/WEEK/MONTH only); alias: begin / start_time / from"
461    )]
462    #[serde(default, alias = "begin", alias = "start_time", alias = "from")]
463    pub begin_time: Option<String>,
464    #[schemars(description = "End time 'yyyy-MM-dd' (optional); alias: end / to")]
465    #[serde(default, alias = "end", alias = "to")]
466    pub end_time: Option<String>,
467}
468
469#[derive(Debug, Deserialize, schemars::JsonSchema)]
470#[serde(deny_unknown_fields)]
471pub struct AccCashFlowReq {
472    #[schemars(description = "Trade env: real / simulate (default real); alias: trd_env")]
473    // v1.4.84 §5 B1
474    #[serde(default = "default_env", alias = "trd_env")]
475    pub env: String,
476    #[schemars(
477        description = "Trading account ID (u64). ⚠️ Call `futu_list_accounts` first to discover — gateway does NOT infer a default."
478    )]
479    pub acc_id: u64,
480    #[schemars(
481        description = "Trade market — accepts STRING (HK / US / CN / HKCC / SG / AU / JP / MY / CA) OR INT (1=HK, 2=US, 3=CN, 4=HKCC, 6=SG, 8=AU, 15=JP, 111=MY, 112=CA per Trd_Common.TrdMarket). v1.4.90 P0-E + P1-G 双接 + 5 markets 扩."
482    )]
483    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
484    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
485    pub market: String,
486    #[schemars(
487        description = "Clearing date (yyyy-MM-dd); queries flow entries for that day; alias: date / query_date"
488    )]
489    // v1.4.84 §5 B1
490    #[serde(alias = "date", alias = "query_date")]
491    pub clearing_date: String,
492    #[schemars(
493        description = "Direction: 1=InFlow, 2=OutFlow, omit for both; alias: flow_direction"
494    )]
495    #[serde(default, alias = "flow_direction")]
496    pub direction: Option<i32>,
497}
498
499#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
500#[serde(deny_unknown_fields)]
501pub struct CancelAllOrderReq {
502    #[schemars(description = "Trading env: simulate (default) / real; alias: trd_env")]
503    // v1.4.84 §5 B1
504    #[serde(default = "default_env_simulate", alias = "trd_env")]
505    pub env: String,
506    #[schemars(
507        description = "Trading account ID (u64). ⚠️ Call `futu_list_accounts` first to discover — gateway does NOT infer a default."
508    )]
509    pub acc_id: u64,
510    #[schemars(
511        description = "Market (REQUIRED, NOT optional) — accepts STRING (HK|US|CN|HKCC|FUTURES|SG|AU|JP|MY|CA) OR INT (1=HK, 2=US, 3=CN, 4=HKCC, 5=Futures, 6=SG, 8=AU, 15=JP, 111=MY, 112=CA). Leaving empty returns a validation error — the backend needs a specific market to cancel orders in. v1.4.90 P0-E + P1-G 双接 + 5 markets 扩, v1.4.93 C2 加 Futures=5."
512    )]
513    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical (空字符串经
514    // 自定义 deser default fn 保留,runtime validate 报错).
515    #[serde(default, deserialize_with = "deser_trd_market_string_allow_empty")]
516    pub market: String,
517    #[schemars(description = "Per-call API key override (optional)")]
518    #[serde(default)]
519    pub api_key: Option<String>,
520}
521
522impl CancelAllOrderReq {
523    /// v1.4.84 §5 B4: runtime validate — market 非空.
524    ///
525    /// Schema description 已明写 REQUIRED, 但 Rust 字段是 `String` +
526    /// `#[serde(default)]`, default 是空字符串, serde 层放过. Runtime check
527    /// 返清晰错误.
528    pub fn validate(&self) -> Result<(), String> {
529        if self.market.trim().is_empty() {
530            return Err(
531                "CancelAllOrderReq: `market` is required and must be non-empty \
532                 (e.g. HK / US / HKCC / A_SH / A_SZ / SG / JP / AU / CA)"
533                    .to_string(),
534            );
535        }
536        Ok(())
537    }
538}
539
540#[derive(Debug, Deserialize, schemars::JsonSchema)]
541#[serde(deny_unknown_fields)]
542pub struct CashLogReq {
543    #[schemars(description = "Trade env: real / simulate (default real)")]
544    #[serde(default = "default_env", alias = "trd_env")]
545    pub env: String,
546    #[schemars(description = "Trading account ID (u64). Call futu_list_accounts first.")]
547    pub acc_id: u64,
548    #[schemars(
549        description = "Optional legacy market hint. Accepted for backward compatibility (HK/US/CN/HKCC/SG/AU/JP/MY/CA/HKFUND/USFUND or 1/2/3/4/6/8/15/111/112/113/123), \
550                       but cash-log identity does not trust this field: daemon derives backend market from acc_id/account cache."
551    )]
552    #[serde(
553        default,
554        deserialize_with = "tool_enums::deser_trd_market_as_option_string"
555    )]
556    pub market: Option<String>,
557    #[schemars(description = "Begin time (epoch seconds, optional)")]
558    #[serde(default)]
559    pub begin_time: Option<u64>,
560    #[schemars(description = "End time (epoch seconds, optional)")]
561    #[serde(default)]
562    pub end_time: Option<u64>,
563    #[schemars(description = "Business group ID filter (default all)")]
564    #[serde(default)]
565    pub biz_group_id: Option<u32>,
566    #[schemars(
567        description = "Business sub-group ID filter (optional; value from futu_get_biz_group sub_groups)"
568    )]
569    #[serde(default)]
570    pub biz_sub_group_id: Option<u32>,
571    #[schemars(description = "In/out direction: 1=in, 2=out, 0/omit=all")]
572    #[serde(default)]
573    pub in_out: Option<u32>,
574    #[schemars(description = "Search keyword (optional)")]
575    #[serde(default)]
576    pub keyword: Option<String>,
577    #[schemars(description = "Stock symbol (e.g. AAPL.US, 00700.HK), exact match (optional)")]
578    #[serde(default)]
579    pub symbol: Option<String>,
580    #[schemars(
581        description = "Backend stock_id filter (optional; use when available from upstream/account UI data)"
582    )]
583    #[serde(default)]
584    pub stock_id: Option<u64>,
585    #[schemars(
586        description = "Cursor: log_id from previous response next_log_id (omit for first page)"
587    )]
588    #[serde(default)]
589    pub log_id: Option<String>,
590    #[schemars(description = "Max entries per response (daemon uses mobile default 50 if omit)")]
591    #[serde(default)]
592    pub max_cnt: Option<u32>,
593    #[schemars(description = "Currency filter: CNY/HKD/USD/JPY/SGD (optional)")]
594    #[serde(default)]
595    pub currency: Option<String>,
596}
597
598#[derive(Debug, Deserialize, schemars::JsonSchema)]
599#[serde(deny_unknown_fields)]
600pub struct CashDetailReq {
601    #[schemars(description = "Trade env: real / simulate (default real)")]
602    #[serde(default = "default_env", alias = "trd_env")]
603    pub env: String,
604    #[schemars(description = "Trading account ID (u64)")]
605    pub acc_id: u64,
606    #[schemars(
607        description = "Optional legacy market hint; accepted for backward compatibility but ignored. Daemon derives backend market from acc_id/account cache."
608    )]
609    #[serde(
610        default,
611        deserialize_with = "tool_enums::deser_trd_market_as_option_string"
612    )]
613    pub market: Option<String>,
614    #[schemars(description = "Cash log ID (from futu_get_cash_log response)")]
615    pub log_id: String,
616}
617
618// v1.4.95 U2-D Tier M (mobile-driven): per-account margin info request
619#[derive(Debug, Deserialize, schemars::JsonSchema)]
620#[serde(deny_unknown_fields)]
621pub struct MarginInfoReq {
622    #[schemars(description = "Trade env: real / simulate (default real)")]
623    #[serde(default = "default_env", alias = "trd_env")]
624    pub env: String,
625    #[schemars(description = "Trading account ID (u64). Call futu_list_accounts first.")]
626    pub acc_id: u64,
627    #[schemars(
628        description = "Market: HK / US / CN_AH (only these 3 supported; mobile cmd 3101/3102/3107). Other markets: use futu_get_margin_ratio (per-security ratio)."
629    )]
630    pub market: String,
631}
632
633// v1.4.95 U2-A Tier M (mobile-driven): account compliance flag query request
634#[derive(Debug, Deserialize, schemars::JsonSchema)]
635#[serde(deny_unknown_fields)]
636pub struct AccountFlagReq {
637    #[schemars(description = "Trade env: real / simulate (default real)")]
638    #[serde(default = "default_env", alias = "trd_env")]
639    pub env: String,
640    #[schemars(description = "Trading account ID (u64) for per-broker routing")]
641    pub acc_id: u64,
642    #[schemars(
643        description = "Flag ID to query. Common: 5=US 期权确认, 8=期权测评, 10=基金 KYC (R1~R5), 11=HK 期权确认, 16=PDT 风披, 22=衍生品风批 (合并新), 23=美股 OTC, 24=港股期权测评, 25=算法交易风披, 34=人脸识别风批, 46=OpenAPI 免责. Full 36+ list in proto header."
644    )]
645    pub flag_id: u32,
646}
647
648#[derive(Debug, Deserialize, schemars::JsonSchema)]
649#[serde(deny_unknown_fields)]
650pub struct BondAccountReq {
651    #[schemars(description = "Trade env: real / simulate (default real)")]
652    #[serde(default = "default_env", alias = "trd_env")]
653    pub env: String,
654    #[schemars(description = "Trading account ID (u64) for per-broker routing")]
655    pub acc_id: u64,
656    #[schemars(description = "Market: HK / US / SG (仅 3 市场有债券业务)")]
657    pub market: String,
658}