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}