← Назад к документации
gate
Исходный код Rust - Trading AI
📄 Rust
📦 Модуль
🔧 Исходный код
use anyhow::Result;
use chrono::Utc;
use hmac::{Hmac, Mac};
use reqwest::{Client, Method};
use serde_json::{json, Value};
use sha2::{Digest, Sha512};
use hex;

use crate::config::ApiKeys;

type HmacSha512 = Hmac<Sha512>;

const BASE_HOST: &str = "https://api.gateio.ws";
const EMPTY_BODY_HASH: &str = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e";

#[derive(Clone)]
pub struct GateClient {
    http: Client,
    pub api_key: String,
    pub api_secret: String,
    pub settle: String,
}

impl GateClient {
    pub fn new() -> Self {
        let api = ApiKeys::load();
        Self {
            http: Client::new(),
            api_key: api.gate_api_key.trim().to_owned(),
            api_secret: api.gate_secret_key.trim().to_owned(),
            settle: "usdt".to_string(),
        }
    }

    fn sign(&self, method: &str, url_path: &str, query: &str, body: &str, ts: &str) -> String {
        let body_hash = if body.is_empty() {
            EMPTY_BODY_HASH.to_string()
        } else {
            hex::encode(Sha512::digest(body.as_bytes()))
        };

        let payload = format!(
            "{}\n{}\n{}\n{}\n{}",
            method.to_uppercase(),
            url_path,
            query,
            body_hash,
            ts
        );

        let mut mac = HmacSha512::new_from_slice(self.api_secret.as_bytes())
            .expect("HMAC init failed");
        mac.update(payload.as_bytes());
        hex::encode(mac.finalize().into_bytes())
    }

    async fn request(
        &self,
        method: Method,
        path: &str,
        query: Option<&Value>,
        body: Option<&Value>,
    ) -> Result<Value, String> {
        let ts = Utc::now().timestamp();
        let timestamp = ts.to_string();

        let query_str = query
            .map(|q| serde_qs::to_string(q).unwrap_or_default())
            .unwrap_or_default();

        let body_str = body.map(|b| b.to_string()).unwrap_or_default();

        let url_path = format!("/api/v4{}", path);

        let signature = self.sign(method.as_str(), &url_path, &query_str, &body_str, &timestamp);

        let url = if query_str.is_empty() {
            format!("{BASE_HOST}{url_path}")
        } else {
            format!("{BASE_HOST}{url_path}?{query_str}")
        };

        let mut req = self.http
            .request(method.clone(), &url)
            .header("KEY", &self.api_key)
            .header("Timestamp", timestamp)
            .header("SIGN", signature)
            .header("Content-Type", "application/json");

        if let Some(b) = body {
            req = req.json(b);
        }

        let resp = req.send().await.map_err(|e| e.to_string())?;
        let status = resp.status();
        let text = resp.text().await.map_err(|e| e.to_string())?;

        if !status.is_success() {
            return Err(format!("Gate.io {} {}: {}", status, url_path, text));
        }

        serde_json::from_str(&text).map_err(|e| format!("JSON parse error: {e}"))
    }

    pub async fn get_futures_account(&self) -> Result<Value, String> {
        self.request(Method::GET, "/futures/usdt/accounts", None, None).await
    }

    pub async fn get_real_balance_usdt(&self) -> Result<f64, String> {
        let acc = self.get_futures_account().await?;
        acc["cross_margin_balance"]
            .as_str()
            .ok_or("no cross_margin_balance")?
            .parse()
            .map_err(|_| "parse error".to_string())
    }

    pub async fn list_open_orders(&self) -> Result<Value, String> {
        let query = json!({"settle": "usdt", "status": "open"});
        self.request(Method::GET, "/futures/usdt/orders", Some(&query), None).await
    }

    pub async fn create_limit_order(
        &self,
        contract: &str,
        is_long: bool,
        size: f64,
        price: f64,
        reduce_only: bool,
    ) -> Result<Value, String> {
        let size_int = if is_long { size.round() as i64 } else { -(size.round() as i64) };
        if size_int == 0 { return Err("size = 0".into()); }

        let body = json!({
            "contract": contract,
            "size": size_int,
            "price": format!("{:.8}", price),
            "tif": "gtc",
            "reduce_only": reduce_only,
            "text": "t-pure_ai_v2"
        });

        self.request(Method::POST, "/futures/usdt/orders", None, Some(&body)).await
    }

    pub async fn create_market_order(
        &self,
        contract: &str,
        is_long: bool,
        size: f64,
    ) -> Result<Value, String> {
        Self::create_market_order_with_reduce(self, contract, is_long, size, false).await
    }

    /// Создать MARKET ордер с опцией reduce_only (для закрытия позиций)
    pub async fn create_reduce_only_order(
        &self,
        contract: &str,
        is_long: bool,
        size: f64,
    ) -> Result<Value, String> {
        Self::create_market_order_with_reduce(self, contract, is_long, size, true).await
    }

    /// Внутренняя функция для создания ордера с опцией reduce_only
    async fn create_market_order_with_reduce(
        &self,
        contract: &str,
        is_long: bool,
        size: f64,
        reduce_only: bool,
    ) -> Result<Value, String> {
        let size_int = if is_long { size.round() as i64 } else { -(size.round() as i64) };
        let timestamp = Utc::now().timestamp_millis();

        let body = json!({
            "contract": contract,
            "size": size_int,
            "price": "0",
            "tif": "ioc",
            "reduce_only": reduce_only,
            // НЕ меняем margin mode - используем тот что установлен в аккаунте
            "text": format!("t-{}", timestamp)
        });

        self.request(Method::POST, "/futures/usdt/orders", None, Some(&body)).await
    }

