futu_rest/routes/trd/
write.rs1use 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_order;
13use futu_proto::trd_reconfirm_order;
14
15use super::ApiResult;
16use super::card_num::{
17 extract_and_resolve_card_num_into_acc_id, normalize_and_resolve_card_num_for_route,
18};
19use super::validation::{
20 read_handler_acc_id_check, rest_handler_limit_check, trd_market_str,
21 validate_header_trd_env_present, validate_header_trd_market_write,
22};
23use crate::adapter::{self, RestState};
24
25pub async fn place_order(
32 State(state): State<RestState>,
33 rec: Option<Extension<Arc<KeyRecord>>>,
34 headers: HeaderMap,
35 Json(mut body): Json<Value>,
36) -> ApiResult {
37 crate::adapter::normalize_json_keys_snake_case(&mut body);
39 let rec_ref_for_card_num = rec.as_ref().map(|Extension(r)| r.as_ref());
49 extract_and_resolve_card_num_into_acc_id(
50 &state,
51 rec_ref_for_card_num,
52 &mut body,
53 "/api/order",
54 )?;
55 validate_header_trd_market_write(&body, "/api/order")?;
58 validate_header_trd_env_present(&body, "/api/order")?;
60 if let Some(Extension(rec)) = rec {
61 match serde_json::from_value::<trd_place_order::Request>(body.clone()) {
63 Ok(parsed) => rest_handler_limit_check(&state, &rec, &parsed)?,
64 Err(_) => {
65 }
68 }
69 }
70 let idem_key = headers
73 .get("idempotency-key")
74 .and_then(|v| v.to_str().ok())
75 .map(|s| s.to_string());
76 adapter::proto_request_with_idempotency::<trd_place_order::Request, trd_place_order::Response>(
77 &state,
78 proto_id::TRD_PLACE_ORDER,
79 Some(body),
80 idem_key,
81 )
82 .await
83}
84
85pub async fn modify_order(
91 State(state): State<RestState>,
92 rec: Option<Extension<Arc<KeyRecord>>>,
93 headers: HeaderMap,
94 Json(mut body): Json<Value>,
95) -> ApiResult {
96 crate::adapter::normalize_json_keys_snake_case(&mut body);
98 let rec_ref_for_card_num = rec.as_ref().map(|Extension(r)| r.as_ref());
102 extract_and_resolve_card_num_into_acc_id(
103 &state,
104 rec_ref_for_card_num,
105 &mut body,
106 "/api/modify-order",
107 )?;
108 validate_header_trd_market_write(&body, "/api/modify-order")?;
113 validate_header_trd_env_present(&body, "/api/modify-order")?;
115 if let Some(Extension(rec)) = rec
116 && let Ok(parsed) = serde_json::from_value::<trd_modify_order::Request>(body.clone())
117 {
118 let market = trd_market_str(parsed.c2s.header.trd_market);
119 const MODIFY_OP_NORMAL: i32 = 1;
123 let (order_value, mutation_no_exposure) = if parsed.c2s.modify_order_op == MODIFY_OP_NORMAL
124 {
125 let v = match (parsed.c2s.qty, parsed.c2s.price) {
126 (Some(q), Some(pr)) => Some(q * pr),
127 _ => None, };
129 (v, false)
130 } else {
131 (None, true)
132 };
133 let ctx = CheckCtx {
134 market: market.to_string(),
135 symbol: String::new(),
136 order_value,
137 trd_side: None,
138 acc_id: Some(parsed.c2s.header.acc_id), mutation_no_exposure,
140 currency: futu_auth::market_to_currency(market).map(String::from),
142 };
143 let now = chrono::Utc::now();
144 let outcome = state
146 .counters
147 .check_full_skip_rate(&rec.id, &rec.limits(), &ctx, now);
148 if let Some(reason) = outcome.reason() {
149 futu_auth::audit::reject(
150 "rest",
151 "/api/modify-order",
152 &rec.id,
153 &format!("limit: {reason}"),
154 );
155 let status = StatusCode::from_u16(outcome.http_status_code())
156 .unwrap_or(StatusCode::TOO_MANY_REQUESTS);
157 return Err((
158 status,
159 Json(serde_json::json!({
160 "error": format!("limit check failed: {reason}")
161 })),
162 ));
163 }
164 }
165 let idem_key = headers
166 .get("idempotency-key")
167 .and_then(|v| v.to_str().ok())
168 .map(|s| s.to_string());
169 adapter::proto_request_with_idempotency::<trd_modify_order::Request, trd_modify_order::Response>(
170 &state,
171 proto_id::TRD_MODIFY_ORDER,
172 Some(body),
173 idem_key,
174 )
175 .await
176}
177
178pub async fn reconfirm_order(
186 State(state): State<RestState>,
187 rec: Option<Extension<Arc<KeyRecord>>>,
188 Json(mut body): Json<Value>,
189) -> ApiResult {
190 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/reconfirm-order")?;
191 read_handler_acc_id_check(
192 &state,
193 rec.as_deref().map(|r| r.as_ref()),
194 &body,
195 "/api/reconfirm-order",
196 )?;
197 validate_header_trd_market_write(&body, "/api/reconfirm-order")?;
200 validate_header_trd_env_present(&body, "/api/reconfirm-order")?;
202 adapter::proto_request::<trd_reconfirm_order::Request, trd_reconfirm_order::Response>(
203 &state,
204 proto_id::TRD_RECONFIRM_ORDER,
205 Some(body),
206 )
207 .await
208}