Skip to main content

futu_rest/routes/trd/
tier_m.rs

1//! REST Tier M / mobile-driven trade account endpoints.
2//!
3//! 这些 endpoint 使用 mobile backend proto 与专用 cmd,和标准 FTAPI trade query
4//! 路径不同;集中到本模块后,主 trade route 不再混入 mobile-driven proto 细节。
5
6use std::sync::Arc;
7
8use axum::Json;
9use axum::extract::{Extension, State};
10use serde_json::Value;
11
12use futu_auth::KeyRecord;
13use futu_backend::proto_internal::realtime_asset_log;
14use futu_core::proto_id;
15
16use crate::adapter::{self, RestState};
17
18use super::RawApiResult;
19use super::card_num::normalize_and_resolve_card_num_for_route;
20use super::validation::{
21    read_handler_acc_id_check, validate_header_trd_env_present, validate_header_trd_market,
22};
23
24// v1.4.94 Tier M (mobile-driven extension): 资金明细 / cash log
25//
26// 来源: ftcnnproto/.../realtime_asset_log.proto + FLCltProtocol.h:123
27// (clt_cmd_trade_cash_log = 3000). Daemon 主动扩展, OpenD 没暴露此 cmd 给
28// FTAPI, 但 mobile 用同一 cmd 直连 backend, daemon 也走同 channel.
29// ============================================================================
30
31/// POST /api/cash-log — 资金明细查询 (mobile-driven, 比 /api/flow-summary 更全字段)
32///
33/// 参数 (body 顶层平铺, REST adapter 自动 wrap 到 c2s):
34/// - acc_id (u64, 必填): 账户 ID
35/// - trd_env (i32, 0=sim 1=real): 必填
36/// - inner.market / inner.account_id: daemon 从已鉴权 acc_id 派生,client-provided 值会被忽略
37/// - inner.begin_time / inner.end_time (epoch 秒, 时间范围)
38/// - inner.biz_group_id / inner.biz_sub_group_id: 业务分组过滤
39/// - inner.in_out: 1=入账, 2=出账, 0=全部
40/// - inner.keyword / inner.symbol / inner.stock_id: 搜索
41/// - inner.currency: 货币过滤
42/// - inner.log_id: cursor 分页 (上次响应的 next_log_id)
43/// - inner.max_cnt: 每页上限
44///
45/// 响应: ret_type / ret_msg / s2c.inner (含 monthly_log_list / has_more / next_log_id)
46///
47/// **Pitfall #42 backend-semantic risk**: 真 backend 接受我们的 proto 序列化
48/// 是 **未真机 verify** 假设. 真机 fail 时 ret_type=-1 + clear hint.
49pub async fn get_cash_log(
50    State(state): State<RestState>,
51    rec: Option<Extension<Arc<KeyRecord>>>,
52    Json(mut body): Json<Value>,
53) -> RawApiResult {
54    // v1.4.102 codex 37 F2 / 38 F3 (P2): Tier M REST 同 funds/positions 走完整
55    // pre-flight (normalize → trd_market 白名单 → trd_env 必填 → acc_id check)
56    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/cash-log")?;
57    validate_header_trd_market(&body, "/api/cash-log")?;
58    validate_header_trd_env_present(&body, "/api/cash-log")?;
59    read_handler_acc_id_check(
60        &state,
61        rec.as_deref().map(|r| r.as_ref()),
62        &body,
63        "/api/cash-log",
64    )?;
65    adapter::proto_request_raw::<
66        realtime_asset_log::DaemonGetCashLogReq,
67        realtime_asset_log::DaemonGetCashLogRsp,
68    >(&state, proto_id::TRD_GET_CASH_LOG, Some(body))
69    .await
70}
71
72/// POST /api/cash-detail — 单条资金流水详情
73///
74/// 参数:
75/// - acc_id (u64, 必填), trd_env
76/// - inner.log_id (string, 必填): 从 GetCashLog 响应里取
77/// - inner.market / inner.account_id: daemon 从已鉴权 acc_id 派生,client-provided 值会被忽略
78pub async fn get_cash_detail(
79    State(state): State<RestState>,
80    rec: Option<Extension<Arc<KeyRecord>>>,
81    Json(mut body): Json<Value>,
82) -> RawApiResult {
83    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first +
84    // 完整 pre-flight (避免 raw ACC_ID 绕过 auth-limit + 缺 trd_env silent default)
85    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/cash-detail")?;
86    validate_header_trd_market(&body, "/api/cash-detail")?;
87    validate_header_trd_env_present(&body, "/api/cash-detail")?;
88    read_handler_acc_id_check(
89        &state,
90        rec.as_deref().map(|r| r.as_ref()),
91        &body,
92        "/api/cash-detail",
93    )?;
94    adapter::proto_request_raw::<
95        realtime_asset_log::DaemonGetCashDetailReq,
96        realtime_asset_log::DaemonGetCashDetailRsp,
97    >(&state, proto_id::TRD_GET_CASH_DETAIL, Some(body))
98    .await
99}
100
101/// POST /api/biz-group — 业务分类元数据 (供前端构造业务分组下拉)
102///
103/// 参数:
104/// - acc_id (u64, 必填), trd_env
105/// - inner.market (默认从 acc_id 派生)
106///
107/// 响应: biz_group_list (业务类型) / currency_config_list (货币) / direction_list (方向)
108pub async fn get_biz_group(
109    State(state): State<RestState>,
110    rec: Option<Extension<Arc<KeyRecord>>>,
111    Json(mut body): Json<Value>,
112) -> RawApiResult {
113    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first +
114    // 完整 pre-flight
115    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/biz-group")?;
116    validate_header_trd_market(&body, "/api/biz-group")?;
117    validate_header_trd_env_present(&body, "/api/biz-group")?;
118    read_handler_acc_id_check(
119        &state,
120        rec.as_deref().map(|r| r.as_ref()),
121        &body,
122        "/api/biz-group",
123    )?;
124    adapter::proto_request_raw::<
125        realtime_asset_log::DaemonGetBizGroupReq,
126        realtime_asset_log::DaemonGetBizGroupRsp,
127    >(&state, proto_id::TRD_GET_BIZ_GROUP, Some(body))
128    .await
129}
130
131// ============================================================================
132// v1.4.95 U2-D Tier M (mobile-driven extension): margin account info per market
133//
134// 来源: ftcnnproto/.../risk_user_account_info.proto + FLCltProtocol.h
135// (clt_cmd_hk_margin_info=3101 / us=3102 / cn_ah=3107).
136// ============================================================================
137
138/// POST /api/margin-info — per-account margin info (mobile-driven, by market)
139///
140/// 与 `/api/margin-ratio` (per-security ratio) 互补: 本 endpoint 给账户全景.
141///
142/// 参数:
143/// - acc_id (u64, 必填)
144/// - trd_env (i32, 0=sim 1=real, default 1)
145/// - market (string, 必填): "HK" / "US" / "CN_AH" (其他市场 → 400 with hint)
146/// - inner.req_flag (uint32, optional): 1 = 过滤新股中签干扰
147///
148/// 响应: ret_type / ret_msg / s2c.inner.user_margin_info[] (含 12 字段子集
149/// 的 MarginInfo: 购买力 / 余额 / 净值 / 保证金 / 流动性 / 风险等级 / HK-specific)
150///
151/// **Pitfall #42 backend-semantic risk**: backend 接受我们 12 字段子集是
152/// **未真机 verify** 假设. 真机 fail 时 ret_type=-1 + clear hint 指 fallback
153/// `/api/margin-ratio` per-security ratio.
154pub async fn get_margin_info(
155    State(state): State<RestState>,
156    rec: Option<Extension<Arc<KeyRecord>>>,
157    Json(mut body): Json<Value>,
158) -> RawApiResult {
159    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first
160    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/margin-info")?;
161    validate_header_trd_market(&body, "/api/margin-info")?;
162    validate_header_trd_env_present(&body, "/api/margin-info")?;
163    read_handler_acc_id_check(
164        &state,
165        rec.as_deref().map(|r| r.as_ref()),
166        &body,
167        "/api/margin-info",
168    )?;
169    adapter::proto_request_raw::<
170        futu_backend::proto_internal::risk_user_account_info::DaemonGetMarginInfoReq,
171        futu_backend::proto_internal::risk_user_account_info::DaemonGetMarginInfoRsp,
172    >(&state, proto_id::TRD_GET_MARGIN_INFO, Some(body))
173    .await
174}
175
176// ============================================================================
177// v1.4.95 U2-A Tier M (mobile-driven extension): account compliance flags
178//
179// 来源: ftcnnproto/.../account_flag.proto + NN cmd 5281.
180// ============================================================================
181
182/// POST /api/account-flag — 查询账户合规标志 (mobile-driven)
183///
184/// 用户:
185/// - 高级交易准入 (期权 / 衍生品 / OTC / CFD 等) 必须 flag=1
186/// - LLM agent 检查用户是否完成 KYC / 风披 / opt-in 等
187///
188/// 参数:
189/// - acc_id (u64, 必填): per-broker 路由
190/// - trd_env (i32, 0=sim 1=real, default 1)
191/// - flag_id (uint32, 必填): 标志 id (常用值 5=US 期权确认, 22=衍生品风批,
192///   10=基金 KYC, 16=PDT, 23=OTC; 详见 proto 头部 36+ 项 flag_id 列表)
193///
194/// 响应: ret_type / ret_msg / s2c.inner.item (general_flag: uid / flag_id /
195/// flag_value / updated_time). flag_value 通常 0=未确认 / 1=已确认 / 部分用 version.
196///
197/// **Pitfall #42**: backend 接受度仍属 real-machine-required;未拿到真机
198/// positive evidence 前保持 UNVERIFIED。
199pub async fn get_account_flag(
200    State(state): State<RestState>,
201    rec: Option<Extension<Arc<KeyRecord>>>,
202    Json(mut body): Json<Value>,
203) -> RawApiResult {
204    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first
205    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/account-flag")?;
206    validate_header_trd_market(&body, "/api/account-flag")?;
207    validate_header_trd_env_present(&body, "/api/account-flag")?;
208    read_handler_acc_id_check(
209        &state,
210        rec.as_deref().map(|r| r.as_ref()),
211        &body,
212        "/api/account-flag",
213    )?;
214    adapter::proto_request_raw::<
215        futu_backend::proto_internal::account_flag::DaemonGetAccountFlagReq,
216        futu_backend::proto_internal::account_flag::DaemonGetAccountFlagRsp,
217    >(&state, proto_id::TRD_GET_ACCOUNT_FLAG, Some(body))
218    .await
219}
220
221// ============================================================================
222// v1.4.95 U2-B Tier M (mobile-driven extension): bond holdings + trade prep
223//
224// 来源: ftcnnproto/.../bond_client_view.proto + FLCltProtocol.h
225// 5 endpoint × 5 cmd_id (9373/9374/9375/10043/10057), 共享 DaemonBondHeader
226// (acc_id + trd_env + market: "HK"/"US"/"SG").
227//
228// **市场覆盖**: 仅 HK / US / SG 债券账户; 其他账户 backend 自动返空.
229//
230// **Pitfall #42 backend-semantic risk**: backend 接受度仍属
231// real-machine-required;未拿到真机 positive evidence 前保持 UNVERIFIED。
232// ============================================================================
233
234/// POST /api/bond-total-asset — 账户债券总持仓 (P&L 汇总, mobile cmd 9373)
235///
236/// 参数:
237/// - acc_id (u64, 必填)
238/// - trd_env (i32, 0=sim 1=real, default 1)
239/// - market (string, 必填): "HK" / "US" / "SG"
240///
241/// 响应字段 (s2c.inner): total_asset / position_incomes / today_incomes /
242/// accrued_interest / show_flag / ccy.
243pub async fn get_bond_total_asset(
244    State(state): State<RestState>,
245    rec: Option<Extension<Arc<KeyRecord>>>,
246    Json(mut body): Json<Value>,
247) -> RawApiResult {
248    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first
249    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/bond-total-asset")?;
250    validate_header_trd_market(&body, "/api/bond-total-asset")?;
251    validate_header_trd_env_present(&body, "/api/bond-total-asset")?;
252    read_handler_acc_id_check(
253        &state,
254        rec.as_deref().map(|r| r.as_ref()),
255        &body,
256        "/api/bond-total-asset",
257    )?;
258    adapter::proto_request_raw::<
259        futu_backend::proto_internal::bond_client_view::DaemonGetBondTotalAssetReq,
260        futu_backend::proto_internal::bond_client_view::DaemonGetBondTotalAssetRsp,
261    >(&state, proto_id::TRD_GET_BOND_TOTAL_ASSET, Some(body))
262    .await
263}
264
265/// POST /api/bond-single-asset — 单只债券持仓 (mobile cmd 9374)
266///
267/// 参数:
268/// - acc_id (u64, 必填)
269/// - trd_env (i32, 0=sim 1=real, default 1)
270/// - market (string, 必填): "HK" / "US" / "SG"
271/// - symbol (string, 必填): 债券代码
272///
273/// 响应字段 (s2c.inner): market_value / today_incomes / position_incomes /
274/// quantity / cost / expired_time / next_dividend_time / dividend_type /
275/// accrued_interest / dividend_option / notice_list / ccy / coupon_cash /
276/// position_cost / price.
277pub async fn get_bond_single_asset(
278    State(state): State<RestState>,
279    rec: Option<Extension<Arc<KeyRecord>>>,
280    Json(mut body): Json<Value>,
281) -> RawApiResult {
282    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first
283    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/bond-single-asset")?;
284    validate_header_trd_market(&body, "/api/bond-single-asset")?;
285    validate_header_trd_env_present(&body, "/api/bond-single-asset")?;
286    read_handler_acc_id_check(
287        &state,
288        rec.as_deref().map(|r| r.as_ref()),
289        &body,
290        "/api/bond-single-asset",
291    )?;
292    adapter::proto_request_raw::<
293        futu_backend::proto_internal::bond_client_view::DaemonGetBondSingleAssetReq,
294        futu_backend::proto_internal::bond_client_view::DaemonGetBondSingleAssetRsp,
295    >(&state, proto_id::TRD_GET_BOND_SINGLE_ASSET, Some(body))
296    .await
297}
298
299/// POST /api/bond-position-list — 账户债券持仓列表 (mobile cmd 9375)
300///
301/// 参数:
302/// - acc_id (u64, 必填)
303/// - trd_env (i32, 0=sim 1=real, default 1)
304/// - market (string, 必填): "HK" / "US" / "SG"
305///
306/// 响应字段 (s2c.inner): total / bond_list[] (PositionBondItem: name /
307/// symbol / market_value / quantity / price / cost / today_incomes /
308/// today_incomes_rate / position_incomes / position_incomes_rate /
309/// accrued_interest / make_for_call_flag / ccy).
310pub async fn get_bond_position_list(
311    State(state): State<RestState>,
312    rec: Option<Extension<Arc<KeyRecord>>>,
313    Json(mut body): Json<Value>,
314) -> RawApiResult {
315    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first
316    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/bond-position-list")?;
317    validate_header_trd_market(&body, "/api/bond-position-list")?;
318    validate_header_trd_env_present(&body, "/api/bond-position-list")?;
319    read_handler_acc_id_check(
320        &state,
321        rec.as_deref().map(|r| r.as_ref()),
322        &body,
323        "/api/bond-position-list",
324    )?;
325    adapter::proto_request_raw::<
326        futu_backend::proto_internal::bond_client_view::DaemonGetBondPositionListReq,
327        futu_backend::proto_internal::bond_client_view::DaemonGetBondPositionListRsp,
328    >(&state, proto_id::TRD_GET_BOND_POSITION_LIST, Some(body))
329    .await
330}
331
332/// POST /api/bond-answer-state — 是否需要答题 (合规性, mobile cmd 10043)
333///
334/// 参数:
335/// - acc_id (u64, 必填)
336/// - trd_env (i32, 0=sim 1=real, default 1)
337/// - market (string, 必填): "HK" / "US" / "SG"
338/// - symbol (string, 必填): 债券 symbol (类似 11000018)
339///
340/// 响应字段 (s2c.inner): need_to_answer (bool) / notice (CltActionOpenURL
341/// 提示弹窗: title / content / confirm_button_title / confirm_url 等).
342pub async fn get_bond_answer_state(
343    State(state): State<RestState>,
344    rec: Option<Extension<Arc<KeyRecord>>>,
345    Json(mut body): Json<Value>,
346) -> RawApiResult {
347    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first
348    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/bond-answer-state")?;
349    validate_header_trd_market(&body, "/api/bond-answer-state")?;
350    validate_header_trd_env_present(&body, "/api/bond-answer-state")?;
351    read_handler_acc_id_check(
352        &state,
353        rec.as_deref().map(|r| r.as_ref()),
354        &body,
355        "/api/bond-answer-state",
356    )?;
357    adapter::proto_request_raw::<
358        futu_backend::proto_internal::bond_client_view::DaemonGetBondAnswerStateReq,
359        futu_backend::proto_internal::bond_client_view::DaemonGetBondAnswerStateRsp,
360    >(&state, proto_id::TRD_GET_BOND_ANSWER_STATE, Some(body))
361    .await
362}
363
364/// POST /api/bond-trade-reminder — 交易提醒 (mobile cmd 10057)
365///
366/// 参数:
367/// - acc_id (u64, 必填)
368/// - trd_env (i32, 0=sim 1=real, default 1)
369/// - market (string, 必填): "HK" / "US" / "SG"
370/// - symbol (string, 必填): 债券 symbol
371///
372/// 响应字段 (s2c.inner): tradeable / complex_product / high_risk /
373/// sell_tradeable / pre_qualification (各 ReminderItem: value / title /
374/// text / reminder_level / url_id).
375pub async fn get_bond_trade_reminder(
376    State(state): State<RestState>,
377    rec: Option<Extension<Arc<KeyRecord>>>,
378    Json(mut body): Json<Value>,
379) -> RawApiResult {
380    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first
381    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/bond-trade-reminder")?;
382    validate_header_trd_market(&body, "/api/bond-trade-reminder")?;
383    validate_header_trd_env_present(&body, "/api/bond-trade-reminder")?;
384    read_handler_acc_id_check(
385        &state,
386        rec.as_deref().map(|r| r.as_ref()),
387        &body,
388        "/api/bond-trade-reminder",
389    )?;
390    adapter::proto_request_raw::<
391        futu_backend::proto_internal::bond_client_view::DaemonGetBondTradeReminderReq,
392        futu_backend::proto_internal::bond_client_view::DaemonGetBondTradeReminderRsp,
393    >(&state, proto_id::TRD_GET_BOND_TRADE_REMINDER, Some(body))
394    .await
395}