import os

DB_CONFIG = {
    'host': 'nlbotinterface.ru',
    'port': 3306,
    'user': 'bitcoin',
    'password': os.environ.get('MOEX_DB_PASSWORD', 'g49020007'),
    'database': 'bitcoin_tickers',
}

TIMEFRAMES = {'H1': 'H1', 'D1': 'D1', 'W1': 'W1'}

MOEX_TICKERS = [
    'ASTR', 'SBER', 'VTBR', 'ROSN', 'NVTK', 'PHOR',
    'SNGSP', 'PLZL', 'X5', 'MTSS', 'LKOH', 'GAZP',
    'MOEX', 'NSVZ',
]

CRYPTO_TICKERS = ['BITCOIN', 'BITCOINC', 'ZCASH']
FOREX_TICKERS = ['EURUSD']

TICKER_TYPES = {'moex': 'moex', 'forex': 'forex', 'crypto': 'crypto'}
# Полный список тикеров (для training), включая тикеры с проблемами данных.
ALL_TICKERS = MOEX_TICKERS + CRYPTO_TICKERS + FOREX_TICKERS

# Список для мониторинга/трейдинга (исключает тикеры с проблемами data-feed).
# AUDIT 2026-07-31: EURUSD data feed не обновляется с 2024-07-24 — временно
# исключён до восстановления. Чтобы вернуть — MONITOR_USE_ALL_TICKERS=True.
MONITOR_USE_ALL_TICKERS = False
MONITOR_TICKERS = (
    ALL_TICKERS
    if MONITOR_USE_ALL_TICKERS
    # EURUSD временно отключён до восстановления data-feed.
    else [t for t in ALL_TICKERS if t != 'EURUSD']
)


def get_market(ticker: str) -> str:
    """Определяет рынок тикера: 'moex', 'crypto' или 'forex'."""
    ticker = ticker.upper()
    if ticker in MOEX_TICKERS:
        return 'moex'
    if ticker in CRYPTO_TICKERS:
        return 'crypto'
    if ticker in FOREX_TICKERS:
        return 'forex'
    # По умолчанию — moex
    return 'moex'


# Per-market ATR конфигурация для target computation и live trade execution.
# Используется ЕДИНЫЙ источник правды для train (features/targets.py),
# MoE backtest (moe.py find_best_thr) и live (trade/manager.py) — иначе
# тренировочный RR не совпадает с live RR (баг обнаружен в аудите 2026-07-31).
#
# v12.9 (2026-08-09): SL и TP УВЕЛИЧЕНЫ. Причины:
#   - MAE к TP1 медиана ~1.23×ATR, но хвосты до 3-4×ATR — старый SL=3×ATR
#     «выносил» из сделок с правильным направлением (направление верно ~52%);
#   - увеличение SL с 3× до 4× (MOEX) даёт рынку больше места; TP 6→8
#     сохраняет RR 1:2 (breakeven = SL/(SL+TP) = 4/12 = 33% — тот же);
#   - лесенка масштабируется автоматически: scale = tp_mult/6 → тиры
#     4.0/6.0/8.0×ATR (tier-1 ≈ новый TP1).
#
# MOEX:   SL=4×ATR, TP=8×ATR (RR 1:2), max_atr=5%
# Crypto: SL=4×ATR, TP=6×ATR (RR 1:1.5), max_atr=15% — шире стоп+толерантнее к gap
# Forex:  SL=2.5×ATR, TP=5×ATR (RR 1:2), max_atr=2%  — уже стоп (низкая волатильность)
MARKET_TARGET_CONFIGS = {
    #max_bars=48 синхронизирован с TIME_STOP=48h в RISK_CONFIG.
    # Ранее max_bars=200 допускал TP-исход на 8-й день, но live trade к 48ч
    # уже закрыт по time-stop → train/live mismatch (audit 2026-08-03).
    'moex':   {'atr_mult_sl': 4.0, 'atr_mult_tp': 8.0,  'max_bars': 48,  'swing_lookback': 20, 'max_atr_ratio': 0.05},
    # Crypto: узкий TP (RR 1:1.5 вместо 1:2). Ранее 4/8 давал long SR=21-27% —
    # цена редко проходит 8×ATR за разумное время (audit 2026-08-03).
    # v12.9: SL 3→4, TP 4.5→6 — расширены без изменения RR.
    'crypto': {'atr_mult_sl': 4.0, 'atr_mult_tp': 6.0,  'max_bars': 72,  'swing_lookback': 20, 'max_atr_ratio': 0.15},
    'forex':  {'atr_mult_sl': 2.5, 'atr_mult_tp': 5.0,  'max_bars': 48,  'swing_lookback': 20, 'max_atr_ratio': 0.02},
}

