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}