use crate::model::spot_trade::{CreateOrderRequest, CreateOrderResponse, OrderType};
use crate::error::*;
use crate::utils::hmac;
use anyhow::{Result, anyhow};
use tracing::{info, error};
use reqwest::blocking::Client;
use serde_json::json;
#[derive(Clone)]
pub struct SpotTradeClient {
pub api_key: String,
pub secret_key: String,
pub base_url: String,
client: Client,
}
impl SpotTradeClient {
pub fn new(api_key: impl Into<String>, secret_key: impl Into<String>, base_url: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
secret_key: secret_key.into(),
base_url: base_url.into(),
client: Client::new(),
}
}
pub fn create_order(&self, req: &CreateOrderRequest) -> Result<CreateOrderResponse> {
let url = format!("{}/v1/order/orders/place", self.base_url);
let mut params = json!({
"account-id": req.account_id,
"symbol": req.symbol,
"type": order_type_to_str(&req.order_type),
"amount": req.amount,
});
if let Some(price) = req.price {
params["price"] = json!(price);
}
if let Some(client_id) = &req.client_order_id {
params["client-order-id"] = json!(client_id);
}
params["source"] = json!(&req.source);
use crate::utils::{signature, url_params::UrlParamsBuilder};
let mut builder = UrlParamsBuilder::new();
builder.put_url("AccessKeyId", &self.api_key);
builder.put_url("SignatureVersion", "2");
builder.put_url("SignatureMethod", "HmacSHA256");
builder.put_url("Timestamp", &signature::utc_now());
builder.put_url("account-id", &req.account_id);
builder.put_url("symbol", &req.symbol);
builder.put_url("type", order_type_to_str(&req.order_type));
builder.put_url("amount", &req.amount.to_string());
if let Some(price) = req.price {
builder.put_url("price", &price.to_string());
}
if let Some(client_id) = &req.client_order_id {
builder.put_url("client-order-id", client_id);
}
builder.put_url("source", &req.source);
let url_obj = reqwest::Url::parse(&url).map_err(|e| anyhow!("URL 解析失败: {e}"))?;
let host = url_obj.host_str().unwrap_or("");
let path = url_obj.path();
let sign_payload = signature::build_sign_payload("POST", host, path, &builder.param_map);
let sign = hmac::sign_sha256(&self.secret_key, &sign_payload)?;
info!("下单参数: {:?}", params);
let resp = self.client
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("AccessKeyId", &self.api_key)
.header("Signature", sign)
.json(¶ms)
.send()
.map_err(|e| anyhow!("HTTP 请求失败: {e}"))?;
let status = resp.status();
let resp_json: serde_json::Value = resp.json().map_err(|e| anyhow!("响应解析失败: {e}"))?;
info!("下单响应: {:?}", resp_json);
if !status.is_success() {
error!("下单失败: {:?}", resp_json);
return Err(anyhow!("下单失败: {:?}", resp_json));
}
let order_id = resp_json["data"].as_str().unwrap_or("").to_string();
Ok(CreateOrderResponse { order_id })
}
pub fn cancel_order(&self, symbol: &str, order_id: &str) -> anyhow::Result<()> {
let url = format!("{}/v1/order/orders/{}/submitcancel", self.base_url, order_id);
use crate::utils::{signature, url_params::UrlParamsBuilder};
let mut builder = UrlParamsBuilder::new();
builder.put_url("AccessKeyId", &self.api_key);
builder.put_url("SignatureVersion", "2");
builder.put_url("SignatureMethod", "HmacSHA256");
builder.put_url("Timestamp", &signature::utc_now());
builder.put_url("order-id", order_id);
builder.put_url("symbol", symbol);
let url_obj = reqwest::Url::parse(&url).map_err(|e| anyhow!("URL 解析失败: {e}"))?;
let host = url_obj.host_str().unwrap_or("");
let path = url_obj.path();
let sign_payload = signature::build_sign_payload("POST", host, path, &builder.param_map);
let sign = hmac::sign_sha256(&self.secret_key, &sign_payload)?;
info!("撤单参数: symbol={}, order_id={}", symbol, order_id);
let resp = self.client
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("AccessKeyId", &self.api_key)
.header("Signature", sign)
.send()
.map_err(|e| anyhow!("HTTP 请求失败: {e}"))?;
let status = resp.status();
let resp_json: serde_json::Value = resp.json().map_err(|e| anyhow!("响应解析失败: {e}"))?;
info!("撤单响应: {:?}", resp_json);
if !status.is_success() {
error!("撤单失败: {:?}", resp_json);
return Err(anyhow!("撤单失败: {:?}", resp_json));
}
Ok(())
}
pub fn get_order(&self, order_id: &str) -> anyhow::Result<crate::model::spot_trade::OrderDetail> {
let url = format!("{}/v1/order/orders/{}", self.base_url, order_id);
use crate::utils::{signature, url_params::UrlParamsBuilder};
let mut builder = UrlParamsBuilder::new();
builder.put_url("AccessKeyId", &self.api_key);
builder.put_url("SignatureVersion", "2");
builder.put_url("SignatureMethod", "HmacSHA256");
builder.put_url("Timestamp", &signature::utc_now());
builder.put_url("order-id", order_id);
let url_obj = reqwest::Url::parse(&url).map_err(|e| anyhow!("URL 解析失败: {e}"))?;
let host = url_obj.host_str().unwrap_or("");
let path = url_obj.path();
let sign_payload = signature::build_sign_payload("GET", host, path, &builder.param_map);
let sign = hmac::sign_sha256(&self.secret_key, &sign_payload)?;
info!("查单参数: order_id={}", order_id);
let resp = self.client
.get(&url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("AccessKeyId", &self.api_key)
.header("Signature", sign)
.send()
.map_err(|e| anyhow!("HTTP 请求失败: {e}"))?;
let status = resp.status();
let resp_json: serde_json::Value = resp.json().map_err(|e| anyhow!("响应解析失败: {e}"))?;
info!("查单响应: {:?}", resp_json);
if !status.is_success() {
error!("查单失败: {:?}", resp_json);
return Err(anyhow!("查单失败: {:?}", resp_json));
}
let detail = serde_json::from_value(resp_json["data"].clone())?;
Ok(detail)
}
pub fn get_order_by_client_order_id(&self, client_order_id: &str) -> anyhow::Result<crate::model::spot_trade::OrderDetail> {
let url = format!("{}/v1/order/orders/getClientOrder", self.base_url);
use crate::utils::{signature, url_params::UrlParamsBuilder};
let mut builder = UrlParamsBuilder::new();
builder.put_url("AccessKeyId", &self.api_key);
builder.put_url("SignatureVersion", "2");
builder.put_url("SignatureMethod", "HmacSHA256");
builder.put_url("Timestamp", &signature::utc_now());
builder.put_url("clientOrderId", client_order_id);
let url_obj = reqwest::Url::parse(&url).map_err(|e| anyhow!("URL 解析失败: {e}"))?;
let host = url_obj.host_str().unwrap_or("");
let path = url_obj.path();
let sign_payload = signature::build_sign_payload("POST", host, path, &builder.param_map);
let sign = hmac::sign_sha256(&self.secret_key, &sign_payload)?;
info!("查单参数: client_order_id={}", client_order_id);
let params = serde_json::json!({"clientOrderId": client_order_id});
let resp = self.client
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("AccessKeyId", &self.api_key)
.header("Signature", sign)
.json(¶ms)
.send()
.map_err(|e| anyhow!("HTTP 请求失败: {e}"))?;
let status = resp.status();
let resp_json: serde_json::Value = resp.json().map_err(|e| anyhow!("响应解析失败: {e}"))?;
info!("查单响应: {:?}", resp_json);
if !status.is_success() {
error!("查单失败: {:?}", resp_json);
return Err(anyhow!("查单失败: {:?}", resp_json));
}
let detail = serde_json::from_value(resp_json["data"].clone())?;
Ok(detail)
}
pub fn cancel_client_order(&self, client_order_id: &str) -> anyhow::Result<()> {
let url = format!("{}/v1/order/orders/submitCancelClientOrder", self.base_url);
use crate::utils::{signature, url_params::UrlParamsBuilder};
let mut builder = UrlParamsBuilder::new();
builder.put_url("AccessKeyId", &self.api_key);
builder.put_url("SignatureVersion", "2");
builder.put_url("SignatureMethod", "HmacSHA256");
builder.put_url("Timestamp", &signature::utc_now());
builder.put_url("clientOrderId", client_order_id);
let url_obj = reqwest::Url::parse(&url).map_err(|e| anyhow!("URL 解析失败: {e}"))?;
let host = url_obj.host_str().unwrap_or("");
let path = url_obj.path();
let sign_payload = signature::build_sign_payload("POST", host, path, &builder.param_map);
let sign = hmac::sign_sha256(&self.secret_key, &sign_payload)?;
info!("撤单参数: client_order_id={}", client_order_id);
let params = serde_json::json!({"clientOrderId": client_order_id});
let resp = self.client
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("AccessKeyId", &self.api_key)
.header("Signature", sign)
.json(¶ms)
.send()
.map_err(|e| anyhow!("HTTP 请求失败: {e}"))?;
let status = resp.status();
let resp_json: serde_json::Value = resp.json().map_err(|e| anyhow!("响应解析失败: {e}"))?;
info!("撤单响应: {:?}", resp_json);
if !status.is_success() {
error!("撤单失败: {:?}", resp_json);
return Err(anyhow!("撤单失败: {:?}", resp_json));
}
Ok(())
}
}
fn order_type_to_str(t: &OrderType) -> &'static str {
match t {
OrderType::BuyLimit => "buy-limit",
OrderType::SellLimit => "sell-limit",
OrderType::BuyMarket => "buy-market",
OrderType::SellMarket => "sell-market",
}
}