1use crate::dto::{AmendOrderRequest, CancelOrderRequest, PlaceOrderRequest};
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20
21#[derive(Debug, Clone, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum TradeOp {
27 OrderCreate,
28 OrderAmend,
29 OrderCancel,
30}
31
32#[derive(Debug, Clone, Serialize)]
34pub struct WsTradeRequest {
35 #[serde(rename = "reqId")]
37 pub req_id: String,
38 pub header: TradeHeader,
40 pub args: Vec<Value>,
42}
43
44#[derive(Debug, Clone, Serialize)]
45pub struct TradeHeader {
46 #[serde(rename = "X-BAPI-TIMESTAMP")]
48 pub timestamp: String,
49 #[serde(rename = "X-BAPI-RECV-WINDOW")]
51 pub recv_window: String,
52 #[serde(rename = "X-BAPI-API-KEY")]
54 pub api_key: String,
55 #[serde(rename = "X-BAPI-SIGN")]
57 pub signature: String,
58}
59
60#[derive(Debug, Clone, Deserialize)]
64pub struct WsTradeResponse {
65 #[serde(rename = "reqId")]
67 #[serde(default)]
68 pub req_id: Option<String>,
69 #[serde(rename = "op")]
71 #[serde(default)]
72 pub op: Option<String>,
73 #[serde(rename = "retCode")]
75 #[serde(default)]
76 pub ret_code: Option<i32>,
77 #[serde(rename = "retMsg")]
79 #[serde(default)]
80 pub ret_msg: Option<String>,
81 #[serde(default)]
83 pub success: Option<bool>,
84 #[serde(rename = "connId")]
86 #[serde(default)]
87 pub conn_id: Option<String>,
88 #[serde(default)]
90 pub data: Option<TradeResultData>,
91}
92
93#[derive(Debug, Clone, Deserialize)]
95pub struct TradeResultData {
96 #[serde(rename = "orderId")]
98 #[serde(default)]
99 pub order_id: Option<String>,
100 #[serde(rename = "orderLinkId")]
102 #[serde(default)]
103 pub order_link_id: Option<String>,
104 #[serde(rename = "orderStatus")]
106 #[serde(default)]
107 pub order_status: Option<String>,
108 #[serde(default)]
110 pub category: Option<String>,
111 #[serde(default)]
113 pub symbol: Option<String>,
114}
115
116impl WsTradeRequest {
119 pub fn new_req_id() -> String {
121 use std::time::{SystemTime, UNIX_EPOCH};
122 let ts = SystemTime::now()
123 .duration_since(UNIX_EPOCH)
124 .unwrap()
125 .as_micros();
126 format!("ws-{}", ts)
127 }
128
129 pub fn create_order(
131 order: PlaceOrderRequest,
132 api_key: &str,
133 api_secret: &str,
134 recv_window: u64,
135 ) -> Self {
136 let req_id = Self::new_req_id();
137 let timestamp = crate::utils::millis().to_string();
138 let body = serde_json::to_value(&order).unwrap();
139
140 let signature_input = format!(
142 "{}{}{}{}",
143 timestamp,
144 api_key,
145 recv_window,
146 serde_json::to_string(&body).unwrap()
147 );
148 let signature = crate::utils::sign(api_secret, &signature_input);
149
150 WsTradeRequest {
151 req_id,
152 header: TradeHeader {
153 timestamp,
154 recv_window: recv_window.to_string(),
155 api_key: api_key.to_string(),
156 signature,
157 },
158 args: vec![body],
159 }
160 }
161
162 pub fn amend_order(
164 amend: AmendOrderRequest,
165 api_key: &str,
166 api_secret: &str,
167 recv_window: u64,
168 ) -> Self {
169 let req_id = Self::new_req_id();
170 let timestamp = crate::utils::millis().to_string();
171 let body = serde_json::to_value(&amend).unwrap();
172 let signature_input = format!(
173 "{}{}{}{}",
174 timestamp,
175 api_key,
176 recv_window,
177 serde_json::to_string(&body).unwrap()
178 );
179 let signature = crate::utils::sign(api_secret, &signature_input);
180
181 WsTradeRequest {
182 req_id,
183 header: TradeHeader {
184 timestamp,
185 recv_window: recv_window.to_string(),
186 api_key: api_key.to_string(),
187 signature,
188 },
189 args: vec![body],
190 }
191 }
192
193 pub fn cancel_order(
195 cancel: CancelOrderRequest,
196 api_key: &str,
197 api_secret: &str,
198 recv_window: u64,
199 ) -> Self {
200 let req_id = Self::new_req_id();
201 let timestamp = crate::utils::millis().to_string();
202 let body = serde_json::to_value(&cancel).unwrap();
203 let signature_input = format!(
204 "{}{}{}{}",
205 timestamp,
206 api_key,
207 recv_window,
208 serde_json::to_string(&body).unwrap()
209 );
210 let signature = crate::utils::sign(api_secret, &signature_input);
211
212 WsTradeRequest {
213 req_id,
214 header: TradeHeader {
215 timestamp,
216 recv_window: recv_window.to_string(),
217 api_key: api_key.to_string(),
218 signature,
219 },
220 args: vec![body],
221 }
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use crate::dto::PlaceOrderRequest;
229 use crate::rest::enums::{Category, OrderType, Side, TimeInForce};
230
231 #[test]
232 fn test_create_order_request() {
233 let order = PlaceOrderRequest {
234 category: Category::Spot,
235 symbol: "BTCUSDT".to_string(),
236 side: Side::Buy,
237 order_type: OrderType::Limit,
238 qty: "0.001".to_string(),
239 price: Some("40000".to_string()),
240 time_in_force: Some(TimeInForce::GTC),
241 ..Default::default()
242 };
243
244 let req = WsTradeRequest::create_order(order, "key", "secret", 5000);
245 assert!(req.req_id.starts_with("ws-"));
246 assert_eq!(req.args.len(), 1);
247 }
248
249 #[test]
250 fn test_req_id_unique() {
251 let id1 = WsTradeRequest::new_req_id();
252 std::thread::sleep(std::time::Duration::from_micros(10));
253 let id2 = WsTradeRequest::new_req_id();
254 assert_ne!(id1, id2);
255 }
256
257 #[test]
258 fn test_deserialize_trade_response() {
259 let json = serde_json::json!({
260 "reqId": "ws-123",
261 "op": "order.create",
262 "retCode": 0,
263 "retMsg": "OK",
264 "success": true,
265 "data": {
266 "orderId": "order-456",
267 "orderLinkId": "link-789",
268 "orderStatus": "New",
269 "category": "spot",
270 "symbol": "BTCUSDT"
271 }
272 });
273
274 let resp: WsTradeResponse = serde_json::from_value(json).unwrap();
275 assert_eq!(resp.ret_code, Some(0));
276 assert_eq!(resp.success, Some(true));
277 assert_eq!(
278 resp.data.as_ref().unwrap().order_id.as_deref(),
279 Some("order-456")
280 );
281 }
282}