Skip to main content

futu_backend/trade_query/
common.rs

1use super::*;
2
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use base64::Engine as _;
6pub(super) use futu_domain_trade_account::{
7    FundsCashInfoFact, FundsMarketAssetFact, FundsMarketCurrencyAssetFact,
8    FundsSecuritiesAssetsFacts, FundsTopLevelFact, ProjectedFundsMarketInfo,
9    api_currency_to_backend_like_cpp as api_currency_to_backend,
10    backend_currency_to_api_like_cpp as backend_currency_to_api,
11    fund_bond_currency_label_like_cpp as currency_to_fund_bond_ccy,
12    project_funds_cash_info_list_like_cpp, project_funds_market_info_list_like_cpp,
13    project_funds_securities_assets_like_cpp, project_funds_top_level_like_cpp,
14    stock_market_to_funds_nn_trd_market_like_cpp,
15};
16use futu_domain_trade_order::{hash_backend_id_to_u64_like_cpp, hash_str_to_u64_like_om};
17use rand::Rng;
18
19const OPEN_D_KEY_PREFIX: &str = "OD|";
20const REQ_ID_RAW_LEN: usize = 24;
21const REQ_ID_PREFIX: &str = "OD|";
22
23/// v1.4.106 codex F7 (P2): 解析 backend `Order.text` 字段, 对齐 C++
24/// `NNProto_Trd_Order.cpp:18-30` —— OpenD 写时把 text 包成
25/// `OD|<localID>|<userRemark>`, 客户端读时反向拆出 `(local_id, user_remark)`.
26///
27/// **输入**:
28/// - `text=Some("OD|123|hello")` → `Some((123, "hello"))`
29/// - `text=Some("OD|123|")` → `Some((123, ""))` (空 remark, daemon 写侧合法)
30/// - `text=Some("plain")` → `None` (非 OpenD 来源, 老订单 / 其他客户端)
31/// - `text=Some("OD|abc|x")` → `None` (localID 非 u64)
32/// - `text=None` → `None`
33///
34/// **返回 None 时 caller 应**:
35/// 1. `local_id` 字段保持 `None` (无 OpenD 标识)
36/// 2. `remark` 字段填**原始 text** (整段, 不剥前缀) — 用户可能用其他客户端
37///    下单的 free-form remark, 不强制 OpenD 格式.
38pub(super) fn parse_open_d_text(text: Option<&str>) -> Option<(u64, String)> {
39    let text = text?;
40    let body = text.strip_prefix(OPEN_D_KEY_PREFIX)?;
41    let (id_part, remark) = body.split_once('|')?;
42    let local_id: u64 = id_part.parse().ok()?;
43    Some((local_id, remark.to_string()))
44}
45
46/// 解析 optional string 为 f64
47pub(super) fn pf(s: &Option<String>) -> f64 {
48    s.as_ref()
49        .and_then(|v| v.parse::<f64>().ok())
50        .unwrap_or(0.0)
51}
52
53/// 解析 optional string 为 Option<f64>
54pub(super) fn pfo(s: &Option<String>) -> Option<f64> {
55    s.as_ref().and_then(|v| v.parse::<f64>().ok())
56}
57
58/// 从 `fund_info_list` 构建 API `market_info_list` 的原生币种展示值。
59///
60/// C++ `FillFunds` 固定输出 8 个市场: HK, US, HKCC, JP, SG, AU, CA, MY
61/// (`APIServer_Trd_GetFunds.cpp:196-224`)。因为 Rust 目前没有单独的
62/// `(acc, currency) -> Vec<MarketFund>` cache,read-side native marketInfo
63/// 继续沿用 backend `fund_info_list` 的分币种 total_asset;证券资产合计另见
64/// `sum_diff_market_fund_assets_in_response_currency`,不可再从这里反推。
65pub(super) fn build_market_info_list(
66    fund_info_list: &[crate::proto_internal::asset_query::AccFundInfo],
67) -> Vec<CachedMarketInfo> {
68    let currency_assets: Vec<FundsMarketCurrencyAssetFact> = fund_info_list
69        .iter()
70        .filter_map(|fi| {
71            let currency = backend_currency_to_api(fi.currency?);
72            Some(FundsMarketCurrencyAssetFact {
73                currency,
74                total_asset: pf(&fi.total_asset),
75            })
76        })
77        .collect();
78
79    project_funds_market_info_list_like_cpp(&currency_assets)
80        .into_iter()
81        .map(|item| CachedMarketInfo {
82            trd_market: item.trd_market,
83            assets: item.assets,
84        })
85        .collect()
86}
87
88pub(super) fn project_securities_assets_in_response_currency(
89    diff_market_fund_info_list: &[crate::proto_internal::asset_query::AccFundInfo],
90    response_currency: Option<i32>,
91    market_info_list: &[CachedMarketInfo],
92) -> Option<f64> {
93    let market_assets: Vec<FundsMarketAssetFact> = diff_market_fund_info_list
94        .iter()
95        .filter_map(|fund_info| {
96            let trd_market = stock_market_to_funds_nn_trd_market_like_cpp(fund_info.stock_market?)?;
97            Some(FundsMarketAssetFact {
98                trd_market,
99                total_asset: pf(&fund_info.total_asset),
100            })
101        })
102        .collect();
103    let legacy_market_info_list: Vec<ProjectedFundsMarketInfo> = market_info_list
104        .iter()
105        .map(|item| ProjectedFundsMarketInfo {
106            trd_market: item.trd_market,
107            assets: item.assets,
108        })
109        .collect();
110
111    project_funds_securities_assets_like_cpp(FundsSecuritiesAssetsFacts {
112        response_currency,
113        market_assets_in_response_currency: &market_assets,
114        legacy_market_info_list: &legacy_market_info_list,
115    })
116}
117
118pub(super) fn project_cash_info_list(
119    cash_info_list: &[crate::proto_internal::asset_query::AccCashInfo],
120) -> Vec<CachedCashInfo> {
121    let facts: Vec<FundsCashInfoFact> = cash_info_list
122        .iter()
123        .map(|cash_info| FundsCashInfoFact {
124            backend_currency: cash_info.currency,
125            cash: pf(&cash_info.balance),
126            available_balance: pf(&cash_info.cash_drawable),
127            net_cash_power: pf(&cash_info.cash_buypower),
128        })
129        .collect();
130
131    project_funds_cash_info_list_like_cpp(&facts)
132        .into_iter()
133        .map(|cash_info| CachedCashInfo {
134            currency: cash_info.currency,
135            cash: cash_info.cash,
136            available_balance: cash_info.available_balance,
137            net_cash_power: cash_info.net_cash_power,
138        })
139        .collect()
140}
141
142pub(super) fn project_real_account_funds(
143    fund_info: &crate::proto_internal::asset_query::AccFundInfo,
144    union_cash_info: Option<&crate::proto_internal::asset_query::AccCashInfo>,
145    cash_info_list_pb: &[crate::proto_internal::asset_query::AccCashInfo],
146    securities_assets: Option<f64>,
147    market_info_list: Vec<CachedMarketInfo>,
148    exposure_level: Option<i32>,
149) -> CachedFunds {
150    let cash_info_list = project_cash_info_list(cash_info_list_pb);
151    let projected = project_funds_top_level_like_cpp(FundsTopLevelFact {
152        fund_currency: fund_info.currency,
153        cash_currency: union_cash_info.and_then(|cash| cash.currency),
154        max_power_long: pf(&fund_info.max_power_long),
155        total_asset: pf(&fund_info.total_asset),
156        market_value: pf(&fund_info.mv),
157        frozen_fund: pf(&fund_info.hold),
158        debit_recover: pf(&fund_info.debit_recover),
159        total_cash: union_cash_info.map(|cash| pf(&cash.balance)).unwrap_or(0.0),
160        total_available_cash: union_cash_info
161            .map(|cash| pf(&cash.cash_drawable))
162            .unwrap_or(0.0),
163        available_funds: pfo(&fund_info.available),
164        unrealized_profit: pfo(&fund_info.unrealized_profit),
165        realized_profit: pfo(&fund_info.realized_profit),
166        risk_level: fund_info.risk_level.map(|risk_level| risk_level as i32),
167        initial_margin: pfo(&fund_info.initial_margin),
168        maintenance_margin: pfo(&fund_info.maintenance_margin),
169        max_power_short: pfo(&fund_info.max_power_short),
170        net_cash_power: pfo(&fund_info.net_cash_power),
171        long_mv: pfo(&fund_info.long_mv),
172        short_mv: pfo(&fund_info.short_mv),
173        pending_asset: pfo(&fund_info.pending_asset),
174        max_withdrawal: pfo(&fund_info.drawable),
175        risk_status: fund_info.risk_status_client,
176        margin_call_margin: pfo(&fund_info.margin_call),
177        is_pdt: fund_info.is_pdt,
178        pdt_seq: &fund_info.pdt_seq,
179        beginning_dtbp: pfo(&fund_info.beginning_dtbp),
180        remaining_dtbp: pfo(&fund_info.remaining_dtbp),
181        dt_call_amount: pfo(&fund_info.dt_call_amount),
182        dt_status: fund_info.dt_status,
183        securities_assets,
184    });
185
186    CachedFunds {
187        power: projected.power,
188        total_assets: projected.total_assets,
189        cash: projected.cash,
190        market_val: projected.market_val,
191        frozen_cash: projected.frozen_cash,
192        debt_cash: projected.debt_cash,
193        avl_withdrawal_cash: projected.avl_withdrawal_cash,
194        currency: projected.currency,
195        available_funds: projected.available_funds,
196        unrealized_pl: projected.unrealized_pl,
197        realized_pl: projected.realized_pl,
198        risk_level: projected.risk_level,
199        initial_margin: projected.initial_margin,
200        maintenance_margin: projected.maintenance_margin,
201        max_power_short: projected.max_power_short,
202        net_cash_power: projected.net_cash_power,
203        long_mv: projected.long_mv,
204        short_mv: projected.short_mv,
205        pending_asset: projected.pending_asset,
206        max_withdrawal: projected.max_withdrawal,
207        risk_status: projected.risk_status,
208        margin_call_margin: projected.margin_call_margin,
209        securities_assets: projected.securities_assets,
210        fund_assets: None,
211        bond_assets: None,
212        crypto_mv: None,
213        exposure_level,
214        exposure_limit: None,
215        used_limit: None,
216        remaining_limit: None,
217        is_pdt: projected.is_pdt,
218        pdt_seq: (!projected.pdt_seq.is_empty()).then_some(projected.pdt_seq),
219        beginning_dtbp: projected.beginning_dtbp,
220        remaining_dtbp: projected.remaining_dtbp,
221        dt_call_amount: projected.dt_call_amount,
222        dt_status: projected.dt_status,
223        cash_info_list,
224        market_info_list,
225    }
226}
227
228/// Create C++-shaped backend `MsgHeader.req_id`.
229///
230/// Ref:
231/// - `FutuOpenD/Src/NNProtoCenter/Trade/_NNProto_Trd_Comm.cpp:9-23`
232/// - `FutuOpenD/Src/NNProtoCenter/NNProtoCenter_Inner_Macro_Send.h:16-24`
233/// - `proto-internal/odr_sys_cmn.proto:819-824`
234///
235/// C++ packs 8 bytes of hash plus a 16-byte random unique id, base64-encodes
236/// the 24 bytes into 32 chars, then overwrites the first bytes with `OD|`.
237/// Rust uses local time plus random data in the hash seed because gateway
238/// translators do not carry the login server-time clock; uniqueness and the
239/// OpenD/base64 wire shape are the backend contract here.
240pub fn create_backend_req_id(acc_id: u64) -> String {
241    let mut rng = rand::rng();
242    let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) {
243        Ok(duration) => duration.as_secs(),
244        Err(_) => 0,
245    };
246    let hash_seed = format!("{}{}{}", timestamp, acc_id, rng.next_u32());
247    let hash = hash_str_to_u64_like_om(&hash_seed);
248
249    let mut raw = [0u8; REQ_ID_RAW_LEN];
250    raw[..8].copy_from_slice(&hash.to_le_bytes());
251    rng.fill_bytes(&mut raw[8..]);
252
253    let mut req_id = base64::engine::general_purpose::STANDARD.encode(raw);
254    req_id.replace_range(0..REQ_ID_PREFIX.len(), REQ_ID_PREFIX);
255    req_id
256}
257
258/// C++ `HashStrToU64` compatibility wrapper for existing backend call sites.
259///
260/// Ref:
261/// - `OM/Src/OMBase/API/OMBase_API_StrHash.cpp:52-55`
262/// - `OM/Src/OMBase/Define/OMBase_Define_Macro.h:165-175`
263///
264/// The domain owner applies OpenD's AP-low/BKDR-high OM hash to every
265/// non-empty backend string, including numeric strings.
266pub fn hash_str_to_u64(s: &str) -> u64 {
267    hash_backend_id_to_u64_like_cpp(s)
268}
269
270#[cfg(test)]
271mod tests;