Skip to main content

futu_trd/
read_plan.rs

1//! Trade read request planning.
2//!
3//! This module owns pure, surface-independent decisions for read-side trade
4//! APIs. Gateway handlers still own cache/backend IO, broker routing, and
5//! in-flight guards.
6
7use crate::currency::{
8    self, AccountKind, currency_id, legacy_backend_fund_market_id as legacy_fund, trd_market_id,
9};
10
11/// Backend refresh decision for cache-backed read APIs.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum RefreshAction {
14    CacheOnly,
15    Forward { reason: &'static str },
16}
17
18#[must_use]
19pub fn decide_refresh_action(
20    refresh_cache_requested: bool,
21    cache_currency_match: bool,
22) -> RefreshAction {
23    if refresh_cache_requested {
24        return RefreshAction::Forward {
25            reason: "user requested refresh_cache=true",
26        };
27    }
28    if !cache_currency_match {
29        return RefreshAction::Forward {
30            reason: "cache currency mismatch (per-currency snapshot missing)",
31        };
32    }
33    RefreshAction::CacheOnly
34}
35
36#[must_use]
37pub fn decide_cache_snapshot_refresh_action(
38    refresh_cache_requested: bool,
39    cache_snapshot_present: bool,
40) -> RefreshAction {
41    if refresh_cache_requested {
42        return RefreshAction::Forward {
43            reason: "user requested refresh_cache=true",
44        };
45    }
46    if !cache_snapshot_present {
47        return RefreshAction::Forward {
48            reason: "cache snapshot missing",
49        };
50    }
51    RefreshAction::CacheOnly
52}
53
54#[must_use]
55pub fn decide_explicit_refresh_action(refresh_cache_requested: bool) -> RefreshAction {
56    if refresh_cache_requested {
57        return RefreshAction::Forward {
58            reason: "user requested refresh_cache=true",
59        };
60    }
61    RefreshAction::CacheOnly
62}
63
64/// User-visible warning when a single-currency backend ignores explicit funds
65/// currency and returns the account native currency instead.
66///
67/// REST / MCP / CLI all expose this as presentation metadata, but the decision
68/// belongs to the shared trade-read domain: only warn when the user explicitly
69/// requested a currency and the response carries a different non-unknown
70/// currency tag.
71#[must_use]
72pub fn funds_currency_mismatch_warning(
73    requested_currency: Option<i32>,
74    returned_currency: Option<i32>,
75) -> Option<String> {
76    let requested = requested_currency?;
77    let returned = returned_currency?;
78    if returned == 0 || returned == requested {
79        return None;
80    }
81
82    let requested_label = currency::currency_label(requested);
83    let returned_label = currency::currency_label(returned);
84    Some(format!(
85        "currency ignored by backend: requested `{requested_label}` (id={requested}), \
86         returned `{returned_label}` (id={returned}). 此账户按账户基准币种返回资金数据."
87    ))
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct GetFundsReadPlan {
92    pub account_kind: AccountKind,
93    pub effective_currency: Option<i32>,
94    pub lookup_currency: Option<i32>,
95}
96
97/// Build the currency-facing read plan for `Trd_GetFunds`.
98///
99/// `effective_currency` is the user-requested currency, or a broker default
100/// derived for Futures/Universal accounts when the user omits it.
101/// `lookup_currency` is the cache/backend key dimension. Legacy
102/// SingleCurrency accounts keep `None` only when the user also omitted
103/// `currency`; when the user explicitly requests a currency, C++ still carries
104/// it into `ReqParams_Trd_Assets.enCurrency` before `QueryAsset`, so the
105/// backend refresh/cache key must preserve it.
106#[must_use]
107pub fn plan_get_funds_read(
108    requested_currency: Option<i32>,
109    security_firm: Option<i32>,
110    trd_market: Option<i32>,
111    uni_card_num: Option<&str>,
112    trd_market_auth_list: &[i32],
113) -> GetFundsReadPlan {
114    let account_kind = currency::classify_account_with_auth_list(
115        trd_market,
116        security_firm,
117        uni_card_num,
118        trd_market_auth_list,
119    );
120    let effective_currency = currency::effective_get_funds_currency_for_account(
121        requested_currency,
122        security_firm,
123        trd_market,
124        uni_card_num,
125        trd_market_auth_list,
126    );
127    let lookup_currency = match account_kind {
128        AccountKind::Futures | AccountKind::Universal => effective_currency,
129        AccountKind::SingleCurrency => requested_currency,
130    };
131    GetFundsReadPlan {
132        account_kind,
133        effective_currency,
134        lookup_currency,
135    }
136}
137
138/// Funds top-level currency projection.
139///
140/// Futures/Universal use the request/effective currency when available, while
141/// SingleCurrency accounts prefer backend/cache native currency and ignore the
142/// user request. Sim-futures and fund sub-markets keep their fixed native
143/// currency even when the request carries another currency.
144#[must_use]
145pub fn derive_top_level_currency(
146    account_kind: AccountKind,
147    effective_currency: Option<i32>,
148    acc_security_firm: Option<i32>,
149    acc_trd_market: Option<i32>,
150    cached_currency: Option<i32>,
151    header_trd_market: i32,
152    trd_env: i32,
153) -> Option<i32> {
154    match account_kind {
155        AccountKind::Futures | AccountKind::Universal => {
156            sim_or_fund_currency_override(trd_env, acc_trd_market)
157                .or(effective_currency)
158                .or(cached_currency)
159                .or_else(|| currency::default_currency_by_security_firm(acc_security_firm))
160                .or_else(|| primary_currency_by_trd_market(acc_trd_market))
161                .or_else(|| primary_currency_by_trd_market(Some(header_trd_market)))
162        }
163        AccountKind::SingleCurrency => sim_or_fund_currency_override(trd_env, acc_trd_market)
164            .or(cached_currency)
165            .or_else(|| currency::default_currency_by_security_firm(acc_security_firm))
166            .or_else(|| primary_currency_by_trd_market(acc_trd_market))
167            .or_else(|| primary_currency_by_trd_market(Some(header_trd_market))),
168    }
169}
170
171/// Top-level `Funds.netCashPower` presentation branches.
172#[must_use]
173pub fn derive_top_level_net_cash_power(
174    account_kind: AccountKind,
175    acc_type: Option<i32>,
176    cached_net_cash_power: Option<f64>,
177) -> Option<f64> {
178    match account_kind {
179        AccountKind::Futures => None,
180        AccountKind::Universal => {
181            // Trd_Common.proto: TrdAccType_Margin = 2.
182            if acc_type == Some(2) { Some(0.0) } else { None }
183        }
184        AccountKind::SingleCurrency => cached_net_cash_power,
185    }
186}
187
188fn primary_currency_by_trd_market(market: Option<i32>) -> Option<i32> {
189    match market? {
190        trd_market_id::HK | trd_market_id::HKCC | trd_market_id::FUTURES => Some(currency_id::HKD),
191        trd_market_id::US => Some(currency_id::USD),
192        trd_market_id::CN => Some(currency_id::CNH),
193        trd_market_id::SG => Some(currency_id::SGD),
194        trd_market_id::AU => Some(currency_id::AUD),
195        trd_market_id::JP => Some(currency_id::JPY),
196        trd_market_id::MY => Some(currency_id::MYR),
197        trd_market_id::CA => Some(currency_id::CAD),
198        _ => None,
199    }
200}
201
202/// Sim-futures and fund accounts expose fixed native currencies.
203///
204/// `FUTURES_SIMULATE_JP=13` and legacy backend `HK_FUND=13` intentionally
205/// alias. `trd_env` disambiguates the sim-futures branch from real fund
206/// backend raw market ids.
207#[must_use]
208pub fn sim_or_fund_currency_override(trd_env: i32, market: Option<i32>) -> Option<i32> {
209    let market = market?;
210    if trd_env == 0 {
211        match market {
212            trd_market_id::FUTURES_SIMULATE_HK => return Some(currency_id::HKD),
213            trd_market_id::FUTURES_SIMULATE_US => return Some(currency_id::USD),
214            trd_market_id::FUTURES_SIMULATE_SG => return Some(currency_id::SGD),
215            trd_market_id::FUTURES_SIMULATE_JP => return Some(currency_id::JPY),
216            _ => {}
217        }
218    }
219
220    match market {
221        trd_market_id::HK_FUND | legacy_fund::HK_FUND => Some(currency_id::HKD),
222        trd_market_id::US_FUND | legacy_fund::US_FUND_OLD | legacy_fund::US_FUND => {
223            Some(currency_id::USD)
224        }
225        trd_market_id::SG_FUND | legacy_fund::SG_FUND => Some(currency_id::SGD),
226        trd_market_id::MY_FUND => Some(currency_id::MYR),
227        trd_market_id::JP_FUND => Some(currency_id::JPY),
228        _ => None,
229    }
230}
231
232#[cfg(test)]
233mod tests;