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