"""
백테스팅 프레임워크 (v3.2 — Realism 보강)

개선 사항 (v3.2):
1. 다음 봉 시가 체결 옵션 (look-ahead bias 제거)
2. 증권거래세 (매도 시 0.2%, 수수료와 별개 부과)
3. 거래량 기반 부분 체결 (한 봉 거래량의 N% 상한)
4. Calmar, Payoff, Turnover 지표 추가
"""

import numpy as np
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Callable


@dataclass
class Trade:
    ticker: str
    entry_date: int
    entry_price: float
    exit_date: int
    exit_price: float
    qty: int
    direction: str = "LONG"

    @property
    def pnl(self):
        if self.direction == "LONG":
            return (self.exit_price - self.entry_price) * self.qty
        return (self.entry_price - self.exit_price) * self.qty

    @property
    def pnl_pct(self):
        return (self.exit_price - self.entry_price) / self.entry_price * 100


@dataclass
class BacktestResult:
    total_return_pct: float
    sharpe_ratio: float
    max_drawdown_pct: float
    win_rate: float
    avg_win_pct: float
    avg_loss_pct: float
    profit_factor: float
    total_trades: int
    winning_trades: int
    losing_trades: int
    calmar_ratio: float = 0.0
    payoff_ratio: float = 0.0      # 평균수익 / |평균손실|
    turnover_ratio: float = 0.0    # 총 매매대금 / 초기자본
    trades: List[Trade] = field(default_factory=list)

    def summary(self) -> str:
        lines = [
            "=" * 50,
            "📊 백테스팅 결과 (v3.2)",
            "=" * 50,
            f"총 수익률:      {self.total_return_pct:+.2f}%",
            f"Sharpe Ratio:   {self.sharpe_ratio:.3f}",
            f"Calmar Ratio:   {self.calmar_ratio:.3f}",
            f"Max Drawdown:   {self.max_drawdown_pct:.2f}%",
            f"승률:           {self.win_rate:.1f}% ({self.winning_trades}/{self.total_trades})",
            f"평균 수익:      {self.avg_win_pct:+.2f}%",
            f"평균 손실:      {self.avg_loss_pct:.2f}%",
            f"손익비(PF):     {self.profit_factor:.2f}",
            f"Payoff Ratio:   {self.payoff_ratio:.2f}",
            f"Turnover:       {self.turnover_ratio:.2f}x",
            "=" * 50,
        ]
        return "\n".join(lines)


