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