1use std::sync::Arc;
4
5use axum::extract::{Extension, Json, State};
6use axum::http::StatusCode;
7use serde_json::Value;
8
9use futu_auth::KeyRecord;
10use futu_core::proto_id;
11use futu_proto::trd_get_combo_max_trd_qtys;
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_trd::{currency, read_plan};
22
23use super::card_num::normalize_and_resolve_card_num_for_route;
24use super::validation::{
25 read_handler_acc_id_check, validate_header_trd_env_present, validate_header_trd_market,
26 validate_header_trd_market_write,
27};
28use super::{ApiResult, RawApiResult};
29use crate::adapter::{self, RestState};
30
31pub async fn get_funds(
33 State(state): State<RestState>,
34 rec: Option<Extension<Arc<KeyRecord>>>,
35 Json(mut body): Json<Value>,
36) -> ApiResult {
37 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/funds")?;
39 validate_header_trd_market(&body, "/api/funds")?;
41 validate_header_trd_env_present(&body, "/api/funds")?;
43 read_handler_acc_id_check(
44 &state,
45 rec.as_deref().map(|r| r.as_ref()),
46 &body,
47 "/api/funds",
48 )?;
49 normalize_trd_currency_labels(&mut body).map_err(|e| {
50 (
51 axum::http::StatusCode::BAD_REQUEST,
52 Json(serde_json::json!({
53 "ret_type": -1,
54 "ret_msg": e,
55 "error": e,
56 })),
57 )
58 })?;
59
60 let requested_currency: Option<i32> = body
63 .pointer("/c2s/currency")
64 .or_else(|| body.pointer("/currency"))
65 .and_then(|v| v.as_i64())
66 .map(|v| v as i32);
67
68 let mut resp = adapter::proto_request::<trd_get_funds::Request, trd_get_funds::Response>(
69 &state,
70 proto_id::TRD_GET_FUNDS,
71 Some(body),
72 )
73 .await?;
74
75 let response_currency = resp
76 .0
77 .pointer("/s2c/funds/currency")
78 .and_then(|v| v.as_i64())
79 .map(|v| v as i32);
80 if let Some(warn_msg) =
81 read_plan::funds_currency_mismatch_warning(requested_currency, response_currency)
82 && let Some(obj) = resp.0.as_object_mut()
83 {
84 obj.insert(
85 "currency_warning".to_string(),
86 serde_json::Value::String(warn_msg.clone()),
87 );
88 let existing_msg = obj
90 .get("ret_msg")
91 .and_then(|v| v.as_str())
92 .unwrap_or("")
93 .to_string();
94 let new_msg = if existing_msg.is_empty() {
95 format!("⚠️ {warn_msg}")
96 } else {
97 format!("⚠️ {warn_msg}\n[既有] {existing_msg}")
98 };
99 obj.insert("ret_msg".to_string(), serde_json::Value::String(new_msg));
100 }
101
102 Ok(resp)
103}
104
105fn normalize_trd_currency_labels(body: &mut Value) -> Result<(), String> {
106 for pointer in ["/c2s/currency", "/currency"] {
107 let Some(value) = body.pointer_mut(pointer) else {
108 continue;
109 };
110 let Value::String(label) = value else {
111 continue;
112 };
113 let parsed = currency::parse_currency_label(label).map_err(|err| err.to_string())?;
114 *value = Value::Number(serde_json::Number::from(parsed));
115 }
116 Ok(())
117}
118
119#[cfg(test)]
120mod tests;
121
122pub async fn get_positions(
124 State(state): State<RestState>,
125 rec: Option<Extension<Arc<KeyRecord>>>,
126 Json(mut body): Json<Value>,
127) -> RawApiResult {
128 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/positions")?;
129 validate_header_trd_market(&body, "/api/positions")?;
131 validate_header_trd_env_present(&body, "/api/positions")?;
133 read_handler_acc_id_check(
134 &state,
135 rec.as_deref().map(|r| r.as_ref()),
136 &body,
137 "/api/positions",
138 )?;
139 normalize_trd_currency_labels(&mut body).map_err(|e| {
140 (
141 axum::http::StatusCode::BAD_REQUEST,
142 Json(serde_json::json!({
143 "ret_type": -1,
144 "ret_msg": e,
145 "error": e,
146 })),
147 )
148 })?;
149 adapter::proto_request_raw::<trd_get_position_list::Request, trd_get_position_list::Response>(
150 &state,
151 proto_id::TRD_GET_POSITION_LIST,
152 Some(body),
153 )
154 .await
155}
156
157pub async fn get_orders(
159 State(state): State<RestState>,
160 rec: Option<Extension<Arc<KeyRecord>>>,
161 Json(mut body): Json<Value>,
162) -> RawApiResult {
163 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/orders")?;
164 validate_header_trd_market_write(&body, "/api/orders")?;
167 validate_header_trd_env_present(&body, "/api/orders")?;
169 read_handler_acc_id_check(
170 &state,
171 rec.as_deref().map(|r| r.as_ref()),
172 &body,
173 "/api/orders",
174 )?;
175 adapter::proto_request_raw::<trd_get_order_list::Request, trd_get_order_list::Response>(
176 &state,
177 proto_id::TRD_GET_ORDER_LIST,
178 Some(body),
179 )
180 .await
181}
182
183pub async fn get_order_fills(
185 State(state): State<RestState>,
186 rec: Option<Extension<Arc<KeyRecord>>>,
187 Json(mut body): Json<Value>,
188) -> RawApiResult {
189 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/order-fills")?;
190 validate_header_trd_market_write(&body, "/api/order-fills")?;
193 validate_header_trd_env_present(&body, "/api/order-fills")?;
195 read_handler_acc_id_check(
196 &state,
197 rec.as_deref().map(|r| r.as_ref()),
198 &body,
199 "/api/order-fills",
200 )?;
201 adapter::proto_request_raw::<
202 trd_get_order_fill_list::Request,
203 trd_get_order_fill_list::Response,
204 >(
205 &state,
206 proto_id::TRD_GET_ORDER_FILL_LIST,
207 Some(body),
208 )
209 .await
210}
211
212pub async fn get_max_trd_qtys(
214 State(state): State<RestState>,
215 rec: Option<Extension<Arc<KeyRecord>>>,
216 Json(mut body): Json<Value>,
217) -> RawApiResult {
218 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/max-trd-qtys")?;
219 validate_header_trd_market_write(&body, "/api/max-trd-qtys")?;
222 validate_header_trd_env_present(&body, "/api/max-trd-qtys")?;
224 read_handler_acc_id_check(
225 &state,
226 rec.as_deref().map(|r| r.as_ref()),
227 &body,
228 "/api/max-trd-qtys",
229 )?;
230 adapter::proto_request_raw::<trd_get_max_trd_qtys::Request, trd_get_max_trd_qtys::Response>(
231 &state,
232 proto_id::TRD_GET_MAX_TRD_QTYS,
233 Some(body),
234 )
235 .await
236}
237
238pub async fn get_combo_max_trd_qtys(
240 State(state): State<RestState>,
241 rec: Option<Extension<Arc<KeyRecord>>>,
242 Json(mut body): Json<Value>,
243) -> RawApiResult {
244 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/combo-max-trd-qtys")?;
245 validate_header_trd_market_write(&body, "/api/combo-max-trd-qtys")?;
246 validate_header_trd_env_present(&body, "/api/combo-max-trd-qtys")?;
247 read_handler_acc_id_check(
248 &state,
249 rec.as_deref().map(|r| r.as_ref()),
250 &body,
251 "/api/combo-max-trd-qtys",
252 )?;
253 adapter::proto_request_raw::<
254 trd_get_combo_max_trd_qtys::Request,
255 trd_get_combo_max_trd_qtys::Response,
256 >(&state, proto_id::TRD_GET_COMBO_MAX_TRD_QTYS, Some(body))
257 .await
258}
259
260pub async fn get_history_orders(
262 State(state): State<RestState>,
263 rec: Option<Extension<Arc<KeyRecord>>>,
264 Json(mut body): Json<Value>,
265) -> RawApiResult {
266 adapter::normalize_json_keys_snake_case(&mut body);
267 adapter::normalize_endpoint_local_request_aliases_for_rest_path(
268 "/api/history-orders",
269 &mut body,
270 )
271 .map_err(history_order_alias_error)?;
272 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/history-orders")?;
273 read_handler_acc_id_check(
274 &state,
275 rec.as_deref().map(|r| r.as_ref()),
276 &body,
277 "/api/history-orders",
278 )?;
279 validate_header_trd_market(&body, "/api/history-orders")?;
282 validate_header_trd_env_present(&body, "/api/history-orders")?;
284 adapter::proto_request_raw::<
285 trd_get_history_order_list::Request,
286 trd_get_history_order_list::Response,
287 >(&state, proto_id::TRD_GET_HISTORY_ORDER_LIST, Some(body))
288 .await
289}
290
291pub async fn get_history_order_fills(
293 State(state): State<RestState>,
294 rec: Option<Extension<Arc<KeyRecord>>>,
295 Json(mut body): Json<Value>,
296) -> RawApiResult {
297 adapter::normalize_json_keys_snake_case(&mut body);
298 adapter::normalize_endpoint_local_request_aliases_for_rest_path(
299 "/api/history-order-fills",
300 &mut body,
301 )
302 .map_err(history_order_alias_error)?;
303 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/history-order-fills")?;
304 read_handler_acc_id_check(
305 &state,
306 rec.as_deref().map(|r| r.as_ref()),
307 &body,
308 "/api/history-order-fills",
309 )?;
310 validate_header_trd_market(&body, "/api/history-order-fills")?;
312 validate_header_trd_env_present(&body, "/api/history-order-fills")?;
314 adapter::proto_request_raw::<
315 trd_get_history_order_fill_list::Request,
316 trd_get_history_order_fill_list::Response,
317 >(
318 &state,
319 proto_id::TRD_GET_HISTORY_ORDER_FILL_LIST,
320 Some(body),
321 )
322 .await
323}
324
325fn history_order_alias_error(message: String) -> (StatusCode, Json<Value>) {
326 (
327 StatusCode::BAD_REQUEST,
328 Json(serde_json::json!({
329 "ret_type": -1,
330 "ret_msg": message,
331 "error": message,
332 })),
333 )
334}
335
336pub async fn get_margin_ratio(
338 State(state): State<RestState>,
339 rec: Option<Extension<Arc<KeyRecord>>>,
340 Json(mut body): Json<Value>,
341) -> RawApiResult {
342 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/margin-ratio")?;
343 validate_header_trd_market_write(&body, "/api/margin-ratio")?;
346 validate_header_trd_env_present(&body, "/api/margin-ratio")?;
348 read_handler_acc_id_check(
349 &state,
350 rec.as_deref().map(|r| r.as_ref()),
351 &body,
352 "/api/margin-ratio",
353 )?;
354 adapter::proto_request_raw::<trd_get_margin_ratio::Request, trd_get_margin_ratio::Response>(
355 &state,
356 proto_id::TRD_GET_MARGIN_RATIO,
357 Some(body),
358 )
359 .await
360}
361
362pub async fn get_order_fee(
364 State(state): State<RestState>,
365 rec: Option<Extension<Arc<KeyRecord>>>,
366 Json(mut body): Json<Value>,
367) -> RawApiResult {
368 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/order-fee")?;
369 read_handler_acc_id_check(
370 &state,
371 rec.as_deref().map(|r| r.as_ref()),
372 &body,
373 "/api/order-fee",
374 )?;
375 validate_header_trd_market_write(&body, "/api/order-fee")?;
378 validate_header_trd_env_present(&body, "/api/order-fee")?;
380 adapter::proto_request_raw::<trd_get_order_fee::Request, trd_get_order_fee::Response>(
381 &state,
382 proto_id::TRD_GET_ORDER_FEE,
383 Some(body),
384 )
385 .await
386}