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::ApiResult;
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) -> ApiResult {
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::<
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) -> ApiResult {
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::<
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) -> ApiResult {
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::<
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) -> ApiResult {
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::<
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 接受度 v1.4.95 仍 UNVERIFIED.
198pub async fn get_account_flag(
199    State(state): State<RestState>,
200    rec: Option<Extension<Arc<KeyRecord>>>,
201    Json(mut body): Json<Value>,
202) -> ApiResult {
203    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first
204    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/account-flag")?;
205    validate_header_trd_market(&body, "/api/account-flag")?;
206    validate_header_trd_env_present(&body, "/api/account-flag")?;
207    read_handler_acc_id_check(
208        &state,
209        rec.as_deref().map(|r| r.as_ref()),
210        &body,
211        "/api/account-flag",
212    )?;
213    adapter::proto_request::<
214        futu_backend::proto_internal::account_flag::DaemonGetAccountFlagReq,
215        futu_backend::proto_internal::account_flag::DaemonGetAccountFlagRsp,
216    >(&state, proto_id::TRD_GET_ACCOUNT_FLAG, Some(body))
217    .await
218}
219
220// ============================================================================
221// v1.4.95 U2-B Tier M (mobile-driven extension): bond holdings + trade prep
222//
223// 来源: ftcnnproto/.../bond_client_view.proto + FLCltProtocol.h
224// 5 endpoint × 5 cmd_id (9373/9374/9375/10043/10057), 共享 DaemonBondHeader
225// (acc_id + trd_env + market: "HK"/"US"/"SG").
226//
227// **市场覆盖**: 仅 HK / US / SG 债券账户; 其他账户 backend 自动返空.
228//
229// **Pitfall #42 backend-semantic risk**: backend 接受度 v1.4.95 仍 UNVERIFIED.
230// ============================================================================
231
232/// POST /api/bond-total-asset — 账户债券总持仓 (P&L 汇总, mobile cmd 9373)
233///
234/// 参数:
235/// - acc_id (u64, 必填)
236/// - trd_env (i32, 0=sim 1=real, default 1)
237/// - market (string, 必填): "HK" / "US" / "SG"
238///
239/// 响应字段 (s2c.inner): total_asset / position_incomes / today_incomes /
240/// accrued_interest / show_flag / ccy.
241pub async fn get_bond_total_asset(
242    State(state): State<RestState>,
243    rec: Option<Extension<Arc<KeyRecord>>>,
244    Json(mut body): Json<Value>,
245) -> ApiResult {
246    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first
247    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/bond-total-asset")?;
248    validate_header_trd_market(&body, "/api/bond-total-asset")?;
249    validate_header_trd_env_present(&body, "/api/bond-total-asset")?;
250    read_handler_acc_id_check(
251        &state,
252        rec.as_deref().map(|r| r.as_ref()),
253        &body,
254        "/api/bond-total-asset",
255    )?;
256    adapter::proto_request::<
257        futu_backend::proto_internal::bond_client_view::DaemonGetBondTotalAssetReq,
258        futu_backend::proto_internal::bond_client_view::DaemonGetBondTotalAssetRsp,
259    >(&state, proto_id::TRD_GET_BOND_TOTAL_ASSET, Some(body))
260    .await
261}
262
263/// POST /api/bond-single-asset — 单只债券持仓 (mobile cmd 9374)
264///
265/// 参数:
266/// - acc_id (u64, 必填)
267/// - trd_env (i32, 0=sim 1=real, default 1)
268/// - market (string, 必填): "HK" / "US" / "SG"
269/// - symbol (string, 必填): 债券代码
270///
271/// 响应字段 (s2c.inner): market_value / today_incomes / position_incomes /
272/// quantity / cost / expired_time / next_dividend_time / dividend_type /
273/// accrued_interest / dividend_option / notice_list / ccy / coupon_cash /
274/// position_cost / price.
275pub async fn get_bond_single_asset(
276    State(state): State<RestState>,
277    rec: Option<Extension<Arc<KeyRecord>>>,
278    Json(mut body): Json<Value>,
279) -> ApiResult {
280    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first
281    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/bond-single-asset")?;
282    validate_header_trd_market(&body, "/api/bond-single-asset")?;
283    validate_header_trd_env_present(&body, "/api/bond-single-asset")?;
284    read_handler_acc_id_check(
285        &state,
286        rec.as_deref().map(|r| r.as_ref()),
287        &body,
288        "/api/bond-single-asset",
289    )?;
290    adapter::proto_request::<
291        futu_backend::proto_internal::bond_client_view::DaemonGetBondSingleAssetReq,
292        futu_backend::proto_internal::bond_client_view::DaemonGetBondSingleAssetRsp,
293    >(&state, proto_id::TRD_GET_BOND_SINGLE_ASSET, Some(body))
294    .await
295}
296
297/// POST /api/bond-position-list — 账户债券持仓列表 (mobile cmd 9375)
298///
299/// 参数:
300/// - acc_id (u64, 必填)
301/// - trd_env (i32, 0=sim 1=real, default 1)
302/// - market (string, 必填): "HK" / "US" / "SG"
303///
304/// 响应字段 (s2c.inner): total / bond_list[] (PositionBondItem: name /
305/// symbol / market_value / quantity / price / cost / today_incomes /
306/// today_incomes_rate / position_incomes / position_incomes_rate /
307/// accrued_interest / make_for_call_flag / ccy).
308pub async fn get_bond_position_list(
309    State(state): State<RestState>,
310    rec: Option<Extension<Arc<KeyRecord>>>,
311    Json(mut body): Json<Value>,
312) -> ApiResult {
313    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first
314    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/bond-position-list")?;
315    validate_header_trd_market(&body, "/api/bond-position-list")?;
316    validate_header_trd_env_present(&body, "/api/bond-position-list")?;
317    read_handler_acc_id_check(
318        &state,
319        rec.as_deref().map(|r| r.as_ref()),
320        &body,
321        "/api/bond-position-list",
322    )?;
323    adapter::proto_request::<
324        futu_backend::proto_internal::bond_client_view::DaemonGetBondPositionListReq,
325        futu_backend::proto_internal::bond_client_view::DaemonGetBondPositionListRsp,
326    >(&state, proto_id::TRD_GET_BOND_POSITION_LIST, Some(body))
327    .await
328}
329
330/// POST /api/bond-answer-state — 是否需要答题 (合规性, mobile cmd 10043)
331///
332/// 参数:
333/// - acc_id (u64, 必填)
334/// - trd_env (i32, 0=sim 1=real, default 1)
335/// - market (string, 必填): "HK" / "US" / "SG"
336/// - symbol (string, 必填): 债券 symbol (类似 11000018)
337///
338/// 响应字段 (s2c.inner): need_to_answer (bool) / notice (CltActionOpenURL
339/// 提示弹窗: title / content / confirm_button_title / confirm_url 等).
340pub async fn get_bond_answer_state(
341    State(state): State<RestState>,
342    rec: Option<Extension<Arc<KeyRecord>>>,
343    Json(mut body): Json<Value>,
344) -> ApiResult {
345    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first
346    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/bond-answer-state")?;
347    validate_header_trd_market(&body, "/api/bond-answer-state")?;
348    validate_header_trd_env_present(&body, "/api/bond-answer-state")?;
349    read_handler_acc_id_check(
350        &state,
351        rec.as_deref().map(|r| r.as_ref()),
352        &body,
353        "/api/bond-answer-state",
354    )?;
355    adapter::proto_request::<
356        futu_backend::proto_internal::bond_client_view::DaemonGetBondAnswerStateReq,
357        futu_backend::proto_internal::bond_client_view::DaemonGetBondAnswerStateRsp,
358    >(&state, proto_id::TRD_GET_BOND_ANSWER_STATE, Some(body))
359    .await
360}
361
362/// POST /api/bond-trade-reminder — 交易提醒 (mobile cmd 10057)
363///
364/// 参数:
365/// - acc_id (u64, 必填)
366/// - trd_env (i32, 0=sim 1=real, default 1)
367/// - market (string, 必填): "HK" / "US" / "SG"
368/// - symbol (string, 必填): 债券 symbol
369///
370/// 响应字段 (s2c.inner): tradeable / complex_product / high_risk /
371/// sell_tradeable / pre_qualification (各 ReminderItem: value / title /
372/// text / reminder_level / url_id).
373pub async fn get_bond_trade_reminder(
374    State(state): State<RestState>,
375    rec: Option<Extension<Arc<KeyRecord>>>,
376    Json(mut body): Json<Value>,
377) -> ApiResult {
378    // v1.4.102 codex 37 F2 / 38 F3 / 40 F1+F2: Tier M normalize-first
379    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/bond-trade-reminder")?;
380    validate_header_trd_market(&body, "/api/bond-trade-reminder")?;
381    validate_header_trd_env_present(&body, "/api/bond-trade-reminder")?;
382    read_handler_acc_id_check(
383        &state,
384        rec.as_deref().map(|r| r.as_ref()),
385        &body,
386        "/api/bond-trade-reminder",
387    )?;
388    adapter::proto_request::<
389        futu_backend::proto_internal::bond_client_view::DaemonGetBondTradeReminderReq,
390        futu_backend::proto_internal::bond_client_view::DaemonGetBondTradeReminderRsp,
391    >(&state, proto_id::TRD_GET_BOND_TRADE_REMINDER, Some(body))
392    .await
393}