htx-rs 0.1.1

火币 HTX 现货交易 Rust SDK,支持下单、撤单、查单等主要接口,签名算法兼容官方。
Documentation
//! 现货交易 API 客户端,封装下单等接口
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(),
        }
    }

    /// 下单接口,对应 create_order
    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);
        // 解析 URL 拆分 host/path
        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(&params)
            .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 })
    }

    /// 撤单接口,对应 cancel_order
    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(())
    }

    /// 查单接口,对应 get_order
    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)
    }

    /// 通过客户端订单ID查询订单
    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(&params)
            .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)
    }

    /// 通过客户端订单ID撤单
    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(&params)
            .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",
    }
}