Skip to main content

futu_mcp/tool_args/
trd.rs

1//! v1.4.110 P1-1: 拆自 `tool_args.rs` 按 handler 域分组.
2
3use rmcp::schemars;
4use serde::{Deserialize, Serialize};
5
6use crate::tool_enums;
7
8use super::*;
9
10#[derive(Debug, Deserialize, schemars::JsonSchema)]
11#[serde(deny_unknown_fields)]
12pub struct TrdAccReq {
13    #[schemars(
14        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)."
15    )]
16    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
17    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
18    pub market: String,
19    #[schemars(
20        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."
21    )]
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub acc_id: Option<u64>,
24    #[schemars(
25        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."
26    )]
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub card_num: Option<String>,
29    #[schemars(description = "Trade environment: real|simulate (default real); alias: trd_env")]
30    // v1.4.83 §5 Phase 3: trd_env alias 对齐 py-futu-api TrdEnv
31    #[serde(default = "default_env", alias = "trd_env")]
32    pub env: String,
33
34    /// v1.4.103 (codex 51 F1 — B5): per-call API key override.
35    ///
36    /// 与 PlaceOrderReq.api_key 同模式 (优先级: tool args > HTTP Bearer > startup
37    /// key). 让 narrow-scope HTTP 客户端能在 read tool 上限定本次 call 的 caller
38    /// key. 留空 / 不传 → 走 HTTP Bearer 或 startup key.
39    #[schemars(
40        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."
41    )]
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub api_key: Option<String>,
44
45    /// 货币种类 (HKD|USD|CNH|JPY|SGD|AUD|CAD|MYR|USDT).
46    ///
47    /// 留空时 daemon 按账户所属券商派生默认资金视图币种;显式传入时会
48    /// 校验该账户是否支持对应币种。普通单市场账户可能由 backend 忽略
49    /// 显式币种, 返回其账户基准币种。
50    #[schemars(
51        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."
52    )]
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub currency: Option<String>,
55}
56
57#[derive(Debug, Deserialize, schemars::JsonSchema)]
58#[serde(deny_unknown_fields)]
59pub struct PositionReq {
60    #[schemars(
61        description = "Trade market — accepts STRING (HK|US|CN|HKCC|FUTURES|SG|AU|JP|MY|CA|CRYPTO) OR INT per Trd_Common.TrdMarket."
62    )]
63    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
64    pub market: String,
65    #[schemars(
66        description = "Trading account ID (u64). Either `acc_id` OR `card_num` is required."
67    )]
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub acc_id: Option<u64>,
70    #[schemars(
71        description = "App-visible card number. Accepts 4-digit suffix or 16-digit full card number."
72    )]
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub card_num: Option<String>,
75    #[schemars(description = "Trade environment: real|simulate (default real); alias: trd_env")]
76    #[serde(default = "default_env", alias = "trd_env")]
77    pub env: String,
78    #[schemars(description = "Optional per-call API key plaintext.")]
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub api_key: Option<String>,
81    #[schemars(
82        description = "Optional position view currency (HKD|USD|CNH|JPY|SGD|AUD|CAD|MYR|USDT). Crypto accounts require an explicit view currency; non-crypto accounts may ignore it."
83    )]
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub currency: Option<String>,
86    #[schemars(description = "Request option strategy/combo position view. Defaults to false.")]
87    #[serde(default, alias = "optionStrategyView")]
88    pub option_strategy_view: bool,
89}
90
91impl PositionReq {
92    pub fn as_trd_acc_req(&self) -> TrdAccReq {
93        TrdAccReq {
94            market: self.market.clone(),
95            acc_id: self.acc_id,
96            card_num: self.card_num.clone(),
97            env: self.env.clone(),
98            api_key: self.api_key.clone(),
99            currency: self.currency.clone(),
100        }
101    }
102}
103
104#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
105#[serde(deny_unknown_fields)]
106pub struct PlaceOrderReq {
107    #[schemars(
108        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)."
109    )]
110    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
111    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
112    pub market: String,
113    #[schemars(
114        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. Alternatively pass `card_num` (App 显示的 4 位末尾或 16 位完整) and daemon resolves it via GetAccList."
115    )]
116    // v1.4.105 D12: acc_id 改 default=0, 让 user 可改传 card_num. handler 端
117    // 如果 acc_id=0 且 card_num=None 仍 reject (二选一必填).
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub acc_id: Option<u64>,
120    #[schemars(
121        description = "Card number shown by the 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)."
122    )]
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub card_num: Option<String>,
125    #[schemars(
126        description = "Trade environment: real|simulate. Defaults to simulate for safety. Alias: trd_env"
127    )]
128    // v1.4.83 §5 Phase 3: trd_env alias 对齐 py-futu-api TrdEnv
129    #[serde(default = "default_env_simulate", alias = "trd_env")]
130    pub env: String,
131    #[schemars(description = "Order side: BUY|SELL|SELL_SHORT|BUY_BACK. Alias: trd_side")]
132    // v1.4.83 §5 Phase 3: trd_side alias 对齐 py-futu-api TrdSide
133    #[serde(alias = "trd_side")]
134    pub side: String,
135    #[schemars(
136        description = "Order type — accepts STRING enum OR INT (Trd_Common.OrderType): \
137         NORMAL=1 (limit) | MARKET=2 | ABSOLUTE_LIMIT=5 | AUCTION=6 | AUCTION_LIMIT=7 | SPECIAL_LIMIT=8 | SPECIAL_LIMIT_ALL=9 | \
138         STOP=10 (止损市价) | STOP_LIMIT=11 (止损限价) | MIT=12 (止盈触及市价) | LIT=13 (止盈触及限价) | TRAILING_STOP=14 (跟踪止损市价) | \
139         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."
140    )]
141    // v1.4.90 P0-E: int OR string 双接, normalize 到 canonical proto string
142    // (NORMAL/STOP/MIT/...). 老 6 variant alias 保留 backward-compat.
143    #[serde(
144        default = "default_order_type",
145        deserialize_with = "tool_enums::deser_order_type_as_string"
146    )]
147    pub order_type: String,
148    #[schemars(description = "Security code WITHOUT market prefix, e.g. 00700 / AAPL / 600519")]
149    pub code: String,
150    #[schemars(description = "Order quantity (shares / contracts)")]
151    pub qty: f64,
152    #[schemars(description = "Limit price (required for NORMAL; optional for MARKET)")]
153    pub price: Option<f64>,
154    #[schemars(
155        description = "JP sub-account type (Trd_Common.TrdSubAccType / TrdHeader.jpAccType). Required by JP account backend paths when no position_id/order_id path supplies the sub-account context. Alias: jpAccType."
156    )]
157    #[serde(default, alias = "jpAccType", skip_serializing_if = "Option::is_none")]
158    pub jp_acc_type: Option<i32>,
159    #[schemars(
160        description = "Optional per-call API key override (plaintext). When set, this key is used for authorization and usage limits instead of the process-wide FUTU_MCP_API_KEY. Useful for multi-tenant scenarios where different calls should be billed or scoped to different keys."
161    )]
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub api_key: Option<String>,
164    #[schemars(
165        description = "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."
166    )]
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub idempotency_key: Option<String>,
169    // ===== v1.4.53 F1 条件单字段 =====
170    #[schemars(
171        description = "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."
172    )]
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub stop_price: Option<f64>,
175    #[schemars(
176        description = "Trailing stop type: 1=Ratio (percentage) / 2=Amount (absolute value). Only for TRAILING_STOP / TRAILING_STOP_LIMIT order types."
177    )]
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    pub trail_type: Option<i32>,
180    #[schemars(
181        description = "Trailing stop value: trail percentage (if trail_type=1) or amount (if trail_type=2)."
182    )]
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub trail_value: Option<f64>,
185    #[schemars(
186        description = "Trailing stop limit price spread for TRAILING_STOP_LIMIT (limit offset from trigger)."
187    )]
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub trail_spread: Option<f64>,
190}
191
192impl PlaceOrderReq {
193    pub fn validate(&self) -> Result<(), String> {
194        validate_positive_finite_f64("PlaceOrderReq", "qty", self.qty)?;
195        validate_optional_finite_f64("PlaceOrderReq", "price", self.price)?;
196        validate_optional_finite_f64("PlaceOrderReq", "stop_price", self.stop_price)?;
197        validate_optional_finite_f64("PlaceOrderReq", "trail_value", self.trail_value)?;
198        validate_optional_finite_f64("PlaceOrderReq", "trail_spread", self.trail_spread)?;
199        Ok(())
200    }
201}
202
203#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
204#[serde(deny_unknown_fields)]
205pub struct ModifyOrderReq {
206    #[schemars(
207        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)."
208    )]
209    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
210    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
211    pub market: String,
212    #[schemars(
213        description = "Trading account ID (u64). Either `acc_id` OR `card_num` is required; alternatively pass `card_num`."
214    )]
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    pub acc_id: Option<u64>,
217    #[schemars(
218        description = "Card number (4-digit suffix or 16-digit full). See PlaceOrderReq.card_num for semantics."
219    )]
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    pub card_num: Option<String>,
222    #[schemars(
223        description = "Trade environment: real|simulate (default simulate); alias: trd_env"
224    )]
225    // v1.4.83 §5 Phase 3
226    #[serde(default = "default_env_simulate", alias = "trd_env")]
227    pub env: String,
228    #[schemars(
229        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."
230    )]
231    // v1.4.110: 双接 numeric orderID + FU/FH orderIDEx.
232    #[serde(deserialize_with = "deser_order_id_raw_from_int_or_str")]
233    pub order_id: String,
234    #[schemars(
235        description = "Modify op: NORMAL (change qty/price) | CANCEL | DISABLE | ENABLE | DELETE"
236    )]
237    #[serde(default = "default_modify_op")]
238    pub op: String,
239    #[schemars(description = "New quantity (for NORMAL op)")]
240    pub qty: Option<f64>,
241    #[schemars(description = "New price (for NORMAL op)")]
242    pub price: Option<f64>,
243    #[schemars(
244        description = "JP sub-account type (Trd_Common.TrdSubAccType / TrdHeader.jpAccType). Alias: jpAccType."
245    )]
246    #[serde(default, alias = "jpAccType", skip_serializing_if = "Option::is_none")]
247    pub jp_acc_type: Option<i32>,
248    #[schemars(description = "Optional per-call API key override. See PlaceOrderReq.api_key.")]
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub api_key: Option<String>,
251    #[schemars(
252        description = "Optional idempotency key (90s TTL). See PlaceOrderReq.idempotency_key."
253    )]
254    #[serde(default, skip_serializing_if = "Option::is_none")]
255    pub idempotency_key: Option<String>,
256}
257
258impl ModifyOrderReq {
259    pub fn validate(&self) -> Result<(), String> {
260        if let Some(qty) = self.qty {
261            validate_non_negative_finite_f64("ModifyOrderReq", "qty", qty)?;
262        }
263        validate_optional_finite_f64("ModifyOrderReq", "price", self.price)?;
264        Ok(())
265    }
266}
267
268#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
269#[serde(deny_unknown_fields)]
270pub struct CancelOrderReq {
271    #[schemars(
272        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)."
273    )]
274    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
275    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
276    pub market: String,
277    #[schemars(
278        description = "Trading account ID (u64). Either `acc_id` OR `card_num` is required; alternatively pass `card_num`."
279    )]
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub acc_id: Option<u64>,
282    #[schemars(
283        description = "Card number (4-digit suffix or 16-digit full). See PlaceOrderReq.card_num for semantics."
284    )]
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub card_num: Option<String>,
287    #[schemars(
288        description = "Trade environment: real|simulate (default simulate); alias: trd_env"
289    )]
290    // v1.4.83 §5 Phase 3
291    #[serde(default = "default_env_simulate", alias = "trd_env")]
292    pub env: String,
293    #[schemars(
294        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."
295    )]
296    // v1.4.110: 双接 numeric orderID + FU/FH orderIDEx.
297    #[serde(deserialize_with = "deser_order_id_raw_from_int_or_str")]
298    pub order_id: String,
299    #[schemars(
300        description = "JP sub-account type (Trd_Common.TrdSubAccType / TrdHeader.jpAccType). Alias: jpAccType."
301    )]
302    #[serde(default, alias = "jpAccType", skip_serializing_if = "Option::is_none")]
303    pub jp_acc_type: Option<i32>,
304    #[schemars(description = "Optional per-call API key override. See PlaceOrderReq.api_key.")]
305    #[serde(default, skip_serializing_if = "Option::is_none")]
306    pub api_key: Option<String>,
307    #[schemars(
308        description = "Optional idempotency key (90s TTL). See PlaceOrderReq.idempotency_key."
309    )]
310    #[serde(default, skip_serializing_if = "Option::is_none")]
311    pub idempotency_key: Option<String>,
312}
313
314#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
315#[serde(deny_unknown_fields)]
316pub struct ReconfirmOrderReq {
317    #[schemars(
318        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)."
319    )]
320    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
321    pub market: String,
322    #[schemars(
323        description = "Trading account ID (u64). Either `acc_id` OR `card_num` is required."
324    )]
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    pub acc_id: Option<u64>,
327    #[schemars(
328        description = "Card number (4-digit suffix or 16-digit full). Either `acc_id` OR `card_num` is required."
329    )]
330    #[serde(default, skip_serializing_if = "Option::is_none")]
331    pub card_num: Option<String>,
332    #[schemars(
333        description = "Trade environment: real|simulate (default simulate); alias: trd_env"
334    )]
335    #[serde(default = "default_env_simulate", alias = "trd_env")]
336    pub env: String,
337    #[schemars(
338        description = "FTAPI numeric order_id to reconfirm. Accepts JSON number or integer string; orderIDEx strings are not supported by Trd_ReconfirmOrder."
339    )]
340    #[serde(deserialize_with = "deser_order_id_raw_from_int_or_str")]
341    pub order_id: String,
342    #[schemars(description = "Reconfirm reason int per Trd_Common.ReconfirmOrderReason.")]
343    pub reason: i32,
344    #[schemars(
345        description = "JP sub-account type (Trd_Common.TrdSubAccType / TrdHeader.jpAccType). Alias: jpAccType."
346    )]
347    #[serde(default, alias = "jpAccType", skip_serializing_if = "Option::is_none")]
348    pub jp_acc_type: Option<i32>,
349    #[schemars(description = "Optional per-call API key override. See PlaceOrderReq.api_key.")]
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    pub api_key: Option<String>,
352}
353
354#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
355#[serde(deny_unknown_fields)]
356pub struct ComboOrderProtoJsonReq {
357    #[schemars(
358        description = "Official Trd_PlaceComboOrder.C2S JSON. Field names use generated proto serde snake_case. `packet_id` may be omitted; daemon fills it before forwarding."
359    )]
360    pub c2s_json: String,
361
362    #[schemars(description = "Optional per-call API key override. See PlaceOrderReq.api_key.")]
363    #[serde(default, skip_serializing_if = "Option::is_none")]
364    pub api_key: Option<String>,
365
366    #[schemars(
367        description = "Optional idempotency key. When set, retries with the same key derive the same PacketId and hit daemon replay guard instead of placing a duplicate combo order."
368    )]
369    #[serde(default, skip_serializing_if = "Option::is_none")]
370    pub idempotency_key: Option<String>,
371}
372
373fn validate_positive_finite_f64(
374    request_name: &str,
375    field_name: &str,
376    value: f64,
377) -> Result<(), String> {
378    if !value.is_finite() || value <= 0.0 {
379        return Err(format!(
380            "{request_name}: `{field_name}` must be a finite number > 0"
381        ));
382    }
383    Ok(())
384}
385
386fn validate_non_negative_finite_f64(
387    request_name: &str,
388    field_name: &str,
389    value: f64,
390) -> Result<(), String> {
391    if !value.is_finite() || value < 0.0 {
392        return Err(format!(
393            "{request_name}: `{field_name}` must be a finite number >= 0"
394        ));
395    }
396    Ok(())
397}
398
399fn validate_optional_finite_f64(
400    request_name: &str,
401    field_name: &str,
402    value: Option<f64>,
403) -> Result<(), String> {
404    if let Some(v) = value
405        && !v.is_finite()
406    {
407        return Err(format!("{request_name}: `{field_name}` must be finite"));
408    }
409    Ok(())
410}
411
412#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
413#[serde(deny_unknown_fields)]
414pub struct UnlockTradeReq {
415    /// true = 解锁(默认),false = 重新锁住交易 cipher(防御用)
416    #[schemars(
417        description = "true to unlock trading (default); false to lock trading cipher back (defensive). Lock does not require a password."
418    )]
419    #[serde(default = "default_true")]
420    pub unlock: bool,
421    /// v1.4.31: OTP / 令牌动态密码。仅在首次调用返回 `need_otp=true` 或
422    /// `err_code=-8` 时需要传;普通账号(无 2FA)留空即可。
423    #[schemars(
424        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"
425    )]
426    // v1.4.84 §5 B1
427    #[serde(
428        default,
429        alias = "token",
430        alias = "one_time_password",
431        skip_serializing_if = "Option::is_none"
432    )]
433    pub otp: Option<String>,
434    /// v1.4.33: 只解锁该券商下的账户(对齐 C++ OpenD per-broker unlock 语义)。
435    /// SecurityFirm enum (i32):
436    ///   1=FutuSecurities (HK), 2=FutuInc (US/MooMoo), 3=FutuSG,
437    ///   4=FutuAU, 5=FutuCA, 6=FutuMY, 7=FutuJP.
438    /// 留空 = 解锁所有 broker(v1.4.31 行为,向后兼容)。
439    #[schemars(
440        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"
441    )]
442    // v1.4.84 §5 B1
443    #[serde(
444        default,
445        alias = "broker",
446        alias = "security_firm_id",
447        skip_serializing_if = "Option::is_none"
448    )]
449    pub security_firm: Option<i32>,
450    /// v1.4.34: 只解锁指定 acc_id 列表(正整数;空 / omitted = 不 per-account filter)。
451    /// 和 security_firm 同时传时取交集(账户必须同时满足)。解决同 broker 内
452    /// 影子账户拖垮主账户的场景——LLM 显式传主账户 acc_id 可以避免影子
453    /// 账户进 unlock 请求。
454    #[schemars(
455        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"
456    )]
457    // v1.4.84 §5 B1
458    #[serde(
459        default,
460        alias = "account_ids",
461        alias = "accounts",
462        skip_serializing_if = "Option::is_none"
463    )]
464    pub acc_ids: Option<Vec<u64>>,
465}
466
467#[derive(Debug, Deserialize, schemars::JsonSchema)]
468#[serde(deny_unknown_fields)]
469pub struct MaxTrdQtysReq {
470    #[schemars(
471        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)."
472    )]
473    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
474    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
475    pub market: String,
476    #[schemars(
477        description = "Trading account ID (u64). ⚠️ Call `futu_list_accounts` first to discover — gateway does NOT infer a default; omitting or inventing an id fails."
478    )]
479    pub acc_id: u64,
480    #[schemars(description = "Trade environment: real|simulate (default real); alias: trd_env")]
481    // v1.4.84 §5 B1
482    #[serde(default = "default_env", alias = "trd_env")]
483    pub env: String,
484    // v1.4.41 (P2.6): description 强调 INTEGER 避免 LLM 传 "NORMAL" 撞 -32602.
485    // v1.4.42 (P3.3 修): 加 deserialize_with 接 int OR string —— LLM agent 传
486    // "NORMAL" / "MARKET" 等字符串也能工作(跟 PlaceOrderReq / ModifyOrderReq
487    // 的 string enum 风格统一)。backward-compat:旧客户端传 int 继续能用。
488    #[schemars(
489        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\"."
490    )]
491    #[serde(deserialize_with = "deser_int_or_order_type_str")]
492    pub order_type: i32,
493    #[schemars(
494        description = "Security code WITHOUT market prefix (e.g. 00700 / AAPL); alias: symbol / stock"
495    )]
496    // v1.4.84 §5 B1
497    #[serde(alias = "symbol", alias = "stock")]
498    pub code: String,
499    #[schemars(description = "Limit price (pass 0.0 for market orders)")]
500    pub price: f64,
501    #[schemars(
502        description = "JP sub-account type (Trd_Common.TrdSubAccType / TrdHeader.jpAccType). Required for JP accounts unless order_id/position context supplies the sub-account. Alias: jpAccType."
503    )]
504    #[serde(default, alias = "jpAccType")]
505    pub jp_acc_type: Option<i32>,
506    #[schemars(description = "Existing order_id (for modify-order max-qty calc, optional)")]
507    #[serde(default)]
508    pub order_id: Option<u64>,
509}
510
511#[derive(Debug, Deserialize, schemars::JsonSchema)]
512#[serde(deny_unknown_fields)]
513pub struct OrderFeeReq {
514    #[schemars(
515        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)."
516    )]
517    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
518    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
519    pub market: String,
520    #[schemars(
521        description = "Trading account ID (u64). ⚠️ Call `futu_list_accounts` first to discover — gateway does NOT infer a default; omitting or inventing an id fails."
522    )]
523    pub acc_id: u64,
524    #[schemars(description = "Trade environment: real|simulate (default real); alias: trd_env")]
525    // v1.4.84 §5 B1
526    #[serde(default = "default_env", alias = "trd_env")]
527    pub env: String,
528    #[schemars(
529        description = "Order_id_ex list (strings) — returned by place_order response; alias: order_ids_ex / order_ids"
530    )]
531    // v1.4.84 §5 B1
532    #[serde(alias = "order_ids_ex", alias = "order_ids")]
533    pub order_id_ex_list: Vec<String>,
534}
535
536#[derive(Debug, Deserialize, schemars::JsonSchema)]
537#[serde(deny_unknown_fields)]
538pub struct MarginRatioReq {
539    #[schemars(
540        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)."
541    )]
542    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
543    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
544    pub market: String,
545    #[schemars(
546        description = "Trading account ID (u64). ⚠️ Call `futu_list_accounts` first to discover — gateway does NOT infer a default; omitting or inventing an id fails."
547    )]
548    pub acc_id: u64,
549    #[schemars(description = "Trade environment: real|simulate (default real); alias: trd_env")]
550    // v1.4.84 §5 B1
551    #[serde(default = "default_env", alias = "trd_env")]
552    pub env: String,
553    #[schemars(
554        description = "Symbols in MARKET.CODE format (e.g. HK.00700, US.AAPL); alias: symbols / code_list / symbol_list"
555    )]
556    // v1.4.84 §5 B1
557    #[serde(alias = "symbols", alias = "code_list", alias = "symbol_list")]
558    pub codes: Vec<String>,
559}
560
561#[derive(Debug, Deserialize, schemars::JsonSchema)]
562#[serde(deny_unknown_fields)]
563pub struct HistoryQueryReq {
564    #[schemars(
565        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)."
566    )]
567    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
568    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
569    pub market: String,
570    #[schemars(
571        description = "Trading account ID (u64). ⚠️ Call `futu_list_accounts` first to discover — gateway does NOT infer a default; omitting or inventing an id fails."
572    )]
573    pub acc_id: u64,
574    #[schemars(description = "Trade environment: real|simulate (default real); alias: trd_env")]
575    // v1.4.83 §5 Phase 3: trd_env alias 对齐 py-futu-api TrdEnv
576    #[serde(default = "default_env", alias = "trd_env")]
577    pub env: String,
578    #[schemars(
579        description = "Filter by codes (empty = all). Each item is bare code without market prefix. \
580                       Alias: symbols / symbol_list"
581    )]
582    // v1.4.83 §5 Phase 3: 接受 symbols / symbol_list alias
583    #[serde(default, alias = "symbols", alias = "symbol_list")]
584    pub code_list: Vec<String>,
585    #[schemars(
586        description = "Begin time 'yyyy-MM-dd HH:mm:ss' (optional); alias: begin / start_time / from"
587    )]
588    #[serde(default, alias = "begin", alias = "start_time", alias = "from")]
589    pub begin_time: Option<String>,
590    #[schemars(description = "End time 'yyyy-MM-dd HH:mm:ss' (optional); alias: end / to")]
591    #[serde(default, alias = "end", alias = "to")]
592    pub end_time: Option<String>,
593}
594
595#[derive(Debug, Deserialize, schemars::JsonSchema)]
596#[serde(deny_unknown_fields)]
597pub struct CapitalFlowReq {
598    #[schemars(
599        description = "Security symbol in MARKET.CODE format (e.g. HK.00700); alias: code / stock"
600    )]
601    // v1.4.83 §5 Phase 3
602    #[serde(alias = "code", alias = "stock")]
603    pub symbol: String,
604    #[schemars(description = "Period type: 1=INTRADAY 2=DAY 3=WEEK 4=MONTH (default 1)")]
605    #[serde(default)]
606    pub period_type: Option<i32>,
607    #[schemars(
608        description = "Begin time 'yyyy-MM-dd' (optional, DAY/WEEK/MONTH only); alias: begin / start_time / from"
609    )]
610    #[serde(default, alias = "begin", alias = "start_time", alias = "from")]
611    pub begin_time: Option<String>,
612    #[schemars(description = "End time 'yyyy-MM-dd' (optional); alias: end / to")]
613    #[serde(default, alias = "end", alias = "to")]
614    pub end_time: Option<String>,
615}
616
617#[derive(Debug, Deserialize, schemars::JsonSchema)]
618#[serde(deny_unknown_fields)]
619pub struct AccCashFlowReq {
620    #[schemars(description = "Trade env: real / simulate (default real); alias: trd_env")]
621    // v1.4.84 §5 B1
622    #[serde(default = "default_env", alias = "trd_env")]
623    pub env: String,
624    #[schemars(
625        description = "Trading account ID (u64). ⚠️ Call `futu_list_accounts` first to discover — gateway does NOT infer a default."
626    )]
627    pub acc_id: u64,
628    #[schemars(
629        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)."
630    )]
631    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical
632    #[serde(deserialize_with = "tool_enums::deser_trd_market_as_string")]
633    pub market: String,
634    #[schemars(
635        description = "Clearing date (yyyy-MM-dd); queries flow entries for that day; alias: date / query_date"
636    )]
637    // v1.4.84 §5 B1
638    #[serde(alias = "date", alias = "query_date")]
639    pub clearing_date: String,
640    #[schemars(
641        description = "Direction: 1=InFlow, 2=OutFlow, omit for both; alias: flow_direction"
642    )]
643    #[serde(default, alias = "flow_direction")]
644    pub direction: Option<i32>,
645}
646
647#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
648#[serde(deny_unknown_fields)]
649pub struct CancelAllOrderReq {
650    #[schemars(description = "Trading env: simulate (default) / real; alias: trd_env")]
651    // v1.4.84 §5 B1
652    #[serde(default = "default_env_simulate", alias = "trd_env")]
653    pub env: String,
654    #[schemars(
655        description = "Trading account ID (u64). ⚠️ Call `futu_list_accounts` first to discover — gateway does NOT infer a default."
656    )]
657    pub acc_id: u64,
658    #[schemars(
659        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."
660    )]
661    // v1.4.90 P0-E: int OR string 双接, normalize 到大写 canonical (空字符串经
662    // 自定义 deser default fn 保留,runtime validate 报错).
663    #[serde(default, deserialize_with = "deser_trd_market_string_allow_empty")]
664    pub market: String,
665    #[schemars(description = "Per-call API key override (optional)")]
666    #[serde(default)]
667    pub api_key: Option<String>,
668}
669
670impl CancelAllOrderReq {
671    /// Runtime validation: market must be non-empty.
672    ///
673    /// The schema description marks this field as required, but serde accepts
674    /// an omitted string as the empty default; this check returns a clear
675    /// validation error before the request reaches the backend.
676    pub fn validate(&self) -> Result<(), String> {
677        if self.market.trim().is_empty() {
678            return Err(
679                "CancelAllOrderReq: `market` is required and must be non-empty \
680                 (e.g. HK / US / HKCC / A_SH / A_SZ / SG / JP / AU / CA)"
681                    .to_string(),
682            );
683        }
684        Ok(())
685    }
686}
687
688#[derive(Debug, Deserialize, schemars::JsonSchema)]
689#[serde(deny_unknown_fields)]
690pub struct CashLogReq {
691    #[schemars(description = "Trade env: real / simulate (default real)")]
692    #[serde(default = "default_env", alias = "trd_env")]
693    pub env: String,
694    #[schemars(description = "Trading account ID (u64). Call futu_list_accounts first.")]
695    pub acc_id: u64,
696    #[schemars(
697        description = "Optional legacy market hint. Accepted for backward compatibility (HK/US/CN/HKCC/FUTURES/SG/CRYPTO/AU/JP/MY/CA/HKFUND/USFUND/SGFUND/MYFUND/JPFUND or 1/2/3/4/5/6/7/8/15/111/112/113/123/124/125/126), \
698                       but cash-log identity does not trust this field: daemon derives backend market from acc_id/account cache."
699    )]
700    #[serde(
701        default,
702        deserialize_with = "tool_enums::deser_trd_market_as_option_string"
703    )]
704    pub market: Option<String>,
705    #[schemars(description = "Begin time (epoch seconds, optional)")]
706    #[serde(default)]
707    pub begin_time: Option<u64>,
708    #[schemars(description = "End time (epoch seconds, optional)")]
709    #[serde(default)]
710    pub end_time: Option<u64>,
711    #[schemars(description = "Business group ID filter (default all)")]
712    #[serde(default)]
713    pub biz_group_id: Option<u32>,
714    #[schemars(
715        description = "Business sub-group ID filter (optional; value from futu_get_biz_group sub_groups)"
716    )]
717    #[serde(default)]
718    pub biz_sub_group_id: Option<u32>,
719    #[schemars(description = "In/out direction: 1=in, 2=out, 0/omit=all")]
720    #[serde(default)]
721    pub in_out: Option<u32>,
722    #[schemars(description = "Search keyword (optional)")]
723    #[serde(default)]
724    pub keyword: Option<String>,
725    #[schemars(description = "Stock symbol (e.g. AAPL.US, 00700.HK), exact match (optional)")]
726    #[serde(default)]
727    pub symbol: Option<String>,
728    #[schemars(
729        description = "Backend stock_id filter (optional; use when available from upstream/account UI data)"
730    )]
731    #[serde(default)]
732    pub stock_id: Option<u64>,
733    #[schemars(
734        description = "Cursor: log_id from previous response next_log_id (omit for first page)"
735    )]
736    #[serde(default)]
737    pub log_id: Option<String>,
738    #[schemars(description = "Max entries per response (daemon uses mobile default 50 if omit)")]
739    #[serde(default)]
740    pub max_cnt: Option<u32>,
741    #[schemars(description = "Currency filter: CNY/HKD/USD/JPY/SGD (optional)")]
742    #[serde(default)]
743    pub currency: Option<String>,
744}
745
746#[derive(Debug, Deserialize, schemars::JsonSchema)]
747#[serde(deny_unknown_fields)]
748pub struct CashDetailReq {
749    #[schemars(description = "Trade env: real / simulate (default real)")]
750    #[serde(default = "default_env", alias = "trd_env")]
751    pub env: String,
752    #[schemars(description = "Trading account ID (u64)")]
753    pub acc_id: u64,
754    #[schemars(
755        description = "Optional legacy market hint; accepted for backward compatibility but ignored. Daemon derives backend market from acc_id/account cache."
756    )]
757    #[serde(
758        default,
759        deserialize_with = "tool_enums::deser_trd_market_as_option_string"
760    )]
761    pub market: Option<String>,
762    #[schemars(description = "Cash log ID (from futu_get_cash_log response)")]
763    pub log_id: String,
764}
765
766// v1.4.95 U2-D Tier M (mobile-driven): per-account margin info request
767#[derive(Debug, Deserialize, schemars::JsonSchema)]
768#[serde(deny_unknown_fields)]
769pub struct MarginInfoReq {
770    #[schemars(description = "Trade env: real / simulate (default real)")]
771    #[serde(default = "default_env", alias = "trd_env")]
772    pub env: String,
773    #[schemars(description = "Trading account ID (u64). Call futu_list_accounts first.")]
774    pub acc_id: u64,
775    #[schemars(
776        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)."
777    )]
778    pub market: String,
779}
780
781// v1.4.95 U2-A Tier M (mobile-driven): account compliance flag query request
782#[derive(Debug, Deserialize, schemars::JsonSchema)]
783#[serde(deny_unknown_fields)]
784pub struct AccountFlagReq {
785    #[schemars(description = "Trade env: real / simulate (default real)")]
786    #[serde(default = "default_env", alias = "trd_env")]
787    pub env: String,
788    #[schemars(description = "Trading account ID (u64) for per-broker routing")]
789    pub acc_id: u64,
790    #[schemars(
791        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."
792    )]
793    pub flag_id: u32,
794}
795
796#[derive(Debug, Deserialize, schemars::JsonSchema)]
797#[serde(deny_unknown_fields)]
798pub struct BondAccountReq {
799    #[schemars(description = "Trade env: real / simulate (default real)")]
800    #[serde(default = "default_env", alias = "trd_env")]
801    pub env: String,
802    #[schemars(description = "Trading account ID (u64) for per-broker routing")]
803    pub acc_id: u64,
804    #[schemars(description = "Market: HK / US / SG; aliases USA and SG_UNIVERSAL are accepted")]
805    pub market: String,
806}