# ── Per-ticker risk overrides (audit 2026-08-03, v12.9 2026-08-09) ─────
# Перекрывает MARKET_TARGET_CONFIGS для отдельных тикеров.
# Применяется как в training (compute_swing_dual_outcomes), так и в live
# (TradeManager._get_ticker_risk), что сохраняет train/live sync.
#
# Логика: тикеры с 0% winrate (SBER 12/12 SHORT, GAZP 8/8 SHORT,
# VTBR 9/9 SHORT, NVTK 5/5 SHORT) получают узкий SL/TP (RR 1:2 сохранён,
# но дистанции сужены), что приближает TP и реже попадает в SL.
# Тикеры-звёзды (ASTR, PHOR — 100% LONG winrate) сохраняют широкие TP.
# v12.9: override масштабирован вместе с базовыми множителями (2.5/5.0).
TICKER_RISK_OVERRIDES = {
    'SBER':  {'atr_mult_sl': 2.5, 'atr_mult_tp': 5.0},   # 0/12 SHORT → узкий
    'GAZP':  {'atr_mult_sl': 2.5, 'atr_mult_tp': 5.0},   # 1/8 SHORT  → узкий
    'VTBR':  {'atr_mult_sl': 2.5, 'atr_mult_tp': 5.0},   # 0/9 SHORT  → узкий
    'NVTK':  {'atr_mult_sl': 2.5, 'atr_mult_tp': 5.0},   # 0/5 SHORT  → узкий
    'MTSS':  {'atr_mult_sl': 2.5, 'atr_mult_tp': 5.0},   # 0/4 LONG   → узкий
    'ZCASH': {'atr_mult_sl': 2.5, 'atr_mult_tp': 5.0},   # 0/2 LONG   → узкий
    # X5 (v13 эксперимент 2026-08-14): базовый SR 12.9% при TP=8×ATR —
    # движение на 8×ATR крайне редкое. RR 1:1/0.67 дают SR 54-70%.
    # SL=1.5×ATR/TP=1.0×ATR (RR 0.67) — лучший конфиг по OOS (val_acc 64-69%,
    # стабильно находит порог с положительным net).
    'X5':    {'atr_mult_sl': 1.5, 'atr_mult_tp': 1.0},   # RR 0.67 → высокий WR
}

# v12.8: использовать swing-based SL вместо чисто ATR-based
USE_SWING_TARGETS = True  # False → старый compute_dual_outcomes; True → compute_swing_dual_outcomes


def get_target_config(ticker: str) -> dict:
    """Возвращает target config (SL/TP множители + max_atr_ratio) для тикера по рынку.

    Per-ticker overrides имеют приоритет над market-default (audit 2026-08-03).
    """
    market = get_market(ticker)
    cfg = MARKET_TARGET_CONFIGS.get(market, MARKET_TARGET_CONFIGS['moex']).copy()
    if ticker in TICKER_RISK_OVERRIDES:
        cfg.update(TICKER_RISK_OVERRIDES[ticker])
    return cfg


def get_risk_params(ticker: str) -> tuple[float, float, float]:
    """Возвращает (sl_mult, tp_mult, max_atr_ratio) для live trade execution.

    Единый источник правды — `MARKET_TARGET_CONFIGS`. Sync между train и live
    гарантирует, что тренировочный RR совпадает с live.
    """
    cfg = get_target_config(ticker)
    return (
        cfg['atr_mult_sl'],
        cfg['atr_mult_tp'],
        cfg.get('max_atr_ratio', 0.05),
    )


def is_ticker_blacklisted(ticker: str) -> bool:
    """Проверяет ручной чёрный список тикеров (TICKER_BLACKLIST в config)."""
    return ticker.upper() in TICKER_BLACKLIST


