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 params.order_type == OrderType::Moc {
158 if params.price.is_some() || params.aux_price.is_some() {
159 return Err(request_validation_error(
160 "place_order MOC does not accept price or aux_price",
161 ));
162 }
163 if params
164 .idempotency_key
165 .as_deref()
166 .map(str::trim)
167 .filter(|key| !key.is_empty())
168 .is_none()
169 {
170 return Err(request_validation_error(
171 "place_order MOC requires a non-empty idempotency_key",
172 ));
173 }
174 }
175
176 if order_type_requires_price(params.order_type) {
177 let price = params.price.ok_or_else(|| {
178 request_validation_error("place_order.price is required for price-based order types")
179 })?;
180 ensure_positive_finite(price, "place_order.price")?;
181 } else {
182 validate_optional_positive_price(params.price, "place_order.price")?;
183 }
184
185 validate_optional_positive_price(params.aux_price, "place_order.aux_price")?;
186 Ok(())
187}
188
189fn validate_modify_order_params(params: &ModifyOrderParams) -> Result<()> {
190 if params.modify_order_op == ModifyOrderOp::Normal {
191 let qty = params.qty.ok_or_else(|| {
192 request_validation_error("modify_order.qty is required for normal modify")
193 })?;
194 ensure_positive_finite(qty, "modify_order.qty")?;
195 validate_optional_positive_price(params.price, "modify_order.price")?;
196 }
197 Ok(())
198}
199
200fn build_place_order_request(
201 packet_id: futu_proto::common::PacketId,
202 params: &PlaceOrderParams,
203 options: &PlaceOrderOptions,
204) -> futu_proto::trd_place_order::Request {
205 futu_proto::trd_place_order::Request {
206 c2s: futu_proto::trd_place_order::C2s {
207 packet_id,
208 header: params.header.to_proto(),
209 trd_side: params.trd_side as i32,
210 order_type: params.order_type as i32,
211 code: params.code.clone(),
212 qty: params.qty,
213 price: params.price,
214 adjust_price: params.adjust_price,
215 adjust_side_and_limit: params.adjust_side_and_limit,
216 sec_market: Some(derive_sec_market_client(
217 params.header.trd_market as i32,
218 ¶ms.code,
219 )),
220 remark: None,
221 time_in_force: options.time_in_force,
222 fill_outside_rth: options.fill_outside_rth,
223 aux_price: params.aux_price,
225 trail_type: params.trail_type,
226 trail_value: params.trail_value,
227 trail_spread: params.trail_spread,
228 session: options.session,
229 position_id: None,
230 expire_time: options.expire_time.clone(),
231 amount: options.amount,
232 pred_side: options.pred_side,
233 extension_version: (params.order_type == OrderType::Moc).then_some(1),
234 },
235 }
236}
237
238pub async fn place_order(
239 client: &FutuClient,
240 params: &PlaceOrderParams,
241) -> Result<PlaceOrderResult> {
242 place_order_with_options(client, params, &PlaceOrderOptions::default()).await
243}
244
245pub async fn place_order_with_identity(
250 client: &FutuClient,
251 params: &PlaceOrderParams,
252) -> Result<PlaceOrderResultWithIdentity> {
253 place_order_with_options_and_identity(client, params, &PlaceOrderOptions::default()).await
254}
255
256pub async fn place_order_with_options(
262 client: &FutuClient,
263 params: &PlaceOrderParams,
264 options: &PlaceOrderOptions,
265) -> Result<PlaceOrderResult> {
266 let result = place_order_with_options_and_identity(client, params, options).await?;
267 Ok(PlaceOrderResult {
268 order_id: result.order_id,
269 })
270}
271
272pub async fn place_order_with_options_and_identity(
274 client: &FutuClient,
275 params: &PlaceOrderParams,
276 options: &PlaceOrderOptions,
277) -> Result<PlaceOrderResultWithIdentity> {
278 let trd_market = params.header.trd_market;
289 if let Some(label) = crate::market::canonical_fund_trd_market_label(trd_market) {
290 return Err(FutuError::ServerError {
291 ret_type: -1,
292 msg: format!(
293 "place_order: trd_market {label} 仅支持 view-only read endpoints; \
294 write 路径 (place_order) 用对应主市场. v1.4.102 audit 28 F3 fix."
295 ),
296 });
297 }
298 validate_place_order_params(params)?;
299
300 let packet_id = params
301 .idempotency_key
302 .as_deref()
303 .map(packet_id_for_idempotency_key)
304 .map(Ok)
305 .unwrap_or_else(|| client_conn_id(client).map(next_packet_id))?;
306 let req = build_place_order_request(packet_id, params, options);
307
308 let body = prost::Message::encode_to_vec(&req);
309 let resp_frame = client.request(proto_id::TRD_PLACE_ORDER, body).await?;
310
311 parse_place_order_response_body(resp_frame.body.as_ref())
312}
313
314pub async fn modify_order(client: &FutuClient, params: &ModifyOrderParams) -> Result<u64> {
316 let trd_market = params.header.trd_market;
318 if let Some(label) = crate::market::canonical_fund_trd_market_label(trd_market) {
319 return Err(FutuError::ServerError {
320 ret_type: -1,
321 msg: format!(
322 "modify_order: trd_market {label} 仅支持 view-only read endpoints; \
323 write 路径用对应主市场. v1.4.102 audit 28 F3 fix."
324 ),
325 });
326 }
327 validate_modify_order_params(params)?;
328
329 let packet_id = params
330 .idempotency_key
331 .as_deref()
332 .map(packet_id_for_idempotency_key)
333 .map(Ok)
334 .unwrap_or_else(|| client_conn_id(client).map(next_packet_id))?;
335 let req = futu_proto::trd_modify_order::Request {
336 c2s: futu_proto::trd_modify_order::C2s {
337 packet_id,
338 header: params.header.to_proto(),
339 order_id: params.order_id,
340 modify_order_op: params.modify_order_op as i32,
341 for_all: params.for_all,
342 trd_market: None,
343 qty: params.qty,
344 price: params.price,
345 adjust_price: None,
346 adjust_side_and_limit: None,
347 aux_price: None,
348 trail_type: None,
349 trail_value: None,
350 trail_spread: None,
351 order_id_ex: params.order_id_ex.clone(),
352 },
353 };
354
355 let body = prost::Message::encode_to_vec(&req);
356 let resp_frame = client.request(proto_id::TRD_MODIFY_ORDER, body).await?;
357
358 let resp: futu_proto::trd_modify_order::Response =
359 prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
360
361 if resp.ret_type != 0 {
362 return Err(crate::server_err(
363 resp.ret_type,
364 resp.ret_msg,
365 resp.err_code,
366 ));
367 }
368
369 let s2c = resp
370 .s2c
371 .ok_or(FutuError::Codec("missing s2c in ModifyOrder".into()))?;
372
373 Ok(s2c.order_id)
374}
375
376pub async fn cancel_order(
378 client: &FutuClient,
379 header: &crate::types::TrdHeader,
380 order_id: u64,
381) -> Result<u64> {
382 modify_order(
383 client,
384 &ModifyOrderParams {
385 header: header.clone(),
386 order_id,
387 order_id_ex: None,
388 modify_order_op: crate::types::ModifyOrderOp::Cancel,
389 qty: None,
390 price: None,
391 for_all: None,
392 idempotency_key: None,
393 },
394 )
395 .await
396}
397
398#[cfg(test)]
399mod order_response_contract_tests;