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