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