SAVE_DIR = 'models/saved'

RANDOM_SEED = 42
TEST_SIZE = 0.2
VALIDATION_SIZE = 0.1
RF_DEFAULTS = {'n_estimators': 200, 'max_depth': 10, 'min_samples_split': 5,
               'min_samples_leaf': 2, 'random_state': RANDOM_SEED, 'n_jobs': -1}

REGRESSION_CONFIG = {
    'horizons': [10, 30, 60],
    'horizon_weights': [0.5, 0.3, 0.2],
    'min_ratio': 3.0,
    'min_abs_move_atr': 1.5,        # мин движение в ATR для сигнала (PnL sim + inference)
    'move_exists_thresholds': {10: 1.5, 30: 2.0, 60: 2.5},  # per-horizon ATR множители для move_exists классификатора
    'atr_period': 14,
    'max_bars_ratio': 0.8,       # макс доля горизонта для времени до цели
    'use_atr_norm': True,        # ATR-нормализация целей
    'use_bars_target': True,     # предсказание времени до цели
    'use_move_classifier': True, # двухстадийный классификатор
    'breakeven_wr': 0.33,
    'xgb_peak_weight': 1.0,      # вес peak в loss (дисбаланс вверх/вниз)
    'xgb_trough_weight': 1.0,
}

RL_CONFIG = {
    'n_epochs': 20,
    'episode_size': 2000,
    'atr_mult_sl': 1.5,
    'atr_mult_tp': 3.0,
    'hidden_dim': 128,
    'lr': 1e-3,
    'gamma': 0.99,
    'epsilon_start': 1.0,
    'epsilon_end': 0.01,
    'epsilon_decay_steps': 10000,
    'buffer_capacity': 50000,
    'batch_size': 128,
    'target_update': 200,
}

TARGET_CONFIG = MARKET_TARGET_CONFIGS['moex'].copy()  # по умолчанию MOEX

# Параметры риск-менеджмента для TradeManager
# ВНИМАНИЕ: sl_mult/tp_mult здесь — глобальный fallback (для unknown тикеров).
# Для реального исполнения используется `config.get_risk_params(ticker)`,
# который берёт per-market значения из MARKET_TARGET_CONFIGS — это синхронизирует
# training RR (targets.py → compute_swing_dual_outcomes) и live RR (manager.py).
# До этого баг-фикса crypto торговался с SL=3×ATR вместо 4×ATR (тренировочного),
# а forex с 3×ATR вместо 2×ATR — live RR был несовместим с обучением модели.
RISK_CONFIG = {
    'risk_per_trade': 0.01,       # 1% риска на сделку
    'sl_mult': 4.0,                # DEPRECATED fallback; используйте get_risk_params(ticker)
    'tp_mult': 8.0,                # DEPRECATED fallback; используйте get_risk_params(ticker)
    'reentry_delay_hours': 4,      # защита от каскада: не входить N часов после SL
    'time_stop_hours': 48,         # принудительное закрытие через N часов
    'weekend_filter': True,        # не торговать в выходные
    'max_atr_ratio': 0.05,         # DEPRECATED global fallback; per-market: get_risk_params(ticker)
    # Ladder Exit Strategy (v12.8, scale v12.9)
    'ladder_enabled': True,        # включить лесенку
    'ladder_tiers': [              # (fraction_to_close, tp_atr_mult) — доля объёма и TP множитель
        # Базовые множители МАСШТАБИРУЮТСЯ: scale = ticker_tp_mult / 6.0.
        # Для MOEX (tp_mult=8) реальные тиры: 4.0 / 6.0 / 8.0×ATR.
        # Для SBER (tp_mult=5): 2.5 / 3.75 / 5.0×ATR.
        (0.4, 3.0),                # Tier 1: закрыть 40% при TP=3×ATR × scale
        (0.3, 4.5),                # Tier 2: закрыть 30% при TP=4.5×ATR × scale
        (0.3, 6.0),                # Tier 3: закрыть 30% при TP=6×ATR × scale
    ],
    'ladder_sl_after_tier': {      # SL adjustment after each tier closes (in ATR from entry)
        1: 0.0,                    # После Tier 1 → SL=entry (безубыток)
        2: 1.0,                    # После Tier 2 → SL=entry + 1×ATR (фиксация части профита)
    },
}

