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