← Назад к главному
# monitor.rs - Режим MONITOR для поиска памп-пар
## 📋 Назначение модуля
`run_monitor()` - сканирует Gate.io фьючерсы, находит памп-пары, выбирает лучшую и запускает PARSE loop.
**Основная логика:**
1. **SCAN фаза** - сканирует все фьючерсы на Gate.io
2. **Фильтрация** - находит пары с волатильностью 10-100%
3. **Выбор лучшей** - сортирует по score, выбирает топ
4. **PARSE фаза** - запускает фоновый loop для тиков выбранной пары
5. **Запись в Redis** - сохраняет тики и мета-данные для LIVE движка
**Используется в:**
- `main.rs` - вызывает `run_monitor()` для выбора пары
---
## 🏗️ Структура модуля
### Структура `RawFuturesContract`
**Строки:** 126-161
```rust
#[derive(Clone, Debug, Deserialize)]
struct RawFuturesContract {
#[serde(default)]
last: String,
#[serde(default)]
lowest_ask: String,
#[serde(default)]
highest_bid: String,
#[serde(default)]
volume_24h_quote: String,
#[serde(default)]
change_percentage: String,
contract: String,
}
```
**Назначение:** Структура для ответа от `/api/v4/futures/usdt/tickers`
**Методы:**
- `parse_f64()` - парсит строку в f64
- `get_last()` - возвращает цену последней сделки
- `get_ask()` - возвращает цену ask
- `get_bid()` - возвращает цену bid
- `get_volume()` - возвращает объём за 24ч
- `get_change_pct()` - возвращает изменение цены за 24ч (%)
---
### Структура `PumpPair`
**Строки:** 164-170
```rust
#[derive(Clone, Debug)]
struct PumpPair {
score: f64, // Рейтинг пары
mid: f64, // Средняя цена (bid + ask) / 2
spread_bps: f64, // Спред в базисных пунктах (1/10000)
vol24h: f64, // Объём за 24ч
chg_pct: f64, // Изменение цены за 24ч (%)
}
```
**Назначение:** Структура памп-пары с расчитанным score
---
### Структура `OneTick`
**Строки:** 174-183
```rust
#[derive(Clone, Debug)]
struct OneTick {
symbol: String,
mid: f64, // Средняя цена
chg_pct: f64, // Изменение цены за 24ч (%)
ts: i64, // Timestamp
bid: f64,
ask: f64,
bid_qty: f64, // Объём bid-уровней вокруг mid (0.5%)
ask_qty: f64, // Объём ask-уровней вокруг mid (0.5%)
}
```
**Назначение:** Внутренний "богатый" тик для расчётов
---
### Структура `ContractDetails`
**Строки:** 186-212
```rust
#[derive(Clone, Debug, Deserialize)]
struct ContractDetails {
#[serde(default)]
leverage_min: String,
#[serde(default)]
leverage_max: String,
#[serde(default)]
order_size_min: i64,
#[serde(default)]
order_size_max: i64,
#[serde(default)]
quanto_multiplier: String,
#[serde(default)]
maintenance_rate: String,
#[serde(default)]
maker_fee_rate: String,
#[serde(default)]
taker_fee_rate: String,
#[serde(default)]
funding_rate: String,
#[serde(default)]
funding_rate_indicative: String,
#[serde(default)]
order_price_round: String,
#[serde(default)]
order_price_deviate: String,
}
```
**Назначение:** Детали контракта из `/api/v4/futures/usdt/contracts/{contract}`
---
### Структура `RawOrderBook`
**Строки:** 215-219
```rust
#[derive(Clone, Debug, Deserialize)]
struct RawOrderBook {
asks: Vec<[String; 2]>, // [[price, size], ...]
bids: Vec<[String; 2]>, // [[price, size], ...]
}
```
**Назначение:** Ордербук из `/api/v4/futures/usdt/order_book`
---
## 🔄 Основные функции
### Функция `run_monitor()`
**Строки:** 19-77
**Сигнатура:**
```rust
pub async fn run_monitor(
redis: Arc,
_session: &str,
gate_base: &str,
scan_top: usize,
parse_interval_ms: u64,
tick_ttl_sec: u64,
min_volatility_pct: f64,
max_volatility_pct: f64,
) -> Result
```
**Возвращает:**
- `Ok(String)` - выбранный символ контракта (например, "MEMES_USDT")
- `Err` - ошибка при выборе пары
**Алгоритм (6 этапов):**
---
#### Этап 1: Получение всех тикеров
**Строки:** 35-37
```rust
info!("MONITOR: mode=SCAN - fetching tickers… base={}", gate_base);
let all = fetch_all_tickers(gate_base).await?;
info!("Tickers loaded: {}", all.len());
```
**Что делает:**
- Вызывает `fetch_all_tickers()` для получения всех фьючерсов
- Логирует количество загруженных тикеров
**Пример:**
```
MONITOR: mode=SCAN - fetching tickers… base=https://api.gateio.ws
Tickers loaded: 1500
```
---
#### Этап 2: Фильтрация памповых пар
**Строки:** 39-58
```rust
let filtered = filter_pump_pairs(&all, min_volatility_pct, max_volatility_pct);
info!("SCAN prefilter: {} → {}", all.len(), filtered.len());
let candidates = if filtered.is_empty() {
warn!(
"⚠️ Не найдено памповых пар в диапазоне {}% - {}%",
min_volatility_pct, max_volatility_pct
);
// fallback: ослабленные критерии
let fallback = filter_pump_pairs(&all, 7.0, 2000.0);
if fallback.is_empty() {
bail!("❌ Нет подходящих пар даже при ослабленных критериях");
}
info!("🔄 Используем расширенные критерии 7-2000%");
fallback
} else {
filtered
};
```
**Что делает:**
- Фильтрует пары с волатильностью `min_volatility_pct` - `max_volatility_pct`
- Если пары не найдены → использует fallback (7% - 2000%)
- Если и fallback пуст → ошибка
**Пример:**
```
SCAN prefilter: 1500 → 25
```
**Пример fallback:**
```
⚠️ Не найдено памповых пар в диапазоне 10% - 100%
🔄 Используем расширенные критерии 7-2000%
```
---
#### Этап 3: Выбор лучшей пары
**Строки:** 60
```rust
let selected_pair = pick_best_pair(candidates, scan_top)?;
```
**Что делает:**
- Вызывает `pick_best_pair()` для выбора лучшей пары
- Передаёт список кандидатов и `scan_top`
**Пример:**
```
=== OPTIMAL PAIR ===
Selected: MEMES_USDT (score=12.345678, mid=0.00200000, spread_bps=5.000000, vol24q=1000000, chg%=45.50)
🔥 ОБНАРУЖЕН СУПЕР-ПАМП: 100% роста за 24ч!
SCAN to PARSE switch: pair=MEMES_USDT, switched=true
Пара выбрана: MEMES_USDT
```
---
#### Этап 4: Запуск PARSE loop в фоне
**Строки:** 62-74
```rust
let symbol = selected_pair.clone();
let gate_base_owned = gate_base.to_string();
let redis_clone = redis.clone();
tokio::spawn(async move {
if let Err(e) =
parse_loop(redis_clone, symbol.clone(), gate_base_owned, parse_interval_ms, tick_ttl_sec)
.await
{
warn!("PARSE loop error for {}: {e:?}", symbol);
}
});
```
**Что делает:**
- Клонирует необходимые переменные
- Запускает `parse_loop()` в отдельном tokio task
- Логирует ошибку если PARSE loop упал
**Важно:**
- PARSE loop работает в фоне параллельно с LIVE
- LIVE движок читает данные из Redis, которые пишет PARSE
---
#### Этап 5: Возврат выбранной пары
**Строки:** 76
```rust
Ok(selected_pair)
```
**Возвращает:** Выбранный символ контракта
---
### Функция `pick_best_pair()`
**Строки:** 80-119
**Сигнатура:**
```rust
fn pick_best_pair(pairs: Vec<(String, PumpPair)>, scan_top: usize) -> Result
```
**Возвращает:**
- `Ok(String)` - лучший символ контракта
- `Err` - нет пар для выбора
**Алгоритм:**
---
#### Этап 1: Проверка пустого списка
**Строки:** 81-83
```rust
if pairs.is_empty() {
bail!("Нет пар для выбора");
}
```
**Что делает:**
- Если список пуст → ошибка
---
#### Этап 2: Сортировка по score
**Строки:** 85-90
```rust
let mut ranked = pairs;
ranked.sort_by(|a, b| {
b.1.score
.partial_cmp(&a.1.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
```
**Что делает:**
- Сортирует пары по убыванию score
- Больший score = выше в списке
**Логика:**
```
sort_by: b.1.score <=> a.1.score
(по убыванию)
```
---
#### Этап 3: Выбор лучшей пары
**Строки:** 97
```rust
let (best_sym, best) = ranked.remove(0);
```
**Что делает:**
- Берёт первую пару из отсортированного списка
- Удаляет её из списка (remove)
---
#### Этап 4: Логирование выбранной пары
**Строки:** 99-116
```rust
info!(
"=== OPTIMAL PAIR ===\n Selected: {} (score={:.6}, mid={:.6}, spread_bps={:.6}, vol24q={}, chg%={:.2})",
best_sym,
best.score,
best.mid,
best.spread_bps,
best.vol24h as i64,
best.chg_pct
);
if best.chg_pct >= 100.0 {
info!("🔥 ОБНАРУЖЕН СУПЕР-ПАМП: {}% роста за 24ч!", best.chg_pct);
} else if best.chg_pct >= 50.0 {
info!("⚡ СИЛЬНЫЙ РОСТ: {}% за 24ч", best.chg_pct);
}
info!("SCAN to PARSE switch: pair={}, switched=true", best_sym);
info!("Пара выбрана: {}", best_sym);
```
**Что делает:**
- Логирует детальную информацию о выбранной паре
- Проверяет уровень пампа (>=100%, >=50%)
- Логирует переход SCAN → PARSE
**Пример (супер-памп):**
```
=== OPTIMAL PAIR ===
Selected: MEMES_USDT (score=12.345678, mid=0.00200000, spread_bps=5.000000, vol24q=1000000, chg%=120.50)
🔥 ОБНАРУЖЕН СУПЕР-ПАМП: 120.5% роста за 24ч!
SCAN to PARSE switch: pair=MEMES_USDT, switched=true
Пара выбрана: MEMES_USDT
```
**Пример (сильный рост):**
```
=== OPTIMAL PAIR ===
Selected: COIN_USDT (score=8.234567, mid=0.00150000, spread_bps=8.000000, vol24q=800000, chg%=65.30)
⚡ СИЛЬНЫЙ РОСТ: 65.3% за 24ч
SCAN to PARSE switch: pair=COIN_USDT, switched=true
Пара выбрана: COIN_USDT
```
---
#### Этап 5: Возврат символа
**Строки:** 118
```rust
Ok(best_sym)
```
**Возвращает:** Лучший символ контракта
---
### Функция `filter_pump_pairs()`
**Строки:** 373-412
**Сигнатура:**
```rust
fn filter_pump_pairs(
all: &[RawFuturesContract],
min_pct: f64,
max_pct: f64,
) -> Vec<(String, PumpPair)>
```
**Возвращает:**
- `Vec<(String, PumpPair)>` - список отфильтрованных памп-пар
**Алгоритм:**
---
#### Этап 1: Префильтр контрактов
**Строки:** 378-391
```rust
let filtered_contracts: Vec<&RawFuturesContract> = all
.iter()
.filter(|t| !t.contract.is_empty())
.filter(|t| {
!t.last.trim().is_empty()
|| (!t.lowest_ask.trim().is_empty() && !t.highest_bid.trim().is_empty())
})
.collect();
info!(
"📊 Префильтр: {} → {}",
all.len(),
filtered_contracts.len()
);
```
**Условия:**
- `contract` не пустой
- `last` не пустой ИЛИ (`ask` не пустой И `bid` не пустой)
**Пример:**
```
📊 Префильтр: 1500 → 1400
```
---
#### Этап 2: Фильтрация по волатильности
**Строки:** 393-404
```rust
let mut pump_pairs = Vec::new();
for contract in filtered_contracts.iter() {
let chg_pct = contract.get_change_pct();
let abs_chg_pct = chg_pct.abs();
if abs_chg_pct >= min_pct && abs_chg_pct <= max_pct {
if let Some((symbol, pair)) = create_pump_pair(contract) {
pump_pairs.push((symbol, pair));
}
}
}
```
**Условия:**
- `min_pct <= |chg_pct| <= max_pct`
**Пример:**
```
min_pct = 10.0, max_pct = 100.0
chg_pct = 45.5
|45.5| = 45.5
10.0 <= 45.5 <= 100.0 ✅
→ добавляем в список
```
---
#### Этап 3: Сортировка по score
**Строки:** 406-410
```rust
pump_pairs.sort_by(|a, b| {
b.1.score
.partial_cmp(&a.1.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
```
**Что делает:**
- Сортирует по убыванию score
---
#### Этап 4: Возврат списка
**Строка:** 411
```rust
pump_pairs
```
---
### Функция `create_pump_pair()`
**Строки:** 414-458
**Сигнатура:**
```rust
fn create_pump_pair(contract: &RawFuturesContract) -> Option<(String, PumpPair)>
```
**Возвращает:**
- `Some((String, PumpPair))` - если пара подходит
- `None` - если пара не подходит
**Алгоритм:**
---
#### Этап 1: Расчёт mid цены
**Строки:** 415-423
```rust
let ask = contract.get_ask();
let bid = contract.get_bid();
let last = contract.get_last();
let mid = if ask > 0.0 && bid > 0.0 {
(ask + bid) / 2.0
} else {
last
};
```
**Формула:**
```
mid = (ask + bid) / 2, если ask > 0 и bid > 0
mid = last, иначе
```
---
#### Этап 2: Проверка mid <= 0
**Строки:** 425-427
```rust
if mid <= 0.0 {
return None;
}
```
**Условие:** Если `mid <= 0` → не подходит
---
#### Этап 3: Фильтр по цене
**Строки:** 429-432
```rust
// ФИЛЬТР ПО ЦЕНЕ: только пары с ценой ≤ 0.01 USDT
if mid > 0.01 {
return None;
}
```
**Условие:** Если `mid > 0.01 USDT` → не подходит
**Почему важно:**
- Дешёвые монеты более волатильны
- Пампы чаще происходят на дешёвых монетах
**Пример:**
```
mid = 0.002000 → 0.002000 <= 0.01 ✅
mid = 0.010000 → 0.010000 <= 0.01 ✅ (равно)
mid = 0.015000 → 0.015000 > 0.01 ❌
```
---
#### Этап 4: Расчёт spread_bps
**Строки:** 434-438
```rust
let spread_bps = if ask > 0.0 && bid > 0.0 {
((ask - bid) / mid) * 10_000.0
} else {
0.0
};
```
**Формула:**
```
spread_bps = ((ask - bid) / mid) × 10000
```
**Единицы:** базисные пункты (1 bps = 0.01%)
**Пример:**
```
ask = 0.002010, bid = 0.001990, mid = 0.002000
spread_bps = ((0.002010 - 0.001990) / 0.002000) × 10000
spread_bps = (0.000020 / 0.002000) × 10000
spread_bps = 0.01 × 10000 = 100 bps = 1.0%
```
---
#### Этап 5: Получение волатильности и объёма
**Строки:** 440-442
```rust
let vol24h = contract.get_volume();
let chg_pct = contract.get_change_pct();
let abs_chg_pct = chg_pct.abs();
```
---
#### Этап 6: Расчёт score
**Строки:** 444-446
```rust
let vol_score = if vol24h > 0.0 { vol24h.ln() } else { 0.0 };
let score = (abs_chg_pct * 5.0) + vol_score - (spread_bps * 0.5);
```
**Формула:**
```
score = (|chg_pct| × 5.0) + ln(vol24h) - (spread_bps × 0.5)
```
**Компоненты score:**
- `|chg_pct| × 5.0` - волатильность (чем больше, тем лучше)
- `ln(vol24h)` - объём (логарифмическая шкала)
- `-spread_bps × 0.5` - штраф за спред (чем меньше спред, тем лучше)
**Пример:**
```
abs_chg_pct = 45.5
vol24h = 1000000
spread_bps = 100
vol_score = ln(1000000) = 13.8155
score = (45.5 × 5.0) + 13.8155 - (100 × 0.5)
score = 227.5 + 13.8155 - 50.0
score = 191.3155
```
---
#### Этап 7: Возврат пары
**Строки:** 448-457
```rust
Some((
contract.contract.clone(),
PumpPair {
score,
mid,
spread_bps,
vol24h,
chg_pct: abs_chg_pct,
},
))
```
---
### Функция `parse_loop()`
**Строки:** 464-651
**Сигнатура:**
```rust
async fn parse_loop(
redis: Arc,
symbol: String,
gate_base: String,
interval_ms: u64,
_tick_ttl_sec: u64,
) -> Result<()>
```
**Возвращает:**
- `Ok(())` - никогда (бесконечный цикл)
- `Err` - ошибка внутри цикла
**Алгоритм (4 этапа):**
---
#### Этап 1: Получение деталей контракта
**Строки:** 476-500
```rust
let contract_details = match fetch_contract_details(&gate_base, &symbol).await {
Ok(cd) => cd,
Err(e) => {
warn!(
"Не удалось получить детали контракта для {}: {e:?}. Продолжаем только с тикером",
symbol
);
// Заглушка, чтобы не падать
ContractDetails {
leverage_min: "10".to_string(),
leverage_max: "50".to_string(),
order_size_min: 1,
order_size_max: 1_000_000,
quanto_multiplier: "1".to_string(),
maintenance_rate: "0.01".to_string(),
maker_fee_rate: "0.0".to_string(),
taker_fee_rate: "0.0".to_string(),
funding_rate: "0.0".to_string(),
funding_rate_indicative: "0.0".to_string(),
order_price_round: "0.000001".to_string(),
order_price_deviate: "0.2".to_string(),
}
}
};
```
**Что делает:**
- Получает детали контракта один раз перед циклом
- Если ошибка → использует заглушки
**Заглушки:**
- `leverage_min = 10`, `leverage_max = 50`
- `order_size_min = 1`, `order_size_max = 1000000`
- `quanto_multiplier = 1`
- `maintenance_rate = 0.01`
- `maker_fee_rate = 0.0`, `taker_fee_rate = 0.0`
- `funding_rate = 0.0`, `funding_rate_indicative = 0.0`
- `order_price_round = 0.000001`
- `order_price_deviate = 0.2`
---
#### Этап 2: Бесконечный цикл получения тиков
**Строки:** 502-650
```rust
let mut depth_cached: Option = None;
loop {
match fetch_one_ticker(&gate_base, &symbol).await {
Ok(mut one) => {
// ... (обработка тика)
}
Err(e) => {
warn!("fetch_one_ticker({symbol}) error: {e:?}");
}
}
sleep(Duration::from_millis(interval_ms)).await;
}
```
**Что делает:**
- Получает тикер через `fetch_one_ticker()`
- Обрабатывает тик (ордребук, глубина, проскальзывание)
- Записывает в Redis
- Ждёт `interval_ms` между итерациями
---
#### Этап 3: Получение ордербука
**Строки:** 508-523
```rust
let order_book = match fetch_order_book(&gate_base, &symbol, 50).await {
Ok(ob) => {
depth_cached = Some(ob.clone());
ob
}
Err(e) => {
warn!(
"fetch_order_book({}) error: {e:?}. Используем кеш или заглушки",
symbol
);
depth_cached.clone().unwrap_or(RawOrderBook {
asks: Vec::new(),
bids: Vec::new(),
})
}
};
```
**Что делает:**
- Получает ордербук глубиной 50 уровней
- Кеширует ордербук
- При ошибке использует кеш или заглушки
---
#### Этап 4: Расчёт глубины и лучших цен
**Строки:** 525-538
```rust
let (bid, ask, bid_depth, ask_depth) =
compute_depth_and_best(&order_book, one.mid);
if bid > 0.0 && ask > 0.0 {
one.bid = bid;
one.ask = ask;
one.mid = (bid + ask) / 2.0;
}
if bid_depth > 0.0 {
one.bid_qty = bid_depth;
}
if ask_depth > 0.0 {
one.ask_qty = ask_depth;
}
```
**Что делает:**
- Вычисляет лучшие bid/ask и глубину
- Обновляет тик с новыми данными
---
#### Этап 5: Запись тика в Redis
**Строки:** 540-554
```rust
let tick = to_public_tick(&one);
let key_list = format!("hb:ticks:{}", tick.symbol);
let key_last = format!("hb:last:{}", tick.symbol);
let key_meta = format!("hb:meta:{}", tick.symbol);
let value = serde_json::to_string(&tick)?;
let mut conn = crate::redis::get_redis_conn(&redis).await?;
// список тиков (хвост)
let _: i64 = conn.lpush(&key_list, &value).await.unwrap_or(0);
let _: () = conn.ltrim(&key_list, 0, 999).await.unwrap_or(());
// ПОСЛЕДНИЙ тик — то, что читает Feed::get_tick_from_redis
let _: () = conn.set(&key_last, &value).await.unwrap_or(());
```
**Redis ключи:**
- `hb:ticks:{symbol}` - список тиков (максимум 1000)
- `hb:last:{symbol}` - последний тик
- `hb:meta:{symbol}` - мета-данные
**Команды Redis:**
- `LPUSH hb:ticks:MEMES_USDT value` - добавить тик в начало списка
- `LTRIM hb:ticks:MEMES_USDT 0 999` - оставить только 1000 тиков
- `SET hb:last:MEMES_USDT value` - записать последний тик
---
#### Этап 6: Запись мета-данных в Redis
**Строки:** 556-631
```rust
let max_leverage = contract_details
.leverage_max
.trim()
.parse::()
.unwrap_or(0.0);
let min_leverage = contract_details
.leverage_min
.trim()
.parse::()
.unwrap_or(0.0);
let contract_multiplier = contract_details
.quanto_multiplier
.trim()
.parse::()
.unwrap_or(1.0);
let maintenance_rate = contract_details
.maintenance_rate
.trim()
.parse::()
.unwrap_or(0.0);
let maker_fee = contract_details
.maker_fee_rate
.trim()
.parse::()
.unwrap_or(0.0);
let taker_fee = contract_details
.taker_fee_rate
.trim()
.parse::()
.unwrap_or(0.0);
let funding_rate = contract_details
.funding_rate
.trim()
.parse::()
.unwrap_or(0.0);
let funding_rate_indicative = contract_details
.funding_rate_indicative
.trim()
.parse::()
.unwrap_or(0.0);
// Простейшая оценка проскальзывания на 1000$ по стакану
let (slip_buy_1000, slip_sell_1000) =
compute_slippage_1000(&order_book, one.mid, contract_multiplier);
let meta = json!({
"symbol": tick.symbol,
"ts": tick.ts,
"price": tick.mid,
"chg_pct_24h": tick.chg_pct,
"bid": tick.bid,
"ask": tick.ask,
"bid_qty_depth": tick.bid_qty,
"ask_qty_depth": tick.ask_qty,
"leverage_min": min_leverage,
"leverage_max": max_leverage,
"min_contracts": contract_details.order_size_min,
"max_contracts": contract_details.order_size_max,
"contract_multiplier": contract_multiplier,
"maintenance_rate": maintenance_rate,
"maker_fee_rate": maker_fee,
"taker_fee_rate": taker_fee,
"funding_rate": funding_rate,
"funding_rate_indicative": funding_rate_indicative,
"order_price_round": contract_details.order_price_round,
"order_price_deviate": contract_details.order_price_deviate,
"slippage_buy_1000_pct": slip_buy_1000,
"slippage_sell_1000_pct": slip_sell_1000,
});
let meta_str = serde_json::to_string(&meta)?;
let _: () = conn.set(&key_meta, &meta_str).await.unwrap_or(());
```
**Поля мета-данных:**
| Поле | Описание |
|------|----------|
| `symbol` | Символ контракта |
| `ts` | Timestamp |
| `price` | Цена (mid) |
| `chg_pct_24h` | Изменение за 24ч (%) |
| `bid`, `ask` | Лучшие bid/ask |
| `bid_qty_depth`, `ask_qty_depth` | Глубина (0.5% вокруг mid) |
| `leverage_min`, `leverage_max` | Мин/макс плечо |
| `min_contracts`, `max_contracts` | Мин/макс контрактов |
| `contract_multiplier` | Мультипликатор контракта |
| `maintenance_rate` | Maintenance rate |
| `maker_fee_rate`, `taker_fee_rate` | Комиссии |
| `funding_rate`, `funding_rate_indicative` | Funding rate |
| `order_price_round` | Округление цены |
| `order_price_deviate` | Отклонение цены |
| `slippage_buy_1000_pct` | Проскальзывание на 1000 USDT (buy) |
| `slippage_sell_1000_pct` | Проскальзывание на 1000 USDT (sell) |
---
#### Этап 7: Логирование тика
**Строки:** 633-642
```rust
info!(
"PARSE tick → {} mid={:.8} chg%={:.2} depth(bid/ask)={:.2}/{:.2} slip(1000)={:.4}/{:.4}%",
tick.symbol,
tick.mid,
tick.chg_pct,
tick.bid_qty,
tick.ask_qty,
slip_buy_1000,
slip_sell_1000
);
```
**Пример:**
```
PARSE tick → MEMES_USDT mid=0.00200050 chg%=45.50 depth(bid/ask)=50000.00/48000.00 slip(1000)=0.0500/0.0450%
```
---
### Функция `compute_depth_and_best()`
**Строки:** 657-708
**Сигнатура:**
```rust
fn compute_depth_and_best(ob: &RawOrderBook, mid_hint: f64) -> (f64, f64, f64, f64)
```
**Возвращает:**
- `(best_bid, best_ask, bid_depth, ask_depth)` - лучшие цены и глубина
**Алгоритм:**
---
#### Этап 1: Проверка пустого ордербука
**Строки:** 658-660
```rust
if ob.bids.is_empty() || ob.asks.is_empty() {
return (0.0, 0.0, 0.0, 0.0);
}
```
---
#### Этап 2: Получение лучших bid/ask
**Строки:** 662-675
```rust
let parse_level = |lvl: &[String; 2]| -> (f64, f64) {
let p = lvl[0].trim().parse::().unwrap_or(0.0);
let s = lvl[1].trim().parse::().unwrap_or(0.0);
(p, s)
};
let (best_bid, _) = parse_level(&ob.bids[0]);
let (best_ask, _) = parse_level(&ob.asks[0]);
let mid = if best_bid > 0.0 && best_ask > 0.0 {
(best_bid + best_ask) / 2.0
} else {
mid_hint
};
if mid <= 0.0 {
return (best_bid, best_ask, 0.0, 0.0);
}
```
---
#### Этап 3: Расчёт глубины (0.5% вокруг mid)
**Строки:** 680-705
```rust
let mut bid_depth = 0.0;
let mut ask_depth = 0.0;
let max_dist = 0.005; // 0.5% вокруг mid
for lvl in ob.bids.iter() {
let (p, s) = parse_level(lvl);
if p <= 0.0 || s <= 0.0 {
continue;
}
let dist = (mid - p) / mid;
if dist >= 0.0 && dist <= max_dist {
bid_depth += s;
}
}
for lvl in ob.asks.iter() {
let (p, s) = parse_level(lvl);
if p <= 0.0 || s <= 0.0 {
continue;
}
let dist = (p - mid) / mid;
if dist >= 0.0 && dist <= max_dist {
ask_depth += s;
}
}
```
**Формулы:**
```
dist_bid = (mid - bid_price) / mid
dist_ask = (ask_price - mid) / mid
Добавляем уровень в глубину, если 0.0 <= dist <= 0.005 (0.5%)
```
---
#### Этап 4: Возврат результатов
**Строка:** 707
```rust
(best_bid, best_ask, bid_depth, ask_depth)
```
---
### Функция `compute_slippage_1000()`
**Строки:** 710-776
**Сигнатура:**
```rust
fn compute_slippage_1000(
ob: &RawOrderBook,
mid: f64,
contract_multiplier: f64,
) -> (f64, f64)
```
**Возвращает:**
- `(slip_buy_pct, slip_sell_pct)` - проскальзывание на 1000 USDT (%)
**Алгоритм:**
---
#### Этап 1: Проверка пустого ордербука
**Строки:** 715-717
```rust
if mid <= 0.0 || ob.bids.is_empty() || ob.asks.is_empty() {
return (0.0, 0.0);
}
```
---
#### Этап 2: Расчёт проскальзывания для BUY
**Строки:** 725-749
```rust
let target_quote = 1000.0_f64; // 1000 USDT
// BUY: идём по ask'ам, пока не зальём target_quote
let mut remain = target_quote;
let mut max_price_buy = mid;
for lvl in ob.asks.iter() {
let (p, s) = parse_level(lvl);
if p <= 0.0 || s <= 0.0 {
continue;
}
let level_quote = p * s * contract_multiplier;
if level_quote >= remain {
max_price_buy = p;
remain = 0.0;
break;
} else {
max_price_buy = p;
remain -= level_quote;
}
}
let slip_buy_pct = if remain > 0.0 {
0.0
} else {
((max_price_buy / mid) - 1.0) * 100.0
};
```
**Логика:**
- Идём по ask уровням от лучшего к худшему
- Накапливаем объём пока не наберём 1000 USDT
- Максимальная цена = цена последнего уровня
- `slip_buy_pct = (max_price_buy / mid - 1.0) × 100`
**Пример:**
```
mid = 0.002000 USDT
contract_multiplier = 1.0
Ask levels:
Level 1: price=0.002000, size=500000 → quote=1000 USDT
→ level_quote = 1000 >= remain=1000 ✅
→ max_price_buy = 0.002000, remain = 0.0
slip_buy_pct = (0.002000 / 0.002000 - 1.0) × 100 = 0.0%
```
**Пример с проскальзыванием:**
```
mid = 0.002000 USDT
contract_multiplier = 1.0
Ask levels:
Level 1: price=0.002000, size=200000 → quote=400 USDT
Level 2: price=0.002010, size=300000 → quote=603 USDT
Level 1: level_quote = 400 < remain=1000
→ max_price_buy = 0.002000, remain = 600
Level 2: level_quote = 603 >= remain=600 ✅
→ max_price_buy = 0.002010, remain = 0.0
slip_buy_pct = (0.002010 / 0.002000 - 1.0) × 100 = 0.5%
```
---
#### Этап 3: Расчёт проскальзывания для SELL
**Строки:** 751-773
```rust
// SELL: идём по bid'ам, пока не сольём target_quote
let mut remain = target_quote;
let mut min_price_sell = mid;
for lvl in ob.bids.iter() {
let (p, s) = parse_level(lvl);
if p <= 0.0 || s <= 0.0 {
continue;
}
let level_quote = p * s * contract_multiplier;
if level_quote >= remain {
min_price_sell = p;
remain = 0.0;
break;
} else {
min_price_sell = p;
remain -= level_quote;
}
}
let slip_sell_pct = if remain > 0.0 {
0.0
} else {
((mid / min_price_sell) - 1.0) * 100.0
};
```
**Логика:**
- Идём по bid уровням от лучшего к худшему
- Накапливаем объём пока не сольём 1000 USDT
- Минимальная цена = цена последнего уровня
- `slip_sell_pct = (mid / min_price_sell - 1.0) × 100`
**Пример:**
```
mid = 0.002000 USDT
contract_multiplier = 1.0
Bid levels:
Level 1: price=0.002000, size=500000 → quote=1000 USDT
→ level_quote = 1000 >= remain=1000 ✅
→ min_price_sell = 0.002000, remain = 0.0
slip_sell_pct = (0.002000 / 0.002000 - 1.0) × 100 = 0.0%
```
---
#### Этап 4: Возврат результатов
**Строка:** 775
```rust
(slip_buy_pct, slip_sell_pct)
```
---
## 📊 Redis ключи
| Ключ | Тип | Описание |
|------|-----|----------|
| `hb:ticks:{symbol}` | List | Список тиков (максимум 1000) |
| `hb:last:{symbol}` | String | Последний тик |
| `hb:meta:{symbol}` | String | Мета-данные контракта |
---
## ⚠️ Критические моменты
### 1. Fallback критерии при отсутствии пар
**Строки:** 43-58
```rust
let candidates = if filtered.is_empty() {
warn!(
"⚠️ Не найдено памповых пар в диапазоне {}% - {}%",
min_volatility_pct, max_volatility_pct
);
// fallback: ослабленные критерии
let fallback = filter_pump_pairs(&all, 7.0, 2000.0);
if fallback.is_empty() {
bail!("❌ Нет подходящих пар даже при ослабленных критериях");
}
info!("🔄 Используем расширенные критерии 7-2000%");
fallback
} else {
filtered
};
```
**Что это значит:**
- Если нет пар в диапазоне 10-100% → используем 7-2000%
- Если и fallback пуст → ошибка
---
### 2. Фильтр по цене ≤ 0.01 USDT
**Строки:** 429-432
```rust
// ФИЛЬТР ПО ЦЕНЕ: только пары с ценой ≤ 0.01 USDT
if mid > 0.01 {
return None;
}
```
**Почему это важно:**
- Дешёвые монеты более волатильны
- Пампы чаще происходят на дешёвых монетах
---
### 3. PARSE loop работает в фоне
**Строки:** 67-74
```rust
tokio::spawn(async move {
if let Err(e) =
parse_loop(redis_clone, symbol.clone(), gate_base_owned, parse_interval_ms, tick_ttl_sec)
.await
{
warn!("PARSE loop error for {}: {e:?}", symbol);
}
});
```
**Что это значит:**
- PARSE loop работает параллельно с LIVE
- LIVE читает данные из Redis, которые пишет PARSE
- Если PARSE упал → только warning, LIVE продолжает
---
### 4. Расчёт score
**Строки:** 444-446
```rust
let vol_score = if vol24h > 0.0 { vol24h.ln() } else { 0.0 };
let score = (abs_chg_pct * 5.0) + vol_score - (spread_bps * 0.5);
```
**Формула:**
```
score = (|chg_pct| × 5.0) + ln(vol24h) - (spread_bps × 0.5)
```
**Компоненты:**
- Волатильность (×5.0) - самый важный фактор
- Объём (ln) - логарифмическая шкала
- Спред (×0.5 штраф) - чем меньше спред, тем лучше
---
### 5. Глубина 0.5% вокруг mid
**Строки:** 683-705
```rust
let max_dist = 0.005; // 0.5% вокруг mid
for lvl in ob.bids.iter() {
let dist = (mid - p) / mid;
if dist >= 0.0 && dist <= max_dist {
bid_depth += s;
}
}
```
**Что это значит:**
- Суммируем объём уровней в радиусе 0.5% от mid
- Для bid: `0.0 <= (mid - bid) / mid <= 0.005`
- Для ask: `0.0 <= (ask - mid) / mid <= 0.005`
---
## 📚 Связанные файлы
| Файл | Связь |
|------|-------|
| `src/main.rs` | Вызывает run_monitor() |
| `src/redis.rs` | Redis клиент |
| `src/types.rs` | Структура Tick |
| `src/live/meta.rs` | fetch_trading_meta() (читает hb:meta:) |
---
## 🎯 Резюме
**Что делает monitor.rs:**
1. ✅ SCAN - сканирует все фьючерсы на Gate.io
2. ✅ Фильтрует пары по волатильности (10-100%, fallback 7-2000%)
3. ✅ Фильтрует пары по цене (≤ 0.01 USDT)
4. ✅ Считает score для каждой пары
5. ✅ Выбирает лучшую пару
6. ✅ Запускает PARSE loop в фоне
7. ✅ PARSE loop записывает тики и мета-данные в Redis
**Redis ключи:**
- `hb:ticks:{symbol}` - список тиков (максимум 1000)
- `hb:last:{symbol}` - последний тик
- `hb:meta:{symbol}` - мета-данные контракта
**Формула score:**
```
score = (|chg_pct| × 5.0) + ln(vol24h) - (spread_bps × 0.5)
```
**Фильтры:**
- Волатильность: 10-100% (fallback: 7-2000%)
- Цена: ≤ 0.01 USDT
- Score: чем выше, тем лучше
**PARSE loop:**
- Получает тикер каждые `interval_ms`
- Получает ордербук глубиной 50 уровней
- Вычисляет глубину (0.5% вокруг mid)
- Вычисляет проскальзывание на 1000 USDT
- Записывает в Redis
---
**Дата создания:** 2026-02-22
**Автор:** Claude Code Assistant
**Версия:** 1.0