#!/usr/bin/env python3
"""Dry-run 实时循环：拉取最新已收盘 K 线，走与回测完全相同的策略/执行器代码，
只模拟成交（不下真实订单）。

用法:
    python scripts/run_paper.py --strategy vol_breakout --interval 300

实盘开关：本脚本刻意【不】提供真实下单。若要实盘，需在 .env 配置
BINANCE_API_KEY/BINANCE_API_SECRET 并自行实现 connector 下单层——
在 trend-grid 研究否决证据与 vol_breakout 仅有 dry-run 前向的现状下，
不建议直接实盘。
"""
import argparse
import logging
import sys
import time
from pathlib import Path

import pandas as pd
import requests

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from backtest.engine import BacktestContext           # noqa: E402

logging.basicConfig(level=logging.INFO,
                    format="%(asctime)s %(levelname)s %(name)s %(message)s")
logger = logging.getLogger("paper")

MAJORS = ["BTC/USDT:USDT", "ETH/USDT:USDT", "SOL/USDT:USDT", "BNB/USDT:USDT"]

# 本机 fapi.binance.com 区域受限；现货镜像与 USDT-M 永续价差对主流币可忽略。
SPOT_MIRROR = "https://data-api.binance.vision/api/v3/klines"


def fetch_recent(pair: str, timeframe: str, limit: int) -> pd.DataFrame:
    sym = pair.split(":")[0].replace("/", "")
    r = requests.get(SPOT_MIRROR, params={"symbol": sym, "interval": timeframe,
                                          "limit": limit}, timeout=15)
    r.raise_for_status()
    rows = r.json()
    df = pd.DataFrame(rows, columns=["ts_ms", "open", "high", "low", "close",
                                     "volume", "close_time", "qv", "n",
                                     "tbv", "tbqv", "ig"])
    df = df.astype({"ts_ms": "int64", "open": float, "high": float,
                    "low": float, "close": float, "volume": float})
    df["ts"] = (df["ts_ms"] // 1000).astype(int)
    return df[["ts", "open", "high", "low", "close", "volume"]]


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--strategy", choices=["vol_breakout", "trend_grid"],
                    default="vol_breakout")
    ap.add_argument("--pairs", nargs="+", default=MAJORS)
    ap.add_argument("--timeframe", default="5m")
    ap.add_argument("--fee", type=float, default=0.00035)
    ap.add_argument("--wallet", type=float, default=1000.0)
    args = ap.parse_args()

    if args.strategy == "vol_breakout":
        from bot.strategies.volatility_breakout import VolatilityBreakoutController as C
    else:
        from bot.strategies.trend_grid import TrendGridController as C

    controller = C(pairs=args.pairs)
    ctx = BacktestContext(args.fee, args.wallet)
    controller.ctx = ctx
    active, pending = [], []
    last_ts: dict[str, int] = {p: 0 for p in args.pairs}

    logger.info("paper 启动 strategy=%s pairs=%s wallet=%.0f（只模拟，不下单）",
                args.strategy, args.pairs, args.wallet)

    while True:
        for pair in args.pairs:
            try:
                df = fetch_recent(pair, args.timeframe, controller.warmup_candles + 50)
                btc_1d = fetch_recent("BTC/USDT:USDT", "1d", 260)
                full = controller.prepare_dataframe(pair, df, btc_1d)
                # 只处理新收盘的 K 线（最后一根未收盘，取倒数第二根）
                closed = full.iloc[:-1]
                if closed.empty or int(closed.iloc[-1]["ts"]) <= last_ts[pair]:
                    continue
                candle = closed.iloc[-1]
                ts = float(candle["ts"])
                last_ts[pair] = int(ts)

                # 待入场：以"当前价≈次根开盘"成交（paper 近似）
                for e in [e for e in pending if e.pair == pair]:
                    px = float(candle["close"])
                    stake = getattr(e, "planned_stake", 0.0)
                    amount = stake * e.leverage / px if px > 0 else 0.0
                    if amount > 0 and stake <= ctx.free_wallet:
                        ctx.apply_open_fill(e, px, amount, ts)
                        e.created_ts = ts
                        active.append(e)
                        logger.info("[paper] 入场 %s %s amount=%.6f @%.2f",
                                    pair, e.side.value, amount, px)
                    pending.remove(e)

                for e in [e for e in active if e.pair == pair and e.is_active]:
                    n0 = len(e.trades)
                    e.on_candle(candle, ctx)
                    for t in e.trades[n0:]:
                        notify = getattr(controller, "notify_trade_closed", None)
                        if notify:
                            notify(t, ts)
                        logger.info("[paper] 平仓 %s %s pnl=%.2f reason=%s",
                                    pair, e.side.value, t.pnl_quote, t.close_type.value)
                    if not e.is_active:
                        active.remove(e)

                controller.dataframes[pair] = closed
                for e in controller.on_candle(pair, ts, active):
                    if getattr(e, "entry_at_next_open", False):
                        pending.append(e)
                    else:
                        e.created_ts = ts
                        active.append(e)
            except requests.RequestException as err:
                logger.warning("网络错误 %s: %s", pair, err)
            except Exception:
                logger.exception("处理 %s 出错", pair)

        invested = sum(e.margin_reserved for e in active)
        logger.info("[paper] 心跳 wallet=%.2f 活动仓位=%d 占用保证金=%.2f",
                    ctx.free_wallet, len(active), invested)
        time.sleep(60)


if __name__ == "__main__":
    main()
