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, PlaceOrderOptions, 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
192fn build_place_order_request(
193 packet_id: futu_proto::common::PacketId,
194 params: &PlaceOrderParams,
195 options: &PlaceOrderOptions,
196) -> futu_proto::trd_place_order::Request {
197 futu_proto::trd_place_order::Request {
198 c2s: futu_proto::trd_place_order::C2s {
199 packet_id,
200 header: params.header.to_proto(),
201 trd_side: params.trd_side as i32,
202 order_type: params.order_type as i32,
203 code: params.code.clone(),
204 qty: params.qty,
205 price: params.price,
206 adjust_price: params.adjust_price,
207 adjust_side_and_limit: params.adjust_side_and_limit,
208 sec_market: Some(derive_sec_market_client(
209 params.header.trd_market as i32,
210 ¶ms.code,
211 )),
212 remark: None,
213 time_in_force: options.time_in_force,
214 fill_outside_rth: options.fill_outside_rth,
215 aux_price: params.aux_price,
217 trail_type: params.trail_type,
218 trail_value: params.trail_value,
219 trail_spread: params.trail_spread,
220 session: options.session,
221 position_id: None,
222 expire_time: options.expire_time.clone(),
223 },
224 }
225}
226
227pub async fn place_order(
228 client: &FutuClient,
229 params: &PlaceOrderParams,
230) -> Result<PlaceOrderResult> {
231 place_order_with_options(client, params, &PlaceOrderOptions::default()).await
232}
233
234pub async fn place_order_with_options(
240 client: &FutuClient,
241 params: &PlaceOrderParams,
242 options: &PlaceOrderOptions,
243) -> Result<PlaceOrderResult> {
244 let trd_market = params.header.trd_market;
255 if let Some(label) = crate::market::canonical_fund_trd_market_label(trd_market) {
256 return Err(FutuError::ServerError {
257 ret_type: -1,
258 msg: format!(
259 "place_order: trd_market {label} 仅支持 view-only read endpoints; \
260 write 路径 (place_order) 用对应主市场. v1.4.102 audit 28 F3 fix."
261 ),
262 });
263 }
264 validate_place_order_params(params)?;
265
266 let packet_id = params
267 .idempotency_key
268 .as_deref()
269 .map(packet_id_for_idempotency_key)
270 .map(Ok)
271 .unwrap_or_else(|| client_conn_id(client).map(next_packet_id))?;
272 let req = build_place_order_request(packet_id, params, options);
273
274 let body = prost::Message::encode_to_vec(&req);
275 let resp_frame = client.request(proto_id::TRD_PLACE_ORDER, body).await?;
276
277 parse_place_order_response_body(resp_frame.body.as_ref())
278}
279
280pub async fn modify_order(client: &FutuClient, params: &ModifyOrderParams) -> Result<u64> {
282 let trd_market = params.header.trd_market;
284 if let Some(label) = crate::market::canonical_fund_trd_market_label(trd_market) {
285 return Err(FutuError::ServerError {
286 ret_type: -1,
287 msg: format!(
288 "modify_order: trd_market {label} 仅支持 view-only read endpoints; \
289 write 路径用对应主市场. v1.4.102 audit 28 F3 fix."
290 ),
291 });
292 }
293 validate_modify_order_params(params)?;
294
295 let packet_id = params
296 .idempotency_key
297 .as_deref()
298 .map(packet_id_for_idempotency_key)
299 .map(Ok)
300 .unwrap_or_else(|| client_conn_id(client).map(next_packet_id))?;
301 let req = futu_proto::trd_modify_order::Request {
302 c2s: futu_proto::trd_modify_order::C2s {
303 packet_id,
304 header: params.header.to_proto(),
305 order_id: params.order_id,
306 modify_order_op: params.modify_order_op as i32,
307 for_all: params.for_all,
308 trd_market: None,
309 qty: params.qty,
310 price: params.price,
311 adjust_price: None,
312 adjust_side_and_limit: None,
313 aux_price: None,
314 trail_type: None,
315 trail_value: None,
316 trail_spread: None,
317 order_id_ex: params.order_id_ex.clone(),
318 },
319 };
320
321 let body = prost::Message::encode_to_vec(&req);
322 let resp_frame = client.request(proto_id::TRD_MODIFY_ORDER, body).await?;
323
324 let resp: futu_proto::trd_modify_order::Response =
325 prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
326
327 if resp.ret_type != 0 {
328 return Err(crate::server_err(
329 resp.ret_type,
330 resp.ret_msg,
331 resp.err_code,
332 ));
333 }
334
335 let s2c = resp
336 .s2c
337 .ok_or(FutuError::Codec("missing s2c in ModifyOrder".into()))?;
338
339 Ok(s2c.order_id)
340}
341
342pub async fn cancel_order(
344 client: &FutuClient,
345 header: &crate::types::TrdHeader,
346 order_id: u64,
347) -> Result<u64> {
348 modify_order(
349 client,
350 &ModifyOrderParams {
351 header: header.clone(),
352 order_id,
353 order_id_ex: None,
354 modify_order_op: crate::types::ModifyOrderOp::Cancel,
355 qty: None,
356 price: None,
357 for_all: None,
358 idempotency_key: None,
359 },
360 )
361 .await
362}
363
364#[cfg(test)]
365mod tests_v1_4_67_bug_3;