1use 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
30fn 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
45fn 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
56fn 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
102pub 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
112pub 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
122pub 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
132pub 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
142pub 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
152pub 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
162pub 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 match serde_json::from_value::<trd_place_order::Request>(body.clone()) {
176 Ok(parsed) => rest_handler_limit_check(&state, &rec, &parsed)?,
177 Err(_) => {
178 }
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
191pub 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
239pub 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
249pub 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
262pub 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
274pub 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
290pub 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
303pub 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}