#!/usr/bin/env python3
"""Summarize the frozen RV-floor experiment from exported Freqtrade results."""

from __future__ import annotations

import json
import zipfile
from datetime import datetime
from pathlib import Path

import matplotlib.pyplot as plt
import matplotlib.dates as mdates


ROOT = Path(__file__).resolve().parent
BASELINE = "VolatilityBreakoutRvWarmupBaseline"
VARIANT = "VolatilityBreakoutRvFloorV1"
PERIODS = {
    "training": ROOT / "training",
    "validation": ROOT / "validation",
}


def load_result(directory: Path) -> dict:
    archive = next(directory.glob("backtest-result-*.zip"))
    with zipfile.ZipFile(archive) as zipped:
        result_name = next(name for name in zipped.namelist() if name.endswith(".json") and "_config" not in name)
        return json.loads(zipped.read(result_name))


def trade_key(trade: dict) -> tuple:
    return trade["pair"], trade["open_date"], trade["is_short"]


def summarize_period(result: dict) -> dict:
    summary = {}
    for name in (BASELINE, VARIANT):
        strategy = result["strategy"][name]
        summary[name] = {
            "trades": strategy["total_trades"],
            "profit_abs": strategy["profit_total_abs"],
            "profit_pct": strategy["profit_total"] * 100,
            "profit_factor": strategy["profit_factor"],
            "winrate_pct": strategy["winrate"] * 100,
            "max_drawdown_pct": strategy["max_drawdown_account"] * 100,
            "max_drawdown_abs": strategy["max_drawdown_abs"],
            "p_value": strategy["p_value"],
            "trades_raw": strategy["trades"],
        }

    baseline_map = {trade_key(t): t for t in summary[BASELINE]["trades_raw"]}
    variant_map = {trade_key(t): t for t in summary[VARIANT]["trades_raw"]}
    baseline_only = [baseline_map[k] for k in baseline_map.keys() - variant_map.keys()]
    variant_only = [variant_map[k] for k in variant_map.keys() - baseline_map.keys()]
    summary["path_change"] = {
        "shared": len(baseline_map.keys() & variant_map.keys()),
        "baseline_only": len(baseline_only),
        "baseline_only_profit_abs": sum(t["profit_abs"] for t in baseline_only),
        "variant_only": len(variant_only),
        "variant_only_profit_abs": sum(t["profit_abs"] for t in variant_only),
    }
    for name in (BASELINE, VARIANT):
        summary[name].pop("trades_raw")
    return summary


def cumulative_series(trades: list[dict]) -> tuple[list[datetime], list[float]]:
    ordered = sorted(trades, key=lambda trade: trade["close_timestamp"])
    dates = [datetime.fromtimestamp(trade["close_timestamp"] / 1000) for trade in ordered]
    values = []
    total = 0.0
    for trade in ordered:
        total += trade["profit_abs"]
        values.append(total)
    return dates, values


def render(results: dict, summaries: dict) -> None:
    colors = {BASELINE: "#334155", VARIANT: "#dc2626"}
    labels = {BASELINE: "RiskCap baseline", VARIANT: "RV floor 40%"}
    fig, axes = plt.subplots(2, 2, figsize=(14, 9), constrained_layout=True)

    for ax, period in zip(axes[0], ("training", "validation")):
        for strategy in (BASELINE, VARIANT):
            dates, values = cumulative_series(results[period]["strategy"][strategy]["trades"])
            ax.plot(dates, values, lw=2, color=colors[strategy], label=labels[strategy])
        ax.axhline(0, color="#94a3b8", lw=0.8)
        ax.set_title(f"{period.title()} — cumulative closed PnL")
        ax.set_ylabel("USDT")
        ax.grid(alpha=0.2)
        ax.xaxis.set_major_formatter(mdates.DateFormatter("%m-%d"))
        ax.legend(frameon=False)

    metrics = [
        ("profit_abs", "Profit"),
        ("profit_factor", "Profit factor"),
        ("winrate_pct", "Win rate"),
        ("max_drawdown_pct", "Max drawdown"),
        ("trades", "Trades"),
    ]
    for ax, period in zip(axes[1], ("training", "validation")):
        x = range(len(metrics))
        width = 0.36
        baseline_values = [100.0 for _ in metrics]
        variant_values = [
            summaries[period][VARIANT][key] / summaries[period][BASELINE][key] * 100
            for key, _ in metrics
        ]
        ax.bar([i - width / 2 for i in x], baseline_values, width, color=colors[BASELINE], label=labels[BASELINE])
        variant_bars = ax.bar(
            [i + width / 2 for i in x],
            variant_values,
            width,
            color=colors[VARIANT],
            label=labels[VARIANT],
        )
        ax.bar_label(variant_bars, labels=[f"{value:.0f}%" for value in variant_values], padding=3, fontsize=9)
        ax.set_xticks(list(x), [label for _, label in metrics])
        ax.set_ylabel("Baseline = 100")
        ax.set_title(f"{period.title()} — relative official metrics")
        ax.axhline(100, color="#64748b", lw=0.8)
        ax.grid(axis="y", alpha=0.2)
        ax.legend(frameon=False)

    fig.suptitle("D48 single-factor experiment: reject low-RV entry filter", fontsize=16, weight="bold")
    fig.savefig(ROOT / "comparison.png", dpi=160)
    plt.close(fig)


