Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -13,7 +13,7 @@ import pandas as pd
|
|
| 13 |
# ─────────────────────────────────────────────
|
| 14 |
# КОНСТАНТЫ СТРАТЕГИИ
|
| 15 |
# ─────────────────────────────────────────────
|
| 16 |
-
SCREENER_INTERVAL_SEC =
|
| 17 |
SLOT_REFILL_DEBOUNCE_SEC = 8 # Пауза перед заполнением освободившегося слота
|
| 18 |
MIN_NOTIONAL_FILTER = 1.0 # Фильтр минимального объема ордера (USDT)
|
| 19 |
MIN_QUOTE_VOLUME = 25_000 # Минимальный суточный объем торгов (USDT) (ослабили фильтр для расширения списка монет)
|
|
@@ -24,8 +24,8 @@ MIN_DRAWDOWN = 0.01 # Минимальная просадка
|
|
| 24 |
ORDER_TIMEOUT_MIN = 30 # Время удержания позиции до авто-выхода (минут)
|
| 25 |
FEE_RATE_STANDARD = 0.001 # Стандартная комиссия (0.1%)
|
| 26 |
FEE_RATE_BNB_DISCOUNT = 0.00075 # Сниженная комиссия со скидкой BNB (0.075%)
|
| 27 |
-
DEFAULT_SLOTS =
|
| 28 |
-
MAX_SLOTS =
|
| 29 |
CANDLE_FETCH_BATCH_SIZE = 25 # Увеличили батч скрининга до 25 для анализа большего количества монет
|
| 30 |
|
| 31 |
|
|
@@ -86,6 +86,8 @@ class BotState:
|
|
| 86 |
self.allowed_symbols_count = 0 # Кол-во доступных USDT пар
|
| 87 |
self.order_timeout_min = 360 # Время удержания до авто-выхода в минутах (6 часов по умолчанию)
|
| 88 |
self.use_timeout = False # Принудительный выход по тайм-ауту по умолчанию выключен по вашей просьбе
|
|
|
|
|
|
|
| 89 |
self.strategy_mode = "⚖️ Умеренный (Сбалансированный)" # Режим скальпинга
|
| 90 |
self.use_dynamic_tp = True # Использовать адаптивный Тейк-Профит на основе волатильности
|
| 91 |
|
|
@@ -499,8 +501,13 @@ def place_limit_sell(exchange: ccxt.binance, slot: Slot, state: BotState) -> boo
|
|
| 499 |
try:
|
| 500 |
symbol = slot.symbol
|
| 501 |
fee = state.fee_rate()
|
| 502 |
-
|
| 503 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
sell_px = float(exchange.price_to_precision(symbol, sell_raw))
|
| 505 |
qty = float(exchange.amount_to_precision(symbol, slot.bought_qty))
|
| 506 |
|
|
@@ -535,10 +542,9 @@ def place_limit_sell(exchange: ccxt.binance, slot: Slot, state: BotState) -> boo
|
|
| 535 |
state.log(f"[Слот {slot.index}] LimitSell ОШИБКА: {e}", "ERR")
|
| 536 |
return False
|
| 537 |
|
| 538 |
-
def panic_sell_slot(exchange: ccxt.binance, slot: Slot, state: BotState,
|
| 539 |
-
prefix = "🚨 PANIC" if manual else "⏰ TIMEOUT"
|
| 540 |
sym = slot.symbol
|
| 541 |
-
state.log(f"{prefix} [Слот {slot.index}]:
|
| 542 |
|
| 543 |
if state.paper_trading:
|
| 544 |
try:
|
|
@@ -546,13 +552,25 @@ def panic_sell_slot(exchange: ccxt.binance, slot: Slot, state: BotState, manual:
|
|
| 546 |
current_bid = ticker.get("bid") or ticker.get("last") or slot.buy_price_avg
|
| 547 |
received = slot.bought_qty * current_bid
|
| 548 |
loss = received - slot.usdt_spent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
state.log(
|
| 550 |
f"📝 {prefix} [Слот {slot.index}]: виртуальная продажа {slot.bought_qty:.6g} "
|
| 551 |
f"{sym.split('/')[0]} по РЕАЛЬНОЙ цене {current_bid:.6g}, получено approx {received:.4f} USDT. "
|
| 552 |
f"изменение approx {loss:.4f} USDT", "WARN"
|
| 553 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
state.record_paper_trade({
|
| 555 |
-
"symbol": sym, "action":
|
| 556 |
"price": current_bid, "qty": slot.bought_qty,
|
| 557 |
"usdt_value": received, "profit": loss,
|
| 558 |
})
|
|
@@ -566,7 +584,7 @@ def panic_sell_slot(exchange: ccxt.binance, slot: Slot, state: BotState, manual:
|
|
| 566 |
exchange.cancel_order(slot.order_id, sym)
|
| 567 |
state.log(f"{prefix} [Слот {slot.index}]: лимит ID={slot.order_id} отменен", "OK")
|
| 568 |
except Exception as e:
|
| 569 |
-
state.log(f"{prefix} [Слот {slot.index}]: ошибка отмены ордера: {e}", "WARN")
|
| 570 |
|
| 571 |
time.sleep(1)
|
| 572 |
|
|
@@ -584,6 +602,11 @@ def panic_sell_slot(exchange: ccxt.binance, slot: Slot, state: BotState, manual:
|
|
| 584 |
order = exchange.create_market_sell_order(sym, qty_sell)
|
| 585 |
received = float(order.get("cost") or 0)
|
| 586 |
loss = received - slot.usdt_spent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
state.log(
|
| 588 |
f"{prefix} [Слот {slot.index}]: продано {qty_sell} {base}, получено approx {received:.4f} USDT. "
|
| 589 |
f"изменение approx {loss:.4f} USDT", "WARN"
|
|
@@ -595,28 +618,42 @@ def panic_sell_slot(exchange: ccxt.binance, slot: Slot, state: BotState, manual:
|
|
| 595 |
slot.clear()
|
| 596 |
|
| 597 |
|
| 598 |
-
def
|
|
|
|
| 599 |
now = time.time()
|
| 600 |
-
|
|
|
|
|
|
|
|
|
|
| 601 |
return
|
| 602 |
-
|
| 603 |
-
slot
|
|
|
|
|
|
|
| 604 |
|
| 605 |
occupied = state.occupied_symbols()
|
| 606 |
-
|
|
|
|
|
|
|
|
|
|
| 607 |
|
| 608 |
if not candidates:
|
| 609 |
-
|
|
|
|
|
|
|
| 610 |
return
|
| 611 |
|
| 612 |
-
|
| 613 |
-
slot
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
|
|
|
|
|
|
|
|
|
| 620 |
|
| 621 |
|
| 622 |
# ─────────────────────────────────────────────
|
|
@@ -672,10 +709,39 @@ def trading_loop(state: BotState):
|
|
| 672 |
|
| 673 |
if slot.order_placed_at:
|
| 674 |
elapsed = (datetime.now(timezone.utc) - slot.order_placed_at).total_seconds()
|
|
|
|
|
|
|
| 675 |
if state.use_timeout and elapsed > state.order_timeout_min * 60:
|
| 676 |
-
state.log(f"[Слот {slot.index}]
|
| 677 |
-
panic_sell_slot(exchange, slot, state)
|
| 678 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
|
| 680 |
try:
|
| 681 |
if state.paper_trading:
|
|
@@ -735,6 +801,7 @@ def trading_loop(state: BotState):
|
|
| 735 |
except Exception as e:
|
| 736 |
state.log(f"[Слот {slot.index}] Ошибка проверки ордера: {e}", "WARN")
|
| 737 |
|
|
|
|
| 738 |
empty_slots = [s for s in state.slots if s.is_empty()]
|
| 739 |
|
| 740 |
if empty_slots:
|
|
@@ -744,10 +811,7 @@ def trading_loop(state: BotState):
|
|
| 744 |
)
|
| 745 |
if should_screen:
|
| 746 |
state.last_full_screen_at = now
|
| 747 |
-
|
| 748 |
-
if not state.is_running:
|
| 749 |
-
break
|
| 750 |
-
refill_slot(exchange, allowed_symbols, slot, state)
|
| 751 |
|
| 752 |
occupied_n = sum(1 for s in state.slots if not s.is_empty())
|
| 753 |
if occupied_n == state.num_slots:
|
|
@@ -904,16 +968,28 @@ def render_ui():
|
|
| 904 |
help="Снижает комиссию с 0.1% до 0.075%. ВАЖНО: должно совпадать с реальной настройкой в Binance, иначе цена продажи будет рассчитана неточно и часть прибыли уходит в погрешность."
|
| 905 |
)
|
| 906 |
use_timeout = st.checkbox(
|
| 907 |
-
"Включить принудительный выход по тайм-ауту",
|
| 908 |
value=state.use_timeout,
|
| 909 |
-
help="Если выключено, бот будет держать купленную монету
|
| 910 |
)
|
| 911 |
order_timeout_min = st.number_input(
|
| 912 |
-
"Тайм-аут удержания позиции (минут)",
|
| 913 |
min_value=10, max_value=14400, value=state.order_timeout_min, step=10,
|
| 914 |
disabled=not use_timeout,
|
| 915 |
-
help="Применяется только при включенном флаге выше."
|
| 916 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
active_fee = FEE_RATE_BNB_DISCOUNT if use_bnb else FEE_RATE_STANDARD
|
| 918 |
|
| 919 |
per_slot_preview = total_budget / num_slots if num_slots else 0
|
|
@@ -941,6 +1017,8 @@ def render_ui():
|
|
| 941 |
state.use_bnb_discount = use_bnb
|
| 942 |
state.use_timeout = use_timeout
|
| 943 |
state.order_timeout_min = order_timeout_min
|
|
|
|
|
|
|
| 944 |
state.strategy_mode = strategy_mode
|
| 945 |
state.use_dynamic_tp = use_dynamic_tp
|
| 946 |
state.resize_slots(num_slots)
|
|
@@ -962,8 +1040,9 @@ def render_ui():
|
|
| 962 |
st.divider()
|
| 963 |
st.markdown("**📐 Формула цены продажи (на слот)**")
|
| 964 |
st.code(
|
| 965 |
-
"sell_price = (usdt_spent + target_profit)\n"
|
| 966 |
-
"
|
|
|
|
| 967 |
f"budget_per_slot = total_budget / num_slots\n"
|
| 968 |
f"fee_rate = {active_fee} ({fee_label})",
|
| 969 |
language="python"
|
|
@@ -1002,13 +1081,19 @@ def render_ui():
|
|
| 1002 |
elapsed_min = 0
|
| 1003 |
if slot.order_placed_at:
|
| 1004 |
elapsed_min = int((datetime.now(timezone.utc) - slot.order_placed_at).total_seconds() / 60)
|
| 1005 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1006 |
st.markdown(f"""
|
| 1007 |
<div class="{css_class}">
|
| 1008 |
<b>Слот {slot.index}: {slot.symbol}</b><br>
|
| 1009 |
Куплено: {slot.bought_qty:.4f} по {slot.buy_price_avg:.6g} ({slot.usdt_spent:.4f} USDT)<br>
|
| 1010 |
Цель продажи: <b>{slot.sell_price_set:.6g}</b> (цель profit: {slot.target_profit_set:.4f} USDT)<br>
|
| 1011 |
-
Статус: {slot.status} | {elapsed_min}{timeout_str}
|
| 1012 |
</div>
|
| 1013 |
""", unsafe_allow_html=True)
|
| 1014 |
|
|
@@ -1019,7 +1104,7 @@ def render_ui():
|
|
| 1019 |
exchange = build_exchange("", "", paper_trading=True)
|
| 1020 |
else:
|
| 1021 |
exchange = build_exchange(state.api_key, state.api_secret, paper_trading=False)
|
| 1022 |
-
panic_sell_slot(exchange, slot, state,
|
| 1023 |
st.warning(f"Слот {slot.index} closed принудительно!")
|
| 1024 |
st.rerun()
|
| 1025 |
except Exception as e:
|
|
|
|
| 13 |
# ─────────────────────────────────────────────
|
| 14 |
# КОНСТАНТЫ СТРАТЕГИИ
|
| 15 |
# ─────────────────────────────────────────────
|
| 16 |
+
SCREENER_INTERVAL_SEC = 90 # Интервал полного сканирования рынка (90 секунд для более частых входов)
|
| 17 |
SLOT_REFILL_DEBOUNCE_SEC = 8 # Пауза перед заполнением освободившегося слота
|
| 18 |
MIN_NOTIONAL_FILTER = 1.0 # Фильтр минимального объема ордера (USDT)
|
| 19 |
MIN_QUOTE_VOLUME = 25_000 # Минимальный суточный объем торгов (USDT) (ослабили фильтр для расширения списка монет)
|
|
|
|
| 24 |
ORDER_TIMEOUT_MIN = 30 # Время удержания позиции до авто-выхода (минут)
|
| 25 |
FEE_RATE_STANDARD = 0.001 # Стандартная комиссия (0.1%)
|
| 26 |
FEE_RATE_BNB_DISCOUNT = 0.00075 # Сниженная комиссия со скидкой BNB (0.075%)
|
| 27 |
+
DEFAULT_SLOTS = 5 # Количество параллельных слотов по умолчанию
|
| 28 |
+
MAX_SLOTS = 12 # Защита от избыточного дробления бюджета (увеличено для поддержки 5+ слотов)
|
| 29 |
CANDLE_FETCH_BATCH_SIZE = 25 # Увеличили батч скрининга до 25 для анализа большего количества монет
|
| 30 |
|
| 31 |
|
|
|
|
| 86 |
self.allowed_symbols_count = 0 # Кол-во доступных USDT пар
|
| 87 |
self.order_timeout_min = 360 # Время удержания до авто-выхода в минутах (6 часов по умолчанию)
|
| 88 |
self.use_timeout = False # Принудительный выход по тайм-ауту по умолчанию выключен по вашей просьбе
|
| 89 |
+
self.use_profit_exit = True # Включить ротацию по плюсу после определенного времени удержания
|
| 90 |
+
self.profit_exit_timeout_min = 120 # Тайм-аут удержания до проверки профита в минутах (2 часа)
|
| 91 |
self.strategy_mode = "⚖️ Умеренный (Сбалансированный)" # Режим скальпинга
|
| 92 |
self.use_dynamic_tp = True # Использовать адаптивный Тейк-Профит на основе волатильности
|
| 93 |
|
|
|
|
| 501 |
try:
|
| 502 |
symbol = slot.symbol
|
| 503 |
fee = state.fee_rate()
|
| 504 |
+
if state.use_bnb_discount:
|
| 505 |
+
# С учетом скидки BNB, монеты при покупке НЕ урезались (qty полный)
|
| 506 |
+
sell_raw = (slot.usdt_spent + slot.target_profit_set) / slot.bought_qty
|
| 507 |
+
else:
|
| 508 |
+
# Без скидки (стандартный расчет)
|
| 509 |
+
usdt_out = slot.usdt_spent + slot.target_profit_set
|
| 510 |
+
sell_raw = usdt_out / (slot.bought_qty * (1 - fee))
|
| 511 |
sell_px = float(exchange.price_to_precision(symbol, sell_raw))
|
| 512 |
qty = float(exchange.amount_to_precision(symbol, slot.bought_qty))
|
| 513 |
|
|
|
|
| 542 |
state.log(f"[Слот {slot.index}] LimitSell ОШИБКА: {e}", "ERR")
|
| 543 |
return False
|
| 544 |
|
| 545 |
+
def panic_sell_slot(exchange: ccxt.binance, slot: Slot, state: BotState, prefix: str = "🚨 PANIC"):
|
|
|
|
| 546 |
sym = slot.symbol
|
| 547 |
+
state.log(f"{prefix} [Слот {slot.index}]: принудительный/таймаут выход из {sym}", "WARN")
|
| 548 |
|
| 549 |
if state.paper_trading:
|
| 550 |
try:
|
|
|
|
| 552 |
current_bid = ticker.get("bid") or ticker.get("last") or slot.buy_price_avg
|
| 553 |
received = slot.bought_qty * current_bid
|
| 554 |
loss = received - slot.usdt_spent
|
| 555 |
+
|
| 556 |
+
# Обновляем общую статистику сессии (прибыль/убыток) при любом выходе
|
| 557 |
+
state.total_profit += loss
|
| 558 |
+
state.trades_count += 1
|
| 559 |
+
|
| 560 |
state.log(
|
| 561 |
f"📝 {prefix} [Слот {slot.index}]: виртуальная продажа {slot.bought_qty:.6g} "
|
| 562 |
f"{sym.split('/')[0]} по РЕАЛЬНОЙ цене {current_bid:.6g}, получено approx {received:.4f} USDT. "
|
| 563 |
f"изменение approx {loss:.4f} USDT", "WARN"
|
| 564 |
)
|
| 565 |
+
|
| 566 |
+
action_type = "panic"
|
| 567 |
+
if "TIMEOUT" in prefix:
|
| 568 |
+
action_type = "timeout"
|
| 569 |
+
elif "ROTATE" in prefix:
|
| 570 |
+
action_type = "rotation"
|
| 571 |
+
|
| 572 |
state.record_paper_trade({
|
| 573 |
+
"symbol": sym, "action": action_type, "slot": slot.index,
|
| 574 |
"price": current_bid, "qty": slot.bought_qty,
|
| 575 |
"usdt_value": received, "profit": loss,
|
| 576 |
})
|
|
|
|
| 584 |
exchange.cancel_order(slot.order_id, sym)
|
| 585 |
state.log(f"{prefix} [Слот {slot.index}]: лимит ID={slot.order_id} отменен", "OK")
|
| 586 |
except Exception as e:
|
| 587 |
+
state.log(f"{prefix} [Слот {slot.index}]: ошибка отмены лимитного ордера: {e}", "WARN")
|
| 588 |
|
| 589 |
time.sleep(1)
|
| 590 |
|
|
|
|
| 602 |
order = exchange.create_market_sell_order(sym, qty_sell)
|
| 603 |
received = float(order.get("cost") or 0)
|
| 604 |
loss = received - slot.usdt_spent
|
| 605 |
+
|
| 606 |
+
# Обновляем общую статистику сессии
|
| 607 |
+
state.total_profit += loss
|
| 608 |
+
state.trades_count += 1
|
| 609 |
+
|
| 610 |
state.log(
|
| 611 |
f"{prefix} [Слот {slot.index}]: продано {qty_sell} {base}, получено approx {received:.4f} USDT. "
|
| 612 |
f"изменение approx {loss:.4f} USDT", "WARN"
|
|
|
|
| 618 |
slot.clear()
|
| 619 |
|
| 620 |
|
| 621 |
+
def refill_empty_slots(exchange: ccxt.binance, allowed_symbols: list[str], empty_slots: list[Slot], state: BotState):
|
| 622 |
+
"""Выполняет один общий пакетный скриншот рынка для всех пустых слотов сразу."""
|
| 623 |
now = time.time()
|
| 624 |
+
|
| 625 |
+
# Фильтруем слоты, готовые к наполнению
|
| 626 |
+
slots_to_refill = [s for s in empty_slots if now - s.last_refill_attempt >= SLOT_REFILL_DEBOUNCE_SEC]
|
| 627 |
+
if not slots_to_refill:
|
| 628 |
return
|
| 629 |
+
|
| 630 |
+
for slot in slots_to_refill:
|
| 631 |
+
slot.last_refill_attempt = now
|
| 632 |
+
slot.status = "🔍 Поиск замены"
|
| 633 |
|
| 634 |
occupied = state.occupied_symbols()
|
| 635 |
+
need_n = len(slots_to_refill)
|
| 636 |
+
|
| 637 |
+
# Делаем один общий скрин рынка и забираем до need_n лучших уникальных кандидатов
|
| 638 |
+
candidates = screen_full_market(exchange, allowed_symbols, occupied, state, need_n=need_n)
|
| 639 |
|
| 640 |
if not candidates:
|
| 641 |
+
for slot in slots_to_refill:
|
| 642 |
+
slot.status = "🔍 Поиск"
|
| 643 |
+
state.log(f"Нет подходящих кандидатов в этом цикле для {need_n} пустых слотов", "INFO")
|
| 644 |
return
|
| 645 |
|
| 646 |
+
# Заполняем слоты найденными кандидатами
|
| 647 |
+
for slot, best in zip(slots_to_refill, candidates):
|
| 648 |
+
slot.status = f"🟡 Покупка {best['symbol']}"
|
| 649 |
+
ok_buy = execute_buy(exchange, best["symbol"], state.budget_per_slot(), slot, state)
|
| 650 |
+
if ok_buy:
|
| 651 |
+
ok_sell = place_limit_sell(exchange, slot, state)
|
| 652 |
+
if not ok_sell:
|
| 653 |
+
state.log(f"[Слот {slot.index}] LimitSell не удался -> экстренная продажа слота", "ERR")
|
| 654 |
+
panic_sell_slot(exchange, slot, state, prefix="🚨 PANIC")
|
| 655 |
+
else:
|
| 656 |
+
slot.clear()
|
| 657 |
|
| 658 |
|
| 659 |
# ─────────────────────────────────────────────
|
|
|
|
| 709 |
|
| 710 |
if slot.order_placed_at:
|
| 711 |
elapsed = (datetime.now(timezone.utc) - slot.order_placed_at).total_seconds()
|
| 712 |
+
|
| 713 |
+
# 1. Жесткий тайм-аут выхода по рынку (если включен)
|
| 714 |
if state.use_timeout and elapsed > state.order_timeout_min * 60:
|
| 715 |
+
state.log(f"[Слот {slot.index}] Жесткий тайм-аут {state.order_timeout_min} мин — авто-выход", "WARN")
|
| 716 |
+
panic_sell_slot(exchange, slot, state, prefix="⏰ TIMEOUT")
|
| 717 |
continue
|
| 718 |
+
|
| 719 |
+
# 2. Ротация по плюсовой прибыли с учетом комиссий по истечении заданного времени (например, 2 часа)
|
| 720 |
+
if state.use_profit_exit and elapsed > state.profit_exit_timeout_min * 60:
|
| 721 |
+
try:
|
| 722 |
+
ticker = exchange.fetch_ticker(slot.symbol)
|
| 723 |
+
current_bid = ticker.get("bid") or ticker.get("last") or 0
|
| 724 |
+
|
| 725 |
+
if current_bid > 0:
|
| 726 |
+
fee = state.fee_rate()
|
| 727 |
+
# Расчет чистой прибыли при рыночной продаже по цене current_bid
|
| 728 |
+
if state.use_bnb_discount:
|
| 729 |
+
# Оплата в BNB (0.075% на входе и выходе)
|
| 730 |
+
net_profit = (slot.bought_qty * current_bid) - slot.usdt_spent - (slot.usdt_spent * fee) - (slot.bought_qty * current_bid * fee)
|
| 731 |
+
else:
|
| 732 |
+
# Стандартная комиссия урезает количество на входе, а на выходе берется из USDT
|
| 733 |
+
net_profit = (slot.bought_qty * current_bid * (1 - fee)) - slot.usdt_spent
|
| 734 |
+
|
| 735 |
+
if net_profit > 0.0:
|
| 736 |
+
state.log(
|
| 737 |
+
f"[Слот {slot.index}] Ротация: удержание {slot.symbol} превысило {state.profit_exit_timeout_min} мин. "
|
| 738 |
+
f"Рыночная цена {current_bid:.6g} дает чистый профит +{net_profit:.4f} USDT с учетом комиссий. "
|
| 739 |
+
f"Закрываем позицию для высвобождения слота!", "OK"
|
| 740 |
+
)
|
| 741 |
+
panic_sell_slot(exchange, slot, state, prefix="⏰ ROTATE")
|
| 742 |
+
continue
|
| 743 |
+
except Exception as e:
|
| 744 |
+
state.log(f"[Слот {slot.index}] Ошибка при расчете плюсовой ротации: {e}", "WARN")
|
| 745 |
|
| 746 |
try:
|
| 747 |
if state.paper_trading:
|
|
|
|
| 801 |
except Exception as e:
|
| 802 |
state.log(f"[Слот {slot.index}] Ошибка проверки ордера: {e}", "WARN")
|
| 803 |
|
| 804 |
+
# Пакетное наполнение пустых слотов за один общий проход скринера
|
| 805 |
empty_slots = [s for s in state.slots if s.is_empty()]
|
| 806 |
|
| 807 |
if empty_slots:
|
|
|
|
| 811 |
)
|
| 812 |
if should_screen:
|
| 813 |
state.last_full_screen_at = now
|
| 814 |
+
refill_empty_slots(exchange, allowed_symbols, empty_slots, state)
|
|
|
|
|
|
|
|
|
|
| 815 |
|
| 816 |
occupied_n = sum(1 for s in state.slots if not s.is_empty())
|
| 817 |
if occupied_n == state.num_slots:
|
|
|
|
| 968 |
help="Снижает комиссию с 0.1% до 0.075%. ВАЖНО: должно совпадать с реальной настройкой в Binance, иначе цена продажи будет рассчитана неточно и часть прибыли уходит в погрешность."
|
| 969 |
)
|
| 970 |
use_timeout = st.checkbox(
|
| 971 |
+
"Включить принудительный выход по жесткому тайм-ауту",
|
| 972 |
value=state.use_timeout,
|
| 973 |
+
help="Если выключено, бот будет держать купленную монету, пока она не продастся по лимиту или плюсовой ротации."
|
| 974 |
)
|
| 975 |
order_timeout_min = st.number_input(
|
| 976 |
+
"Тайм-аут жесткого удержания позиции (минут)",
|
| 977 |
min_value=10, max_value=14400, value=state.order_timeout_min, step=10,
|
| 978 |
disabled=not use_timeout,
|
| 979 |
+
help="Применяется только при включенном жестком фланге выше."
|
| 980 |
)
|
| 981 |
+
use_profit_exit = st.checkbox(
|
| 982 |
+
"Включить ротацию по профиту через время",
|
| 983 |
+
value=state.use_profit_exit,
|
| 984 |
+
help="Если включено, по истечении тайм-аута удержания бот закроет позицию по рынку, только если текущая чистая прибыль с учетом всех комиссий положительна (> 0 USDT)."
|
| 985 |
+
)
|
| 986 |
+
profit_exit_timeout_min = st.number_input(
|
| 987 |
+
"Тайм-аут до проверки плюсовой ротации (минут)",
|
| 988 |
+
min_value=10, max_value=14400, value=state.profit_exit_timeout_min, step=10,
|
| 989 |
+
disabled=not use_profit_exit,
|
| 990 |
+
help="Минимальное время удержания позиции (например, 120 минут = 2 часа) перед началом регулярных проверок на выход в плюс."
|
| 991 |
+
)
|
| 992 |
+
|
| 993 |
active_fee = FEE_RATE_BNB_DISCOUNT if use_bnb else FEE_RATE_STANDARD
|
| 994 |
|
| 995 |
per_slot_preview = total_budget / num_slots if num_slots else 0
|
|
|
|
| 1017 |
state.use_bnb_discount = use_bnb
|
| 1018 |
state.use_timeout = use_timeout
|
| 1019 |
state.order_timeout_min = order_timeout_min
|
| 1020 |
+
state.use_profit_exit = use_profit_exit
|
| 1021 |
+
state.profit_exit_timeout_min = profit_exit_timeout_min
|
| 1022 |
state.strategy_mode = strategy_mode
|
| 1023 |
state.use_dynamic_tp = use_dynamic_tp
|
| 1024 |
state.resize_slots(num_slots)
|
|
|
|
| 1040 |
st.divider()
|
| 1041 |
st.markdown("**📐 Формула цены продажи (на слот)**")
|
| 1042 |
st.code(
|
| 1043 |
+
"sell_price = (usdt_spent + target_profit) / bought_qty [При оплате в BNB]\n"
|
| 1044 |
+
"или\n"
|
| 1045 |
+
"sell_price = (usdt_spent + target_profit) / (bought_qty * (1 - fee_rate)) [Стандарт]\n\n"
|
| 1046 |
f"budget_per_slot = total_budget / num_slots\n"
|
| 1047 |
f"fee_rate = {active_fee} ({fee_label})",
|
| 1048 |
language="python"
|
|
|
|
| 1081 |
elapsed_min = 0
|
| 1082 |
if slot.order_placed_at:
|
| 1083 |
elapsed_min = int((datetime.now(timezone.utc) - slot.order_placed_at).total_seconds() / 60)
|
| 1084 |
+
timeout_parts = []
|
| 1085 |
+
if state.use_timeout:
|
| 1086 |
+
timeout_parts.append(f"timeout {state.order_timeout_min}м")
|
| 1087 |
+
if state.use_profit_exit:
|
| 1088 |
+
timeout_parts.append(f"плюс-ротация > {state.profit_exit_timeout_min}м")
|
| 1089 |
+
timeout_str = " (" + " | ".join(timeout_parts) + ")" if timeout_parts else " (безлимит)"
|
| 1090 |
+
|
| 1091 |
st.markdown(f"""
|
| 1092 |
<div class="{css_class}">
|
| 1093 |
<b>Слот {slot.index}: {slot.symbol}</b><br>
|
| 1094 |
Куплено: {slot.bought_qty:.4f} по {slot.buy_price_avg:.6g} ({slot.usdt_spent:.4f} USDT)<br>
|
| 1095 |
Цель продажи: <b>{slot.sell_price_set:.6g}</b> (цель profit: {slot.target_profit_set:.4f} USDT)<br>
|
| 1096 |
+
Статус: {slot.status} | Удержание: {elapsed_min} мин{timeout_str}
|
| 1097 |
</div>
|
| 1098 |
""", unsafe_allow_html=True)
|
| 1099 |
|
|
|
|
| 1104 |
exchange = build_exchange("", "", paper_trading=True)
|
| 1105 |
else:
|
| 1106 |
exchange = build_exchange(state.api_key, state.api_secret, paper_trading=False)
|
| 1107 |
+
panic_sell_slot(exchange, slot, state, prefix="🚨 PANIC")
|
| 1108 |
st.warning(f"Слот {slot.index} closed принудительно!")
|
| 1109 |
st.rerun()
|
| 1110 |
except Exception as e:
|