Files
TradeBot/crypto_spot_bot/config.py
T
2026-06-25 22:39:25 +03:00

317 lines
14 KiB
Python

from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
FIXED_SPOT_SYMBOLS = ("BTCUSDT", "ETHUSDT", "SOLUSDT", "LTCUSDT")
STRATEGY_MODES = {"legacy", "trend_macd", "torch_forecast"}
def _load_dotenv(path: Path) -> None:
if not path.exists():
return
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
os.environ.setdefault(key, value)
def _bool_env(name: str, default: bool) -> bool:
raw = os.getenv(name)
if raw is None or raw == "":
return default
return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
def _float_env(name: str, default: float) -> float:
raw = os.getenv(name)
return default if raw in (None, "") else float(raw)
def _int_env(name: str, default: int) -> int:
raw = os.getenv(name)
return default if raw in (None, "") else int(raw)
def _symbols_env(name: str) -> tuple[str, ...]:
raw = os.getenv(name, "")
return tuple(part.strip().upper() for part in raw.split(",") if part.strip())
@dataclass
class Settings:
trading_mode: str
host: str
port: int
bybit_testnet: bool
bybit_api_key: str
bybit_api_secret: str
starting_balance_usdt: float
auto_select_symbols: bool
top_symbols_count: int
symbols: tuple[str, ...]
strategy_mode: str
base_interval: str
kline_limit: int
trend_interval: str
trend_kline_limit: int
loop_interval_seconds: int
fast_trading_enabled: bool
fast_loop_interval_seconds: float
fast_entry_cooldown_seconds: int
max_entries_per_minute: int
websocket_enabled: bool
min_signal_confidence: float
max_spread_percent: float
min_24h_turnover_usdt: float
pattern_analysis_enabled: bool
pattern_score_weight: float
learning_enabled: bool
learning_lookback_trades: int
learning_min_samples: int
learning_max_adjustment: float
learning_max_position_multiplier: float
llm_advisor_enabled: bool
ollama_base_url: str
ollama_model: str
llm_advisor_min_interval_seconds: int
llm_advisor_timeout_seconds: int
llm_advisor_max_adjustment: float
min_position_usdt: float
max_position_usdt: float
max_symbol_exposure_usdt: float
max_total_exposure_usdt: float
max_open_positions: int
max_positions_per_symbol: int
grid_trading_enabled: bool
grid_entry_confidence: float
grid_buy_zone: float
grid_max_position_usdt: float
rebound_trading_enabled: bool
rebound_entry_confidence: float
rebound_min_probability: float
rebound_max_position_usdt: float
kelly_sizing_enabled: bool
kelly_fraction: float
kelly_max_fraction: float
risk_per_trade_percent: float
risk_guard_enabled: bool
risk_recent_trade_window: int
risk_max_consecutive_losses: int
risk_min_recent_profit_factor: float
risk_reduce_multiplier: float
atr_trailing_multiplier: float
trend_rsi_min: float
trend_rsi_max: float
time_series_forecast_enabled: bool
time_series_min_candles: int
time_series_forecast_horizon: int
time_series_min_edge_percent: float
time_series_min_probability_up: float
time_series_min_confidence: float
time_series_max_adjustment: float
time_series_lstm_enabled: bool
time_series_lstm_model_path: Path
time_series_probe_enabled: bool
time_series_probe_min_edge_percent: float
time_series_probe_min_probability_up: float
time_series_probe_size_multiplier: float
time_series_rebound_fallback_enabled: bool
stop_loss_percent: float
take_profit_percent: float
trailing_stop_percent: float
min_hold_seconds: int
entry_cooldown_seconds: int
max_daily_drawdown_usdt: float
min_cash_reserve_usdt: float
taker_fee_rate: float
slippage_rate: float
enable_live_trading: bool
live_trading_confirm: str
live_order_max_usdt: float
database_path: Path
log_path: Path
env_file_path: Path
@property
def rest_base_url(self) -> str:
return "https://api-testnet.bybit.com" if self.bybit_testnet else "https://api.bybit.com"
@property
def websocket_url(self) -> str:
return (
"wss://stream-testnet.bybit.com/v5/public/spot"
if self.bybit_testnet
else "wss://stream.bybit.com/v5/public/spot"
)
@property
def live_ready(self) -> bool:
return (
self.trading_mode == "live"
and self.enable_live_trading
and self.live_trading_confirm == "I_ACCEPT_REAL_RISK"
and bool(self.bybit_api_key)
and bool(self.bybit_api_secret)
)
@property
def effective_loop_interval_seconds(self) -> float:
interval = (
self.fast_loop_interval_seconds
if self.fast_trading_enabled
else float(self.loop_interval_seconds)
)
return max(0.25, interval)
@property
def effective_entry_cooldown_seconds(self) -> int:
return (
max(0, self.fast_entry_cooldown_seconds)
if self.fast_trading_enabled
else max(0, self.entry_cooldown_seconds)
)
def load_settings(env_file: str | Path | None = None) -> Settings:
root = Path.cwd()
env_path = Path(env_file) if env_file else root / ".env"
_load_dotenv(env_path)
mode = os.getenv("TRADING_MODE", "paper").strip().lower()
if mode not in {"paper", "live"}:
raise ValueError("TRADING_MODE must be paper or live")
strategy_mode = os.getenv("STRATEGY_MODE", "torch_forecast").strip().lower()
if strategy_mode not in STRATEGY_MODES:
raise ValueError("STRATEGY_MODE must be legacy, trend_macd or torch_forecast")
auto_select_symbols = _bool_env("AUTO_SELECT_SYMBOLS", False)
top_symbols_count = _int_env("TOP_SYMBOLS_COUNT", len(FIXED_SPOT_SYMBOLS))
requested_symbols = _symbols_env("SYMBOLS")
symbols = requested_symbols if requested_symbols else (() if auto_select_symbols else FIXED_SPOT_SYMBOLS)
forecast_enabled_default = strategy_mode == "torch_forecast"
min_signal_confidence = _float_env("MIN_SIGNAL_CONFIDENCE", 0.64)
settings = Settings(
trading_mode=mode,
host=os.getenv("HOST", "127.0.0.1"),
port=_int_env("PORT", 8787),
bybit_testnet=_bool_env("BYBIT_TESTNET", False),
bybit_api_key=os.getenv("BYBIT_API_KEY", ""),
bybit_api_secret=os.getenv("BYBIT_API_SECRET", ""),
starting_balance_usdt=_float_env("STARTING_BALANCE_USDT", 100.0),
auto_select_symbols=auto_select_symbols,
top_symbols_count=top_symbols_count,
symbols=symbols,
strategy_mode=strategy_mode,
base_interval=os.getenv("BASE_INTERVAL", "60"),
kline_limit=_int_env("KLINE_LIMIT", 240),
trend_interval=os.getenv("TREND_INTERVAL", "D"),
trend_kline_limit=_int_env("TREND_KLINE_LIMIT", 260),
loop_interval_seconds=_int_env("LOOP_INTERVAL_SECONDS", 5),
fast_trading_enabled=_bool_env("FAST_TRADING_ENABLED", False),
fast_loop_interval_seconds=_float_env("FAST_LOOP_INTERVAL_SECONDS", 1.0),
fast_entry_cooldown_seconds=_int_env("FAST_ENTRY_COOLDOWN_SECONDS", 20),
max_entries_per_minute=_int_env("MAX_ENTRIES_PER_MINUTE", 12),
websocket_enabled=_bool_env("WEBSOCKET_ENABLED", True),
min_signal_confidence=min_signal_confidence,
max_spread_percent=_float_env("MAX_SPREAD_PERCENT", 0.18),
min_24h_turnover_usdt=_float_env("MIN_24H_TURNOVER_USDT", 1000000.0),
pattern_analysis_enabled=_bool_env("PATTERN_ANALYSIS_ENABLED", False),
pattern_score_weight=_float_env("PATTERN_SCORE_WEIGHT", 0.18),
learning_enabled=_bool_env("LEARNING_ENABLED", False),
learning_lookback_trades=_int_env("LEARNING_LOOKBACK_TRADES", 120),
learning_min_samples=_int_env("LEARNING_MIN_SAMPLES", 3),
learning_max_adjustment=_float_env("LEARNING_MAX_ADJUSTMENT", 0.12),
learning_max_position_multiplier=_float_env("LEARNING_MAX_POSITION_MULTIPLIER", 1.6),
llm_advisor_enabled=_bool_env("LLM_ADVISOR_ENABLED", False),
ollama_base_url=os.getenv("OLLAMA_BASE_URL", "http://192.168.0.210:11434").rstrip("/"),
ollama_model=os.getenv("OLLAMA_MODEL", "gemma4:e4b"),
llm_advisor_min_interval_seconds=_int_env("LLM_ADVISOR_MIN_INTERVAL_SECONDS", 180),
llm_advisor_timeout_seconds=_int_env("LLM_ADVISOR_TIMEOUT_SECONDS", 45),
llm_advisor_max_adjustment=_float_env("LLM_ADVISOR_MAX_ADJUSTMENT", 0.06),
min_position_usdt=_float_env("MIN_POSITION_USDT", 1.0),
max_position_usdt=_float_env("MAX_POSITION_USDT", 25.0),
max_symbol_exposure_usdt=_float_env("MAX_SYMBOL_EXPOSURE_USDT", 25.0),
max_total_exposure_usdt=_float_env("MAX_TOTAL_EXPOSURE_USDT", 75.0),
max_open_positions=_int_env("MAX_OPEN_POSITIONS", len(FIXED_SPOT_SYMBOLS)),
max_positions_per_symbol=_int_env("MAX_POSITIONS_PER_SYMBOL", 1),
grid_trading_enabled=_bool_env("GRID_TRADING_ENABLED", False),
grid_entry_confidence=_float_env("GRID_ENTRY_CONFIDENCE", 0.58),
grid_buy_zone=_float_env("GRID_BUY_ZONE", 0.45),
grid_max_position_usdt=_float_env("GRID_MAX_POSITION_USDT", 8.0),
rebound_trading_enabled=_bool_env("REBOUND_TRADING_ENABLED", False),
rebound_entry_confidence=_float_env("REBOUND_ENTRY_CONFIDENCE", 0.58),
rebound_min_probability=_float_env("REBOUND_MIN_PROBABILITY", 0.58),
rebound_max_position_usdt=_float_env("REBOUND_MAX_POSITION_USDT", 6.0),
kelly_sizing_enabled=_bool_env("KELLY_SIZING_ENABLED", False),
kelly_fraction=_float_env("KELLY_FRACTION", 0.25),
kelly_max_fraction=_float_env("KELLY_MAX_FRACTION", 0.20),
risk_per_trade_percent=_float_env("RISK_PER_TRADE_PERCENT", 0.01),
risk_guard_enabled=_bool_env("RISK_GUARD_ENABLED", True),
risk_recent_trade_window=_int_env("RISK_RECENT_TRADE_WINDOW", 20),
risk_max_consecutive_losses=_int_env("RISK_MAX_CONSECUTIVE_LOSSES", 4),
risk_min_recent_profit_factor=_float_env("RISK_MIN_RECENT_PROFIT_FACTOR", 0.85),
risk_reduce_multiplier=_float_env("RISK_REDUCE_MULTIPLIER", 0.50),
atr_trailing_multiplier=_float_env("ATR_TRAILING_MULTIPLIER", 2.2),
trend_rsi_min=_float_env("TREND_RSI_MIN", 45.0),
trend_rsi_max=_float_env("TREND_RSI_MAX", 65.0),
time_series_forecast_enabled=_bool_env("TIME_SERIES_FORECAST_ENABLED", forecast_enabled_default),
time_series_min_candles=_int_env("TIME_SERIES_MIN_CANDLES", 120),
time_series_forecast_horizon=_int_env("TIME_SERIES_FORECAST_HORIZON", 3),
time_series_min_edge_percent=_float_env("TIME_SERIES_MIN_EDGE_PERCENT", 0.08),
time_series_min_probability_up=_float_env("TIME_SERIES_MIN_PROBABILITY_UP", 0.58),
time_series_min_confidence=_float_env("TIME_SERIES_MIN_CONFIDENCE", 0.4),
time_series_max_adjustment=_float_env("TIME_SERIES_MAX_ADJUSTMENT", 0.08),
time_series_lstm_enabled=_bool_env("TIME_SERIES_LSTM_ENABLED", True),
time_series_lstm_model_path=Path(os.getenv("TIME_SERIES_LSTM_MODEL_PATH", "runtime/lstm_forecaster.json")),
time_series_probe_enabled=_bool_env("TIME_SERIES_PROBE_ENABLED", True),
time_series_probe_min_edge_percent=_float_env("TIME_SERIES_PROBE_MIN_EDGE_PERCENT", 0.02),
time_series_probe_min_probability_up=_float_env("TIME_SERIES_PROBE_MIN_PROBABILITY_UP", 0.55),
time_series_probe_size_multiplier=_float_env("TIME_SERIES_PROBE_SIZE_MULTIPLIER", 0.40),
time_series_rebound_fallback_enabled=_bool_env("TIME_SERIES_REBOUND_FALLBACK_ENABLED", True),
stop_loss_percent=_float_env("STOP_LOSS_PERCENT", 0.04),
take_profit_percent=_float_env("TAKE_PROFIT_PERCENT", 0.035),
trailing_stop_percent=_float_env("TRAILING_STOP_PERCENT", 0.015),
min_hold_seconds=_int_env("MIN_HOLD_SECONDS", 180),
entry_cooldown_seconds=_int_env("ENTRY_COOLDOWN_SECONDS", 180),
max_daily_drawdown_usdt=_float_env("MAX_DAILY_DRAWDOWN_USDT", 6.0),
min_cash_reserve_usdt=_float_env("MIN_CASH_RESERVE_USDT", 5.0),
taker_fee_rate=_float_env("TAKER_FEE_RATE", 0.001),
slippage_rate=_float_env("SLIPPAGE_RATE", 0.0003),
enable_live_trading=_bool_env("ENABLE_LIVE_TRADING", False),
live_trading_confirm=os.getenv("LIVE_TRADING_CONFIRM", ""),
live_order_max_usdt=_float_env("LIVE_ORDER_MAX_USDT", 10.0),
database_path=Path(os.getenv("DATABASE_PATH", "runtime/tradebot.sqlite3")),
log_path=Path(os.getenv("LOG_PATH", "runtime/tradebot.log")),
env_file_path=env_path,
)
if settings.trading_mode == "live" and not settings.live_ready:
raise ValueError(
"Live mode is locked. Set ENABLE_LIVE_TRADING=true, "
"LIVE_TRADING_CONFIRM=I_ACCEPT_REAL_RISK and Bybit keys."
)
return settings
def update_env_value(path: Path, key: str, value: str) -> None:
lines = path.read_text(encoding="utf-8").splitlines() if path.exists() else []
output: list[str] = []
replaced = False
for line in lines:
stripped = line.strip()
if stripped and not stripped.startswith("#") and "=" in stripped:
current_key = stripped.split("=", 1)[0].strip()
if current_key == key:
output.append(f"{key}={value}")
replaced = True
continue
output.append(line)
if not replaced:
output.append(f"{key}={value}")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(output).rstrip() + "\n", encoding="utf-8")