# ── Auto-disable losers (audit 2026-08-03) ───────────────────────────────
# Механизм автоматической блокировки тикеров, которые стабильно теряют деньги.
# Если за последние `min_trades` закрытых сделок PnL < `min_window_pnl_pct` от
# текущего депозита — тикер блокируется на `cooldown_hours` (сигналы NEUTRAL).
# Это останавливает «кровотечение» на тикерах типа SBER 12/12 SHORT SL
# (−4367 ₽) или GAZP 8/8 SHORT SL (−3318 ₽).
TICKER_AUTO_DISABLE = {
    'enabled':            True,    # master switch
    'min_trades':        10,       # минимум сделок для оценки (меньше — пропускаем)
    'min_window_pnl_pct': -0.15,   # если PnL/частичный PnL < −15% депозита → disable
    'cooldown_hours':     168,     # 7 дней блокировки
    # Ручной чёрный список — добавлять тикеры, если модель переобучена/битая:
    # 'blacklist': ['SBER', 'GAZP'],  # раскомментировать при необходимости
    'blacklist':          [],
}

# ── Manual blacklist (codename: empty = нет ручных блокировок) ──────
# Тикеры здесь полностью блокируются (NEUTRAL) — вне зависимости от auto-disable.
# Управляется вручную при обнаружении битой модели или данных.
TICKER_BLACKLIST = set(TICKER_AUTO_DISABLE.get('blacklist', []))

# Per-ticker асимметричные пороги (v12.5)
# Long: вероятность BUY при которой генерируется сигнал
# Short: вероятность SELL при которой генерируется сигнал
# Значения подобраны эмпирически через opt_th sweep по каждому тикеру
TICKER_THRESHOLDS = {
    'ASTR':  {'long': 0.48, 'short': 0.38},
    'GAZP':  {'long': 0.48, 'short': 0.48},
    'LKOH':  {'long': 0.48, 'short': 0.48},
    'MOEX':  {'long': 0.48, 'short': 0.37},
    'MTSS':  {'long': 0.48, 'short': 0.31},
    'NSVZ':  {'long': 0.48, 'short': 0.30},
    'NVTK':  {'long': 0.48, 'short': 0.31},
    'PHOR':  {'long': 0.48, 'short': 0.31},
    'PLZL':  {'long': 0.48, 'short': 0.30},
    'ROSN':  {'long': 0.48, 'short': 0.30},
    'SBER':  {'long': 0.48, 'short': 0.40},
    'SNGSP': {'long': 0.48, 'short': 0.44},
    'VTBR':  {'long': 0.48, 'short': 0.48},
    'X5':    {'long': 0.48, 'short': 0.46},
}
# Ticker-specific class_weight для short RF (FN penalty)
# v3: снижены с 4.5→3.0 и 3.5→2.5 для баланса accuracy vs recall
TICKER_SHORT_WEIGHTS = {
    'ASTR':  3.0, 'GAZP':  3.0, 'LKOH':  3.0,
    'MOEX':  2.5, 'MTSS':  2.0, 'NSVZ':  2.0,
    'NVTK':  2.5, 'PHOR':  2.5, 'PLZL':  2.0,
    'ROSN':  2.5, 'SBER':  3.0, 'SNGSP': 3.0,
    'VTBR':  3.0, 'X5':    3.0,
}
# Ticker-specific scale_pos_weight multiplier для LONG XGBoost.
# Значения: 1.5–3.0. Выше — для тикеров с сильным дисбалансом outcome_long.
# Снимает cap scale_pos_long с 5→15, плюс множитель для усиления сигнала.
TICKER_LONG_WEIGHTS = {
    'ASTR': 3.0,  'GAZP': 2.0,  'LKOH': 2.5,
    'MOEX': 2.5,  'MTSS': 3.0,  'NSVZ': 2.5,
    'NVTK': 1.5,  'PHOR': 2.5,  'PLZL': 3.0,
    'ROSN': 2.5,  'SBER': 2.0,  'SNGSP': 3.0,
    'VTBR': 2.0,  'X5':   3.0,
}

# Default thresholds and weights (consolidated)
DEFAULT_CONFIG = {
    'long_threshold': 0.55,
    'short_threshold': 0.42,
    'short_weight': 3.0,
    'long_weight': 2.0,
}

