1use std::sync::Arc;
4
5use axum::extract::{Extension, Json, State};
6use axum::http::{HeaderMap, StatusCode};
7use serde_json::Value;
8
9use futu_auth::{CheckCtx, KeyRecord};
10use futu_core::proto_id;
11use futu_proto::trd_modify_order;
12use futu_proto::trd_place_combo_order;
13use futu_proto::trd_place_order;
14use futu_proto::trd_reconfirm_order;
15
16use super::ApiResult;
17use super::card_num::{
18 extract_and_resolve_card_num_into_acc_id, normalize_and_resolve_card_num_for_route,
19};
20use super::validation::{
21 authorize_trade_write, read_handler_acc_id_check, rest_handler_limit_check, trd_market_str,
22 validate_header_trd_market_write,
23};
24use crate::adapter::{self, RestState};
25
26use super::write_pipeline::{
27 idempotency_key_from_headers, limit_reject_response, surface_spec_or_internal_error,
28};
29
30pub async fn place_order(
37 State(state): State<RestState>,
38 rec: Option<Extension<Arc<KeyRecord>>>,
39 headers: HeaderMap,
40 Json(mut body): Json<Value>,
41) -> ApiResult {
42 crate::adapter::normalize_json_keys_snake_case(&mut body);
44 authorize_trade_write(
45 &state,
46 rec.as_ref().map(|Extension(rec)| rec.as_ref()),
47 &body,
48 "/api/order",
49 )?;
50 let rec_ref_for_card_num = rec.as_ref().map(|Extension(r)| r.as_ref());
60 extract_and_resolve_card_num_into_acc_id(
61 &state,
62 rec_ref_for_card_num,
63 &mut body,
64 "/api/order",
65 )?;
66 validate_header_trd_market_write(&body, "/api/order")?;
69 if let Some(Extension(rec)) = rec {
70 match serde_json::from_value::<trd_place_order::Request>(body.clone()) {
72 Ok(parsed) => rest_handler_limit_check(&state, &rec, &parsed)?,
73 Err(_) => {
74 }
77 }
78 }
79 let idem_key = idempotency_key_from_headers(&headers);
82 adapter::proto_request_with_idempotency::<trd_place_order::Request, trd_place_order::Response>(
83 &state,
84 proto_id::TRD_PLACE_ORDER,
85 Some(body),
86 idem_key,
87 )
88 .await
89}
90
91pub async fn place_combo_order(
93 State(state): State<RestState>,
94 rec: Option<Extension<Arc<KeyRecord>>>,
95 headers: HeaderMap,
96 Json(mut body): Json<Value>,
97) -> ApiResult {
98 crate::adapter::normalize_json_keys_snake_case(&mut body);
99 authorize_trade_write(
100 &state,
101 rec.as_ref().map(|Extension(rec)| rec.as_ref()),
102 &body,
103 "/api/combo-order",
104 )?;
105 let rec_ref_for_card_num = rec.as_ref().map(|Extension(r)| r.as_ref());
106 extract_and_resolve_card_num_into_acc_id(
107 &state,
108 rec_ref_for_card_num,
109 &mut body,
110 "/api/combo-order",
111 )?;
112 validate_header_trd_market_write(&body, "/api/combo-order")?;
113 if let Some(Extension(rec)) = rec
114 && let Ok(parsed) = serde_json::from_value::<trd_place_combo_order::Request>(body.clone())
115 {
116 let market = trd_market_str(parsed.c2s.header.trd_market);
117 let symbol = parsed
118 .c2s
119 .combo_legs
120 .first()
121 .map(|leg| leg.security.code.as_str())
122 .filter(|code| !market.is_empty() && !code.is_empty())
123 .map(|code| format!("{market}.{code}"))
124 .unwrap_or_default();
125 let ctx = CheckCtx {
126 market: market.to_string(),
127 symbol,
128 order_value: parsed.c2s.price.map(|price| price * parsed.c2s.qty),
129 trd_side: None,
130 acc_id: Some(parsed.c2s.header.acc_id),
131 mutation_no_exposure: false,
132 currency: futu_auth::market_to_currency(market).map(String::from),
133 };
134 let now = chrono::Utc::now();
135 let outcome = state
136 .counters
137 .check_full_skip_rate(&rec.id, rec.as_ref(), &ctx, now);
138 if let Some(reason) = outcome.reason() {
139 return Err(limit_reject_response(
140 "/api/combo-order",
141 &rec,
142 &reason,
143 outcome.http_status_code(),
144 ));
145 }
146 }
147 let idem_key = idempotency_key_from_headers(&headers);
148 adapter::proto_request_with_idempotency::<
149 trd_place_combo_order::Request,
150 trd_place_combo_order::Response,
151 >(
152 &state,
153 proto_id::TRD_PLACE_COMBO_ORDER,
154 Some(body),
155 idem_key,
156 )
157 .await
158}
159
160pub async fn modify_order(
178 State(state): State<RestState>,
179 rec: Option<Extension<Arc<KeyRecord>>>,
180 headers: HeaderMap,
181 Json(mut body): Json<Value>,
182) -> ApiResult {
183 crate::adapter::normalize_json_keys_snake_case(&mut body);
185 authorize_trade_write(
186 &state,
187 rec.as_ref().map(|Extension(rec)| rec.as_ref()),
188 &body,
189 "/api/modify-order",
190 )?;
191 let rec_ref_for_card_num = rec.as_ref().map(|Extension(r)| r.as_ref());
195 extract_and_resolve_card_num_into_acc_id(
196 &state,
197 rec_ref_for_card_num,
198 &mut body,
199 "/api/modify-order",
200 )?;
201 validate_header_trd_market_write(&body, "/api/modify-order")?;
206 if let Some(Extension(rec)) = rec
207 && let Ok(parsed) = serde_json::from_value::<trd_modify_order::Request>(body.clone())
208 {
209 let market = trd_market_str(parsed.c2s.header.trd_market);
210 const MODIFY_OP_NORMAL: i32 = 1;
214 let (order_value, mutation_no_exposure) = if parsed.c2s.modify_order_op == MODIFY_OP_NORMAL
215 {
216 let v = match (parsed.c2s.qty, parsed.c2s.price) {
217 (Some(q), Some(pr)) => Some(q * pr),
218 _ => None, };
220 (v, false)
221 } else {
222 (None, true)
223 };
224 let ctx = CheckCtx {
225 market: market.to_string(),
226 symbol: String::new(),
227 order_value,
228 trd_side: None,
229 acc_id: Some(parsed.c2s.header.acc_id), mutation_no_exposure,
231 currency: futu_auth::market_to_currency(market).map(String::from),
233 };
234 let now = chrono::Utc::now();
235 let outcome = state
237 .counters
238 .check_full_skip_rate(&rec.id, rec.as_ref(), &ctx, now);
239 if let Some(reason) = outcome.reason() {
240 return Err(limit_reject_response(
241 "/api/modify-order",
242 &rec,
243 &reason,
244 outcome.http_status_code(),
245 ));
246 }
247 }
248 let idem_key = idempotency_key_from_headers(&headers);
249 adapter::proto_request_with_idempotency::<trd_modify_order::Request, trd_modify_order::Response>(
250 &state,
251 proto_id::TRD_MODIFY_ORDER,
252 Some(body),
253 idem_key,
254 )
255 .await
256}
257
258pub async fn cancel_order(
267 State(state): State<RestState>,
268 rec: Option<Extension<Arc<KeyRecord>>>,
269 headers: HeaderMap,
270 Json(mut body): Json<Value>,
271) -> ApiResult {
272 crate::adapter::normalize_json_keys_snake_case(&mut body);
273 adapter::normalize_cancel_order_env_alias(&mut body).map_err(cancel_order_alias_error)?;
274 authorize_trade_write(
275 &state,
276 rec.as_ref().map(|Extension(rec)| rec.as_ref()),
277 &body,
278 "/api/cancel-order",
279 )?;
280 adapter::normalize_endpoint_local_request_aliases_for_rest_path("/api/cancel-order", &mut body)
281 .map_err(cancel_order_alias_error)?;
282 let rec_ref_for_card_num = rec.as_ref().map(|Extension(r)| r.as_ref());
283 extract_and_resolve_card_num_into_acc_id(
284 &state,
285 rec_ref_for_card_num,
286 &mut body,
287 "/api/cancel-order",
288 )?;
289 adapter::maybe_wrap_flat_body_as_c2s(&mut body);
292 adapter::normalize_endpoint_local_request_aliases_for_rest_path("/api/cancel-order", &mut body)
293 .map_err(cancel_order_alias_error)?;
294 adapter::maybe_expand_flat_trd_header(&mut body);
295 validate_header_trd_market_write(&body, "/api/cancel-order")?;
296 if let Some(Extension(rec)) = rec
297 && let Ok(parsed) = serde_json::from_value::<trd_modify_order::Request>(body.clone())
298 {
299 let market = trd_market_str(parsed.c2s.header.trd_market);
300 let ctx = CheckCtx {
301 market: market.to_string(),
302 symbol: String::new(),
303 order_value: None,
304 trd_side: None,
305 acc_id: Some(parsed.c2s.header.acc_id),
306 mutation_no_exposure: true,
307 currency: futu_auth::market_to_currency(market).map(String::from),
308 };
309 let now = chrono::Utc::now();
310 let outcome = state
311 .counters
312 .check_full_skip_rate(&rec.id, rec.as_ref(), &ctx, now);
313 if let Some(reason) = outcome.reason() {
314 return Err(limit_reject_response(
315 "/api/cancel-order",
316 &rec,
317 &reason,
318 outcome.http_status_code(),
319 ));
320 }
321 }
322 let cancel_spec = surface_spec_or_internal_error("/api/cancel-order", "CancelOrder")?;
323 let idem_key = idempotency_key_from_headers(&headers);
324 adapter::proto_request_with_surface_spec_and_idempotency::<
325 trd_modify_order::Request,
326 trd_modify_order::Response,
327 >(
328 &state,
329 proto_id::TRD_MODIFY_ORDER,
330 Some(body),
331 idem_key,
332 cancel_spec,
333 )
334 .await
335}
336
337fn cancel_order_alias_error(message: String) -> (StatusCode, Json<Value>) {
338 (
339 StatusCode::BAD_REQUEST,
340 Json(serde_json::json!({
341 "ret_type": -1,
342 "ret_msg": message,
343 "error": message,
344 })),
345 )
346}
347
348pub async fn reconfirm_order(
356 State(state): State<RestState>,
357 rec: Option<Extension<Arc<KeyRecord>>>,
358 headers: HeaderMap,
359 Json(mut body): Json<Value>,
360) -> ApiResult {
361 crate::adapter::normalize_json_keys_snake_case(&mut body);
362 authorize_trade_write(
363 &state,
364 rec.as_ref().map(|Extension(rec)| rec.as_ref()),
365 &body,
366 "/api/reconfirm-order",
367 )?;
368 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/reconfirm-order")?;
369 read_handler_acc_id_check(
370 &state,
371 rec.as_deref().map(|r| r.as_ref()),
372 &body,
373 "/api/reconfirm-order",
374 )?;
375 validate_header_trd_market_write(&body, "/api/reconfirm-order")?;
378 if let Some(Extension(rec)) = rec
379 && let Ok(parsed) = serde_json::from_value::<trd_reconfirm_order::Request>(body.clone())
380 {
381 let market = trd_market_str(parsed.c2s.header.trd_market);
382 let ctx = CheckCtx {
383 market: market.to_string(),
384 symbol: String::new(),
385 order_value: None,
386 trd_side: None,
387 acc_id: Some(parsed.c2s.header.acc_id),
388 mutation_no_exposure: true,
389 currency: futu_auth::market_to_currency(market).map(String::from),
390 };
391 let now = chrono::Utc::now();
392 let outcome = state
393 .counters
394 .check_full_skip_rate(&rec.id, rec.as_ref(), &ctx, now);
395 if let Some(reason) = outcome.reason() {
396 return Err(limit_reject_response(
397 "/api/reconfirm-order",
398 &rec,
399 &reason,
400 outcome.http_status_code(),
401 ));
402 }
403 }
404 let reconfirm_spec = surface_spec_or_internal_error("/api/reconfirm-order", "ReconfirmOrder")?;
405 let idem_key = idempotency_key_from_headers(&headers);
406 adapter::proto_request_with_surface_spec_and_idempotency::<
407 trd_reconfirm_order::Request,
408 trd_reconfirm_order::Response,
409 >(
410 &state,
411 proto_id::TRD_RECONFIRM_ORDER,
412 Some(body),
413 idem_key,
414 reconfirm_spec,
415 )
416 .await
417}