Skip to main content

futu_mcp/handlers/trade/
positions.rs

1//! mcp/handlers/trade/positions — PositionOut + derive_cost_basis_method_hint + get_positions
2//! (v1.4.110 CC Batch O: 拆自 trade.rs L451-536)
3
4use std::sync::Arc;
5
6use anyhow::Result;
7use futu_net::client::FutuClient;
8use futu_trd::currency;
9use serde::Serialize;
10
11use super::helpers::*;
12
13#[derive(Serialize)]
14struct PositionOut {
15    position_id: u64,
16    code: String,
17    name: String,
18    qty: f64,
19    can_sell_qty: f64,
20    price: f64,
21    /// **deprecated** — use diluted_cost_price / average_cost_price (v1.4.94 Tier M2)
22    cost_price: f64,
23    val: f64,
24    pl_val: f64,
25    pl_ratio: f64,
26    // v1.4.94 Tier M2 (mobile-driven extension): 暴露 OpenD proto 已有字段
27    // (`Trd_Common.proto Position` 32-34, 30-31). 之前 MCP 只暴露 cost_price
28    // (deprecated 字段) — 客户端无法对齐 mobile NN 显示口径.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    diluted_cost_price: Option<f64>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    average_cost_price: Option<f64>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    average_pl_ratio: Option<f64>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    currency: Option<i32>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    trd_market: Option<i32>,
39    /// v1.4.94 Tier M2: 推荐使用的成本价口径 (mobile NN `aas_cmn.proto`
40    /// `CostProfitCalcMethod` 派生). 客户端可用 hint 决定显示哪个 cost field.
41    /// - "diluted": 推荐 dilute_cost_price (HK / US / CN 默认)
42    /// - "average": 推荐 average_cost_price (JP 信用 / 加权平均场景)
43    /// - "open_price": 推荐 cost_price (旧字段, 部分 US 市场显示开仓价)
44    cost_basis_method_hint: &'static str,
45}
46
47/// v1.4.94 Tier M2: 按 trd_market + currency 派生 cost_basis_method_hint.
48///
49/// 对齐 mobile NN `aas_cmn.proto` `CostProfitCalcMethod`:
50/// - `WEIGHTED_AVERAGE` (1) — JP 信用账户用 average
51/// - `OPEN_PRICE` (2) — 部分 US (FX/期货) 用开仓价
52/// - `EQUIVALENT_WEIGHTED_AVERAGE` (3) — JP 等权平均
53/// - `DEFAULT` (0) — 其他用 diluted (默认 OpenD)
54pub fn derive_cost_basis_method_hint(
55    trd_market: Option<i32>,
56    currency: Option<i32>,
57) -> &'static str {
58    // JP market (TrdMarket enum 15 = JP) → average
59    if trd_market == Some(15) {
60        return "average";
61    }
62    // JPY currency (Currency enum 4 = JPY) → average (备用判断, JP 信用账户)
63    if currency == Some(4) {
64        return "average";
65    }
66    // 默认 diluted (HK / US / CN / 其他)
67    "diluted"
68}
69
70pub async fn get_positions(
71    client: &Arc<FutuClient>,
72    env: &str,
73    acc_id: u64,
74    market: &str,
75    requested_currency: Option<&str>,
76    option_strategy_view: bool,
77) -> Result<String> {
78    let header = build_header(env, acc_id, market)?;
79    let currency_int = match requested_currency {
80        Some(s) => Some(currency::parse_currency_label(s)?),
81        None => None,
82    };
83    let list = futu_trd::account::get_position_list_with_options(
84        client,
85        &header,
86        futu_trd::account::PositionListOptions {
87            filter_market: Some(header.trd_market as i32),
88            currency: currency_int,
89            option_strategy_view: option_strategy_view.then_some(true),
90        },
91    )
92    .await?;
93    let out: Vec<PositionOut> = list
94        .iter()
95        .map(|p| PositionOut {
96            position_id: p.position_id,
97            code: p.code.clone(),
98            name: p.name.clone(),
99            qty: p.qty,
100            can_sell_qty: p.can_sell_qty,
101            price: p.price,
102            cost_price: p.cost_price,
103            val: p.val,
104            pl_val: p.pl_val,
105            pl_ratio: p.pl_ratio,
106            // v1.4.94 Tier M2 (mobile-driven extension)
107            diluted_cost_price: p.diluted_cost_price,
108            average_cost_price: p.average_cost_price,
109            average_pl_ratio: p.average_pl_ratio,
110            currency: p.currency,
111            trd_market: p.trd_market,
112            cost_basis_method_hint: derive_cost_basis_method_hint(p.trd_market, p.currency),
113        })
114        .collect();
115    Ok(serde_json::to_string_pretty(&out)?)
116}