futu_mcp/handlers/
trade_write.rs

1//! 交易写 handler:place / modify / cancel
2//!
3//! 本模块的函数本身不做权限检查;调用前由 tools.rs 根据 ServerState 的
4//! `enable_trading` / `allow_real_trading` 做前置守卫。
5
6use std::sync::Arc;
7
8use anyhow::{bail, Result};
9use futu_net::client::FutuClient;
10use futu_trd::types::{
11    ModifyOrderOp, ModifyOrderParams, OrderType, PlaceOrderParams, TrdEnv, TrdHeader, TrdMarket,
12    TrdSide,
13};
14use serde::Serialize;
15
16// ========== 枚举解析(复用只读 handler 的字符串约定) ==========
17
18pub fn parse_trd_market(s: &str) -> Result<TrdMarket> {
19    let m = match s.trim().to_ascii_uppercase().as_str() {
20        "HK" => TrdMarket::HK,
21        "US" => TrdMarket::US,
22        "CN" => TrdMarket::CN,
23        "HKCC" => TrdMarket::HKCC,
24        other => bail!("unknown trd market {other:?} (HK|US|CN|HKCC)"),
25    };
26    Ok(m)
27}
28
29pub fn parse_trd_env(s: &str) -> Result<TrdEnv> {
30    let e = match s.trim().to_ascii_lowercase().as_str() {
31        "simulate" | "sim" => TrdEnv::Simulate,
32        "real" => TrdEnv::Real,
33        other => bail!("unknown trd env {other:?} (real|simulate)"),
34    };
35    Ok(e)
36}
37
38pub fn parse_trd_side(s: &str) -> Result<TrdSide> {
39    let v = match s.trim().to_ascii_uppercase().as_str() {
40        "BUY" => TrdSide::Buy,
41        "SELL" => TrdSide::Sell,
42        "SELL_SHORT" | "SHORT" => TrdSide::SellShort,
43        "BUY_BACK" | "COVER" => TrdSide::BuyBack,
44        other => bail!("unknown trd side {other:?} (BUY|SELL|SELL_SHORT|BUY_BACK)"),
45    };
46    Ok(v)
47}
48
49pub fn parse_order_type(s: &str) -> Result<OrderType> {
50    let v = match s.trim().to_ascii_uppercase().as_str() {
51        "NORMAL" | "LIMIT" => OrderType::Normal,
52        "MARKET" => OrderType::Market,
53        "ABSOLUTE_LIMIT" => OrderType::AbsoluteLimit,
54        "AUCTION" => OrderType::Auction,
55        "AUCTION_LIMIT" => OrderType::AuctionLimit,
56        "SPECIAL_LIMIT" => OrderType::SpecialLimit,
57        other => bail!(
58            "unknown order type {other:?} (NORMAL|MARKET|ABSOLUTE_LIMIT|AUCTION|AUCTION_LIMIT|SPECIAL_LIMIT)"
59        ),
60    };
61    Ok(v)
62}
63
64pub fn parse_modify_op(s: &str) -> Result<ModifyOrderOp> {
65    let v = match s.trim().to_ascii_uppercase().as_str() {
66        "NORMAL" | "MODIFY" => ModifyOrderOp::Normal,
67        "CANCEL" => ModifyOrderOp::Cancel,
68        "DISABLE" => ModifyOrderOp::Disable,
69        "ENABLE" => ModifyOrderOp::Enable,
70        "DELETE" => ModifyOrderOp::Delete,
71        other => bail!("unknown modify op {other:?} (NORMAL|CANCEL|DISABLE|ENABLE|DELETE)"),
72    };
73    Ok(v)
74}
75
76fn build_header(env: &str, acc_id: u64, market: &str) -> Result<TrdHeader> {
77    Ok(TrdHeader {
78        trd_env: parse_trd_env(env)?,
79        acc_id,
80        trd_market: parse_trd_market(market)?,
81    })
82}
83
84// ========== place ==========
85
86#[derive(Serialize)]
87struct PlaceOut {
88    order_id: u64,
89    env: &'static str,
90    market: String,
91    acc_id: u64,
92    side: String,
93    order_type: String,
94    code: String,
95    qty: f64,
96    price: Option<f64>,
97}
98
99#[allow(clippy::too_many_arguments)]
100pub async fn place_order(
101    client: &Arc<FutuClient>,
102    env: &str,
103    acc_id: u64,
104    market: &str,
105    side: &str,
106    order_type: &str,
107    code: &str,
108    qty: f64,
109    price: Option<f64>,
110) -> Result<String> {
111    let header = build_header(env, acc_id, market)?;
112    let trd_side = parse_trd_side(side)?;
113    let ord_type = parse_order_type(order_type)?;
114
115    let params = PlaceOrderParams {
116        header: header.clone(),
117        trd_side,
118        order_type: ord_type,
119        code: code.to_string(),
120        qty,
121        price,
122        adjust_price: None,
123        adjust_side_and_limit: None,
124    };
125    let res = futu_trd::order::place_order(client, &params).await?;
126
127    let out = PlaceOut {
128        order_id: res.order_id,
129        env: match header.trd_env {
130            TrdEnv::Simulate => "simulate",
131            TrdEnv::Real => "real",
132        },
133        market: market.to_ascii_uppercase(),
134        acc_id,
135        side: side.to_ascii_uppercase(),
136        order_type: order_type.to_ascii_uppercase(),
137        code: code.to_string(),
138        qty,
139        price,
140    };
141    Ok(serde_json::to_string_pretty(&out)?)
142}
143
144// ========== modify ==========
145
146#[derive(Serialize)]
147struct ModifyOut {
148    order_id: u64,
149    op: String,
150    env: &'static str,
151    qty: Option<f64>,
152    price: Option<f64>,
153}
154
155#[allow(clippy::too_many_arguments)]
156pub async fn modify_order(
157    client: &Arc<FutuClient>,
158    env: &str,
159    acc_id: u64,
160    market: &str,
161    order_id: u64,
162    op: &str,
163    qty: Option<f64>,
164    price: Option<f64>,
165) -> Result<String> {
166    let header = build_header(env, acc_id, market)?;
167    let mop = parse_modify_op(op)?;
168
169    let params = ModifyOrderParams {
170        header: header.clone(),
171        order_id,
172        modify_order_op: mop,
173        qty,
174        price,
175        for_all: None,
176    };
177    let returned_id = futu_trd::order::modify_order(client, &params).await?;
178
179    let out = ModifyOut {
180        order_id: returned_id,
181        op: op.to_ascii_uppercase(),
182        env: match header.trd_env {
183            TrdEnv::Simulate => "simulate",
184            TrdEnv::Real => "real",
185        },
186        qty,
187        price,
188    };
189    Ok(serde_json::to_string_pretty(&out)?)
190}
191
192// ========== cancel ==========
193
194#[derive(Serialize)]
195struct CancelOut {
196    order_id: u64,
197    op: &'static str,
198    env: &'static str,
199}
200
201pub async fn cancel_order(
202    client: &Arc<FutuClient>,
203    env: &str,
204    acc_id: u64,
205    market: &str,
206    order_id: u64,
207) -> Result<String> {
208    let header = build_header(env, acc_id, market)?;
209    let returned_id = futu_trd::order::cancel_order(client, &header, order_id).await?;
210    let out = CancelOut {
211        order_id: returned_id,
212        op: "CANCEL",
213        env: match header.trd_env {
214            TrdEnv::Simulate => "simulate",
215            TrdEnv::Real => "real",
216        },
217    };
218    Ok(serde_json::to_string_pretty(&out)?)
219}
220
221// ========== 环境守卫 ==========
222
223/// 判断给定的 env 字符串是否指向真实环境。
224pub fn is_real_env(env: &str) -> bool {
225    matches!(env.trim().to_ascii_lowercase().as_str(), "real")
226}