def write_report(summaries: dict) -> None:
    train = summaries["training"]
    valid = summaries["validation"]

    def row(period: str, data: dict, strategy: str) -> str:
        item = data[strategy]
        label = "基线" if strategy == BASELINE else "RV过滤"
        return (
            f"| {period} | {label} | {item['trades']} | {item['profit_abs']:.3f} | "
            f"{item['profit_factor']:.3f} | {item['winrate_pct']:.1f}% | "
            f"{item['max_drawdown_pct']:.2f}% |"
        )

    report = f"""# D48 单因子优化实验：低波动入场过滤

## 结论

**判废，不进入前向验证，不改生产配置。**

候选仅增加一个因果条件：当 5 分钟收益率计算的 1 小时实现波动率，不高于其过去 7 日滚动分布的 40 分位时，禁止原 D48 入场。训练段略有改善，但独立验证段收益下降 41.42%，最大回撤扩大 76.0%，方向不稳定。

## 冻结口径

- 基线：`VolatilityBreakoutRvWarmupBaseline`，除启动蜡烛数与候选一致外，继承线上 RiskCap 逻辑。
- 候选：`VolatilityBreakoutRvFloorV1`。
- 唯一因子：`rv_fast_percentile_7d > 0.40`。
- 训练段：2026-01-01 至 2026-05-01；有效回测从 2026-01-08 01:05 UTC 开始。
- 验证段：2026-05-01 至 2026-07-10。
- 共同参数：5m、12 pairs、2x isolated futures、最多 4 仓、手续费 0.035%、protections 开启。
- 数据与市场元数据冻结在本地；未访问交易账户，未在生产机执行重回测。

## 结果

| 区间 | 策略 | 交易数 | 净收益 USDT | PF | 胜率 | 最大回撤 |
|---|---|---:|---:|---:|---:|---:|
{row("训练", train, BASELINE)}
{row("训练", train, VARIANT)}
{row("验证", valid, BASELINE)}
{row("验证", valid, VARIANT)}

训练段净收益变化：{train[VARIANT]['profit_abs'] - train[BASELINE]['profit_abs']:+.3f} USDT；最大回撤变化：{train[VARIANT]['max_drawdown_pct'] - train[BASELINE]['max_drawdown_pct']:+.2f} 个百分点。

验证段净收益变化：{valid[VARIANT]['profit_abs'] - valid[BASELINE]['profit_abs']:+.3f} USDT；最大回撤变化：{valid[VARIANT]['max_drawdown_pct'] - valid[BASELINE]['max_drawdown_pct']:+.2f} 个百分点。

## 路径归因

由于最多同时持有 4 仓，过滤一次入场会释放/改变后续仓位槽位，所以候选交易并非简单的基线交易子集：

- 训练段：共同交易 {train['path_change']['shared']}；仅基线 {train['path_change']['baseline_only']} 笔（合计 {train['path_change']['baseline_only_profit_abs']:+.3f} USDT）；仅候选 {train['path_change']['variant_only']} 笔（合计 {train['path_change']['variant_only_profit_abs']:+.3f} USDT）。
- 验证段：共同交易 {valid['path_change']['shared']}；仅基线 {valid['path_change']['baseline_only']} 笔（合计 {valid['path_change']['baseline_only_profit_abs']:+.3f} USDT）；仅候选 {valid['path_change']['variant_only']} 笔（合计 {valid['path_change']['variant_only_profit_abs']:+.3f} USDT）。

这也解释了为什么离线分桶显示“低 RV 桶偏弱”，但完整组合回测反而变差：过滤改变了仓位竞争和后续入场路径，不能把单笔事后分桶直接当成组合优化。

## 决策

保留当前 RiskCap 生产策略。此候选未达到“训练与验证方向一致”的最低门槛；无需做 lookahead/recursive 分析、dry-run A/B 或微额实盘。下一轮若继续优化，应优先研究执行层或退出层，但一次只改一个因子，并保持同样的独立验证流程。
"""
    (ROOT / "report.md").write_text(report, encoding="utf-8")


def main() -> None:
    results = {period: load_result(directory) for period, directory in PERIODS.items()}
    summaries = {period: summarize_period(result) for period, result in results.items()}
    (ROOT / "metrics.json").write_text(json.dumps(summaries, indent=2), encoding="utf-8")
    render(results, summaries)
    write_report(summaries)


if __name__ == "__main__":
    main()