# ── MOEX: отключить SHORT на уровне обучения и инференса (audit 2026-08-06) ──
# MOEX инструменты (акции российских компаний) не имеют прямой возможности
# short-selling на основной бирже (нет коротких продаж, нужен маржинальный
# счёт). Поэтому для всех MOEX-тикеров:
#   - Training: outcome_short не вычисляется, rf_short не обучается
#   - Inference: p_short = 0, сигнал всегда BUY или NEUTRAL
# Crypto/Forex остаются двусторонними (BUY + SELL).
MOEX_ONLY_LONG = True
SHORT_TRAIN_DISABLED_MOEX = True  # skip outcome_short при обучении

def is_moex_only_long(ticker: str) -> bool:
    """True если тикер относится к MOEX и включён режим только-LONG."""
    return MOEX_ONLY_LONG and get_market(ticker) == 'moex'

def should_disable_short_training(ticker: str) -> bool:
    """True если short-таргеты не должны вычисляться при обучении для этого тикера."""
    return SHORT_TRAIN_DISABLED_MOEX and get_market(ticker) == 'moex'

# ─────────────────────────────────────────────────────────────────────
# EMERGENCY KILL-SWITCH (audit 2026-07-31)
# ВРЕМЕННЫЙ список тикеров, для которых SHORT-сигналы принудительно
# нейтрализуются. Основание: live-сделки 21-31 июля показали WR=0% на
# short для этого набора (см. db-explorer отчёт по trades_closed).
# Запрет снимается после переобучения моделей на новых таргетах
# (swing-SL + исправленные эксперты) и подтверждения WR > breakeven
# на walk-forward. Чтобы временно отключить весь kill-switch — очистить
# список или SHORT_BLOCKLIST_ENABLED=False.
# ─────────────────────────────────────────────────────────────────────
SHORT_BLOCKLIST_ENABLED = True
SHORT_BLOCKLIST = set()  # очищен 2026-08-11: модели переобучены (SBER 41-признак, EURUSD 41-признак с SHORT-порогом 0.73 и т.д.)
# Также EURUSD полностью исключён из MONITOR_TICKERS ниже (как
#坏的 data source). Это действие можно откатить после восстановления feed.
LONG_BLOCKLIST = set()  # long сторона здоровая — пусто

# Backward compatibility aliases
DEFAULT_LONG_TH = DEFAULT_CONFIG['long_threshold']
DEFAULT_SHORT_TH = DEFAULT_CONFIG['short_threshold']
DEFAULT_SHORT_WEIGHT = DEFAULT_CONFIG['short_weight']
DEFAULT_LONG_WEIGHT = DEFAULT_CONFIG['long_weight']

# Rolling WinRate filter — динамическая блокировка сигналов
# Если rolling WR направления ниже breakeven — сигнал отменяется
# +2% запас, как в find_best_thr
# Rolling WinRate filter — динамическая блокировка сигналов
# Если rolling WR направления ниже breakeven — сигнал отменяется
# +2% запас, как в find_best_thr
# Значения по умолчанию — для MOEX (SL=4, TP=8 → breakeven=4/12+0.02=0.353)
SL_MULT = MARKET_TARGET_CONFIGS['moex']['atr_mult_sl']  # 4.0
TP_MULT = MARKET_TARGET_CONFIGS['moex']['atr_mult_tp']  # 8.0
ROLLING_WR_BREAKEVEN = 0.35  # RR 1:2 breakeven = 4/(4+8) = 0.333 + 2% запас = 0.353.
                               # 0.30 был ниже breakeven — отменял прибыльные сигналы (WR 0.30–0.333).
                               # 0.35 ≈ threshold search breakeven (0.353), корректный фильтр.
ROLLING_WR_MIN_SAMPLES = 30  # минимум resolved свечей для статистики

try:
    import torch
    _CUDA_AVAILABLE = torch.cuda.is_available()
    _CUDA_VERSION = torch.version.cuda if torch.version.cuda else 'N/A'
    _GPU_COUNT = torch.cuda.device_count()
    _GPU_NAME = torch.cuda.get_device_name(0) if _CUDA_AVAILABLE else 'N/A'
