Skip to main content

futu_rest/routes/trd/
write.rs

1//! REST trade write routes.
2
3use 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
25/// POST /api/order — 下单
26///
27/// v1.2:在 dispatch 之前先解析 JSON 提取 CheckCtx,跑 `check_full_skip_rate`
28/// 做 market/symbol/value/side/daily 细粒度检查(auth 层已做 rate/hours 闸门)。
29/// `Extension<Arc<KeyRecord>>` 来自 bearer_auth middleware;scope 模式下必有,
30/// legacy 模式下没有该 extension → 跳过 handler 层检查(保持旧行为)。
31pub 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    // v1.4.45: normalize camelCase → snake_case(兼容 FTAPI 官方文档字段名)
38    crate::adapter::normalize_json_keys_snake_case(&mut body);
39    // v1.4.105 D12 (Phase 2): 提取 card_num 字段 (top-level / c2s.header) →
40    // resolve via trd_cache → 写进 c2s.header.acc_id. user 可用 4 位末尾或
41    // 16 位完整卡号代替 acc_id (App 显示的便利)。**必须在 trd_market /
42    // trd_env validate 之前**, 因为 promote_flat / acc_id 写入要在 validate
43    // 之前完成 — 但实际上 placeholder header 字段在 validate 后, 我们这里
44    // 把 card_num strip + acc_id 填回不会影响 trd_market validation 顺序.
45    //
46    // v1.4.105 contract-hardening 补丁: 传 rec 让 helper 同步做 string-level
47    // allowed_card_nums whitelist 校验 (UX 清晰).
48    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    // v1.4.93 P0-4 (NEW-C-01): trd_market enum 白名单(在 normalize 后做,保证看到 snake_case)
56    // v1.4.102 codex 26 F1 (P1): write 路径用更窄 allowlist (无 113/123)
57    validate_header_trd_market_write(&body, "/api/order")?;
58    // v1.4.102 BUG-005: 缺 trd_env 直接 400 (避免 "Nonexisting acc_id" 误导)
59    validate_header_trd_env_present(&body, "/api/order")?;
60    if let Some(Extension(rec)) = rec {
61        // 解析 JSON → trd_place_order::Request 提取 CheckCtx
62        match serde_json::from_value::<trd_place_order::Request>(body.clone()) {
63            Ok(parsed) => rest_handler_limit_check(&state, &rec, &parsed)?,
64            Err(_) => {
65                // 反序列化失败时不挡,让下游 dispatch 报真正的 400/格式错误
66                // (limits 不该挡格式问题)
67            }
68        }
69    }
70    // v1.4.38 Phase 4: 提取 `Idempotency-Key` header(客户端显式 opt-in)。
71    // 无 header → 透传不加幂等保护(backward-compat)。
72    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
85/// POST /api/modify-order — 改单/撤单
86///
87/// v1.2:和 `place_order` 类似但 ModifyOrder protobuf 给不出 symbol/qty/value
88/// (只有 order_id),只能做 market 白名单 + hours 检查;symbol/value/side
89/// 留空让 `check_full_skip_rate` 自动跳过。
90pub 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    // v1.4.45: normalize camelCase → snake_case
97    crate::adapter::normalize_json_keys_snake_case(&mut body);
98    // v1.4.105 D12 (Phase 2): card_num → acc_id 解析 (与 place_order 一致).
99    // v1.4.105 D12 contract-hardening 补丁: 同 place_order, 加 rec 做 string-level
100    // allowed_card_nums whitelist 校验.
101    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    // v1.4.96 BUG #003 hotfix (eli double-tester report 2026-04-26):
109    // 之前漏 validate, trd_market=999 silent accept. 加 validate 让 modify-order
110    // 与 place-order 校验对齐.
111    // v1.4.102 codex 26 F1 (P1): write 路径用更窄 allowlist (无 113/123)
112    validate_header_trd_market_write(&body, "/api/modify-order")?;
113    // v1.4.102 BUG-005: 缺 trd_env 直接 400 (避免 "Nonexisting acc_id" 误导)
114    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        // v1.4.106 codex 0538 F2 (P2): ModifyOrder Normal (op=1) 改价/改量 →
120        // 新 exposure delta, 算 qty*price 进 daily counter; Cancel/Disable/
121        // Enable/Delete 标 mutation_no_exposure=true 跳 daily counter.
122        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, // MARKET 单 / proto 不全 → 跳金额检查
128            };
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), // v1.4.35
139            mutation_no_exposure,
140            // v1.4.106 F4 (P3): 派生 per-market currency
141            currency: futu_auth::market_to_currency(market).map(String::from),
142        };
143        let now = chrono::Utc::now();
144        // v1.4.36 Bug #1:Whitelist → 403
145        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
178/// v1.4.40 #4 fix (eli exhaustive report): 暴露 `/api/reconfirm-order` endpoint。
179///
180/// daemon 内部一直注册了 `TRD_RECONFIRM_ORDER` (proto_id 2237) 的 handler,但 REST
181/// 层没路由过来,用户无法通过 REST 重新确认订单(比如美股下 day-trade 订单需要
182/// 用户显式确认才会真正提交)。v1.4.40 补上此 endpoint。
183///
184/// POST /api/reconfirm-order — 重新确认订单(美股 PDT 规则下的二次确认路径)
185pub 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    // v1.4.96 BUG #003 hotfix
198    // v1.4.102 codex 26 F1 (P1): write 路径用更窄 allowlist (无 113/123)
199    validate_header_trd_market_write(&body, "/api/reconfirm-order")?;
200    // v1.4.102 BUG-005: 缺 trd_env 直接 400 (避免 "Nonexisting acc_id" 误导)
201    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}