    pub async fn get_contract_details(&self, contract: &str) -> Result<Value, String> {
        let path = format!("/futures/usdt/contracts/{}", contract);
        self.request(Method::GET, &path, None, None).await
    }

    pub async fn get_leverage_limits(&self, contract: &str) -> Result<(f64, f64), String> {
        let details = self.get_contract_details(contract).await?;

        let min = details["leverage_min"]
            .as_str()
            .ok_or("no leverage_min")?
            .parse::<f64>()
            .map_err(|_| "parse leverage_min error")?;

        let max = details["leverage_max"]
            .as_str()
            .ok_or("no leverage_max")?
            .parse::<f64>()
            .map_err(|_| "parse leverage_max error")?;

        Ok((min, max))
    }

    pub async fn update_leverage(
        &self,
        contract: &str,
        leverage: f64,
    ) -> Result<Value, String> {
        let (min_lev, max_lev) = self.get_leverage_limits(contract).await?;
        let lev = leverage.clamp(min_lev, max_lev).round() as u32;

        let query = json!({
            "leverage": lev.to_string()
            // НЕ меняем margin mode - используем тот что установлен в аккаунте
        });

        let path = format!("/futures/usdt/positions/{}/leverage", contract);

        self.request(Method::POST, &path, Some(&query), None).await
    }

    /// Установить leverage для DUAL+CROSS режима
    /// ВАЖНО: Для CROSS mode нужно использовать специальный endpoint и параметры!
    ///
    /// Параметры:
    ///   - leverage: 0 (отключает isolated leverage)
    ///   - cross_leverage_limit: реальное плечо для CROSS margin
    ///
    /// Endpoint: /futures/usdt/dual_comp/positions/{contract}/leverage
    pub async fn update_dual_leverage(
        &self,
        contract: &str,
        cross_leverage: f64,
    ) -> Result<Value, String> {
        let (min_lev, max_lev) = self.get_leverage_limits(contract).await?;
        let lev = cross_leverage.clamp(min_lev, max_lev).round() as u32;

        let query = json!({
            "leverage": "0",                      // Отключаем isolated leverage
            "cross_leverage_limit": lev.to_string()  // Устанавливаем CROSS leverage
        });

        let path = format!("/futures/usdt/dual_comp/positions/{}/leverage", contract);

        self.request(Method::POST, &path, Some(&query), None).await
    }

    pub async fn get_positions(&self, contract: &str) -> Result<Value, String> {
        let path = "/futures/usdt/positions";
        let query = json!({"contract": contract});
        self.request(Method::GET, &path, Some(&query), None).await
    }

    /// Установить DUAL position mode (для одновременного LONG и SHORT)
    /// position_mode: "dual_long" или "dual_short"
    pub async fn set_position_mode(&self, mode: &str) -> Result<Value, String> {
        let body = json!({
            "position_mode": mode  // "dual_long" или "dual_short"
        });

        self.request(Method::POST, "/futures/usdt/set_position_mode", None, Some(&body)).await
    }

    /// Установить CROSS margin mode для DUAL position mode
    /// ВАЖНО: Это ДРУГОЙ endpoint чем для обычного режима!
    /// - Обычный: /futures/usdt/positions/cross_mode
    /// - Dual:    /futures/usdt/dual_comp/positions/cross_mode
    pub async fn set_dual_cross_mode(
        &self,
        contract: &str,
        mode: &str,  // "CROSS" или "ISOLATED"
    ) -> Result<Value, String> {
        let body = json!({
            "mode": mode,
            "contract": contract
        });

        self.request(
            Method::POST,
            "/futures/usdt/dual_comp/positions/cross_mode",
            None,
            Some(&body)
        ).await
    }

    /// Включить DUAL position mode (если выключен)
    /// ВАЖНО: dual_mode передаётся как QUERY parameter, а не в body!
    /// POST /futures/usdt/dual_mode?dual_mode=true
    pub async fn set_dual_mode_enabled(&self, dual_mode: bool) -> Result<Value, String> {
        let query = json!({
            "dual_mode": dual_mode
        });

        self.request(
            Method::POST,
            "/futures/usdt/dual_mode",
            Some(&query),
            None  // No body!
        ).await
    }

    /// Получить детальную информацию о позициях с mode
    pub async fn get_positions_with_mode(&self, contract: &str) -> Result<Value, String> {
        let path = "/futures/usdt/positions";
        let query = json!({"contract": contract});
        self.request(Method::GET, &path, Some(&query), None).await
    }

    /// Получить позиции из DUAL_COMP endpoint (правильный для DUAL mode)
    pub async fn get_dual_positions(&self, contract: &str) -> Result<Value, String> {
        let path = format!("/futures/usdt/dual_comp/positions/{}", contract);
        self.request(Method::GET, &path, None, None).await
    }

    pub async fn cancel_all_orders(&self) -> Result<(), String> {
        self.request(Method::DELETE, "/futures/usdt/orders", None, None).await?;
        Ok(())
    }
}