Askhat777 commited on
Commit
08d44fc
·
verified ·
1 Parent(s): cd0fcd8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +124 -39
app.py CHANGED
@@ -13,7 +13,7 @@ import pandas as pd
13
  # ─────────────────────────────────────────────
14
  # КОНСТАНТЫ СТРАТЕГИИ
15
  # ─────────────────────────────────────────────
16
- SCREENER_INTERVAL_SEC = 300 # Интервал полного сканирования рынка (5 минут)
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 = 3 # Количество параллельных слотов по умолчанию
28
- MAX_SLOTS = 6 # Защита от избыточного дробления бюджета
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
- usdt_out = slot.usdt_spent + slot.target_profit_set
503
- sell_raw = usdt_out / (slot.bought_qty * (1 - fee))
 
 
 
 
 
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, manual: bool = False):
539
- prefix = "🚨 PANIC" if manual else "⏰ TIMEOUT"
540
  sym = slot.symbol
541
- state.log(f"{prefix} [Слот {slot.index}]: экстренный выход из {sym}", "WARN")
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": "panic", "slot": slot.index,
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 refill_slot(exchange: ccxt.binance, allowed_symbols: list[str], slot: Slot, state: BotState):
 
599
  now = time.time()
600
- if now - slot.last_refill_attempt < SLOT_REFILL_DEBOUNCE_SEC:
 
 
 
601
  return
602
- slot.last_refill_attempt = now
603
- slot.status = "🔍 Поиск замены"
 
 
604
 
605
  occupied = state.occupied_symbols()
606
- candidates = screen_full_market(exchange, allowed_symbols, occupied, state, need_n=1)
 
 
 
607
 
608
  if not candidates:
609
- state.log(f"[Слот {slot.index}] Нет подходящих кандидатов в этом цикле", "INFO")
 
 
610
  return
611
 
612
- best = candidates[0]
613
- slot.status = f"🟡 Покупка {best['symbol']}"
614
- ok_buy = execute_buy(exchange, best["symbol"], state.budget_per_slot(), slot, state)
615
- if ok_buy:
616
- ok_sell = place_limit_sell(exchange, slot, state)
617
- if not ok_sell:
618
- state.log(f"[Слот {slot.index}] LimitSell не удался -> экстренная продажа слота", "ERR")
619
- panic_sell_slot(exchange, slot, state)
 
 
 
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}] Тайм-аут {state.order_timeout_min} мин — авто-выход", "WARN")
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
- for slot in empty_slots:
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
- " / (bought_qty * (1 - fee_rate))\n\n"
 
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
- timeout_str = f"/{state.order_timeout_min} мин" if state.use_timeout else " (безлимит)"
 
 
 
 
 
 
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, manual=True)
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: