Skip to main content

futu_mcp/handlers/trade/
helpers.rs

1//! mcp/handlers/trade/helpers — parse_* + build_header_* (跨 trade sub-mod 共用)
2//! (v1.4.110 CC Batch O: 拆自 trade.rs L26-110)
3
4use anyhow::{Result, bail};
5use futu_core::{trade_market, trade_parsing};
6use futu_trd::types::{TrdEnv, TrdHeader, TrdMarket};
7
8use crate::trd_sdk_adapter;
9
10pub fn parse_trd_market(s: &str) -> Result<TrdMarket> {
11    let market = trade_market::parse_trd_market_id(s).ok_or_else(|| {
12        anyhow::anyhow!(
13            "unknown trd market {:?} ({})",
14            s.trim().to_ascii_uppercase(),
15            trade_market::TRD_MARKET_PARSE_CHOICES
16        )
17    })?;
18    trd_sdk_adapter::trd_market_from_id(market).ok_or_else(|| {
19        anyhow::anyhow!("unknown trd market id {market} after core parser accepted it")
20    })
21}
22
23pub fn parse_trd_env(s: &str) -> Result<TrdEnv> {
24    let env = trade_parsing::parse_trd_env_id(s).ok_or_else(|| {
25        anyhow::anyhow!(
26            "unknown trd env {:?} ({})",
27            s.trim().to_ascii_lowercase(),
28            trade_parsing::TRD_ENV_PARSE_CHOICES
29        )
30    })?;
31    trd_sdk_adapter::trd_env_from_id(env)
32        .ok_or_else(|| anyhow::anyhow!("unknown trd env id {env} after core parser accepted it"))
33}
34
35/// v1.4.102 codex 38 F4 / 41 F2 / 41 F3 / 42 F3 (P2): 严格 env → i32
36/// 共享 helper, 替换 cash_flow / margin_info / account_flag / bond 4 处
37/// 旧"non-real => 0 silent"模式. typo (e.g. "reel" / "prod") 现在 reject
38/// at MCP boundary (BUG-005 反 silent wrong-env).
39pub fn parse_trd_env_int(s: &str) -> Result<i32> {
40    Ok(parse_trd_env(s)? as i32)
41}
42
43/// v1.4.102 codex 33 F5 / 34 F4 / 35 F4 (P2): 拒 fund market 在 calculation /
44/// active trade-read path. 用于 max_trd_qtys / margin_ratio / order_fee /
45/// orders / deals 等 active backend call (区别于 view-only positions/funds/
46/// cash-log/history 真机 verified).
47pub fn parse_trd_market_strict_no_fund(s: &str) -> Result<TrdMarket> {
48    let m = parse_trd_market(s)?;
49    if let Some(label) = trade_market::canonical_fund_trd_market_label(m as i32) {
50        bail!(
51            "trd market {label} 仅 view-only read endpoints \
52             (positions/funds/cash-log/history-orders/history-fills) 真机 verified; \
53             active/calculation path (orders/deals/max_trd_qtys/margin_ratio/order_fee) \
54             用主市场. v1.4.102 audit 33 F5 / 34 F4 / 35 F4 fix."
55        );
56    }
57    // v1.8.0 Task 12 Reviewer A1: same allowlist as REST
58    // `validate_header_trd_market_write` and MCP `trade_write::parse_trd_market`.
59    // KRX=18 stays read-visible; ordinary active/calculation tools fail closed
60    // (Desktop `PositionCloseUtil.cpp:159-184` admits KRX only via batch-close).
61    if !trade_market::TRD_MARKET_NON_FUND_INT_VALUES.contains(&(m as i32)) {
62        bail!(
63            "trd market {} ({}) 不在 active/calculation 市场 allowlist 内 ({}); \
64             KRX=18 仅通过 batch-close-positions 原子合同放行, \
65             orders/deals/max_trd_qtys/margin_ratio/order_fee fail closed",
66            m as i32,
67            s.trim().to_ascii_uppercase(),
68            trade_market::TRD_MARKET_NON_FUND_PARSE_CHOICES
69        );
70    }
71    Ok(m)
72}
73
74/// v1.4.102 codex 35 F4: build_header for active/calculation MCP tools.
75/// view-only read tools (positions/funds/cash-log/history) 用普通 build_header
76/// 接受 fund market.
77pub fn build_header_strict_no_fund(env: &str, acc_id: u64, market: &str) -> Result<TrdHeader> {
78    build_header_strict_no_fund_with_jp_acc_type(env, acc_id, market, None)
79}
80
81pub fn build_header_strict_no_fund_with_jp_acc_type(
82    env: &str,
83    acc_id: u64,
84    market: &str,
85    jp_acc_type: Option<i32>,
86) -> Result<TrdHeader> {
87    Ok(TrdHeader {
88        trd_env: parse_trd_env(env)?,
89        acc_id,
90        trd_market: parse_trd_market_strict_no_fund(market)?,
91        jp_acc_type,
92    })
93}
94
95pub fn build_header(env: &str, acc_id: u64, market: &str) -> Result<TrdHeader> {
96    Ok(TrdHeader {
97        trd_env: parse_trd_env(env)?,
98        acc_id,
99        trd_market: parse_trd_market(market)?,
100        jp_acc_type: None,
101    })
102}