Skip to main content

futu_backend/trade_query/
account_info.rs

1use super::*;
2use futu_core::error::FutuError;
3mod command;
4mod funds_cache;
5mod funds_sidecar;
6mod position_cache;
7mod positions;
8mod request;
9mod runtime_plan;
10mod status;
11mod workflow;
12#[cfg(test)]
13use positions::{ComboPositionMeta, PositionAccountContext, cached_position_from_account_pstn};
14use runtime_plan::plan_cmd3020_asset_categories_runtime;
15use workflow::query_cmd3020_account_info_one;
16
17/// C++ `NNProto_Trd_AccReal::QueryAssetInner` quote-level fact for real
18/// CMD3020 requests. The caller must snapshot this from the shared
19/// `QotRightCache`; the backend adapter deliberately has no silent default.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum Cmd3020QuoteLevel {
22    UsBasic,
23    UsUtp,
24}
25
26impl Cmd3020QuoteLevel {
27    /// Ref: FutuOpenD/Src/NNProtoCenter/Trade/Acc/NNProto_Trd_AccReal.cpp:542.
28    #[must_use]
29    pub const fn from_has_us_utp(has_us_utp: bool) -> Self {
30        if has_us_utp {
31            Self::UsUtp
32        } else {
33            Self::UsBasic
34        }
35    }
36
37    pub(super) const fn backend_value(self) -> u32 {
38        match self {
39            Self::UsBasic => 1,
40            Self::UsUtp => 2,
41        }
42    }
43}
44
45/// 查询真实账户资金+持仓 (CMD 3020: AccountInfoReq)
46///
47/// **v1.4.106 codex 1556 F1+F2 (P1) 修法**:
48///
49/// - F1: 接 `currency: Option<i32>` — caller (handler) 把 user 请求的
50///   currency 透传进来; None 时由 daemon 补账户默认币种再发 CMD3020。
51///   对齐 C++ `QueryFundNoLimit` 最终要求 backend `AccountInfoReq`
52///   携带有效 `union_currency`; backend 对缺省值会报
53///   `unsupported currency:NONE`.
54///
55/// - F2: transport / decode error → `Err` (loud propagate). 之前 `Ok(())`
56///   silent 让 caller 看到 cache miss + `ret_type=0 + s2c.funds=None`
57///   (silent-success 反模式 D / pitfall #45). C++ 失败不会伪装成功.
58///
59/// - v1.4.107: when caller has no user-requested `assetCategory`, match C++
60///   `GetCategoriesByKouzaType`: FutuJP margin sends Foreign(2), FutuJP
61///   derivative fans out Domestic(1) + Foreign(2), other accounts send no
62///   asset_category field.
63pub async fn query_account_info(
64    backend: &BackendConn,
65    acc_id: u64,
66    trd_cache: &TrdCache,
67    requested_currency: Option<i32>,
68    requested_asset_category: Option<i32>,
69    quote_level: Cmd3020QuoteLevel,
70) -> Result<()> {
71    let category_plan =
72        plan_cmd3020_asset_categories_runtime(trd_cache, acc_id, requested_asset_category);
73    for category in category_plan {
74        query_cmd3020_account_info_one(
75            backend,
76            acc_id,
77            trd_cache,
78            requested_currency,
79            category,
80            None,
81            quote_level,
82            TradeQueryOperation::Funds,
83        )
84        .await?;
85    }
86    Ok(())
87}
88
89/// Refresh real-account assets after CMD4716 ASSET/QUOTE notification.
90///
91/// C++ forwards the top-level NotifyMsg `version` and nested
92/// `AssetChangeData.asset_category` into `AccountInfoReq`; ordinary FTAPI reads
93/// keep both absent. The deprecated top-level notify asset_category is not an
94/// input to this adapter.
95pub async fn query_account_info_after_trade_push(
96    backend: &BackendConn,
97    acc_id: u64,
98    trd_cache: &TrdCache,
99    push_version: Option<u64>,
100    asset_category: Option<u32>,
101    quote_level: Cmd3020QuoteLevel,
102) -> Result<()> {
103    let requested_asset_category = asset_category.map(i32::try_from).transpose().map_err(|_| {
104        FutuError::Codec("trade notify asset_category exceeds i32 range".to_string())
105    })?;
106    let category_plan =
107        plan_cmd3020_asset_categories_runtime(trd_cache, acc_id, requested_asset_category);
108    for category in category_plan {
109        query_cmd3020_account_info_one(
110            backend,
111            acc_id,
112            trd_cache,
113            None,
114            category,
115            push_version,
116            quote_level,
117            TradeQueryOperation::Funds,
118        )
119        .await?;
120    }
121    Ok(())
122}
123
124#[cfg(test)]
125mod tests;
126
127/// Query real-account positions through CMD3020 using the C++ PositionList
128/// request shape.
129///
130/// C++ `QueryPositionListNoLimit` calls `QueryAssetInner(...,
131/// bWithoutFund=true, ...)`, so this wrapper keeps the same asset-category
132/// fanout as [`query_account_info`] but asks backend to omit fund/bond asset
133/// data from the response.
134pub async fn query_position_account_info(
135    backend: &BackendConn,
136    acc_id: u64,
137    trd_cache: &TrdCache,
138    requested_asset_category: Option<i32>,
139    requested_currency: Option<i32>,
140    quote_level: Cmd3020QuoteLevel,
141) -> Result<()> {
142    let category_plan =
143        plan_cmd3020_asset_categories_runtime(trd_cache, acc_id, requested_asset_category);
144    for category in category_plan {
145        query_cmd3020_account_info_one(
146            backend,
147            acc_id,
148            trd_cache,
149            requested_currency,
150            category,
151            None,
152            quote_level,
153            TradeQueryOperation::Positions,
154        )
155        .await?;
156    }
157    Ok(())
158}