class Backtester:
    """
    OHLCV 기반 전략 백테스터.

    체결 모델 (v3.2):
      - next_bar_open=True: 신호 발생 다음 봉 시가로 체결 (look-ahead 제거)
      - slippage: 체결가에 ±slippage_rate 적용
      - commission_rate: 매수/매도 양쪽에 부과 (증권사 수수료)
      - sell_tax_rate: 매도 시에만 부과 (증권거래세 0.2%)
      - max_volume_participation: 봉 거래량의 N% 이하로 체결 제한
    """

    def __init__(self,
                 initial_capital: float = 10_000_000,
                 commission_rate: float = 0.00015,
                 slippage_rate: float = 0.001,
                 sell_tax_rate: float = 0.002,
                 next_bar_open: bool = True,
                 max_volume_participation: float = 0.01):
        self.initial_capital = initial_capital
        self.commission_rate = commission_rate
        self.slippage_rate = slippage_rate
        self.sell_tax_rate = sell_tax_rate
        self.next_bar_open = next_bar_open
        self.max_volume_participation = max_volume_participation

    # ──────────────────────────────────────────────
    # 단일 종목
    # ──────────────────────────────────────────────
    def run(self, ohlcv_data: Dict, strategy_fn: Callable,
            ticker: str = "UNKNOWN", warmup: int = 60) -> BacktestResult:
        closes = np.array(ohlcv_data.get('close', []), dtype=float)
        opens = np.array(ohlcv_data.get('open', closes), dtype=float)
        highs = np.array(ohlcv_data.get('high', closes), dtype=float)
        lows = np.array(ohlcv_data.get('low', closes), dtype=float)
        volumes = np.array(ohlcv_data.get('volume', np.zeros_like(closes)), dtype=float)

        n = len(closes)
        if n < warmup + 10:
            return self._empty_result()

        capital = self.initial_capital
        position = 0
        entry_price = 0.0
        entry_idx = 0
        equity_curve = [capital]
        trades: List[Trade] = []
        total_turnover = 0.0   # 누적 매매대금

        # 마지막 인덱스는 next-bar 체결 시 여유 필요
        last_signal_idx = n - 2 if self.next_bar_open else n - 1

        for i in range(warmup, last_signal_idx + 1):
            slice_data = {
                'close': closes[:i+1].tolist(),
                'open': opens[:i+1].tolist(),
                'high': highs[:i+1].tolist(),
                'low': lows[:i+1].tolist(),
                'volume': volumes[:i+1].tolist(),
            }
            signal = "HOLD"
            try:
                signal = strategy_fn(slice_data)
            except Exception:
                pass

            # 체결가 산출 — next_bar_open이면 i+1 시가, 아니면 i 종가
            fill_idx = i + 1 if self.next_bar_open and i + 1 < n else i
            base_price = opens[fill_idx] if self.next_bar_open else closes[fill_idx]
            fill_volume = volumes[fill_idx]

            buy_price = base_price * (1 + self.slippage_rate)
            sell_price = base_price * (1 - self.slippage_rate)

            if signal == "BUY" and position == 0:
                # 전액 투자 (수수료 포함 총비용 기준)
                raw_qty = int(capital / (buy_price * (1 + self.commission_rate)))
                # 거래량 상한 — 봉 거래량의 N%까지만 체결
                vol_cap = int(fill_volume * self.max_volume_participation)
                qty = min(raw_qty, vol_cap) if vol_cap > 0 else raw_qty
                if qty > 0:
                    cost = qty * buy_price * (1 + self.commission_rate)
                    capital -= cost
                    position = qty
                    entry_price = buy_price
                    entry_idx = fill_idx
                    total_turnover += qty * buy_price

            elif signal == "SELL" and position > 0:
                # 매도: 수수료 + 증권거래세
                sell_cost_rate = self.commission_rate + self.sell_tax_rate
                vol_cap = int(fill_volume * self.max_volume_participation) if fill_volume > 0 else position
                exec_qty = min(position, vol_cap) if vol_cap > 0 else position
                proceeds = exec_qty * sell_price * (1 - sell_cost_rate)
                capital += proceeds
                total_turnover += exec_qty * sell_price
                trades.append(Trade(
                    ticker=ticker,
                    entry_date=entry_idx,
                    entry_price=entry_price,
                    exit_date=fill_idx,
                    exit_price=sell_price,
                    qty=exec_qty
                ))
                position -= exec_qty
                if position == 0:
                    entry_price = 0.0

            current_equity = capital + (position * closes[i] if position > 0 else 0)
            equity_curve.append(current_equity)

        # 미청산 포지션: 마지막 종가 기준 강제 청산 (수수료+세금 반영)
        if position > 0:
            last_price = closes[-1] * (1 - self.slippage_rate)
            sell_cost_rate = self.commission_rate + self.sell_tax_rate
            proceeds = position * last_price * (1 - sell_cost_rate)
            capital += proceeds
            total_turnover += position * last_price
            trades.append(Trade(
                ticker=ticker,
                entry_date=entry_idx,
                entry_price=entry_price,
                exit_date=n - 1,
                exit_price=last_price,
                qty=position
            ))
            equity_curve[-1] = capital
            position = 0

        return self._compute_metrics(equity_curve, trades, total_turnover)

    def run_multi(self, ohlcv_dict: Dict[str, Dict], strategy_fn: Callable,
                  warmup: int = 60) -> Dict[str, BacktestResult]:
        return {t: self.run(d, strategy_fn, t, warmup) for t, d in ohlcv_dict.items()}

    # ──────────────────────────────────────────────
    # 지표 계산
    # ──────────────────────────────────────────────
    def _compute_metrics(self, equity_curve: List[float], trades: List[Trade],
                         total_turnover: float) -> BacktestResult:
        equity = np.array(equity_curve, dtype=float)
        total_return_pct = (equity[-1] / equity[0] - 1) * 100

        daily_returns = np.diff(equity) / (equity[:-1] + 1e-9)
        sharpe = (daily_returns.mean() / daily_returns.std()) * np.sqrt(252) \
            if daily_returns.std() > 0 else 0.0

        peak = np.maximum.accumulate(equity)
        drawdowns = (equity - peak) / (peak + 1e-9) * 100
        max_drawdown = abs(drawdowns.min())

        wins = [t for t in trades if t.pnl_pct > 0]
        losses = [t for t in trades if t.pnl_pct <= 0]

        win_rate = len(wins) / len(trades) * 100 if trades else 0
        avg_win = float(np.mean([t.pnl_pct for t in wins])) if wins else 0.0
        avg_loss = float(np.mean([t.pnl_pct for t in losses])) if losses else 0.0

        total_win = sum(t.pnl for t in wins)
        total_loss = abs(sum(t.pnl for t in losses))
        profit_factor = total_win / (total_loss + 1e-9)

        # 신규 지표
        calmar = (total_return_pct / max_drawdown) if max_drawdown > 0 else 0.0
        payoff = (avg_win / abs(avg_loss)) if avg_loss != 0 else 0.0
        turnover_ratio = total_turnover / (self.initial_capital + 1e-9)

        return BacktestResult(
            total_return_pct=round(total_return_pct, 2),
            sharpe_ratio=round(sharpe, 3),
            max_drawdown_pct=round(max_drawdown, 2),
            win_rate=round(win_rate, 1),
            avg_win_pct=round(avg_win, 2),
            avg_loss_pct=round(avg_loss, 2),
            profit_factor=round(profit_factor, 3),
            total_trades=len(trades),
            winning_trades=len(wins),
            losing_trades=len(losses),
            calmar_ratio=round(calmar, 3),
            payoff_ratio=round(payoff, 3),
            turnover_ratio=round(turnover_ratio, 3),
            trades=trades,
        )

    def _empty_result(self) -> BacktestResult:
        return BacktestResult(
            total_return_pct=0.0, sharpe_ratio=0.0, max_drawdown_pct=0.0,
            win_rate=0.0, avg_win_pct=0.0, avg_loss_pct=0.0,
            profit_factor=0.0, total_trades=0, winning_trades=0, losing_trades=0
        )


def compare_strategies(ohlcv_data: Dict, strategies: Dict[str, Callable],
                       initial_capital: float = 10_000_000) -> Dict[str, BacktestResult]:
    bt = Backtester(initial_capital=initial_capital)
    results = {}
    for name, fn in strategies.items():
        results[name] = bt.run(ohlcv_data, fn)
        print(f"\n[{name}]")
        print(results[name].summary())
    return results
