1use std::sync::atomic::{AtomicU32, Ordering};
2
3use futu_core::error::{FutuError, Result};
4use futu_core::proto_id;
5use futu_net::client::FutuClient;
6
7use crate::types::{ModifyOrderOp, OrderType};
8use crate::types::{ModifyOrderParams, PlaceOrderParams, PlaceOrderResult};
9
10static PACKET_SERIAL: AtomicU32 = AtomicU32::new(1);
12
13fn client_conn_id(client: &FutuClient) -> Result<u64> {
14 client.conn_id().ok_or(FutuError::NotInitialized)
15}
16
17fn next_packet_id(conn_id: u64) -> futu_proto::common::PacketId {
18 let serial = PACKET_SERIAL.fetch_add(1, Ordering::Relaxed);
19 futu_proto::common::PacketId {
20 conn_id,
23 serial_no: serial,
24 }
25}
26
27fn packet_id_for_idempotency_key(key: &str) -> futu_proto::common::PacketId {
34 use std::collections::hash_map::DefaultHasher;
35 use std::hash::{Hash, Hasher};
36 let mut hasher = DefaultHasher::new();
37 key.hash(&mut hasher);
38 futu_proto::common::PacketId {
39 conn_id: hasher.finish(),
40 serial_no: 0,
41 }
42}
43
44fn derive_sec_market_client(trd_market: i32, code: &str) -> i32 {
61 match trd_market {
62 1 | 4 | 113 => 1, 2 | 11 | 123 => 2, 3 => {
65 let bare = code
67 .trim_start_matches("SH.")
68 .trim_start_matches("SZ.")
69 .trim_start_matches("CN.");
70 match bare.chars().next() {
71 Some('6') | Some('9') => 31, Some('0') | Some('2') | Some('3') => 32, _ => 31, }
75 }
76 6 | 12 | 124 => 41, 8 => 61, 15 => 51, 111 => 71, 112 => 81, _ => 0, }
85}
86
87fn parse_place_order_response_body(body: &[u8]) -> Result<PlaceOrderResult> {
88 let resp: futu_proto::trd_place_order::Response =
89 prost::Message::decode(body).map_err(FutuError::Proto)?;
90
91 if resp.ret_type != 0 {
92 return Err(crate::server_err(
93 resp.ret_type,
94 resp.ret_msg,
95 resp.err_code,
96 ));
97 }
98
99 let s2c = resp
100 .s2c
101 .ok_or(FutuError::Codec("missing s2c in PlaceOrder".into()))?;
102
103 let order_id = s2c.order_id.ok_or_else(|| {
104 FutuError::Codec(
105 "missing orderID in successful PlaceOrder response; C++ \
106 APIServer_Trd_PlaceOrder.cpp:856-864 sets orderID before \
107 returning success"
108 .into(),
109 )
110 })?;
111
112 Ok(PlaceOrderResult { order_id })
113}
114
115fn request_validation_error(msg: impl Into<String>) -> FutuError {
116 FutuError::ServerError {
117 ret_type: -1,
118 msg: msg.into(),
119 }
120}
121
122fn ensure_positive_finite(value: f64, field: &'static str) -> Result<()> {
123 if value.is_finite() && value > 0.0 {
124 Ok(())
125 } else {
126 Err(request_validation_error(format!(
127 "{field} must be finite and > 0 (C++ APIServer trade request validation)"
128 )))
129 }
130}
131
132fn order_type_requires_price(order_type: OrderType) -> bool {
133 matches!(
134 order_type,
135 OrderType::Normal
136 | OrderType::AbsoluteLimit
137 | OrderType::AuctionLimit
138 | OrderType::SpecialLimit
139 | OrderType::SpecialLimitAll
140 | OrderType::StopLimit
141 | OrderType::LimitifTouched
142 | OrderType::TrailingStopLimit
143 | OrderType::TwapLimit
144 | OrderType::VwapLimit
145 )
146}
147
148fn validate_optional_positive_price(price: Option<f64>, field: &'static str) -> Result<()> {
149 if let Some(price) = price {
150 ensure_positive_finite(price, field)?;
151 }
152 Ok(())
153}
154
155fn validate_place_order_params(params: &PlaceOrderParams) -> Result<()> {
156 ensure_positive_finite(params.qty, "place_order.qty")?;
161
162 if order_type_requires_price(params.order_type) {
163 let price = params.price.ok_or_else(|| {
164 request_validation_error(
165 "place_order.price is required for price-based order types \
166 (C++ APIServer_Trd_PlaceOrder.cpp:323-344)",
167 )
168 })?;
169 ensure_positive_finite(price, "place_order.price")?;
170 } else {
171 validate_optional_positive_price(params.price, "place_order.price")?;
172 }
173
174 validate_optional_positive_price(params.aux_price, "place_order.aux_price")?;
175 Ok(())
176}
177
178fn validate_modify_order_params(params: &ModifyOrderParams) -> Result<()> {
179 if params.modify_order_op == ModifyOrderOp::Normal {
180 let qty = params.qty.ok_or_else(|| {
181 request_validation_error(
182 "modify_order.qty is required for normal modify \
183 (C++ APIServer_Trd_ModifyOrder.cpp:27-37)",
184 )
185 })?;
186 ensure_positive_finite(qty, "modify_order.qty")?;
187 validate_optional_positive_price(params.price, "modify_order.price")?;
188 }
189 Ok(())
190}
191
192pub async fn place_order(
193 client: &FutuClient,
194 params: &PlaceOrderParams,
195) -> Result<PlaceOrderResult> {
196 if let Some(label) = crate::market::view_only_fund_market_label(params.header.trd_market as i32)
207 {
208 return Err(FutuError::ServerError {
209 ret_type: -1,
210 msg: format!(
211 "place_order: trd_market {label} 仅支持 view-only read endpoints; \
212 write 路径 (place_order) 用对应主市场. v1.4.102 audit 28 F3 fix."
213 ),
214 });
215 }
216 validate_place_order_params(params)?;
217
218 let packet_id = params
219 .idempotency_key
220 .as_deref()
221 .map(packet_id_for_idempotency_key)
222 .map(Ok)
223 .unwrap_or_else(|| client_conn_id(client).map(next_packet_id))?;
224 let req = futu_proto::trd_place_order::Request {
225 c2s: futu_proto::trd_place_order::C2s {
226 packet_id,
227 header: params.header.to_proto(),
228 trd_side: params.trd_side as i32,
229 order_type: params.order_type as i32,
230 code: params.code.clone(),
231 qty: params.qty,
232 price: params.price,
233 adjust_price: params.adjust_price,
234 adjust_side_and_limit: params.adjust_side_and_limit,
235 sec_market: Some(derive_sec_market_client(
236 params.header.trd_market as i32,
237 ¶ms.code,
238 )),
239 remark: None,
240 time_in_force: None,
241 fill_outside_rth: None,
242 aux_price: params.aux_price,
244 trail_type: params.trail_type,
245 trail_value: params.trail_value,
246 trail_spread: params.trail_spread,
247 session: None,
248 position_id: None,
249 expire_time: None,
250 },
251 };
252
253 let body = prost::Message::encode_to_vec(&req);
254 let resp_frame = client.request(proto_id::TRD_PLACE_ORDER, body).await?;
255
256 parse_place_order_response_body(resp_frame.body.as_ref())
257}
258
259pub async fn modify_order(client: &FutuClient, params: &ModifyOrderParams) -> Result<u64> {
261 if let Some(label) = crate::market::view_only_fund_market_label(params.header.trd_market as i32)
263 {
264 return Err(FutuError::ServerError {
265 ret_type: -1,
266 msg: format!(
267 "modify_order: trd_market {label} 仅支持 view-only read endpoints; \
268 write 路径用对应主市场. v1.4.102 audit 28 F3 fix."
269 ),
270 });
271 }
272 validate_modify_order_params(params)?;
273
274 let packet_id = params
275 .idempotency_key
276 .as_deref()
277 .map(packet_id_for_idempotency_key)
278 .map(Ok)
279 .unwrap_or_else(|| client_conn_id(client).map(next_packet_id))?;
280 let req = futu_proto::trd_modify_order::Request {
281 c2s: futu_proto::trd_modify_order::C2s {
282 packet_id,
283 header: params.header.to_proto(),
284 order_id: params.order_id,
285 modify_order_op: params.modify_order_op as i32,
286 for_all: params.for_all,
287 trd_market: None,
288 qty: params.qty,
289 price: params.price,
290 adjust_price: None,
291 adjust_side_and_limit: None,
292 aux_price: None,
293 trail_type: None,
294 trail_value: None,
295 trail_spread: None,
296 order_id_ex: params.order_id_ex.clone(),
297 },
298 };
299
300 let body = prost::Message::encode_to_vec(&req);
301 let resp_frame = client.request(proto_id::TRD_MODIFY_ORDER, body).await?;
302
303 let resp: futu_proto::trd_modify_order::Response =
304 prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
305
306 if resp.ret_type != 0 {
307 return Err(crate::server_err(
308 resp.ret_type,
309 resp.ret_msg,
310 resp.err_code,
311 ));
312 }
313
314 let s2c = resp
315 .s2c
316 .ok_or(FutuError::Codec("missing s2c in ModifyOrder".into()))?;
317
318 Ok(s2c.order_id)
319}
320
321pub async fn cancel_order(
323 client: &FutuClient,
324 header: &crate::types::TrdHeader,
325 order_id: u64,
326) -> Result<u64> {
327 modify_order(
328 client,
329 &ModifyOrderParams {
330 header: header.clone(),
331 order_id,
332 order_id_ex: None,
333 modify_order_op: crate::types::ModifyOrderOp::Cancel,
334 qty: None,
335 price: None,
336 for_all: None,
337 idempotency_key: None,
338 },
339 )
340 .await
341}
342
343#[cfg(test)]
344mod tests_v1_4_67_bug_3;