futu_rest/routes/
trd.rs

1//! 交易 REST API 路由
2
3use std::sync::Arc;
4
5use axum::extract::{Extension, Json, State};
6use axum::http::StatusCode;
7use serde_json::Value;
8
9use futu_auth::{CheckCtx, KeyRecord, LimitOutcome};
10use futu_core::proto_id;
11use futu_proto::trd_get_acc_list;
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_proto::trd_modify_order;
22use futu_proto::trd_place_order;
23use futu_proto::trd_sub_acc_push;
24use futu_proto::trd_unlock_trade;
25
26use crate::adapter::{self, RestState};
27
28type ApiResult = Result<Json<Value>, (StatusCode, Json<Value>)>;
29
30/// `Trd_Common.TrdMarket` enum int → 市场字符串。Unknown / 未覆盖 → 空串
31/// (让 `check_full_skip_rate` 跳过 market 白名单检查)
32fn trd_market_str(i: i32) -> &'static str {
33    match i {
34        1 => "HK",
35        2 => "US",
36        3 => "CN",
37        4 => "HKCC",
38        5 => "FUTURES",
39        6 => "SG",
40        7 => "JP",
41        _ => "",
42    }
43}
44
45/// `Trd_Common.TrdSide` enum int → 方向字符串
46fn trd_side_str(i: i32) -> &'static str {
47    match i {
48        1 => "BUY",
49        2 => "SELL",
50        3 => "SELL_SHORT",
51        4 => "BUY_BACK",
52        _ => "",
53    }
54}
55
56/// REST handler 层细粒度限额检查(v1.2):解析下单 JSON → CheckCtx →
57/// `RuntimeCounters::check_full_skip_rate`。
58///
59/// 路径:握手 + scope + 通用 rate/hours 闸门已经在 bearer_auth 跑过;
60/// 这里只是再补一刀 market/symbol/value/side/daily 的细粒度检查。
61/// 失败返回 `429 + JSON error`,对齐 auth 层的限额拒响应。
62fn rest_handler_limit_check(
63    state: &RestState,
64    rec: &KeyRecord,
65    parsed_req: &trd_place_order::Request,
66) -> Result<(), (StatusCode, Json<Value>)> {
67    let c2s = &parsed_req.c2s;
68    let market = trd_market_str(c2s.header.trd_market);
69    let symbol = if market.is_empty() {
70        String::new()
71    } else {
72        format!("{market}.{}", c2s.code)
73    };
74    let order_value = c2s.price.map(|p| p * c2s.qty);
75    let trd_side = match trd_side_str(c2s.trd_side) {
76        "" => None,
77        s => Some(s.to_string()),
78    };
79    let ctx = CheckCtx {
80        market: market.to_string(),
81        symbol,
82        order_value,
83        trd_side,
84    };
85    let now = chrono::Utc::now();
86    if let LimitOutcome::Reject(reason) =
87        state
88            .counters
89            .check_full_skip_rate(&rec.id, &rec.limits(), &ctx, now)
90    {
91        futu_auth::audit::reject("rest", "/api/order", &rec.id, &format!("limit: {reason}"));
92        return Err((
93            StatusCode::TOO_MANY_REQUESTS,
94            Json(serde_json::json!({
95                "error": format!("limit check failed: {reason}")
96            })),
97        ));
98    }
99    Ok(())
100}
101
102/// GET /api/accounts — 获取交易账户列表
103pub async fn get_acc_list(State(state): State<RestState>) -> ApiResult {
104    adapter::proto_request::<trd_get_acc_list::Request, trd_get_acc_list::Response>(
105        &state,
106        proto_id::TRD_GET_ACC_LIST,
107        None,
108    )
109    .await
110}
111
112/// POST /api/unlock-trade — 解锁交易
113pub async fn unlock_trade(State(state): State<RestState>, Json(body): Json<Value>) -> ApiResult {
114    adapter::proto_request::<trd_unlock_trade::Request, trd_unlock_trade::Response>(
115        &state,
116        proto_id::TRD_UNLOCK_TRADE,
117        Some(body),
118    )
119    .await
120}
121
122/// POST /api/sub-acc-push — 订阅交易推送
123pub async fn sub_acc_push(State(state): State<RestState>, Json(body): Json<Value>) -> ApiResult {
124    adapter::proto_request::<trd_sub_acc_push::Request, trd_sub_acc_push::Response>(
125        &state,
126        proto_id::TRD_SUB_ACC_PUSH,
127        Some(body),
128    )
129    .await
130}
131
132/// POST /api/funds — 获取资金
133pub async fn get_funds(State(state): State<RestState>, Json(body): Json<Value>) -> ApiResult {
134    adapter::proto_request::<trd_get_funds::Request, trd_get_funds::Response>(
135        &state,
136        proto_id::TRD_GET_FUNDS,
137        Some(body),
138    )
139    .await
140}
141
142/// POST /api/positions — 获取持仓
143pub async fn get_positions(State(state): State<RestState>, Json(body): Json<Value>) -> ApiResult {
144    adapter::proto_request::<trd_get_position_list::Request, trd_get_position_list::Response>(
145        &state,
146        proto_id::TRD_GET_POSITION_LIST,
147        Some(body),
148    )
149    .await
150}
151
152/// POST /api/orders — 获取订单列表
153pub async fn get_orders(State(state): State<RestState>, Json(body): Json<Value>) -> ApiResult {
154    adapter::proto_request::<trd_get_order_list::Request, trd_get_order_list::Response>(
155        &state,
156        proto_id::TRD_GET_ORDER_LIST,
157        Some(body),
158    )
159    .await
160}
161
162/// POST /api/order — 下单
163///
164/// v1.2:在 dispatch 之前先解析 JSON 提取 CheckCtx,跑 `check_full_skip_rate`
165/// 做 market/symbol/value/side/daily 细粒度检查(auth 层已做 rate/hours 闸门)。
166/// `Extension<Arc<KeyRecord>>` 来自 bearer_auth middleware;scope 模式下必有,
167/// legacy 模式下没有该 extension → 跳过 handler 层检查(保持旧行为)。
168pub async fn place_order(
169    State(state): State<RestState>,
170    rec: Option<Extension<Arc<KeyRecord>>>,
171    Json(body): Json<Value>,
172) -> ApiResult {
173    if let Some(Extension(rec)) = rec {
174        // 解析 JSON → trd_place_order::Request 提取 CheckCtx
175        match serde_json::from_value::<trd_place_order::Request>(body.clone()) {
176            Ok(parsed) => rest_handler_limit_check(&state, &rec, &parsed)?,
177            Err(_) => {
178                // 反序列化失败时不挡,让下游 dispatch 报真正的 400/格式错误
179                // (limits 不该挡格式问题)
180            }
181        }
182    }
183    adapter::proto_request::<trd_place_order::Request, trd_place_order::Response>(
184        &state,
185        proto_id::TRD_PLACE_ORDER,
186        Some(body),
187    )
188    .await
189}
190
191/// POST /api/modify-order — 改单/撤单
192///
193/// v1.2:和 `place_order` 类似但 ModifyOrder protobuf 给不出 symbol/qty/value
194/// (只有 order_id),只能做 market 白名单 + hours 检查;symbol/value/side
195/// 留空让 `check_full_skip_rate` 自动跳过。
196pub async fn modify_order(
197    State(state): State<RestState>,
198    rec: Option<Extension<Arc<KeyRecord>>>,
199    Json(body): Json<Value>,
200) -> ApiResult {
201    if let Some(Extension(rec)) = rec {
202        if let Ok(parsed) = serde_json::from_value::<trd_modify_order::Request>(body.clone()) {
203            let market = trd_market_str(parsed.c2s.header.trd_market);
204            let ctx = CheckCtx {
205                market: market.to_string(),
206                symbol: String::new(),
207                order_value: None,
208                trd_side: None,
209            };
210            let now = chrono::Utc::now();
211            if let LimitOutcome::Reject(reason) =
212                state
213                    .counters
214                    .check_full_skip_rate(&rec.id, &rec.limits(), &ctx, now)
215            {
216                futu_auth::audit::reject(
217                    "rest",
218                    "/api/modify-order",
219                    &rec.id,
220                    &format!("limit: {reason}"),
221                );
222                return Err((
223                    StatusCode::TOO_MANY_REQUESTS,
224                    Json(serde_json::json!({
225                        "error": format!("limit check failed: {reason}")
226                    })),
227                ));
228            }
229        }
230    }
231    adapter::proto_request::<trd_modify_order::Request, trd_modify_order::Response>(
232        &state,
233        proto_id::TRD_MODIFY_ORDER,
234        Some(body),
235    )
236    .await
237}
238
239/// POST /api/order-fills — 获取成交列表
240pub async fn get_order_fills(State(state): State<RestState>, Json(body): Json<Value>) -> ApiResult {
241    adapter::proto_request::<trd_get_order_fill_list::Request, trd_get_order_fill_list::Response>(
242        &state,
243        proto_id::TRD_GET_ORDER_FILL_LIST,
244        Some(body),
245    )
246    .await
247}
248
249/// POST /api/max-trd-qtys — 获取最大交易数量
250pub async fn get_max_trd_qtys(
251    State(state): State<RestState>,
252    Json(body): Json<Value>,
253) -> ApiResult {
254    adapter::proto_request::<trd_get_max_trd_qtys::Request, trd_get_max_trd_qtys::Response>(
255        &state,
256        proto_id::TRD_GET_MAX_TRD_QTYS,
257        Some(body),
258    )
259    .await
260}
261
262/// POST /api/history-orders — 获取历史订单
263pub async fn get_history_orders(
264    State(state): State<RestState>,
265    Json(body): Json<Value>,
266) -> ApiResult {
267    adapter::proto_request::<
268        trd_get_history_order_list::Request,
269        trd_get_history_order_list::Response,
270    >(&state, proto_id::TRD_GET_HISTORY_ORDER_LIST, Some(body))
271    .await
272}
273
274/// POST /api/history-order-fills — 获取历史成交
275pub async fn get_history_order_fills(
276    State(state): State<RestState>,
277    Json(body): Json<Value>,
278) -> ApiResult {
279    adapter::proto_request::<
280        trd_get_history_order_fill_list::Request,
281        trd_get_history_order_fill_list::Response,
282    >(
283        &state,
284        proto_id::TRD_GET_HISTORY_ORDER_FILL_LIST,
285        Some(body),
286    )
287    .await
288}
289
290/// POST /api/margin-ratio — 获取融资融券比率
291pub async fn get_margin_ratio(
292    State(state): State<RestState>,
293    Json(body): Json<Value>,
294) -> ApiResult {
295    adapter::proto_request::<trd_get_margin_ratio::Request, trd_get_margin_ratio::Response>(
296        &state,
297        proto_id::TRD_GET_MARGIN_RATIO,
298        Some(body),
299    )
300    .await
301}
302
303/// POST /api/order-fee — 获取订单费用
304pub async fn get_order_fee(State(state): State<RestState>, Json(body): Json<Value>) -> ApiResult {
305    adapter::proto_request::<trd_get_order_fee::Request, trd_get_order_fee::Response>(
306        &state,
307        proto_id::TRD_GET_ORDER_FEE,
308        Some(body),
309    )
310    .await
311}