except ImportError:
    _CUDA_AVAILABLE = False
    _CUDA_VERSION = 'N/A'
    _GPU_COUNT = 0
    _GPU_NAME = 'N/A'

# ── Device separation: train → CUDA, inference → CPU ──────────────────────
# Training (LSTM experts + XGBoost) runs on GPU for speed.
# Inference (predict, monitor) runs on CPU — no need for GPU, avoids OOM.
# Use set_device(mode) before importing models/experts.py to switch.

TRAIN_DEVICE = 'cuda' if _CUDA_AVAILABLE else 'cpu'
INFERENCE_DEVICE = 'cpu'  # always CPU for inference

# Current DEVICE — default to TRAIN (training mode by default).
# IMPORTANT: Call set_device('inference') before loading models for prediction.
DEVICE = TRAIN_DEVICE


def set_device(mode: str = 'auto') -> str:
    """
    Switches global DEVICE for training vs inference.

    Args:
        mode: 'train' → CUDA (if available), 'inference' → CPU, 'auto' → auto-detect.

    Returns:
        The device string that was set.

    IMPORTANT: Call this BEFORE importing models.experts in training scripts,
    because experts.py reads config.DEVICE at module load time.
    """
    global DEVICE
    if mode == 'train':
        DEVICE = TRAIN_DEVICE  # 'cuda' if available, else 'cpu'
    elif mode == 'inference':
        DEVICE = INFERENCE_DEVICE  # always 'cpu'
    elif mode == 'auto':
        DEVICE = 'cuda' if _CUDA_AVAILABLE else 'cpu'
    else:
        DEVICE = mode
    return DEVICE


def print_device_info() -> str:
    """Returns a formatted string with current device info."""
    return (
        f"Device: {DEVICE} | CUDA avail: {_CUDA_AVAILABLE} | "
        f"CUDA ver: {_CUDA_VERSION} | GPU: {_GPU_COUNT} × {_GPU_NAME}"
    )

# Feature ranking — используется directional model с --top-k.
# Актуальный рейтинг: TOP_FEATURES_BY_RANK в main.py.

# Конфигурация моделей
MODEL_CONFIG = {
    'rf': {
        'n_estimators': 200,
        'max_depth': 10,
        'random_state': RANDOM_SEED,
        'n_jobs': -1,
        'class_weight': 'balanced'
    },
    'xgb': {
        'n_estimators': 300,
        'max_depth': 6,
        'learning_rate': 0.1,
        'subsample': 0.8,
        'colsample_bytree': 0.8,
        'eval_metric': 'logloss',
        'random_state': RANDOM_SEED,
        'n_jobs': -1,
        'verbosity': 0,
        'scale_pos_weight': 2.5,  # Фаза 6: штраф за пропуск редкого класса
    },
    'lightgbm': {
        'n_estimators': 500,
        'max_depth': 8,
        'learning_rate': 0.05,
        'subsample': 0.8,
        'colsample_bytree': 0.8,
        'reg_alpha': 0.1,
        'reg_lambda': 0.1,
        'min_child_samples': 20,
        'random_state': RANDOM_SEED,
        'n_jobs': -1,
        'verbosity': -1,
        'scale_pos_weight': 2.5,  # Фаза 6: штраф за пропуск редкого класса
    }
}

# Топ фичей (вынесено из main.py для удобства изменения)
TOP_FEATURES_BY_RANK = [
    'atr_pct', 'bb_width', 'close_to_sma_50', 'macd',
    'bb_width_x_atr_pct', 'return_20', 'macd_hist', 'bb_width_x_rsi',
    'cts50_x_ret20', 'close_to_sma_20', 'strength_trend', 'rsi',
    'trend_signal', 'bb_position', 'close_to_sma_10', 'volume_ratio',
    'macd_signal_accel', 'atr_pct_x_vol_ratio', 'rsi_x_bb_pos', 'signal_strength',
    'return_1', 'bb_signal', 'directional_bias', 'rsi_signal_accel',
    'directional_bias_accel', 'close_to_sma_5', 'return_5', 'momentum_signal',
    'rsi_signal', 'macd_signal', 'hh_streak', 'll_streak',
    'volume_signal', 'engulfing', 'doji',
]
