Skip to main content

futu_rest/routes/trd/
read.rs

1//! REST trade/account read routes.
2
3use std::sync::Arc;
4
5use axum::extract::{Extension, Json, State};
6use axum::http::StatusCode;
7use serde_json::Value;
8
9use futu_auth::KeyRecord;
10use futu_core::proto_id;
11use futu_proto::trd_get_combo_max_trd_qtys;
12use futu_proto::trd_get_funds;
13use futu_proto::trd_get_history_order_fill_list;
14use futu_proto::trd_get_history_order_list;
15use futu_proto::trd_get_margin_ratio;
16use futu_proto::trd_get_max_trd_qtys;
17use futu_proto::trd_get_order_fee;
18use futu_proto::trd_get_order_fill_list;
19use futu_proto::trd_get_order_list;
20use futu_proto::trd_get_position_list;
21use futu_trd::{currency, read_plan};
22
23use super::card_num::normalize_and_resolve_card_num_for_route;
24use super::validation::{
25    read_handler_acc_id_check, validate_header_trd_env_present, validate_header_trd_market,
26    validate_header_trd_market_write,
27};
28use super::{ApiResult, RawApiResult};
29use crate::adapter::{self, RestState};
30
31/// POST /api/funds — 获取资金
32pub async fn get_funds(
33    State(state): State<RestState>,
34    rec: Option<Extension<Arc<KeyRecord>>>,
35    Json(mut body): Json<Value>,
36) -> ApiResult {
37    // normalize + card_num -> acc_id 必须在 validate / allowed_acc_ids 之前完成。
38    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/funds")?;
39    // v1.4.93 P0-4 (NEW-C-01): trd_market enum 白名单(防 silent misroute)
40    validate_header_trd_market(&body, "/api/funds")?;
41    // v1.4.102 BUG-005: 缺 trd_env 直接 400 (避免 "Nonexisting acc_id" 误导)
42    validate_header_trd_env_present(&body, "/api/funds")?;
43    read_handler_acc_id_check(
44        &state,
45        rec.as_deref().map(|r| r.as_ref()),
46        &body,
47        "/api/funds",
48    )?;
49    normalize_trd_currency_labels(&mut body).map_err(|e| {
50        (
51            axum::http::StatusCode::BAD_REQUEST,
52            Json(serde_json::json!({
53                "ret_type": -1,
54                "ret_msg": e,
55                "error": e,
56            })),
57        )
58    })?;
59
60    // 记录 user 显式请求 currency, 用于 response post-process: 若 backend
61    // 返回了不同币种, 对 REST caller 明确暴露短 warning。
62    let requested_currency: Option<i32> = body
63        .pointer("/c2s/currency")
64        .or_else(|| body.pointer("/currency"))
65        .and_then(|v| v.as_i64())
66        .map(|v| v as i32);
67
68    let mut resp = adapter::proto_request::<trd_get_funds::Request, trd_get_funds::Response>(
69        &state,
70        proto_id::TRD_GET_FUNDS,
71        Some(body),
72    )
73    .await?;
74
75    let response_currency = resp
76        .0
77        .pointer("/s2c/funds/currency")
78        .and_then(|v| v.as_i64())
79        .map(|v| v as i32);
80    if let Some(warn_msg) =
81        read_plan::funds_currency_mismatch_warning(requested_currency, response_currency)
82        && let Some(obj) = resp.0.as_object_mut()
83    {
84        obj.insert(
85            "currency_warning".to_string(),
86            serde_json::Value::String(warn_msg.clone()),
87        );
88        // 也在 ret_msg 追加 hint (若已有 ret_msg 则前置 warning)
89        let existing_msg = obj
90            .get("ret_msg")
91            .and_then(|v| v.as_str())
92            .unwrap_or("")
93            .to_string();
94        let new_msg = if existing_msg.is_empty() {
95            format!("⚠️  {warn_msg}")
96        } else {
97            format!("⚠️  {warn_msg}\n[既有] {existing_msg}")
98        };
99        obj.insert("ret_msg".to_string(), serde_json::Value::String(new_msg));
100    }
101
102    Ok(resp)
103}
104
105fn normalize_trd_currency_labels(body: &mut Value) -> Result<(), String> {
106    for pointer in ["/c2s/currency", "/currency"] {
107        let Some(value) = body.pointer_mut(pointer) else {
108            continue;
109        };
110        let Value::String(label) = value else {
111            continue;
112        };
113        let parsed = currency::parse_currency_label(label).map_err(|err| err.to_string())?;
114        *value = Value::Number(serde_json::Number::from(parsed));
115    }
116    Ok(())
117}
118
119#[cfg(test)]
120mod tests;
121
122/// POST /api/positions — 获取持仓
123pub async fn get_positions(
124    State(state): State<RestState>,
125    rec: Option<Extension<Arc<KeyRecord>>>,
126    Json(mut body): Json<Value>,
127) -> RawApiResult {
128    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/positions")?;
129    // v1.4.93 P0-4 (NEW-C-01): trd_market enum 白名单
130    validate_header_trd_market(&body, "/api/positions")?;
131    // v1.4.102 BUG-005: 缺 trd_env 直接 400 (避免 "Nonexisting acc_id" 误导)
132    validate_header_trd_env_present(&body, "/api/positions")?;
133    read_handler_acc_id_check(
134        &state,
135        rec.as_deref().map(|r| r.as_ref()),
136        &body,
137        "/api/positions",
138    )?;
139    normalize_trd_currency_labels(&mut body).map_err(|e| {
140        (
141            axum::http::StatusCode::BAD_REQUEST,
142            Json(serde_json::json!({
143                "ret_type": -1,
144                "ret_msg": e,
145                "error": e,
146            })),
147        )
148    })?;
149    adapter::proto_request_raw::<trd_get_position_list::Request, trd_get_position_list::Response>(
150        &state,
151        proto_id::TRD_GET_POSITION_LIST,
152        Some(body),
153    )
154    .await
155}
156
157/// POST /api/orders — 获取订单列表
158pub async fn get_orders(
159    State(state): State<RestState>,
160    rec: Option<Extension<Arc<KeyRecord>>>,
161    Json(mut body): Json<Value>,
162) -> RawApiResult {
163    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/orders")?;
164    // v1.4.93 P0-4 (NEW-C-01): trd_market enum 白名单
165    // v1.4.102 codex 32 F6 (P2): active order reads 用 write allowlist (无 fund markets, 未 verified)
166    validate_header_trd_market_write(&body, "/api/orders")?;
167    // v1.4.102 BUG-005: 缺 trd_env 直接 400 (避免 "Nonexisting acc_id" 误导)
168    validate_header_trd_env_present(&body, "/api/orders")?;
169    read_handler_acc_id_check(
170        &state,
171        rec.as_deref().map(|r| r.as_ref()),
172        &body,
173        "/api/orders",
174    )?;
175    adapter::proto_request_raw::<trd_get_order_list::Request, trd_get_order_list::Response>(
176        &state,
177        proto_id::TRD_GET_ORDER_LIST,
178        Some(body),
179    )
180    .await
181}
182
183/// POST /api/order-fills — 获取成交列表
184pub async fn get_order_fills(
185    State(state): State<RestState>,
186    rec: Option<Extension<Arc<KeyRecord>>>,
187    Json(mut body): Json<Value>,
188) -> RawApiResult {
189    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/order-fills")?;
190    // v1.4.93 P0-4 (NEW-C-01): trd_market enum 白名单
191    // v1.4.102 codex 32 F6 (P2): active order reads 用 write allowlist (无 fund markets, 未 verified)
192    validate_header_trd_market_write(&body, "/api/order-fills")?;
193    // v1.4.102 BUG-005: 缺 trd_env 直接 400 (避免 "Nonexisting acc_id" 误导)
194    validate_header_trd_env_present(&body, "/api/order-fills")?;
195    read_handler_acc_id_check(
196        &state,
197        rec.as_deref().map(|r| r.as_ref()),
198        &body,
199        "/api/order-fills",
200    )?;
201    adapter::proto_request_raw::<
202        trd_get_order_fill_list::Request,
203        trd_get_order_fill_list::Response,
204    >(
205        &state,
206        proto_id::TRD_GET_ORDER_FILL_LIST,
207        Some(body),
208    )
209    .await
210}
211
212/// POST /api/max-trd-qtys — 获取最大交易数量
213pub async fn get_max_trd_qtys(
214    State(state): State<RestState>,
215    rec: Option<Extension<Arc<KeyRecord>>>,
216    Json(mut body): Json<Value>,
217) -> RawApiResult {
218    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/max-trd-qtys")?;
219    // v1.4.93 P0-4 (NEW-C-01): trd_market enum 白名单
220    // v1.4.102 codex 29 F3 / 31 F5 (P2): trade-calculation endpoint 用 write allowlist (无 fund markets)
221    validate_header_trd_market_write(&body, "/api/max-trd-qtys")?;
222    // v1.4.102 BUG-005: 缺 trd_env 直接 400 (避免 "Nonexisting acc_id" 误导)
223    validate_header_trd_env_present(&body, "/api/max-trd-qtys")?;
224    read_handler_acc_id_check(
225        &state,
226        rec.as_deref().map(|r| r.as_ref()),
227        &body,
228        "/api/max-trd-qtys",
229    )?;
230    adapter::proto_request_raw::<trd_get_max_trd_qtys::Request, trd_get_max_trd_qtys::Response>(
231        &state,
232        proto_id::TRD_GET_MAX_TRD_QTYS,
233        Some(body),
234    )
235    .await
236}
237
238/// POST /api/combo-max-trd-qtys — 获取组合订单最大交易数量
239pub async fn get_combo_max_trd_qtys(
240    State(state): State<RestState>,
241    rec: Option<Extension<Arc<KeyRecord>>>,
242    Json(mut body): Json<Value>,
243) -> RawApiResult {
244    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/combo-max-trd-qtys")?;
245    validate_header_trd_market_write(&body, "/api/combo-max-trd-qtys")?;
246    validate_header_trd_env_present(&body, "/api/combo-max-trd-qtys")?;
247    read_handler_acc_id_check(
248        &state,
249        rec.as_deref().map(|r| r.as_ref()),
250        &body,
251        "/api/combo-max-trd-qtys",
252    )?;
253    adapter::proto_request_raw::<
254        trd_get_combo_max_trd_qtys::Request,
255        trd_get_combo_max_trd_qtys::Response,
256    >(&state, proto_id::TRD_GET_COMBO_MAX_TRD_QTYS, Some(body))
257    .await
258}
259
260/// POST /api/history-orders — 获取历史订单
261pub async fn get_history_orders(
262    State(state): State<RestState>,
263    rec: Option<Extension<Arc<KeyRecord>>>,
264    Json(mut body): Json<Value>,
265) -> RawApiResult {
266    adapter::normalize_json_keys_snake_case(&mut body);
267    adapter::normalize_endpoint_local_request_aliases_for_rest_path(
268        "/api/history-orders",
269        &mut body,
270    )
271    .map_err(history_order_alias_error)?;
272    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/history-orders")?;
273    read_handler_acc_id_check(
274        &state,
275        rec.as_deref().map(|r| r.as_ref()),
276        &body,
277        "/api/history-orders",
278    )?;
279    // v1.4.96 BUG #003 hotfix (external reviewer matrix-double-confirmed): trd_market=999
280    // 之前 silent accept 200 OK, broker routing 风险.
281    validate_header_trd_market(&body, "/api/history-orders")?;
282    // v1.4.102 BUG-005: 缺 trd_env 直接 400 (避免 "Nonexisting acc_id" 误导)
283    validate_header_trd_env_present(&body, "/api/history-orders")?;
284    adapter::proto_request_raw::<
285        trd_get_history_order_list::Request,
286        trd_get_history_order_list::Response,
287    >(&state, proto_id::TRD_GET_HISTORY_ORDER_LIST, Some(body))
288    .await
289}
290
291/// POST /api/history-order-fills — 获取历史成交
292pub async fn get_history_order_fills(
293    State(state): State<RestState>,
294    rec: Option<Extension<Arc<KeyRecord>>>,
295    Json(mut body): Json<Value>,
296) -> RawApiResult {
297    adapter::normalize_json_keys_snake_case(&mut body);
298    adapter::normalize_endpoint_local_request_aliases_for_rest_path(
299        "/api/history-order-fills",
300        &mut body,
301    )
302    .map_err(history_order_alias_error)?;
303    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/history-order-fills")?;
304    read_handler_acc_id_check(
305        &state,
306        rec.as_deref().map(|r| r.as_ref()),
307        &body,
308        "/api/history-order-fills",
309    )?;
310    // v1.4.96 BUG #003 hotfix (external reviewer matrix-double-confirmed)
311    validate_header_trd_market(&body, "/api/history-order-fills")?;
312    // v1.4.102 BUG-005: 缺 trd_env 直接 400 (避免 "Nonexisting acc_id" 误导)
313    validate_header_trd_env_present(&body, "/api/history-order-fills")?;
314    adapter::proto_request_raw::<
315        trd_get_history_order_fill_list::Request,
316        trd_get_history_order_fill_list::Response,
317    >(
318        &state,
319        proto_id::TRD_GET_HISTORY_ORDER_FILL_LIST,
320        Some(body),
321    )
322    .await
323}
324
325fn history_order_alias_error(message: String) -> (StatusCode, Json<Value>) {
326    (
327        StatusCode::BAD_REQUEST,
328        Json(serde_json::json!({
329            "ret_type": -1,
330            "ret_msg": message,
331            "error": message,
332        })),
333    )
334}
335
336/// POST /api/margin-ratio — 获取融资融券比率
337pub async fn get_margin_ratio(
338    State(state): State<RestState>,
339    rec: Option<Extension<Arc<KeyRecord>>>,
340    Json(mut body): Json<Value>,
341) -> RawApiResult {
342    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/margin-ratio")?;
343    // v1.4.93 P0-4 (NEW-C-01): trd_market enum 白名单
344    // v1.4.102 codex 29 F3 / 31 F5 (P2): trade-calculation endpoint 用 write allowlist (无 fund markets)
345    validate_header_trd_market_write(&body, "/api/margin-ratio")?;
346    // v1.4.102 BUG-005: 缺 trd_env 直接 400 (避免 "Nonexisting acc_id" 误导)
347    validate_header_trd_env_present(&body, "/api/margin-ratio")?;
348    read_handler_acc_id_check(
349        &state,
350        rec.as_deref().map(|r| r.as_ref()),
351        &body,
352        "/api/margin-ratio",
353    )?;
354    adapter::proto_request_raw::<trd_get_margin_ratio::Request, trd_get_margin_ratio::Response>(
355        &state,
356        proto_id::TRD_GET_MARGIN_RATIO,
357        Some(body),
358    )
359    .await
360}
361
362/// POST /api/order-fee — 获取订单费用
363pub async fn get_order_fee(
364    State(state): State<RestState>,
365    rec: Option<Extension<Arc<KeyRecord>>>,
366    Json(mut body): Json<Value>,
367) -> RawApiResult {
368    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/order-fee")?;
369    read_handler_acc_id_check(
370        &state,
371        rec.as_deref().map(|r| r.as_ref()),
372        &body,
373        "/api/order-fee",
374    )?;
375    // v1.4.96 BUG #003 hotfix (external reviewer matrix-double-confirmed)
376    // v1.4.102 codex 29 F3 / 31 F5 (P2): trade-calculation endpoint 用 write allowlist (无 fund markets)
377    validate_header_trd_market_write(&body, "/api/order-fee")?;
378    // v1.4.102 BUG-005: 缺 trd_env 直接 400 (避免 "Nonexisting acc_id" 误导)
379    validate_header_trd_env_present(&body, "/api/order-fee")?;
380    adapter::proto_request_raw::<trd_get_order_fee::Request, trd_get_order_fee::Response>(
381        &state,
382        proto_id::TRD_GET_ORDER_FEE,
383        Some(body),
384    )
385    .await
386}