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::{
9 ModifyOrderParams, PlaceOrderOptions, PlaceOrderParams, PlaceOrderResult,
10 PlaceOrderResultWithIdentity,
11};
12
13static PACKET_SERIAL: AtomicU32 = AtomicU32::new(1);
15
16fn client_conn_id(client: &FutuClient) -> Result<u64> {
17 client.conn_id().ok_or(FutuError::NotInitialized)
18}
19
20fn next_packet_id(conn_id: u64) -> futu_proto::common::PacketId {
21 let serial = PACKET_SERIAL.fetch_add(1, Ordering::Relaxed);
22 futu_proto::common::PacketId {
23 conn_id,
26 serial_no: serial,
27 }
28}
29
30fn packet_id_for_idempotency_key(key: &str) -> futu_proto::common::PacketId {
37 use std::collections::hash_map::DefaultHasher;
38 use std::hash::{Hash, Hasher};
39 let mut hasher = DefaultHasher::new();
40 key.hash(&mut hasher);
41 futu_proto::common::PacketId {
42 conn_id: hasher.finish(),
43 serial_no: 0,
44 }
45}
46
47fn derive_sec_market_client(trd_market: i32, code: &str) -> i32 {
66 futu_core::trade_security::derive_trd_sec_market_like_cpp(
67 futu_core::trade_security::TrdSecMarketInput {
68 ftapi_sec_market: 0,
69 trd_market,
70 code,
71 },
72 )
73}
74
75fn parse_place_order_response_body(body: &[u8]) -> Result<PlaceOrderResultWithIdentity> {
76 let resp: futu_proto::trd_place_order::Response =
77 prost::Message::decode(body).map_err(FutuError::Proto)?;
78
79 if resp.ret_type != 0 {
80 return Err(crate::server_err(
81 resp.ret_type,
82 resp.ret_msg,
83 resp.err_code,
84 ));
85 }
86
87 let s2c = resp
88 .s2c
89 .ok_or(FutuError::Codec("missing s2c in PlaceOrder".into()))?;
90
91 let order_id = s2c.order_id.ok_or_else(|| {
92 FutuError::Codec("missing orderID in successful PlaceOrder response".into())
94 })?;
95 let order_id_ex = s2c
96 .order_id_ex
97 .filter(|value| !value.is_empty())
98 .ok_or_else(|| {
99 FutuError::Codec("missing orderIDEx in successful PlaceOrder response".into())
102 })?;
103
104 Ok(PlaceOrderResultWithIdentity {
105 order_id,
106 order_id_ex,
107 })
108}
109
110fn request_validation_error(msg: impl Into<String>) -> FutuError {
111 FutuError::ServerError {
112 ret_type: -1,
113 msg: msg.into(),
114 }
115}
116
117fn ensure_positive_finite(value: f64, field: &'static str) -> Result<()> {
118 if value.is_finite() && value > 0.0 {
119 Ok(())
120 } else {
121 Err(request_validation_error(format!(
122 "{field} must be finite and > 0 (C++ APIServer trade request validation)"
123 )))
124 }
125}
126
127fn order_type_requires_price(order_type: OrderType) -> bool {
128 matches!(
129 order_type,
130 OrderType::Normal
131 | OrderType::AbsoluteLimit
132 | OrderType::AuctionLimit
133 | OrderType::SpecialLimit
134 | OrderType::SpecialLimitAll
135 | OrderType::StopLimit
136 | OrderType::LimitifTouched
137 | OrderType::TrailingStopLimit
138 | OrderType::TwapLimit
139 | OrderType::VwapLimit
140 )
141}
142
143fn validate_optional_positive_price(price: Option<f64>, field: &'static str) -> Result<()> {
144 if let Some(price) = price {
145 ensure_positive_finite(price, field)?;
146 }
147 Ok(())
148}
149
150fn validate_place_order_params(params: &PlaceOrderParams) -> Result<()> {
151 ensure_positive_finite(params.qty, "place_order.qty")?;
156
157 if order_type_requires_price(params.order_type) {
158 let price = params.price.ok_or_else(|| {
159 request_validation_error("place_order.price is required for price-based order types")
160 })?;
161 ensure_positive_finite(price, "place_order.price")?;
162 } else {
163 validate_optional_positive_price(params.price, "place_order.price")?;
164 }
165
166 validate_optional_positive_price(params.aux_price, "place_order.aux_price")?;
167 Ok(())
168}
169
170fn validate_modify_order_params(params: &ModifyOrderParams) -> Result<()> {
171 if params.modify_order_op == ModifyOrderOp::Normal {
172 let qty = params.qty.ok_or_else(|| {
173 request_validation_error("modify_order.qty is required for normal modify")
174 })?;
175 ensure_positive_finite(qty, "modify_order.qty")?;
176 validate_optional_positive_price(params.price, "modify_order.price")?;
177 }
178 Ok(())
179}
180
181fn build_place_order_request(
182 packet_id: futu_proto::common::PacketId,
183 params: &PlaceOrderParams,
184 options: &PlaceOrderOptions,
185) -> futu_proto::trd_place_order::Request {
186 futu_proto::trd_place_order::Request {
187 c2s: futu_proto::trd_place_order::C2s {
188 packet_id,
189 header: params.header.to_proto(),
190 trd_side: params.trd_side as i32,
191 order_type: params.order_type as i32,
192 code: params.code.clone(),
193 qty: params.qty,
194 price: params.price,
195 adjust_price: params.adjust_price,
196 adjust_side_and_limit: params.adjust_side_and_limit,
197 sec_market: Some(derive_sec_market_client(
198 params.header.trd_market as i32,
199 ¶ms.code,
200 )),
201 remark: None,
202 time_in_force: options.time_in_force,
203 fill_outside_rth: options.fill_outside_rth,
204 aux_price: params.aux_price,
206 trail_type: params.trail_type,
207 trail_value: params.trail_value,
208 trail_spread: params.trail_spread,
209 session: options.session,
210 position_id: None,
211 expire_time: options.expire_time.clone(),
212 amount: options.amount,
213 pred_side: options.pred_side,
214 },
215 }
216}
217
218pub async fn place_order(
219 client: &FutuClient,
220 params: &PlaceOrderParams,
221) -> Result<PlaceOrderResult> {
222 place_order_with_options(client, params, &PlaceOrderOptions::default()).await
223}
224
225pub async fn place_order_with_identity(
230 client: &FutuClient,
231 params: &PlaceOrderParams,
232) -> Result<PlaceOrderResultWithIdentity> {
233 place_order_with_options_and_identity(client, params, &PlaceOrderOptions::default()).await
234}
235
236pub async fn place_order_with_options(
242 client: &FutuClient,
243 params: &PlaceOrderParams,
244 options: &PlaceOrderOptions,
245) -> Result<PlaceOrderResult> {
246 let result = place_order_with_options_and_identity(client, params, options).await?;
247 Ok(PlaceOrderResult {
248 order_id: result.order_id,
249 })
250}
251
252pub async fn place_order_with_options_and_identity(
254 client: &FutuClient,
255 params: &PlaceOrderParams,
256 options: &PlaceOrderOptions,
257) -> Result<PlaceOrderResultWithIdentity> {
258 let trd_market = params.header.trd_market;
269 if let Some(label) = crate::market::canonical_fund_trd_market_label(trd_market) {
270 return Err(FutuError::ServerError {
271 ret_type: -1,
272 msg: format!(
273 "place_order: trd_market {label} 仅支持 view-only read endpoints; \
274 write 路径 (place_order) 用对应主市场. v1.4.102 audit 28 F3 fix."
275 ),
276 });
277 }
278 validate_place_order_params(params)?;
279
280 let packet_id = params
281 .idempotency_key
282 .as_deref()
283 .map(packet_id_for_idempotency_key)
284 .map(Ok)
285 .unwrap_or_else(|| client_conn_id(client).map(next_packet_id))?;
286 let req = build_place_order_request(packet_id, params, options);
287
288 let body = prost::Message::encode_to_vec(&req);
289 let resp_frame = client.request(proto_id::TRD_PLACE_ORDER, body).await?;
290
291 parse_place_order_response_body(resp_frame.body.as_ref())
292}
293
294pub async fn modify_order(client: &FutuClient, params: &ModifyOrderParams) -> Result<u64> {
296 let trd_market = params.header.trd_market;
298 if let Some(label) = crate::market::canonical_fund_trd_market_label(trd_market) {
299 return Err(FutuError::ServerError {
300 ret_type: -1,
301 msg: format!(
302 "modify_order: trd_market {label} 仅支持 view-only read endpoints; \
303 write 路径用对应主市场. v1.4.102 audit 28 F3 fix."
304 ),
305 });
306 }
307 validate_modify_order_params(params)?;
308
309 let packet_id = params
310 .idempotency_key
311 .as_deref()
312 .map(packet_id_for_idempotency_key)
313 .map(Ok)
314 .unwrap_or_else(|| client_conn_id(client).map(next_packet_id))?;
315 let req = futu_proto::trd_modify_order::Request {
316 c2s: futu_proto::trd_modify_order::C2s {
317 packet_id,
318 header: params.header.to_proto(),
319 order_id: params.order_id,
320 modify_order_op: params.modify_order_op as i32,
321 for_all: params.for_all,
322 trd_market: None,
323 qty: params.qty,
324 price: params.price,
325 adjust_price: None,
326 adjust_side_and_limit: None,
327 aux_price: None,
328 trail_type: None,
329 trail_value: None,
330 trail_spread: None,
331 order_id_ex: params.order_id_ex.clone(),
332 },
333 };
334
335 let body = prost::Message::encode_to_vec(&req);
336 let resp_frame = client.request(proto_id::TRD_MODIFY_ORDER, body).await?;
337
338 let resp: futu_proto::trd_modify_order::Response =
339 prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
340
341 if resp.ret_type != 0 {
342 return Err(crate::server_err(
343 resp.ret_type,
344 resp.ret_msg,
345 resp.err_code,
346 ));
347 }
348
349 let s2c = resp
350 .s2c
351 .ok_or(FutuError::Codec("missing s2c in ModifyOrder".into()))?;
352
353 Ok(s2c.order_id)
354}
355
356pub async fn cancel_order(
358 client: &FutuClient,
359 header: &crate::types::TrdHeader,
360 order_id: u64,
361) -> Result<u64> {
362 modify_order(
363 client,
364 &ModifyOrderParams {
365 header: header.clone(),
366 order_id,
367 order_id_ex: None,
368 modify_order_op: crate::types::ModifyOrderOp::Cancel,
369 qty: None,
370 price: None,
371 for_all: None,
372 idempotency_key: None,
373 },
374 )
375 .await
376}
377
378#[cfg(test)]
379mod order_response_contract_tests;