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 read_handler_acc_id_check, rest_handler_limit_check, trd_market_str,
22 validate_header_trd_env_present, 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 let rec_ref_for_card_num = rec.as_ref().map(|Extension(r)| r.as_ref());
54 extract_and_resolve_card_num_into_acc_id(
55 &state,
56 rec_ref_for_card_num,
57 &mut body,
58 "/api/order",
59 )?;
60 validate_header_trd_market_write(&body, "/api/order")?;
63 validate_header_trd_env_present(&body, "/api/order")?;
65 if let Some(Extension(rec)) = rec {
66 match serde_json::from_value::<trd_place_order::Request>(body.clone()) {
68 Ok(parsed) => rest_handler_limit_check(&state, &rec, &parsed)?,
69 Err(_) => {
70 }
73 }
74 }
75 let idem_key = idempotency_key_from_headers(&headers);
78 adapter::proto_request_with_idempotency::<trd_place_order::Request, trd_place_order::Response>(
79 &state,
80 proto_id::TRD_PLACE_ORDER,
81 Some(body),
82 idem_key,
83 )
84 .await
85}
86
87pub async fn place_combo_order(
89 State(state): State<RestState>,
90 rec: Option<Extension<Arc<KeyRecord>>>,
91 headers: HeaderMap,
92 Json(mut body): Json<Value>,
93) -> ApiResult {
94 crate::adapter::normalize_json_keys_snake_case(&mut body);
95 let rec_ref_for_card_num = rec.as_ref().map(|Extension(r)| r.as_ref());
96 extract_and_resolve_card_num_into_acc_id(
97 &state,
98 rec_ref_for_card_num,
99 &mut body,
100 "/api/combo-order",
101 )?;
102 validate_header_trd_market_write(&body, "/api/combo-order")?;
103 validate_header_trd_env_present(&body, "/api/combo-order")?;
104 if let Some(Extension(rec)) = rec
105 && let Ok(parsed) = serde_json::from_value::<trd_place_combo_order::Request>(body.clone())
106 {
107 let market = trd_market_str(parsed.c2s.header.trd_market);
108 let symbol = parsed
109 .c2s
110 .combo_legs
111 .first()
112 .map(|leg| leg.security.code.as_str())
113 .filter(|code| !market.is_empty() && !code.is_empty())
114 .map(|code| format!("{market}.{code}"))
115 .unwrap_or_default();
116 let ctx = CheckCtx {
117 market: market.to_string(),
118 symbol,
119 order_value: parsed.c2s.price.map(|price| price * parsed.c2s.qty),
120 trd_side: None,
121 acc_id: Some(parsed.c2s.header.acc_id),
122 mutation_no_exposure: false,
123 currency: futu_auth::market_to_currency(market).map(String::from),
124 };
125 let now = chrono::Utc::now();
126 let outcome = state
127 .counters
128 .check_full_skip_rate(&rec.id, &rec.limits(), &ctx, now);
129 if let Some(reason) = outcome.reason() {
130 return Err(limit_reject_response(
131 "/api/combo-order",
132 &rec,
133 &reason,
134 outcome.http_status_code(),
135 ));
136 }
137 }
138 let idem_key = idempotency_key_from_headers(&headers);
139 adapter::proto_request_with_idempotency::<
140 trd_place_combo_order::Request,
141 trd_place_combo_order::Response,
142 >(
143 &state,
144 proto_id::TRD_PLACE_COMBO_ORDER,
145 Some(body),
146 idem_key,
147 )
148 .await
149}
150
151pub async fn modify_order(
169 State(state): State<RestState>,
170 rec: Option<Extension<Arc<KeyRecord>>>,
171 headers: HeaderMap,
172 Json(mut body): Json<Value>,
173) -> ApiResult {
174 crate::adapter::normalize_json_keys_snake_case(&mut body);
176 let rec_ref_for_card_num = rec.as_ref().map(|Extension(r)| r.as_ref());
180 extract_and_resolve_card_num_into_acc_id(
181 &state,
182 rec_ref_for_card_num,
183 &mut body,
184 "/api/modify-order",
185 )?;
186 validate_header_trd_market_write(&body, "/api/modify-order")?;
191 validate_header_trd_env_present(&body, "/api/modify-order")?;
193 if let Some(Extension(rec)) = rec
194 && let Ok(parsed) = serde_json::from_value::<trd_modify_order::Request>(body.clone())
195 {
196 let market = trd_market_str(parsed.c2s.header.trd_market);
197 const MODIFY_OP_NORMAL: i32 = 1;
201 let (order_value, mutation_no_exposure) = if parsed.c2s.modify_order_op == MODIFY_OP_NORMAL
202 {
203 let v = match (parsed.c2s.qty, parsed.c2s.price) {
204 (Some(q), Some(pr)) => Some(q * pr),
205 _ => None, };
207 (v, false)
208 } else {
209 (None, true)
210 };
211 let ctx = CheckCtx {
212 market: market.to_string(),
213 symbol: String::new(),
214 order_value,
215 trd_side: None,
216 acc_id: Some(parsed.c2s.header.acc_id), mutation_no_exposure,
218 currency: futu_auth::market_to_currency(market).map(String::from),
220 };
221 let now = chrono::Utc::now();
222 let outcome = state
224 .counters
225 .check_full_skip_rate(&rec.id, &rec.limits(), &ctx, now);
226 if let Some(reason) = outcome.reason() {
227 return Err(limit_reject_response(
228 "/api/modify-order",
229 &rec,
230 &reason,
231 outcome.http_status_code(),
232 ));
233 }
234 }
235 let idem_key = idempotency_key_from_headers(&headers);
236 adapter::proto_request_with_idempotency::<trd_modify_order::Request, trd_modify_order::Response>(
237 &state,
238 proto_id::TRD_MODIFY_ORDER,
239 Some(body),
240 idem_key,
241 )
242 .await
243}
244
245pub async fn cancel_order(
254 State(state): State<RestState>,
255 rec: Option<Extension<Arc<KeyRecord>>>,
256 headers: HeaderMap,
257 Json(mut body): Json<Value>,
258) -> ApiResult {
259 crate::adapter::normalize_json_keys_snake_case(&mut body);
260 adapter::normalize_endpoint_local_request_aliases_for_rest_path("/api/cancel-order", &mut body)
261 .map_err(cancel_order_alias_error)?;
262 let rec_ref_for_card_num = rec.as_ref().map(|Extension(r)| r.as_ref());
263 extract_and_resolve_card_num_into_acc_id(
264 &state,
265 rec_ref_for_card_num,
266 &mut body,
267 "/api/cancel-order",
268 )?;
269 adapter::maybe_wrap_flat_body_as_c2s(&mut body);
272 adapter::normalize_endpoint_local_request_aliases_for_rest_path("/api/cancel-order", &mut body)
273 .map_err(cancel_order_alias_error)?;
274 adapter::maybe_expand_flat_trd_header(&mut body);
275 validate_header_trd_market_write(&body, "/api/cancel-order")?;
276 validate_header_trd_env_present(&body, "/api/cancel-order")?;
277 if let Some(Extension(rec)) = rec
278 && let Ok(parsed) = serde_json::from_value::<trd_modify_order::Request>(body.clone())
279 {
280 let market = trd_market_str(parsed.c2s.header.trd_market);
281 let ctx = CheckCtx {
282 market: market.to_string(),
283 symbol: String::new(),
284 order_value: None,
285 trd_side: None,
286 acc_id: Some(parsed.c2s.header.acc_id),
287 mutation_no_exposure: true,
288 currency: futu_auth::market_to_currency(market).map(String::from),
289 };
290 let now = chrono::Utc::now();
291 let outcome = state
292 .counters
293 .check_full_skip_rate(&rec.id, &rec.limits(), &ctx, now);
294 if let Some(reason) = outcome.reason() {
295 return Err(limit_reject_response(
296 "/api/cancel-order",
297 &rec,
298 &reason,
299 outcome.http_status_code(),
300 ));
301 }
302 }
303 let cancel_spec = surface_spec_or_internal_error("/api/cancel-order", "CancelOrder")?;
304 let idem_key = idempotency_key_from_headers(&headers);
305 adapter::proto_request_with_surface_spec_and_idempotency::<
306 trd_modify_order::Request,
307 trd_modify_order::Response,
308 >(
309 &state,
310 proto_id::TRD_MODIFY_ORDER,
311 Some(body),
312 idem_key,
313 cancel_spec,
314 )
315 .await
316}
317
318#[cfg(test)]
319mod tests;
320
321fn cancel_order_alias_error(message: String) -> (StatusCode, Json<Value>) {
322 (
323 StatusCode::BAD_REQUEST,
324 Json(serde_json::json!({
325 "ret_type": -1,
326 "ret_msg": message,
327 "error": message,
328 })),
329 )
330}
331
332pub async fn reconfirm_order(
340 State(state): State<RestState>,
341 rec: Option<Extension<Arc<KeyRecord>>>,
342 headers: HeaderMap,
343 Json(mut body): Json<Value>,
344) -> ApiResult {
345 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/reconfirm-order")?;
346 read_handler_acc_id_check(
347 &state,
348 rec.as_deref().map(|r| r.as_ref()),
349 &body,
350 "/api/reconfirm-order",
351 )?;
352 validate_header_trd_market_write(&body, "/api/reconfirm-order")?;
355 validate_header_trd_env_present(&body, "/api/reconfirm-order")?;
357 if let Some(Extension(rec)) = rec
358 && let Ok(parsed) = serde_json::from_value::<trd_reconfirm_order::Request>(body.clone())
359 {
360 let market = trd_market_str(parsed.c2s.header.trd_market);
361 let ctx = CheckCtx {
362 market: market.to_string(),
363 symbol: String::new(),
364 order_value: None,
365 trd_side: None,
366 acc_id: Some(parsed.c2s.header.acc_id),
367 mutation_no_exposure: true,
368 currency: futu_auth::market_to_currency(market).map(String::from),
369 };
370 let now = chrono::Utc::now();
371 let outcome = state
372 .counters
373 .check_full_skip_rate(&rec.id, &rec.limits(), &ctx, now);
374 if let Some(reason) = outcome.reason() {
375 return Err(limit_reject_response(
376 "/api/reconfirm-order",
377 &rec,
378 &reason,
379 outcome.http_status_code(),
380 ));
381 }
382 }
383 let reconfirm_spec = surface_spec_or_internal_error("/api/reconfirm-order", "ReconfirmOrder")?;
384 let idem_key = idempotency_key_from_headers(&headers);
385 adapter::proto_request_with_surface_spec_and_idempotency::<
386 trd_reconfirm_order::Request,
387 trd_reconfirm_order::Response,
388 >(
389 &state,
390 proto_id::TRD_RECONFIRM_ORDER,
391 Some(body),
392 idem_key,
393 reconfirm_spec,
394 )
395 .await
396}