Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -47,6 +47,8 @@ HTF_TREND_CANDLE_LIMIT = 50 # Свечей 4h для анализа (5
|
|
| 47 |
HTF_TREND_SMA_PERIOD = 20 # Период SMA на старшем ТФ (20 x 4ч ≈ 3.3 дня)
|
| 48 |
HTF_TREND_SMA_SLOPE_LOOKBACK = 5 # На сколько свечей назад сравниваем SMA для определения наклона (растет/падает)
|
| 49 |
HTF_TREND_MAX_BELOW_SMA_PCT = 8.0 # Макс. допустимое отклонение цены НИЖЕ SMA (%), если SMA при этом падает
|
|
|
|
|
|
|
| 50 |
|
| 51 |
# ── Подтверждение объемом на входе ──
|
| 52 |
# Разворотные индикаторы (RSI-дно/дивергенция/Боллинджер) сами по себе не отличают "реальный
|
|
@@ -64,6 +66,16 @@ BOOK_IMBALANCE_ENABLED = True
|
|
| 64 |
BOOK_IMBALANCE_DEPTH_PCT = 1.0 # Анализируем стакан в пределах ±1% от текущей цены
|
| 65 |
BOOK_IMBALANCE_MIN_RATIO = 1.2 # bid-ликвидность должна быть минимум в 1.2 раза больше ask-ликвидности
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
# ── Убывающий тейк-профит для зависших позиций ──
|
| 68 |
# use_timeout=False и профит-ротация (срабатывает только при net_profit > 0) означают, что
|
| 69 |
# позиция, ушедшая в минус и не отросшая обратно к изначальной цели, может висеть в слоте
|
|
@@ -284,6 +296,10 @@ class BotState:
|
|
| 284 |
self._network_error_lock = threading.Lock()
|
| 285 |
self.recent_network_errors: list[float] = [] # метки времени недавних сетевых ошибок (для скользящего окна)
|
| 286 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
def budget_per_slot(self) -> float:
|
| 288 |
if self.num_slots <= 0:
|
| 289 |
return 0.0
|
|
@@ -676,14 +692,16 @@ def estimate_buy_slippage(exchange: ccxt.binance, symbol: str, budget_usdt: floa
|
|
| 676 |
return avg_price, slippage_pct
|
| 677 |
|
| 678 |
|
| 679 |
-
def check_book_liquidity_imbalance(exchange: ccxt.binance, symbol: str, current_price: float, state: BotState) -> bool:
|
| 680 |
"""
|
| 681 |
Проверяет дисбаланс ликвидности в стакане в пределах BOOK_IMBALANCE_DEPTH_PCT% от текущей
|
| 682 |
цены: суммарная ликвидность на покупку (bids) должна ощутимо превышать ликвидность на
|
| 683 |
продажу (asks) - это признак реального спроса ("поддержки") прямо сейчас, а не просто
|
| 684 |
технического сигнала по свечам. Используется как ДОПОЛНИТЕЛЬНОЕ подтверждение точки входа
|
| 685 |
-
поверх RSI/дивергенции/Боллинджера, а не вместо них.
|
| 686 |
-
|
|
|
|
|
|
|
| 687 |
"""
|
| 688 |
try:
|
| 689 |
order_book = safe_call(exchange.fetch_order_book, symbol, 50, state=state, label=f"book-imbalance {symbol}")
|
|
@@ -708,7 +726,7 @@ def check_book_liquidity_imbalance(exchange: ccxt.binance, symbol: str, current_
|
|
| 708 |
return True
|
| 709 |
|
| 710 |
ratio = bid_liquidity / ask_liquidity
|
| 711 |
-
return ratio >=
|
| 712 |
|
| 713 |
|
| 714 |
def compute_target_slots(current_balance: float) -> int:
|
|
@@ -941,7 +959,7 @@ def get_allowed_symbols(exchange: ccxt.binance, max_notional: float = MIN_NOTION
|
|
| 941 |
return allowed
|
| 942 |
|
| 943 |
|
| 944 |
-
def
|
| 945 |
"""
|
| 946 |
Фильтр старшего тренда (4h) перед входом - защита от "падающих ножей".
|
| 947 |
Разворотные сигналы на 1m/15m ловят только краткосрочный микро-отскок и ничего не знают
|
|
@@ -985,6 +1003,26 @@ def check_htf_trend_ok(exchange: ccxt.binance, symbol: str, state: BotState) ->
|
|
| 985 |
return True
|
| 986 |
|
| 987 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 988 |
def screen_full_market(
|
| 989 |
exchange: ccxt.binance,
|
| 990 |
allowed_symbols: list[str],
|
|
@@ -1194,18 +1232,27 @@ def screen_full_market(
|
|
| 1194 |
if not is_entry_triggered:
|
| 1195 |
continue
|
| 1196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1197 |
# ── Подтверждение объемом ──
|
| 1198 |
# Разворотные индикаторы (RSI-дно, дивергенция, Боллинджер) сами по себе НЕ отличают
|
| 1199 |
# "настоящий отскок с реальным спросом" от "монета просто затихла и продолжит валиться
|
| 1200 |
# дальше на тонком объеме". Требуем, чтобы объем на последней 1m свече был заметно выше
|
| 1201 |
# среднего за последние VOLUME_CONFIRMATION_LOOKBACK свечей - это признак того, что в
|
| 1202 |
# разворот заходит реальный объем покупателей, а не штиль перед новым проливом.
|
|
|
|
| 1203 |
if VOLUME_CONFIRMATION_ENABLED:
|
| 1204 |
volumes_1m = [k[5] for k in ohlcv_1m]
|
| 1205 |
vol_window = volumes_1m[-(VOLUME_CONFIRMATION_LOOKBACK + 1):-1]
|
| 1206 |
avg_vol = sum(vol_window) / len(vol_window) if vol_window else 0
|
| 1207 |
last_vol = volumes_1m[-1]
|
| 1208 |
-
if avg_vol <= 0 or last_vol < avg_vol *
|
| 1209 |
continue
|
| 1210 |
|
| 1211 |
# Вычисляем адаптивную просадку на основе суточной волатильности
|
|
@@ -1222,9 +1269,9 @@ def screen_full_market(
|
|
| 1222 |
# продажу) - монете, скорее всего, еще есть куда падать даже после "дна" по индикаторам.
|
| 1223 |
# Проверяем это ПОСЛЕДНИМ (после всех остальных, более дешевых фильтров), т.к. это
|
| 1224 |
# отдельный сетевой запрос стакана на каждого кандидата, а до этой строки доходят уже
|
| 1225 |
-
# немногие.
|
| 1226 |
if BOOK_IMBALANCE_ENABLED:
|
| 1227 |
-
if not check_book_liquidity_imbalance(exchange, sym, c["ask"], state):
|
| 1228 |
continue
|
| 1229 |
|
| 1230 |
# Считаем итоговый скоринг (чем ниже rsi_1h и rsi_val_1m, тем выше балл)
|
|
|
|
| 47 |
HTF_TREND_SMA_PERIOD = 20 # Период SMA на старшем ТФ (20 x 4ч ≈ 3.3 дня)
|
| 48 |
HTF_TREND_SMA_SLOPE_LOOKBACK = 5 # На сколько свечей назад сравниваем SMA для определения наклона (растет/падает)
|
| 49 |
HTF_TREND_MAX_BELOW_SMA_PCT = 8.0 # Макс. допустимое отклонение цены НИЖЕ SMA (%), если SMA при этом падает
|
| 50 |
+
HTF_TREND_CACHE_TTL_SEC = 1800 # Кэш результата на 30 минут - 4h свеча меняется медленно, незачем
|
| 51 |
+
# дергать 4h-эндпоинт на КАЖДОМ скрине (раз в ~90-270с) для той же монеты
|
| 52 |
|
| 53 |
# ── Подтверждение объемом на входе ──
|
| 54 |
# Разворотные индикаторы (RSI-дно/дивергенция/Боллинджер) сами по себе не отличают "реальный
|
|
|
|
| 66 |
BOOK_IMBALANCE_DEPTH_PCT = 1.0 # Анализируем стакан в пределах ±1% от текущей цены
|
| 67 |
BOOK_IMBALANCE_MIN_RATIO = 1.2 # bid-ликвидность должна быть минимум в 1.2 раза больше ask-ликвидности
|
| 68 |
|
| 69 |
+
# ── "Мем-тир" - ужесточение для аномально волатильных монет ──
|
| 70 |
+
# Кейс PUMP: RSI-дно + всплеск объема сработали на резкий, но ЛОЖНЫЙ импульс посреди бокового
|
| 71 |
+
# шума - типично для низкокапных/мемных монет, где объемные всплески чаще манипулятивные, чем
|
| 72 |
+
# признак реального разворота. Вместо хрупкого списка тикеров определяем "мем-тир" ПО ПОВЕДЕНИЮ
|
| 73 |
+
# (аномальная суточная волатильность) и для таких монет требуем заметно более сильное
|
| 74 |
+
# подтверждение объемом и стаканом, чем для обычных кандидатов.
|
| 75 |
+
HIGH_VOLATILITY_MEME_THRESHOLD = 0.15 # 24h волатильность (high-low)/low, выше которой считаем монету мем-тиром
|
| 76 |
+
MEME_TIER_VOLUME_MULTIPLIER = 2.0 # Для мем-тира: объем свечи должен быть минимум в 2x выше среднего (вместо 1.3x)
|
| 77 |
+
MEME_TIER_BOOK_MIN_RATIO = 1.8 # Для мем-тира: bid-ликвидность должна перевешивать ask сильнее (вместо 1.2x)
|
| 78 |
+
|
| 79 |
# ── Убывающий тейк-профит для зависших позиций ──
|
| 80 |
# use_timeout=False и профит-ротация (срабатывает только при net_profit > 0) означают, что
|
| 81 |
# позиция, ушедшая в минус и не отросшая обратно к изначальной цели, может висеть в слоте
|
|
|
|
| 296 |
self._network_error_lock = threading.Lock()
|
| 297 |
self.recent_network_errors: list[float] = [] # метки времени недавних сетевых ошибок (для скользящего окна)
|
| 298 |
|
| 299 |
+
# ── Кэш фильтра старшего тренда (4h) - снижает нагрузку на API ──
|
| 300 |
+
self._htf_cache_lock = threading.Lock()
|
| 301 |
+
self.htf_trend_cache: dict[str, tuple[float, bool]] = {} # symbol -> (когда проверяли, прошел ли фильтр)
|
| 302 |
+
|
| 303 |
def budget_per_slot(self) -> float:
|
| 304 |
if self.num_slots <= 0:
|
| 305 |
return 0.0
|
|
|
|
| 692 |
return avg_price, slippage_pct
|
| 693 |
|
| 694 |
|
| 695 |
+
def check_book_liquidity_imbalance(exchange: ccxt.binance, symbol: str, current_price: float, state: BotState, min_ratio: float = BOOK_IMBALANCE_MIN_RATIO) -> bool:
|
| 696 |
"""
|
| 697 |
Проверяет дисбаланс ликвидности в стакане в пределах BOOK_IMBALANCE_DEPTH_PCT% от текущей
|
| 698 |
цены: суммарная ликвидность на покупку (bids) должна ощутимо превышать ликвидность на
|
| 699 |
продажу (asks) - это признак реального спроса ("поддержки") прямо сейчас, а не просто
|
| 700 |
технического сигнала по свечам. Используется как ДОПОЛНИТЕЛЬНОЕ подтверждение точки входа
|
| 701 |
+
поверх RSI/дивергенции/Боллинджера, а не вместо них. min_ratio настраиваемый - для
|
| 702 |
+
аномально волатильных ("мем-тир") монет вызывающая сторона передает более строгий порог.
|
| 703 |
+
При ошибке сети/пустом стакане - считаем подтверждение не пройденным (безопаснее пропустить
|
| 704 |
+
сомнительный вход).
|
| 705 |
"""
|
| 706 |
try:
|
| 707 |
order_book = safe_call(exchange.fetch_order_book, symbol, 50, state=state, label=f"book-imbalance {symbol}")
|
|
|
|
| 726 |
return True
|
| 727 |
|
| 728 |
ratio = bid_liquidity / ask_liquidity
|
| 729 |
+
return ratio >= min_ratio
|
| 730 |
|
| 731 |
|
| 732 |
def compute_target_slots(current_balance: float) -> int:
|
|
|
|
| 959 |
return allowed
|
| 960 |
|
| 961 |
|
| 962 |
+
def _compute_htf_trend_ok(exchange: ccxt.binance, symbol: str, state: BotState) -> bool:
|
| 963 |
"""
|
| 964 |
Фильтр старшего тренда (4h) перед входом - защита от "падающих ножей".
|
| 965 |
Разворотные сигналы на 1m/15m ловят только краткосрочный микро-отскок и ничего не знают
|
|
|
|
| 1003 |
return True
|
| 1004 |
|
| 1005 |
|
| 1006 |
+
def check_htf_trend_ok(exchange: ccxt.binance, symbol: str, state: BotState) -> bool:
|
| 1007 |
+
"""
|
| 1008 |
+
Кэширующая обертка над _compute_htf_trend_ok. 4h свеча физически не может измениться за
|
| 1009 |
+
90-270 секунд между скринами, поэтому дергать 4h-эндпоинт на КАЖДОМ цикле для одной и той же
|
| 1010 |
+
монеты - чистый перерасход лимитов API (именно это внесло вклад в 429 Too Many Requests).
|
| 1011 |
+
Результат кэшируется на HTF_TREND_CACHE_TTL_SEC (30 минут) на символ.
|
| 1012 |
+
"""
|
| 1013 |
+
now = time.time()
|
| 1014 |
+
with state._htf_cache_lock:
|
| 1015 |
+
cached = state.htf_trend_cache.get(symbol)
|
| 1016 |
+
if cached is not None and (now - cached[0]) < HTF_TREND_CACHE_TTL_SEC:
|
| 1017 |
+
return cached[1]
|
| 1018 |
+
|
| 1019 |
+
result = _compute_htf_trend_ok(exchange, symbol, state)
|
| 1020 |
+
|
| 1021 |
+
with state._htf_cache_lock:
|
| 1022 |
+
state.htf_trend_cache[symbol] = (now, result)
|
| 1023 |
+
return result
|
| 1024 |
+
|
| 1025 |
+
|
| 1026 |
def screen_full_market(
|
| 1027 |
exchange: ccxt.binance,
|
| 1028 |
allowed_symbols: list[str],
|
|
|
|
| 1232 |
if not is_entry_triggered:
|
| 1233 |
continue
|
| 1234 |
|
| 1235 |
+
# ── "Мем-тир": аномально волатильные монеты требуют более строгого подтверждения ──
|
| 1236 |
+
# Определяем ПО ПОВЕДЕНИЮ (суточная волатильность), а не по хрупкому списку тикеров -
|
| 1237 |
+
# кейс PUMP показал, что на таких монетах RSI-дно + обычный всплеск объема могут
|
| 1238 |
+
# сработать на ложный, манипулятивный импульс, а не на реальный разворот.
|
| 1239 |
+
is_meme_tier = c["volatility_24h"] >= HIGH_VOLATILITY_MEME_THRESHOLD
|
| 1240 |
+
vol_multiplier = MEME_TIER_VOLUME_MULTIPLIER if is_meme_tier else VOLUME_CONFIRMATION_MULTIPLIER
|
| 1241 |
+
book_min_ratio = MEME_TIER_BOOK_MIN_RATIO if is_meme_tier else BOOK_IMBALANCE_MIN_RATIO
|
| 1242 |
+
|
| 1243 |
# ── Подтверждение объемом ──
|
| 1244 |
# Разворотные индикаторы (RSI-дно, дивергенция, Боллинджер) сами по себе НЕ отличают
|
| 1245 |
# "настоящий отскок с реальным спросом" от "монета просто затихла и продолжит валиться
|
| 1246 |
# дальше на тонком объеме". Требуем, чтобы объем на последней 1m свече был заметно выше
|
| 1247 |
# среднего за последние VOLUME_CONFIRMATION_LOOKBACK свечей - это признак того, что в
|
| 1248 |
# разворот заходит реальный объем покупателей, а не штиль перед новым проливом.
|
| 1249 |
+
# Для мем-тира порог заметно строже (vol_multiplier).
|
| 1250 |
if VOLUME_CONFIRMATION_ENABLED:
|
| 1251 |
volumes_1m = [k[5] for k in ohlcv_1m]
|
| 1252 |
vol_window = volumes_1m[-(VOLUME_CONFIRMATION_LOOKBACK + 1):-1]
|
| 1253 |
avg_vol = sum(vol_window) / len(vol_window) if vol_window else 0
|
| 1254 |
last_vol = volumes_1m[-1]
|
| 1255 |
+
if avg_vol <= 0 or last_vol < avg_vol * vol_multiplier:
|
| 1256 |
continue
|
| 1257 |
|
| 1258 |
# Вычисляем адаптивную просадку на основе суточной волатильности
|
|
|
|
| 1269 |
# продажу) - монете, скорее всего, еще есть куда падать даже после "дна" по индикаторам.
|
| 1270 |
# Проверяем это ПОСЛЕДНИМ (после всех остальных, более дешевых фильтров), т.к. это
|
| 1271 |
# отдельный сетевой запрос стакана на каждого кандидата, а до этой строки доходят уже
|
| 1272 |
+
# немногие. Для мем-тира требуем более выраженный перевес (book_min_ratio).
|
| 1273 |
if BOOK_IMBALANCE_ENABLED:
|
| 1274 |
+
if not check_book_liquidity_imbalance(exchange, sym, c["ask"], state, min_ratio=book_min_ratio):
|
| 1275 |
continue
|
| 1276 |
|
| 1277 |
# Считаем итоговый скоринг (чем ниже rsi_1h и rsi_val_1m, тем выше балл)
|