← Назад к главному
# 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