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::{ModifyOrderParams, PlaceOrderParams, PlaceOrderResult};
8
9static PACKET_SERIAL: AtomicU32 = AtomicU32::new(1);
11
12fn client_conn_id(client: &FutuClient) -> Result<u64> {
13 client.conn_id().ok_or(FutuError::NotInitialized)
14}
15
16fn next_packet_id(conn_id: u64) -> futu_proto::common::PacketId {
17 let serial = PACKET_SERIAL.fetch_add(1, Ordering::Relaxed);
18 futu_proto::common::PacketId {
19 conn_id,
22 serial_no: serial,
23 }
24}
25
26fn packet_id_for_idempotency_key(key: &str) -> futu_proto::common::PacketId {
33 use std::collections::hash_map::DefaultHasher;
34 use std::hash::{Hash, Hasher};
35 let mut hasher = DefaultHasher::new();
36 key.hash(&mut hasher);
37 futu_proto::common::PacketId {
38 conn_id: hasher.finish(),
39 serial_no: 0,
40 }
41}
42
43fn derive_sec_market_client(trd_market: i32, code: &str) -> i32 {
60 match trd_market {
61 1 | 4 | 113 => 1, 2 | 11 | 123 => 2, 3 => {
64 let bare = code
66 .trim_start_matches("SH.")
67 .trim_start_matches("SZ.")
68 .trim_start_matches("CN.");
69 match bare.chars().next() {
70 Some('6') | Some('9') => 31, Some('0') | Some('2') | Some('3') => 32, _ => 31, }
74 }
75 6 | 12 | 124 => 41, 8 => 61, 15 => 51, 111 => 71, 112 => 81, _ => 0, }
84}
85
86fn parse_place_order_response_body(body: &[u8]) -> Result<PlaceOrderResult> {
87 let resp: futu_proto::trd_place_order::Response =
88 prost::Message::decode(body).map_err(FutuError::Proto)?;
89
90 if resp.ret_type != 0 {
91 return Err(crate::server_err(
92 resp.ret_type,
93 resp.ret_msg,
94 resp.err_code,
95 ));
96 }
97
98 let s2c = resp
99 .s2c
100 .ok_or(FutuError::Codec("missing s2c in PlaceOrder".into()))?;
101
102 let order_id = s2c.order_id.ok_or_else(|| {
103 FutuError::Codec(
104 "missing orderID in successful PlaceOrder response; C++ \
105 APIServer_Trd_PlaceOrder.cpp:856-864 sets orderID before \
106 returning success"
107 .into(),
108 )
109 })?;
110
111 Ok(PlaceOrderResult { order_id })
112}
113
114pub async fn place_order(
115 client: &FutuClient,
116 params: &PlaceOrderParams,
117) -> Result<PlaceOrderResult> {
118 if matches!(
129 params.header.trd_market,
130 crate::types::TrdMarket::HKFund | crate::types::TrdMarket::USFund
131 ) {
132 return Err(FutuError::ServerError {
133 ret_type: -1,
134 msg: "place_order: trd_market HKFUND (113) / USFUND (123) 仅支持 view-only \
135 read endpoints; write 路径 (place_order) 用主市场 HK=1 / US=2. \
136 v1.4.102 codex 28 F3 fix."
137 .to_string(),
138 });
139 }
140
141 let packet_id = params
142 .idempotency_key
143 .as_deref()
144 .map(packet_id_for_idempotency_key)
145 .map(Ok)
146 .unwrap_or_else(|| client_conn_id(client).map(next_packet_id))?;
147 let req = futu_proto::trd_place_order::Request {
148 c2s: futu_proto::trd_place_order::C2s {
149 packet_id,
150 header: params.header.to_proto(),
151 trd_side: params.trd_side as i32,
152 order_type: params.order_type as i32,
153 code: params.code.clone(),
154 qty: params.qty,
155 price: params.price,
156 adjust_price: params.adjust_price,
157 adjust_side_and_limit: params.adjust_side_and_limit,
158 sec_market: Some(derive_sec_market_client(
159 params.header.trd_market as i32,
160 ¶ms.code,
161 )),
162 remark: None,
163 time_in_force: None,
164 fill_outside_rth: None,
165 aux_price: params.aux_price,
167 trail_type: params.trail_type,
168 trail_value: params.trail_value,
169 trail_spread: params.trail_spread,
170 session: None,
171 position_id: None,
172 },
173 };
174
175 let body = prost::Message::encode_to_vec(&req);
176 let resp_frame = client.request(proto_id::TRD_PLACE_ORDER, body).await?;
177
178 parse_place_order_response_body(resp_frame.body.as_ref())
179}
180
181pub async fn modify_order(client: &FutuClient, params: &ModifyOrderParams) -> Result<u64> {
183 if matches!(
185 params.header.trd_market,
186 crate::types::TrdMarket::HKFund | crate::types::TrdMarket::USFund
187 ) {
188 return Err(FutuError::ServerError {
189 ret_type: -1,
190 msg: "modify_order: trd_market HKFUND (113) / USFUND (123) 仅支持 view-only \
191 read endpoints; write 路径用主市场. v1.4.102 codex 28 F3 fix."
192 .to_string(),
193 });
194 }
195
196 let packet_id = params
197 .idempotency_key
198 .as_deref()
199 .map(packet_id_for_idempotency_key)
200 .map(Ok)
201 .unwrap_or_else(|| client_conn_id(client).map(next_packet_id))?;
202 let req = futu_proto::trd_modify_order::Request {
203 c2s: futu_proto::trd_modify_order::C2s {
204 packet_id,
205 header: params.header.to_proto(),
206 order_id: params.order_id,
207 modify_order_op: params.modify_order_op as i32,
208 for_all: params.for_all,
209 trd_market: None,
210 qty: params.qty,
211 price: params.price,
212 adjust_price: None,
213 adjust_side_and_limit: None,
214 aux_price: None,
215 trail_type: None,
216 trail_value: None,
217 trail_spread: None,
218 order_id_ex: params.order_id_ex.clone(),
219 },
220 };
221
222 let body = prost::Message::encode_to_vec(&req);
223 let resp_frame = client.request(proto_id::TRD_MODIFY_ORDER, body).await?;
224
225 let resp: futu_proto::trd_modify_order::Response =
226 prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
227
228 if resp.ret_type != 0 {
229 return Err(crate::server_err(
230 resp.ret_type,
231 resp.ret_msg,
232 resp.err_code,
233 ));
234 }
235
236 let s2c = resp
237 .s2c
238 .ok_or(FutuError::Codec("missing s2c in ModifyOrder".into()))?;
239
240 Ok(s2c.order_id)
241}
242
243pub async fn cancel_order(
245 client: &FutuClient,
246 header: &crate::types::TrdHeader,
247 order_id: u64,
248) -> Result<u64> {
249 modify_order(
250 client,
251 &ModifyOrderParams {
252 header: header.clone(),
253 order_id,
254 order_id_ex: None,
255 modify_order_op: crate::types::ModifyOrderOp::Cancel,
256 qty: None,
257 price: None,
258 for_all: None,
259 idempotency_key: None,
260 },
261 )
262 .await
263}
264
265#[cfg(test)]
266mod tests_v1_4_67_bug_3;