Files
gridbot/config.py.bak.live_killswitch.20260622_134404

215 lines
12 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Grid Trading Bot — config.
All secrets MUST come from environment (.env), never from defaults in code.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from project root (cwd or script dir) so all os.getenv() below
# see real values, not code defaults. Safe to call multiple times.
_ENV_PATH = Path(__file__).resolve().parent / ".env"
if _ENV_PATH.exists():
load_dotenv(_ENV_PATH, override=False)
else:
# fallback: cwd/.env (for 'python main.py' from project dir)
load_dotenv(override=False)
def _require(name: str) -> str:
"""Read required env var. Empty/None → raise (only when LIVE mode is on)."""
v = os.getenv(name, "").strip()
if not v:
raise RuntimeError(
f"Environment variable {name!r} is empty. "
f"Set it in .env (see .env.example). Aborting."
)
return v
def _opt(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
# === Tradernet API credentials ===
# LIVE: ключи обязательны → main.py вызовет _require().
# DEMO: ключи могут быть пустыми (например, dry-run без сети).
TRADERNET_PUBLIC_KEY = os.getenv("TRADERNET_PUBLIC_KEY", "")
TRADERNET_PRIVATE_KEY = os.getenv("TRADERNET_PRIVATE_KEY", "")
TRADERNET_LOGIN = os.getenv("TRADERNET_LOGIN", "")
TRADERNET_PASSWORD = os.getenv("TRADERNET_PASSWORD", "")
TRADERNET_BASE_URL = os.getenv("TRADERNET_BASE_URL", "https://tradernet.by")
def require_tradernet_keys() -> None:
"""Вызывайте из main.py в начале LIVE-режима.
Падает, если хотя бы один ключ пустой."""
_require("TRADERNET_PUBLIC_KEY")
_require("TRADERNET_PRIVATE_KEY")
# === Trading settings ===
SYMBOL = os.getenv("SYMBOL", "BTC-USDT.IMEX")
QUOTE = os.getenv("QUOTE", "USDT")
BASE = os.getenv("BASE", "BTC")
# === Grid settings (default 10 levels = 20 orders total) ===
GRID_LEVELS = int(os.getenv("GRID_LEVELS", "10"))
GRID_STEP_PERCENT = float(os.getenv("GRID_STEP_PERCENT", "0.005"))
GRID_TAKE_PROFIT_PERCENT = float(os.getenv("GRID_TAKE_PROFIT_PERCENT", "0.5"))
# === Fee-coverage validator (QuantDinger validator pattern) ===
# Оценочная комиссия биржи Cifra Markets за сделку. Не подтверждена
# официально (см. MEMORY.md). 0.2% — безопасная оценка для BTC.
FEE_RATE_PERCENT = float(os.getenv("FEE_RATE_PERCENT", "0.2")) # 0.2% = 0.002
# Множитель запаса: round-trip fee * safety = минимально допустимый
# шаг сетки. QuantDinger default = 1.25.
FEE_SAFETY_MULTIPLIER = float(os.getenv("FEE_SAFETY_MULTIPLIER", "1.25"))
GRID_RENEW_THRESHOLD = int(os.getenv("GRID_RENEW_THRESHOLD", "3"))
GRID_PRICE_OFFSET = float(os.getenv("GRID_PRICE_OFFSET", "0"))
# Режим построения сетки: 'arithmetic' (равные $шаги, по умолчанию)
# или 'geometric' (равные %шаги, не зависит от цены — точнее для
# широких диапазонов BTC).
GRID_MODE = os.getenv("GRID_MODE", "arithmetic").lower().strip()
# === Stale grid recovery ===
GRID_STALE_DROP_PERCENT = float(os.getenv("GRID_STALE_DROP_PERCENT", "5.0"))
# === Inactivity-driven soft rebuild ===
# Если за INACTIVITY_RESTART_HOURS часов не было ни одной сделки (BUY/SELL fill)
# И BTC отошёл от центра сетки (start_price) более чем на
# INACTIVITY_PRICE_DRIFT_PCT процентов — отменяем висящие BUY-ордера и
# пересобираем сетку вокруг текущей цены (мягкий рестарт: процесс не
# перезапускается). Применяется ТОЛЬКО в DEMO-режиме.
INACTIVITY_RESTART_HOURS = float(os.getenv("INACTIVITY_RESTART_HOURS", "1"))
INACTIVITY_PRICE_DRIFT_PCT = float(os.getenv("INACTIVITY_PRICE_DRIFT_PCT", "0.3"))
# === SMA-ATR strategy (2026-06-21) ===
# Replaces "grid_center = current_price" with SMA-anchored grid + lock FSM.
# Lock triggers when |price - sma| > LOCK_PCT%, unlock when < UNLOCK_PCT%.
# Bias source for asymmetry (3:2 split): Kronos primary, TREND fallback.
STRATEGY_CENTER_MODE = os.getenv("STRATEGY_CENTER_MODE", "current_price") # "sma" | "current_price"
STRATEGY_SMA_TF_MIN = int(os.getenv("STRATEGY_SMA_TF_MIN", "60")) # 60 = 1h
STRATEGY_SMA_PERIOD = int(os.getenv("STRATEGY_SMA_PERIOD", "24"))
STRATEGY_LOCK_PCT = float(os.getenv("STRATEGY_LOCK_PCT", "2.0")) # |dev| > 2% → lock
STRATEGY_UNLOCK_PCT = float(os.getenv("STRATEGY_UNLOCK_PCT", "1.5")) # |dev| < 1.5% → unlock
STRATEGY_BIAS_SOURCE = os.getenv("STRATEGY_BIAS_SOURCE", "kronos") # "kronos" | "trend" | "none"
STRATEGY_BIAS_FALLBACK = os.getenv("STRATEGY_BIAS_FALLBACK", "trend") # used when primary is unavailable
STRATEGY_SMA_CACHE_SEC = int(os.getenv("STRATEGY_SMA_CACHE_SEC", "300")) # recompute SMA every 5 min
STRATEGY_LOCK_COOLDOWN_SEC = int(os.getenv("STRATEGY_LOCK_COOLDOWN_SEC", "900")) # 15 min between lock state changes
STRATEGY_ASYMMETRY_UP = os.getenv("STRATEGY_ASYMMETRY_UP", "3:2") # bias=up → 3 BUY / 2 SELL
STRATEGY_ASYMMETRY_DOWN = os.getenv("STRATEGY_ASYMMETRY_DOWN", "2:3") # bias=down → 2 BUY / 3 SELL
STRATEGY_ASYMMETRY_RANGE = os.getenv("STRATEGY_ASYMMETRY_RANGE", "3:2") # bias=range → 3 BUY / 2 SELL
# === State ===
STATE_DIR = Path(os.getenv("STATE_DIR", str(Path.home() / ".openclaw")))
STATE_FILE = str(STATE_DIR / "grid-state.json")
# === Kill switch (2026-06-22) ===
# Защита для live-режима: автоматически останавливает бота при:
# 1. Drawdown > KILL_DD_PCT% от пикового баланса (balance + unrealized position)
# 2. > KILL_API_ERRORS ошибок API за KILL_API_WINDOW_SEC секунд
# 3. Telegram /stop команда от TELEGRAM_CHAT_ID (long-poll каждые KILL_TG_POLL_SEC)
# При срабатывании: cancel all open orders (для live - через _live_cancel_all_pending),
# bot_running=False, требует ручного /api/bot/start для resume.
# KILL_SWITCH_ENABLED=false - весь модуль no-op (для отладки).
KILL_SWITCH_ENABLED = os.getenv("KILL_SWITCH_ENABLED", "true").lower() in ("true", "1", "yes")
KILL_DD_PCT = float(os.getenv("KILL_DD_PCT", "10.0"))
KILL_API_ERRORS = int(os.getenv("KILL_API_ERRORS", "5"))
KILL_API_WINDOW_SEC = int(os.getenv("KILL_API_WINDOW_SEC", "600"))
KILL_TG_POLL_SEC = int(os.getenv("KILL_TG_POLL_SEC", "30"))
KILL_TG_LONG_POLL = int(os.getenv("KILL_TG_LONG_POLL", "25"))
# === Reconcile (QuantDinger ledger_reconcile pattern) ===
# Периодическая сверка demo.position с trade_log: ловит
# "зависшие" позиции, открытые ДО последнего TP/SELL-fill.
# 0 = выключено. 300 = каждые 5 мин (рекомендация QuantDinger).
RECONCILE_INTERVAL_SEC = int(os.getenv("RECONCILE_INTERVAL_SEC", "300"))
# Если найдена shadow-позиция: 'log' (только предупреждение, безопасно)
# или 'clear' (auto-clear). По умолчанию log.
RECONCILE_AUTO_CLEAR = os.getenv("RECONCILE_AUTO_CLEAR", "log").lower() in ("true", "1", "yes")
# === Public server URL ===
SERVER_URL = os.getenv("SERVER_URL", "https://gridbot.kolp.pro")
# === Demo / Live ===
DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() in ("true", "1", "yes")
DEMO_START_BALANCE = float(os.getenv("DEMO_START_BALANCE", "100"))
LIVE_TRADING_DRY_RUN = os.getenv("LIVE_TRADING_DRY_RUN", "true").lower() in ("true", "1", "yes")
# === Telegram ===
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# === Flask ===
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "5555"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
# === Web auth (Basic Auth для дашборда /api/*) ===
# ВАЖНО: обязательно сменить дефолты в .env. Сейчас — deprecation-дефолты,
# которые выдаются на 1 запуск, если .env не заполнен.
WEB_USERNAME = os.getenv("WEB_USERNAME", "admin")
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "changeme")
# === Kronos advisor ===
# KRONOS_ENABLED: подмешивать kronos_advice.json в параметры сетки
# KRONOS_LIVE_ENABLED: только в LIVE — реально применять советы к ордерам
# KRONOS_MODEL: "kronos-mini" | "kronos-small" (mini быстрее, small точнее)
# KRONOS_TF_MIN: таймфрейм свечей (1, 5, 15, 60, 1440)
# KRONOS_LOOKBACK: сколько свечей подавать в модель (≤ 512 для small/mini)
# KRONOS_PRED_LEN: сколько свечей прогнозировать
# KRONOS_MIN_CONFIDENCE: ниже этого — fallback на константы из .env
KRONOS_ENABLED = os.getenv("KRONOS_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_LIVE_ENABLED = os.getenv("KRONOS_LIVE_ENABLED", "false").lower() in ("true", "1", "yes")
KRONOS_MODEL = os.getenv("KRONOS_MODEL", "kronos-mini")
KRONOS_TF_MIN = int(os.getenv("KRONOS_TF_MIN", "60"))
KRONOS_LOOKBACK = int(os.getenv("KRONOS_LOOKBACK", "400"))
KRONOS_PRED_LEN = int(os.getenv("KRONOS_PRED_LEN", "24"))
KRONOS_MIN_CONFIDENCE = float(os.getenv("KRONOS_MIN_CONFIDENCE", "0.4"))
KRONOS_ADVICE_FILE = os.getenv("KRONOS_ADVICE_FILE", str(STATE_DIR / "kronos_advice.json"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))
# === Local trend detector (slope-based, on price_history) ===
# Independent of Kronos. Linear-regression slope over the last TREND_LOOKBACK
# points in price_history. When slope > TREND_THRESHOLD_PCT% AND R² >
# TREND_MIN_CONFIDENCE — bias="up" (or "down" for negative slope).
# On uptrend: shift buy_anchor up by TREND_UP_ANCHOR_SHIFT to make BUY
# levels sit closer to current price (catch pullbacks in rising trends).
# On downtrend: shift buy_anchor down (skip falling knives).
TREND_ENABLED = os.getenv("TREND_ENABLED", "false").lower() in ("true", "1", "yes")
TREND_LOOKBACK = int(os.getenv("TREND_LOOKBACK", "60"))
TREND_THRESHOLD_PCT = float(os.getenv("TREND_THRESHOLD_PCT", "0.3"))
TREND_MIN_CONFIDENCE = float(os.getenv("TREND_MIN_CONFIDENCE", "0.5"))
TREND_UP_ANCHOR_SHIFT = float(os.getenv("TREND_UP_ANCHOR_SHIFT", "0.0015"))
TREND_DOWN_ANCHOR_SHIFT = float(os.getenv("TREND_DOWN_ANCHOR_SHIFT", "0.0015"))