1use 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 #[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 #[serde(default = "default_env", alias = "trd_env")]
32 pub env: String,
33
34 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[serde(default = "default_env", alias = "trd_env")]
483 pub env: String,
484 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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#[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#[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}