use crate::dto::{AmendOrderRequest, CancelOrderRequest, PlaceOrderRequest};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TradeOp {
OrderCreate,
OrderAmend,
OrderCancel,
}
#[derive(Debug, Clone, Serialize)]
pub struct WsTradeRequest {
#[serde(rename = "reqId")]
pub req_id: String,
pub header: TradeHeader,
pub args: Vec<Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct TradeHeader {
#[serde(rename = "X-BAPI-TIMESTAMP")]
pub timestamp: String,
#[serde(rename = "X-BAPI-RECV-WINDOW")]
pub recv_window: String,
#[serde(rename = "X-BAPI-API-KEY")]
pub api_key: String,
#[serde(rename = "X-BAPI-SIGN")]
pub signature: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WsTradeResponse {
#[serde(rename = "reqId")]
#[serde(default)]
pub req_id: Option<String>,
#[serde(rename = "op")]
#[serde(default)]
pub op: Option<String>,
#[serde(rename = "retCode")]
#[serde(default)]
pub ret_code: Option<i32>,
#[serde(rename = "retMsg")]
#[serde(default)]
pub ret_msg: Option<String>,
#[serde(default)]
pub success: Option<bool>,
#[serde(rename = "connId")]
#[serde(default)]
pub conn_id: Option<String>,
#[serde(default)]
pub data: Option<TradeResultData>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TradeResultData {
#[serde(rename = "orderId")]
#[serde(default)]
pub order_id: Option<String>,
#[serde(rename = "orderLinkId")]
#[serde(default)]
pub order_link_id: Option<String>,
#[serde(rename = "orderStatus")]
#[serde(default)]
pub order_status: Option<String>,
#[serde(default)]
pub category: Option<String>,
#[serde(default)]
pub symbol: Option<String>,
}
impl WsTradeRequest {
pub fn new_req_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros();
format!("ws-{}", ts)
}
pub fn create_order(
order: PlaceOrderRequest,
api_key: &str,
api_secret: &str,
recv_window: u64,
) -> Self {
let req_id = Self::new_req_id();
let timestamp = crate::utils::millis().to_string();
let body = serde_json::to_value(&order).unwrap();
let signature_input = format!(
"{}{}{}{}",
timestamp,
api_key,
recv_window,
serde_json::to_string(&body).unwrap()
);
let signature = crate::utils::sign(api_secret, &signature_input);
WsTradeRequest {
req_id,
header: TradeHeader {
timestamp,
recv_window: recv_window.to_string(),
api_key: api_key.to_string(),
signature,
},
args: vec![body],
}
}
pub fn amend_order(
amend: AmendOrderRequest,
api_key: &str,
api_secret: &str,
recv_window: u64,
) -> Self {
let req_id = Self::new_req_id();
let timestamp = crate::utils::millis().to_string();
let body = serde_json::to_value(&amend).unwrap();
let signature_input = format!(
"{}{}{}{}",
timestamp,
api_key,
recv_window,
serde_json::to_string(&body).unwrap()
);
let signature = crate::utils::sign(api_secret, &signature_input);
WsTradeRequest {
req_id,
header: TradeHeader {
timestamp,
recv_window: recv_window.to_string(),
api_key: api_key.to_string(),
signature,
},
args: vec![body],
}
}
pub fn cancel_order(
cancel: CancelOrderRequest,
api_key: &str,
api_secret: &str,
recv_window: u64,
) -> Self {
let req_id = Self::new_req_id();
let timestamp = crate::utils::millis().to_string();
let body = serde_json::to_value(&cancel).unwrap();
let signature_input = format!(
"{}{}{}{}",
timestamp,
api_key,
recv_window,
serde_json::to_string(&body).unwrap()
);
let signature = crate::utils::sign(api_secret, &signature_input);
WsTradeRequest {
req_id,
header: TradeHeader {
timestamp,
recv_window: recv_window.to_string(),
api_key: api_key.to_string(),
signature,
},
args: vec![body],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dto::PlaceOrderRequest;
use crate::rest::enums::{Category, OrderType, Side, TimeInForce};
#[test]
fn test_create_order_request() {
let order = PlaceOrderRequest {
category: Category::Spot,
symbol: "BTCUSDT".to_string(),
side: Side::Buy,
order_type: OrderType::Limit,
qty: "0.001".to_string(),
price: Some("40000".to_string()),
time_in_force: Some(TimeInForce::GTC),
..Default::default()
};
let req = WsTradeRequest::create_order(order, "key", "secret", 5000);
assert!(req.req_id.starts_with("ws-"));
assert_eq!(req.args.len(), 1);
}
#[test]
fn test_req_id_unique() {
let id1 = WsTradeRequest::new_req_id();
std::thread::sleep(std::time::Duration::from_micros(10));
let id2 = WsTradeRequest::new_req_id();
assert_ne!(id1, id2);
}
#[test]
fn test_deserialize_trade_response() {
let json = serde_json::json!({
"reqId": "ws-123",
"op": "order.create",
"retCode": 0,
"retMsg": "OK",
"success": true,
"data": {
"orderId": "order-456",
"orderLinkId": "link-789",
"orderStatus": "New",
"category": "spot",
"symbol": "BTCUSDT"
}
});
let resp: WsTradeResponse = serde_json::from_value(json).unwrap();
assert_eq!(resp.ret_code, Some(0));
assert_eq!(resp.success, Some(true));
assert_eq!(
resp.data.as_ref().unwrap().order_id.as_deref(),
Some("order-456")
);
}
}