← Назад к главному
# gate.rs - Клиент Gate.io API ## 📋 Назначение модуля `GateClient` - HTTP клиент для взаимодействия с Gate.io Futures API. **Основная логика:** 1. **Аутентификация** - подпись запросов через HMAC-SHA512 2. **HTTP запросы** - GET/POST/DELETE с заголовками KEY/Timestamp/SIGN 3. **Фьючерсы API** - работа с позициями, ордерами, плечом 4. **DUAL режим** - поддержка одновременных LONG и SHORT позиций **Используется в:** - `main.rs` - получение баланса, открытых ордеров - `src/live/step.rs` - создание ордеров, обновление плеча, получение позиций - `src/live/strategy_core.rs` - проверка DUAL+CROSS режима --- ## 🏗️ Структура модуля ### Структура `GateClient` **Строки:** 16-22 ```rust #[derive(Clone)] pub struct GateClient { http: Client, pub api_key: String, pub api_secret: String, pub settle: String, } ``` **Назначение:** HTTP клиент для Gate.io API **Поля:** | Поле | Тип | Описание | |------|-----|----------| | `http` | `Client` | HTTP клиент (reqwest) | | `api_key` | `String` | API ключ Gate.io | | `api_secret` | `String` | Секретный ключ Gate.io | | `settle` | `String` | Валюта расчётов (USDT) | --- ### Константы **Строки:** 13-14 ```rust const BASE_HOST: &str = "https://api.gateio.ws"; const EMPTY_BODY_HASH: &str = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"; ``` **Назначение:** - `BASE_HOST` - базовый URL Gate.io API - `EMPTY_BODY_HASH` - SHA512 хеш пустой строки (для подписи без body) --- ## 🔄 Основные функции ### Функция `GateClient::new()` **Строки:** 25-33 **Сигнатура:** ```rust pub fn new() -> Self ``` **Возвращает:** - `GateClient` - клиент Gate.io API **Алгоритм:** ```rust 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(), } ``` **Что делает:** - Загружает API ключи через `ApiKeys::load()` - Создаёт HTTP клиент - Сохраняет ключи (без пробелов в начале и конце) --- ### Функция `GateClient::sign()` **Строки:** 35-55 **Сигнатура:** ```rust fn sign(&self, method: &str, url_path: &str, query: &str, body: &str, ts: &str) -> String ``` **Параметры:** - `method` - HTTP метод (GET, POST, DELETE) - `url_path` - путь к API (например, `/futures/usdt/orders`) - `query` - query string (например, `settle=usdt&status=open`) - `body` - тело запроса (JSON) - `ts` - timestamp **Возвращает:** - `String` - HMAC-SHA512 подпись в hex **Алгоритм:** --- #### Этап 1: Расчёт хеша body **Строки:** 36-40 ```rust let body_hash = if body.is_empty() { EMPTY_BODY_HASH.to_string() } else { hex::encode(Sha512::digest(body.as_bytes())) }; ``` **Формула:** ``` body_hash = SHA512(body) в hex Если body пустой → EMPTY_BODY_HASH ``` **Пример:** ``` body = '{"contract":"MEMES_USDT","size":1000,"price":"0.002000"}' SHA512(body) = a1b2c3d4e5f6... hex = "a1b2c3d4e5f6..." ``` --- #### Этап 2: Формирование payload **Строки:** 42-49 ```rust let payload = format!( "{}\n{}\n{}\n{}\n{}", method.to_uppercase(), url_path, query, body_hash, ts ); ``` **Формат payload:** ``` METHOD PATH QUERY BODY_HASH TIMESTAMP ``` **Пример:** ``` payload = "POST\n/api/v4/futures/usdt/orders\ncontract=MEMES_USDT\na1b2c3d4...\n1700000000" ``` --- #### Этап 3: Подпись HMAC-SHA512 **Строки:** 51-54 ```rust 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()) ``` **Формула:** ``` signature = HMAC-SHA512(secret, payload) в hex ``` --- ### Функция `GateClient::request()` **Строки:** 57-103 **Сигнатура:** ```rust async fn request( &self, method: Method, path: &str, query: Option<&Value>, body: Option<&Value>, ) -> Result ``` **Параметры:** - `method` - HTTP метод (GET, POST, DELETE) - `path` - путь к API (например, `/futures/usdt/orders`) - `query` - JSON query параметры - `body` - JSON тело запроса **Возвращает:** - `Ok(Value)` - JSON ответ от API - `Err(String)` - ошибка запроса или парсинга **Алгоритм:** --- #### Этап 1: Генерация timestamp **Строки:** 64-65 ```rust let ts = Utc::now().timestamp(); let timestamp = ts.to_string(); ``` --- #### Этап 2: Формирование query и body строк **Строки:** 67-71 ```rust 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(); ``` --- #### Этап 3: Подпись запроса **Строки:** 73-75 ```rust let url_path = format!("/api/v4{}", path); let signature = self.sign(method.as_str(), &url_path, &query_str, &body_str, ×tamp); ``` --- #### Этап 4: Формирование URL **Строки:** 77-81 ```rust let url = if query_str.is_empty() { format!("{BASE_HOST}{url_path}") } else { format!("{BASE_HOST}{url_path}?{query_str}") }; ``` --- #### Этап 5: Создание запроса с заголовками **Строки:** 83-92 ```rust 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); } ``` **Заголовки:** - `KEY` - API ключ - `Timestamp` - timestamp запроса - `SIGN` - HMAC-SHA512 подпись - `Content-Type` - `application/json` --- #### Этап 6: Отправка запроса **Строки:** 94-100 ```rust 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)); } ``` **Пример ошибки:** ``` Gate.io 400 /api/v4/futures/usdt/orders: {"label":"INVALID_PARAMETER","message":"Invalid parameter: size"} ``` --- #### Этап 7: Парсинг JSON **Строки:** 102 ```rust serde_json::from_str(&text).map_err(|e| format!("JSON parse error: {e}")) ``` --- ### Функция `GateClient::get_futures_account()` **Строки:** 105-107 **Сигнатура:** ```rust pub async fn get_futures_account(&self) -> Result ``` **Возвращает:** - `Ok(Value)` - JSON с информацией о фьючерсном аккаунте - `Err(String)` - ошибка запроса **Алгоритм:** ```rust self.request(Method::GET, "/futures/usdt/accounts", None, None).await ``` **Пример ответа:** ```json { "total": "100.0", "unrealised_pnl": "1.234", "cross_margin_balance": "100.0", "position_margin": "10.0", "available": "90.0" } ``` --- ### Функция `GateClient::get_real_balance_usdt()` **Строки:** 109-116 **Сигнатура:** ```rust pub async fn get_real_balance_usdt(&self) -> Result ``` **Возвращает:** - `Ok(f64)` - реальный баланс USDT - `Err(String)` - ошибка запроса или парсинга **Алгоритм:** ```rust 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()) ``` **Что делает:** - Получает аккаунт через `get_futures_account()` - Извлекает `cross_margin_balance` (баланс для CROSS режима) - Парсит в f64 **Пример:** ``` cross_margin_balance = "100.123456" → 100.123456 f64 ``` --- ### Функция `GateClient::list_open_orders()` **Строки:** 118-121 **Сигнатура:** ```rust pub async fn list_open_orders(&self) -> Result ``` **Возвращает:** - `Ok(Value)` - JSON с открытыми ордерами - `Err(String)` - ошибка запроса **Алгоритм:** ```rust let query = json!({"settle": "usdt", "status": "open"}); self.request(Method::GET, "/futures/usdt/orders", Some(&query), None).await ``` **Пример запроса:** ``` GET /api/v4/futures/usdt/orders?settle=usdt&status=open ``` --- ### Функция `GateClient::create_limit_order()` **Строки:** 123-144 **Сигнатура:** ```rust pub async fn create_limit_order( &self, contract: &str, is_long: bool, size: f64, price: f64, reduce_only: bool, ) -> Result ``` **Параметры:** - `contract` - символ контракта (например, "MEMES_USDT") - `is_long` - true для LONG, false для SHORT - `size` - размер позиции - `price` - цена ордера - `reduce_only` - только закрытие позиции **Возвращает:** - `Ok(Value)` - JSON ответ API - `Err(String)` - ошибка запроса **Алгоритм:** --- #### Этап 1: Расчёт размера с учётом направления **Строки:** 131-132 ```rust let size_int = if is_long { size.round() as i64 } else { -(size.round() as i64) }; if size_int == 0 { return Err("size = 0".into()); } ``` **Логика:** - LONG → положительный размер - SHORT → отрицательный размер **Пример:** ``` is_long = true, size = 1000.5 → size_int = 1000 is_long = false, size = 500.7 → size_int = -501 ``` --- #### Этап 2: Формирование тела запроса **Строки:** 134-141 ```rust let body = json!({ "contract": contract, "size": size_int, "price": format!("{:.8}", price), "tif": "gtc", "reduce_only": reduce_only, "text": "t-pure_ai_v2" }); ``` **Поля:** - `contract` - символ контракта - `size` - размер (позитивный для LONG, отрицательный для SHORT) - `price` - цена (8 знаков после запятой) - `tif` - "gtc" (Good Till Cancel) - `reduce_only` - только закрытие - `text` - пользовательский идентификатор --- #### Этап 3: Отправка запроса **Строка:** 143 ```rust self.request(Method::POST, "/futures/usdt/orders", None, Some(&body)).await ``` --- ### Функция `GateClient::create_market_order()` **Строки:** 146-153 **Сигнатура:** ```rust pub async fn create_market_order( &self, contract: &str, is_long: bool, size: f64, ) -> Result ``` **Алгоритм:** ```rust Self::create_market_order_with_reduce(self, contract, is_long, size, false).await ``` **Что делает:** - Создаёт MARKET ордер с `reduce_only = false` --- ### Функция `GateClient::create_reduce_only_order()` **Строки:** 156-163 **Сигнатура:** ```rust pub async fn create_reduce_only_order( &self, contract: &str, is_long: bool, size: f64, ) -> Result ``` **Алгоритм:** ```rust Self::create_market_order_with_reduce(self, contract, is_long, size, true).await ``` **Что делает:** - Создаёт MARKET ордер с `reduce_only = true` (для закрытия позиций) --- ### Функция `GateClient::create_market_order_with_reduce()` **Строки:** 166-187 **Сигнатура:** ```rust async fn create_market_order_with_reduce( &self, contract: &str, is_long: bool, size: f64, reduce_only: bool, ) -> Result ``` **Алгоритм:** --- #### Этап 1: Расчёт размера **Строка:** 173 ```rust let size_int = if is_long { size.round() as i64 } else { -(size.round() as i64) }; ``` --- #### Этап 2: Генерация timestamp **Строка:** 174 ```rust let timestamp = Utc::now().timestamp_millis(); ``` --- #### Этап 3: Формирование тела запроса **Строки:** 176-184 ```rust let body = json!({ "contract": contract, "size": size_int, "price": "0", "tif": "ioc", "reduce_only": reduce_only, // НЕ меняем margin mode - используем тот что установлен в аккаунте "text": format!("t-{}", timestamp) }); ``` **Поля:** - `price` - "0" (MARKET ордер) - `tif` - "ioc" (Immediate Or Cancel) - `reduce_only` - только закрытие (параметр) --- #### Этап 4: Отправка запроса **Строка:** 186 ```rust self.request(Method::POST, "/futures/usdt/orders", None, Some(&body)).await ``` --- ### Функция `GateClient::get_contract_details()` **Строки:** 189-192 **Сигнатура:** ```rust pub async fn get_contract_details(&self, contract: &str) -> Result ``` **Алгоритм:** ```rust let path = format!("/futures/usdt/contracts/{}", contract); self.request(Method::GET, &path, None, None).await ``` --- ### Функция `GateClient::get_leverage_limits()` **Строки:** 194-210 **Сигнатура:** ```rust pub async fn get_leverage_limits(&self, contract: &str) -> Result<(f64, f64), String> ``` **Возвращает:** - `Ok((min, max))` - минимальное и максимальное плечо - `Err(String)` - ошибка запроса или парсинга **Алгоритм:** ```rust let details = self.get_contract_details(contract).await?; let min = details["leverage_min"] .as_str() .ok_or("no leverage_min")? .parse::() .map_err(|_| "parse leverage_min error")?; let max = details["leverage_max"] .as_str() .ok_or("no leverage_max")? .parse::() .map_err(|_| "parse leverage_max error")?; Ok((min, max)) ``` --- ### Функция `GateClient::update_leverage()` **Строки:** 212-228 **Сигнатура:** ```rust pub async fn update_leverage( &self, contract: &str, leverage: f64, ) -> Result ``` **Алгоритм:** ```rust 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 ``` --- ### Функция `GateClient::update_dual_leverage()` **Строки:** 238-254 **Сигнатура:** ```rust pub async fn update_dual_leverage( &self, contract: &str, cross_leverage: f64, ) -> Result ``` **Описание:** Установить leverage для DUAL+CROSS режима **Алгоритм:** ```rust 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 ``` **Параметры:** - `leverage: "0"` - отключает isolated leverage - `cross_leverage_limit` - реальное плечо для CROSS margin --- ### Функция `GateClient::get_positions()` **Строки:** 256-260 **Сигнатура:** ```rust pub async fn get_positions(&self, contract: &str) -> Result ``` **Алгоритм:** ```rust let path = "/futures/usdt/positions"; let query = json!({"contract": contract}); self.request(Method::GET, &path, Some(&query), None).await ``` --- ### Функция `GateClient::set_position_mode()` **Строки:** 264-270 **Сигнатура:** ```rust pub async fn set_position_mode(&self, mode: &str) -> Result ``` **Параметры:** - `mode` - "dual_long" или "dual_short" **Алгоритм:** ```rust let body = json!({ "position_mode": mode // "dual_long" или "dual_short" }); self.request(Method::POST, "/futures/usdt/set_position_mode", None, Some(&body)).await ``` --- ### Функция `GateClient::set_dual_cross_mode()` **Строки:** 276-292 **Сигнатура:** ```rust pub async fn set_dual_cross_mode( &self, contract: &str, mode: &str, // "CROSS" или "ISOLATED" ) -> Result ``` **Описание:** Установить CROSS margin mode для DUAL position mode **Алгоритм:** ```rust let body = json!({ "mode": mode, "contract": contract }); self.request( Method::POST, "/futures/usdt/dual_comp/positions/cross_mode", None, Some(&body) ).await ``` **Endpoint:** `/futures/usdt/dual_comp/positions/cross_mode` (специальный для DUAL) --- ### Функция `GateClient::set_dual_mode_enabled()` **Строки:** 297-308 **Сигнатура:** ```rust pub async fn set_dual_mode_enabled(&self, dual_mode: bool) -> Result ``` **Алгоритм:** ```rust let query = json!({ "dual_mode": dual_mode }); self.request( Method::POST, "/futures/usdt/dual_mode", Some(&query), None // No body! ).await ``` **Важно:** `dual_mode` передаётся как QUERY parameter, а не в body! --- ### Функция `GateClient::get_dual_positions()` **Строки:** 318-321 **Сигнатура:** ```rust pub async fn get_dual_positions(&self, contract: &str) -> Result ``` **Алгоритм:** ```rust let path = format!("/futures/usdt/dual_comp/positions/{}", contract); self.request(Method::GET, &path, None, None).await ``` **Endpoint:** `/futures/usdt/dual_comp/positions/{contract}` (правильный для DUAL mode) --- ### Функция `GateClient::cancel_all_orders()` **Строки:** 323-326 **Сигнатура:** ```rust pub async fn cancel_all_orders(&self) -> Result<(), String> ``` **Алгоритм:** ```rust self.request(Method::DELETE, "/futures/usdt/orders", None, None).await?; Ok(()) ``` --- ## 📊 API Endpoints | Endpoint | Метод | Назначение | |----------|--------|-----------| | `/futures/usdt/accounts` | GET | Информация о фьючерсном аккаунте | | `/futures/usdt/orders` | GET | Список открытых ордеров | | `/futures/usdt/orders` | POST | Создать ордер | | `/futures/usdt/orders` | DELETE | Отменить все ордера | | `/futures/usdt/contracts/{contract}` | GET | Детали контракта | | `/futures/usdt/positions/{contract}/leverage` | POST | Обновить плечо | | `/futures/usdt/dual_comp/positions/{contract}/leverage` | POST | Обновить плечо (DUAL) | | `/futures/usdt/positions` | GET | Получить позиции | | `/futures/usdt/set_position_mode` | POST | Установить режим позиции | | `/futures/usdt/dual_comp/positions/cross_mode` | POST | Установить CROSS режим (DUAL) | | `/futures/usdt/dual_mode` | POST | Включить/выключить DUAL режим | | `/futures/usdt/dual_comp/positions/{contract}` | GET | Получить позиции (DUAL) | --- ## ⚠️ Критические моменты ### 1. HMAC-SHA512 подпись **Строки:** 35-55 ```rust let payload = format!( "{}\n{}\n{}\n{}\n{}", method.to_uppercase(), url_path, query, body_hash, ts ); ``` **Формат подписи:** ``` METHOD PATH QUERY BODY_HASH TIMESTAMP ``` **Пример:** ``` POST /api/v4/futures/usdt/orders contract=MEMES_USDT a1b2c3d4e5f6... 1700000000 ``` --- ### 2. DUAL+CROSS режим **Строки:** 238-254, 276-292 ```rust let query = json!({ "leverage": "0", // Отключаем isolated leverage "cross_leverage_limit": lev.to_string() // Устанавливаем CROSS leverage }); ``` **Почему это важно:** - Для DUAL режима используется специальный endpoint - `leverage: "0"` отключает isolated leverage - `cross_leverage_limit` устанавливает CROSS leverage --- ### 3. dual_mode как query parameter **Строки:** 302-305 ```rust self.request( Method::POST, "/futures/usdt/dual_mode", Some(&query), // dual_mode передаётся как QUERY! None // No body! ).await ``` **Важно:** `dual_mode` передаётся как QUERY parameter, а не в body! --- ### 4. Размер ордера с учётом направления **Строки:** 131, 173 ```rust let size_int = if is_long { size.round() as i64 } else { -(size.round() as i64) }; ``` **Логика:** - LONG → положительный размер - SHORT → отрицательный размер **Пример:** ``` LONG: size = 1000 → size_int = 1000 SHORT: size = 500 → size_int = -500 ``` --- ### 5. MARKET ордер с price = "0" **Строки:** 179 ```rust "price": "0", ``` **Что это значит:** - MARKET ордер задаётся через `price = "0"` - `tif = "ioc"` - Immediate Or Cancel --- ## 📚 Связанные файлы | Файл | Связь | |------|-------| | `src/main.rs` | Использует get_futures_account(), get_real_balance_usdt(), list_open_orders() | | `src/config.rs` | ApiKeys::load() для получения ключей | | `src/live/step.rs` | Использует create_market_order(), create_reduce_only_order(), update_leverage(), get_positions() | | `src/live/strategy_core.rs` | Использует set_position_mode(), set_dual_cross_mode(), set_dual_mode_enabled(), update_dual_leverage() | --- ## 🎯 Резюме **Что делает gate.rs:** 1. ✅ Создаёт HTTP клиент для Gate.io API 2. ✅ Подписывает запросы через HMAC-SHA512 3. ✅ Получает информацию о фьючерсном аккаунте 4. ✅ Получает реальный баланс USDT 5. ✅ Создаёт LIMIT и MARKET ордера 6. ✅ Получает открытые ордера и позиции 7. ✅ Обновляет плечо (обычный и DUAL режим) 8. ✅ Устанавливает DUAL+CROSS режим **GateClient методы:** - `new()` - создание клиента - `get_futures_account()` - информация об аккаунте - `get_real_balance_usdt()` - реальный баланс - `list_open_orders()` - открытые ордера - `create_limit_order()` - создать LIMIT ордер - `create_market_order()` - создать MARKET ордер - `create_reduce_only_order()` - создать MARKET для закрытия - `get_contract_details()` - детали контракта - `get_leverage_limits()` - лимиты плеча - `update_leverage()` - обновить плечо - `update_dual_leverage()` - обновить плечо (DUAL) - `get_positions()` - получить позиции - `set_position_mode()` - установить режим позиции - `set_dual_cross_mode()` - установить CROSS режим (DUAL) - `set_dual_mode_enabled()` - включить/выключить DUAL - `get_dual_positions()` - получить позиции (DUAL) - `cancel_all_orders()` - отменить все ордера **Подпись запроса:** ``` payload = METHOD\nPATH\nQUERY\nBODY_HASH\nTIMESTAMP signature = HMAC-SHA512(secret, payload) ``` **Критические моменты:** - 🔥 DUAL+CROSS режим использует специальные endpoints - 🔥 dual_mode передаётся как query parameter - 🔥 Размер ордера: LONG → +N, SHORT → -N - 🔥 MARKET ордер: price = "0", tif = "ioc" --- **Дата создания:** 2026-02-22 **Автор:** Claude Code Assistant **Версия:** 1.0