"""
1단계: walk-forward·민감도·실거래 한도 가정 시뮬·Go/No-Go 리포트.
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import numpy as np
import pandas as pd

from config import (
    GT_INITIAL_CASH_KRW,
    LIVE_ORDER_KRW,
    LIVE_SLIPPAGE_PCT,
    MATCH_HOLDOUT_RATIO,
    MATCH_MIN_EV_VALID,
    MATCH_MIN_FIRES_HOLDOUT,
    MATCH_MIN_PROFIT_FACTOR,
    MATCH_TRAIN_RATIO,
    SIM_FEE_STRESS_MULT,
    SIM_GO_MIN_HOLDOUT_EV,
    SIM_GO_MIN_HOLDOUT_PF,
    SIM_GO_WF_POSITIVE_RATIO,
    SIM_HYBRID_MAX_MDD_PCT,
    SIM_HYBRID_MIN_HOLDOUT_PNL_PCT,
    SIM_OPTION_C_MIN_GT_CAPTURE,
    SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO,
    SIM_OPTION_C_PHASE2_TARGET_PNL_PCT,
    SIM_OPTION_C_TARGET_PNL_PCT,
    SIM_WALK_FORWARD_MIN_MONTHS,
    TRADING_FEE_RATE,
)
from deepcoin.ground_truth.ground_truth import (
    load_ground_truth,
    order_trades_chronological,
)
from deepcoin.ground_truth.gt_allocation import simulate_portfolio_summary
from deepcoin.ground_truth.gt_model import (
    default_model,
    model_to_dict,
    summarize_leg_weights,
    weight_policy_summary,
)
from deepcoin.matching.portfolio_sim import (
    fires_to_trade_list,
    simulate_fixed_order_portfolio,
    simulate_sized_portfolio,
    sort_fires_chronological,
)
from deepcoin.matching.select_rules import _rule_metrics, _split_train_valid_holdout
from deepcoin.paths import resolve_ground_truth_file
from deepcoin.paths import (
    ANALYSIS_GT_CALIBRATION_JSON,
    MATCHING_FIRE_OUTCOMES,
    MATCHING_MATCHED_RULES,
    MATCHING_SIMULATION_HTML,
    MATCHING_SIMULATION_JSON,
    resolve_ground_truth_file,
)


def _fee_adjust_ret(series: pd.Series, mult: float) -> pd.Series:
    """
    수수료 스트레스: 왕복 수수료 %p를 (mult-1)배 추가 차감.

    Args:
        series: forward_ret_pct.
        mult: 수수료 배수 (2.0 = 2배).

    Returns:
        조정된 수익률 %.
    """
    extra = TRADING_FEE_RATE * 2 * 100 * (mult - 1.0)
    return series - extra


def walk_forward_by_month(outcomes: pd.DataFrame) -> list[dict[str, Any]]:
    """
    규칙·월별 EV·PF 집계.

    Args:
        outcomes: fire_outcomes.

    Returns:
        월별 행 dict 리스트.
    """
    if outcomes.empty:
        return []
    df = outcomes.copy()
    df["ts"] = pd.to_datetime(df["dt"])
    df["month"] = df["ts"].dt.to_period("M").astype(str)
    rows: list[dict[str, Any]] = []
    for (rid, month), grp in df.groupby(["rule_id", "month"]):
        m = _rule_metrics(grp)
        rows.append(
            {
                "rule_id": rid,
                "side": grp["side"].iloc[0],
                "month": month,
                **m,
            }
        )
    return rows


def walk_forward_summary(wf_rows: list[dict[str, Any]]) -> dict[str, Any]:
    """
    규칙별 월별 EV 양수 비율 요약.

    Args:
        wf_rows: walk_forward_by_month 결과.

    Returns:
        rule_id → {positive_ratio, months, ...}.
    """
    if not wf_rows:
        return {}
    df = pd.DataFrame(wf_rows)
    out: dict[str, Any] = {}
    for rid, grp in df.groupby("rule_id"):
        n = len(grp)
        pos = int((grp["ev_pct"] > 0).sum())
        out[rid] = {
            "months": n,
            "positive_months": pos,
            "positive_ratio": round(pos / n, 4) if n else 0.0,
            "mean_ev_pct": round(float(grp["ev_pct"].mean()), 4),
        }
    return out


def simulate_live_order_cap(
    outcomes: pd.DataFrame,
    *,
    rule_ids: set[str] | None = None,
    holdout_only: bool = True,
) -> dict[str, Any]:
    """
    GT 복리 배분·슬리피지 가정으로 체결 가능한 발화 집계 (일·금액 한도 없음).

    Args:
        outcomes: fire_outcomes (split 컬럼 있으면 holdout 필터 가능).
        rule_ids: None이면 전 규칙, 지정 시 해당 rule만.
        holdout_only: True면 split==holdout 만.

    Returns:
        규칙별·전체 요약.
    """
    if outcomes.empty:
        return {"rules": {}, "note": "발화 없음"}

    df = outcomes.copy()
    if holdout_only and "split" in df.columns:
        df = df[df["split"] == "holdout"]
    if rule_ids is not None:
        df = df[df["rule_id"].isin(rule_ids)]
    slip = LIVE_SLIPPAGE_PCT

    trades = fires_to_trade_list(sort_fires_chronological(df), apply_dynamic_sizing=True)
    executed_dts = {
        t["dt"]
        for t in trades
        if t.get("action") == "sell" or float(t.get("amount_krw") or 0) > 0
    }
    if not executed_dts:
        return {"rules": {}, "taken_count": 0, "total_count": int(len(df))}

    taken = df[df["dt"].astype(str).isin(executed_dts)].copy()
    taken["adj_ret_pct"] = taken["forward_ret_pct"] - slip

    by_rule: dict[str, Any] = {}
    for rid, grp in taken.groupby("rule_id"):
        g = grp.copy()
        g["forward_ret_pct"] = g["adj_ret_pct"]
        by_rule[rid] = {
            "taken_count": int(len(grp)),
            "total_count": int((df["rule_id"] == rid).sum()),
            "metrics": _rule_metrics(g),
        }

    return {
        "assumptions": {
            "slippage_pct": slip,
            "sizing": "gt_model_compound_no_daily_cap",
        },
        "taken_count": int(len(taken)),
        "total_count": int(len(df)),
        "rules": by_rule,
        "portfolio_adj_ev_pct": round(float(taken["adj_ret_pct"].mean()), 4),
    }


def evaluate_go_no_go(
    matched: dict[str, Any],
    wf_summary: dict[str, Any],
    fee_stress: dict[str, Any],
    live_cap: dict[str, Any],
) -> dict[str, Any]:
    """
    monitor_rules·holdout·walk-forward·수수료 스트레스 기준 Go/No-Go.

    Args:
        matched: matched_rules.json 내용.
        wf_summary: walk_forward_summary.
        fee_stress: 규칙별 fee 2x EV.
        live_cap: simulate_live_order_cap.

    Returns:
        go, checks, monitor_rules 판정.
    """
    rules = matched.get("monitor_rules") or matched.get("selected") or []
    checks: list[dict[str, Any]] = []
    all_go = True

    for rule in rules:
        rid = rule["rule_id"]
        h = rule.get("metrics", {}).get("holdout", {})
        ev_h = float(h.get("ev_pct", -999))
        pf_h = float(h.get("profit_factor", 0))
        wf = wf_summary.get(rid, {})
        wf_ratio = float(wf.get("positive_ratio", 0))
        wf_months = int(wf.get("months", 0))
        stress_ev = fee_stress.get(rid, {}).get("ev_pct", -999)

        c_holdout = ev_h >= SIM_GO_MIN_HOLDOUT_EV and pf_h >= SIM_GO_MIN_HOLDOUT_PF
        c_wf = wf_months >= SIM_WALK_FORWARD_MIN_MONTHS and wf_ratio >= SIM_GO_WF_POSITIVE_RATIO
        c_fee = stress_ev >= SIM_GO_MIN_HOLDOUT_EV
        ok = c_holdout and c_wf and c_fee
        if not ok:
            all_go = False
        checks.append(
            {
                "rule_id": rid,
                "side": rule.get("side"),
                "pass": ok,
                "holdout_ev": ev_h,
                "holdout_pf": pf_h,
                "wf_positive_ratio": wf_ratio,
                "fee_stress_ev": stress_ev,
            }
        )

    return {
        "go": all_go and len(checks) > 0,
        "checks": checks,
        "live_cap_taken_ratio": round(
            live_cap.get("taken_count", 0) / max(live_cap.get("total_count", 1), 1),
            4,
        ),
    }


def portfolio_holdout_from_steps(
    steps: list[dict[str, Any]],
    holdout_start: pd.Timestamp,
    *,
    initial_if_empty: float = GT_INITIAL_CASH_KRW,
    trade_count: int = 0,
    note: str = "",
) -> dict[str, Any]:
    """
    포트폴리오 step에서 holdout 구간 자산 증감.

    Args:
        steps: simulate_portfolio_steps 결과.
        holdout_start: holdout 시작 시각.
        initial_if_empty: step 없을 때 시작 자산.
        trade_count: holdout 발화 수.
        note: 설명.

    Returns:
        holdout pnl 요약 dict.
    """
    if not steps:
        return {"pnl_pct": 0.0, "note": "steps empty"}
    assets = [(pd.to_datetime(s["dt"]), float(s["total_asset_krw"])) for s in steps]
    pre = [a for d, a in assets if d < holdout_start]
    in_h = [a for d, a in assets if d >= holdout_start]
    asset_start = pre[-1] if pre else float(initial_if_empty)
    asset_end = in_h[-1] if in_h else assets[-1][1]
    ho_pnl_pct = (
        (asset_end - asset_start) / asset_start * 100.0 if asset_start > 0 else 0.0
    )
    return {
        "initial_asset_krw": round(asset_start, 0),
        "final_asset_krw": round(asset_end, 0),
        "pnl_krw": round(asset_end - asset_start, 0),
        "pnl_pct": round(ho_pnl_pct, 2),
        "trade_count": int(trade_count),
        "note": note,
    }


def evaluate_hybrid_sizing_go(
    base_go: dict[str, Any],
    hybrid_full: dict[str, Any],
    hybrid_holdout: dict[str, Any],
    hybrid_fee_stress: dict[str, Any],
) -> dict[str, Any]:
    """
    hybrid DD tier 배분 승격 Go/No-Go (규칙 Go + holdout·MDD·수수료 스트레스).

    Args:
        base_go: monitor 규칙 evaluate_go_no_go 결과.
        hybrid_full: 전기간 hybrid 포트폴리오 요약.
        hybrid_holdout: holdout 구간 자산 증감.
        hybrid_fee_stress: 수수료 스트레스 hybrid 포트폴리오.

    Returns:
        go, checks, primary_sizing 권장.
    """
    from config import (
        SIM_HYBRID_MAX_MDD_PCT,
        SIM_HYBRID_MIN_HOLDOUT_PNL_PCT,
        SIM_OPTION_C_TARGET_PNL_PCT,
        SIM_PRIMARY_SIZING,
    )

    base_ok = bool(base_go.get("go"))
    ho_pnl = float(hybrid_holdout.get("pnl_pct", -999))
    full_pnl = float(hybrid_full.get("pnl_pct", 0))
    mdd = float(hybrid_full.get("max_drawdown_pct", 999))
    stress_pnl = float(hybrid_fee_stress.get("pnl_pct", -999))

    c_base = base_ok
    c_holdout = ho_pnl >= SIM_HYBRID_MIN_HOLDOUT_PNL_PCT
    c_mdd = mdd <= SIM_HYBRID_MAX_MDD_PCT
    c_fee = stress_pnl > 0.0
    c_target = full_pnl >= SIM_OPTION_C_TARGET_PNL_PCT

    all_go = c_base and c_holdout and c_mdd and c_fee
    if SIM_PRIMARY_SIZING == "hybrid":
        primary = "hybrid"
    elif SIM_PRIMARY_SIZING == "causal_tier":
        primary = "causal_tier"
    else:
        primary = "hybrid" if all_go else "causal_tier"

    return {
        "go": all_go,
        "primary_sizing": primary,
        "checks": [
            {"name": "monitor_rules_go", "pass": c_base},
            {"name": "hybrid_holdout_pnl", "pass": c_holdout, "value": ho_pnl},
            {"name": "hybrid_max_mdd", "pass": c_mdd, "value": mdd},
            {"name": "hybrid_fee_stress_pnl", "pass": c_fee, "value": stress_pnl},
            {
                "name": "option_c_target_300pct",
                "pass": c_target,
                "value": full_pnl,
                "optional": True,
            },
        ],
    }


def simulate_hybrid_order_cap(
    outcomes: pd.DataFrame,
    ohlc_df: pd.DataFrame,
    *,
    rule_ids: set[str] | None = None,
    holdout_only: bool = True,
    fee_rate: float = TRADING_FEE_RATE,
    dd_large_pct: float | None = None,
    dd_medium_pct: float | None = None,
) -> dict[str, Any]:
    """
    hybrid tier 복리 배분·슬리피지 가정 체결 가능 발화 집계.

    Args:
        outcomes: fire_outcomes.
        ohlc_df: 3m OHLC (drawdown).
        rule_ids: monitor rule_id 필터.
        holdout_only: holdout만.
        fee_rate: 수수료율.

    Returns:
        simulate_live_order_cap과 동일 구조.
    """
    from deepcoin.ground_truth.causal_gt_hybrid import build_monitor_hybrid_sized_trades

    if outcomes.empty:
        return {"rules": {}, "note": "발화 없음"}

    df = outcomes.copy()
    if holdout_only and "split" in df.columns:
        df = df[df["split"] == "holdout"]
    if rule_ids is not None:
        df = df[df["rule_id"].isin(rule_ids)]
    slip = LIVE_SLIPPAGE_PCT

    sized, _ = build_monitor_hybrid_sized_trades(
        sort_fires_chronological(df),
        ohlc_df,
        enhanced=False,
        fee_rate=fee_rate,
        dd_large_pct=dd_large_pct,
        dd_medium_pct=dd_medium_pct,
    )
    executed_dts = {
        t["dt"]
        for t in sized
        if t.get("action") == "sell" or float(t.get("amount_krw") or 0) > 0
    }
    if not executed_dts:
        return {"rules": {}, "taken_count": 0, "total_count": int(len(df))}

    taken = df[df["dt"].astype(str).isin(executed_dts)].copy()
    taken["adj_ret_pct"] = taken["forward_ret_pct"] - slip

    by_rule: dict[str, Any] = {}
    for rid, grp in taken.groupby("rule_id"):
        g = grp.copy()
        g["forward_ret_pct"] = g["adj_ret_pct"]
        by_rule[rid] = {
            "taken_count": int(len(grp)),
            "total_count": int((df["rule_id"] == rid).sum()),
            "metrics": _rule_metrics(g),
        }

    return {
        "assumptions": {
            "slippage_pct": slip,
            "sizing": "hybrid_dd_tier_compound",
        },
        "taken_count": int(len(taken)),
        "total_count": int(len(df)),
        "rules": by_rule,
        "portfolio_adj_ev_pct": round(float(taken["adj_ret_pct"].mean()), 4),
    }


def build_simulation_report(
    outcomes_path: Path | None = None,
    matched_path: Path | None = None,
) -> dict[str, Any]:
    """
    시뮬레이션 리포트 dict 생성.

    Args:
        outcomes_path: fire_outcomes.csv.
        matched_path: matched_rules.json.

    Returns:
        simulation_report 전체 dict.
    """
    op = outcomes_path or MATCHING_FIRE_OUTCOMES
    mp = matched_path or MATCHING_MATCHED_RULES
    if not op.is_file():
        raise FileNotFoundError(f"fire_outcomes 없음: {op} — 04_match_rules.py 먼저 실행")
    outcomes = pd.read_csv(op)
    matched: dict[str, Any] = {}
    if mp.is_file():
        matched = json.loads(mp.read_text(encoding="utf-8"))

    outcomes["split"] = _split_train_valid_holdout(outcomes)
    wf_rows = walk_forward_by_month(outcomes)
    wf_sum = walk_forward_summary(wf_rows)

    fee_stress: dict[str, Any] = {}
    for rid in outcomes["rule_id"].unique():
        sub = outcomes[outcomes["rule_id"] == rid]
        adj = _fee_adjust_ret(sub["forward_ret_pct"], SIM_FEE_STRESS_MULT)
        fee_stress[rid] = _rule_metrics(
            sub.assign(forward_ret_pct=adj)
        )

    monitor_ids = {r["rule_id"] for r in matched.get("monitor_rules", [])}
    live_cap = simulate_live_order_cap(
        outcomes, rule_ids=monitor_ids, holdout_only=True
    )
    go = evaluate_go_no_go(matched, wf_sum, fee_stress, live_cap)

    portfolio_compare: dict[str, Any] = {}
    gt_data = load_ground_truth(resolve_ground_truth_file()) or {}
    gt_trades = gt_data.get("trades") or []
    mark = (gt_data.get("summary") or {}).get("mark_price")
    gt_chrono = order_trades_chronological(gt_trades) if gt_trades else []

    from deepcoin.ground_truth.gt_signal_rules import gt_signal_rule_ids
    from config import GT_SIGNAL_CAUSAL, SIM_CAUSAL_TIER
    from deepcoin.matching.position_sizing import load_gt_allocation_analysis

    gt_alloc_analysis = load_gt_allocation_analysis(gt_trades) if gt_trades else {}

    if gt_chrono:
        if not any(float(t.get("amount_krw") or 0) > 0 for t in gt_chrono):
            from deepcoin.ground_truth.ground_truth import allocate_gt_order_amounts

            allocate_gt_order_amounts(gt_chrono)
        portfolio_compare["ground_truth_chrono"] = simulate_portfolio_summary(
            gt_chrono,
            last_price=float(mark) if mark else None,
            use_amount_krw=True,
        )

    # 전기간 monitor 규칙 — GT_INITIAL_CASH_KRW에서 복리 (holdout만 X)
    all_monitor = outcomes[outcomes["rule_id"].isin(monitor_ids)]
    if not all_monitor.empty:
        sim_trades_full = fires_to_trade_list(sort_fires_chronological(all_monitor))
        portfolio_compare["sim_sized"] = simulate_sized_portfolio(
            sim_trades_full,
            last_price=float(mark) if mark else None,
        )
        portfolio_compare["sim_fixed_order"] = simulate_fixed_order_portfolio(
            fires_to_trade_list(all_monitor, apply_dynamic_sizing=False),
            last_price=float(mark) if mark else None,
        )

    # GT 모델 일반화 규칙 (ZigZag+BB 매수 / ZigZag 고점 매도)
    gt_buy_rule = "gt_model_buy_zigzag_bb"
    gt_sell_rule = "gt_model_sell_zigzag_peak"
    gt_pair_ids = {gt_buy_rule, gt_sell_rule}
    if gt_pair_ids.issubset(set(outcomes["rule_id"].unique())):
        gt_pair_fires = outcomes[outcomes["rule_id"].isin(gt_pair_ids)]
        gt_pair_trades = fires_to_trade_list(sort_fires_chronological(gt_pair_fires))
        portfolio_compare["sim_gt_model"] = simulate_sized_portfolio(
            gt_pair_trades,
            last_price=float(mark) if mark else None,
        )

    # 인과 GT leg 엔진 (split_buy + peak_sell, 캘리브레이션 파라미터)
    cg_df = None
    try:
        from config import CHART_LOOKBACK_DAYS, MATCH_PRIMARY_INTERVAL, SYMBOL
        from deepcoin.data.mtf_bb import load_frames_from_db
        from deepcoin.ground_truth.causal_gt_calibrate import load_causal_gt_params
        from deepcoin.ground_truth.causal_gt_trades import simulate_causal_gt_portfolio
        from deepcoin.ops.monitor import Monitor

        cg_params = load_causal_gt_params()
        mon_cg = Monitor(cooldown_file=None)
        cg_frames = load_frames_from_db(mon_cg, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS)
        cg_df = cg_frames[MATCH_PRIMARY_INTERVAL]
        portfolio_compare["sim_causal_gt"] = simulate_causal_gt_portfolio(
            cg_df,
            last_price=float(mark) if mark else None,
            **cg_params,
        )
        # Phase 3: monitor buy + 인과 peak sell + drawdown tier
        from deepcoin.ground_truth.causal_gt_hybrid import (
            simulate_causal_gt_hybrid_portfolio,
            simulate_monitor_tier_enhanced_portfolio,
        )
        from deepcoin.ground_truth.hybrid_dd_calibrate import load_hybrid_dd_params

        dd_params = load_hybrid_dd_params()

        buy_only = all_monitor[all_monitor["side"] == "buy"]
        portfolio_compare["sim_causal_hybrid"] = simulate_causal_gt_hybrid_portfolio(
            buy_only,
            cg_df,
            monitor_fires=all_monitor,
            last_price=float(mark) if mark else None,
            cg_params=cg_params,
            dd_large_pct=dd_params.get("dd_large_pct"),
            dd_medium_pct=dd_params.get("dd_medium_pct"),
        )
        portfolio_compare["hybrid_dd_params"] = dd_params
        portfolio_compare["sim_tier_enhanced"] = simulate_monitor_tier_enhanced_portfolio(
            all_monitor,
            cg_df,
            last_price=float(mark) if mark else None,
        )
    except Exception as exc:
        portfolio_compare["sim_causal_gt"] = {
            "pnl_pct": 0.0,
            "note": f"causal_gt sim skipped: {exc}",
        }
        portfolio_compare["sim_causal_hybrid"] = {
            "pnl_pct": 0.0,
            "note": f"causal_hybrid sim skipped: {exc}",
        }
        portfolio_compare["sim_tier_enhanced"] = {
            "pnl_pct": 0.0,
            "note": f"tier_enhanced sim skipped: {exc}",
        }

    holdout = outcomes[
        outcomes["rule_id"].isin(monitor_ids) & (outcomes["split"] == "holdout")
    ]
    if not holdout.empty and not all_monitor.empty:
        full_trades = fires_to_trade_list(sort_fires_chronological(all_monitor))
        if full_trades:
            from deepcoin.ground_truth.gt_allocation import simulate_portfolio_steps

            steps = simulate_portfolio_steps(full_trades, use_amount_krw=True)
            if steps:
                outcomes_ts = outcomes.copy()
                outcomes_ts["ts"] = pd.to_datetime(outcomes_ts["dt"])
                h0 = outcomes_ts["ts"].quantile(1.0 - MATCH_HOLDOUT_RATIO)
                portfolio_compare["sim_sized_holdout"] = portfolio_holdout_from_steps(
                    steps,
                    h0,
                    trade_count=int(len(holdout)),
                    note="전기간 복리(causal tier) 후 holdout 구간 자산 증감",
                )

    go_hybrid: dict[str, Any] = {"go": False, "note": "hybrid sim unavailable"}
    go_option_c_phase2: dict[str, Any] = {"go": False, "note": "phase2 unavailable"}
    if (
        cg_df is not None
        and not all_monitor.empty
        and portfolio_compare.get("sim_causal_hybrid", {}).get("sizing_mode")
    ):
        from deepcoin.ground_truth.causal_gt_hybrid import build_monitor_hybrid_sized_trades
        from deepcoin.ground_truth.gt_allocation import simulate_portfolio_steps
        from deepcoin.ground_truth.hybrid_dd_calibrate import load_hybrid_dd_params
        from deepcoin.matching.option_c_phase2 import (
            evaluate_option_c_phase2_go,
            simulate_hybrid_slippage_stress,
            walk_forward_portfolio_by_month,
            walk_forward_portfolio_summary,
        )

        dd_params = portfolio_compare.get("hybrid_dd_params") or load_hybrid_dd_params()
        dd_large = dd_params.get("dd_large_pct")
        dd_medium = dd_params.get("dd_medium_pct")

        hybrid_full = portfolio_compare["sim_causal_hybrid"]
        sized_h, _ = build_monitor_hybrid_sized_trades(
            sort_fires_chronological(all_monitor),
            cg_df,
            enhanced=False,
            dd_large_pct=dd_large,
            dd_medium_pct=dd_medium,
        )
        steps_h = simulate_portfolio_steps(sized_h, use_amount_krw=True)
        if steps_h and not holdout.empty:
            outcomes_ts = outcomes.copy()
            outcomes_ts["ts"] = pd.to_datetime(outcomes_ts["dt"])
            h0 = outcomes_ts["ts"].quantile(1.0 - MATCH_HOLDOUT_RATIO)
            portfolio_compare["sim_hybrid_holdout"] = portfolio_holdout_from_steps(
                steps_h,
                h0,
                trade_count=int(len(holdout)),
                note="전기간 복리(hybrid DD tier) 후 holdout 구간 자산 증감",
            )

        stress_fee = TRADING_FEE_RATE * SIM_FEE_STRESS_MULT
        sized_stress, _ = build_monitor_hybrid_sized_trades(
            sort_fires_chronological(all_monitor),
            cg_df,
            enhanced=False,
            fee_rate=stress_fee,
            dd_large_pct=dd_large,
            dd_medium_pct=dd_medium,
        )
        portfolio_compare["sim_hybrid_fee_stress"] = simulate_portfolio_summary(
            sized_stress,
            fee_rate=stress_fee,
            last_price=float(mark) if mark else None,
            use_amount_krw=True,
        )

        portfolio_compare["sim_hybrid_slippage_stress"] = simulate_hybrid_slippage_stress(
            sized_h,
            last_price=float(mark) if mark else None,
            fee_rate=TRADING_FEE_RATE,
        )
        wf_rows = walk_forward_portfolio_by_month(steps_h)
        wf_port = walk_forward_portfolio_summary(wf_rows)
        portfolio_compare["hybrid_portfolio_walk_forward"] = wf_rows
        portfolio_compare["hybrid_portfolio_wf_summary"] = wf_port

        gt_pnl_for_phase2 = float(
            (portfolio_compare.get("ground_truth_chrono") or {}).get("pnl_pct", 0)
        )
        go_hybrid = evaluate_hybrid_sizing_go(
            go,
            hybrid_full,
            portfolio_compare.get("sim_hybrid_holdout") or {},
            portfolio_compare.get("sim_hybrid_fee_stress") or {},
        )
        go_option_c_phase2 = evaluate_option_c_phase2_go(
            go_hybrid,
            hybrid_full,
            portfolio_compare.get("sim_hybrid_holdout") or {},
            portfolio_compare.get("sim_hybrid_fee_stress") or {},
            portfolio_compare.get("sim_hybrid_slippage_stress") or {},
            wf_port,
            gt_pnl_for_phase2,
        )

        primary = go_hybrid.get("primary_sizing", "causal_tier")
        portfolio_compare["primary_sizing"] = primary
        if primary == "hybrid":
            portfolio_compare["sim_primary"] = {
                **hybrid_full,
                "sizing_mode": "primary_hybrid_dd_tier",
                "sizing_note": (
                    "권장: monitor + past-leg·drawdown tier (검증 통과, 미래 미사용)"
                ),
            }
            live_cap = simulate_hybrid_order_cap(
                outcomes,
                cg_df,
                rule_ids=monitor_ids,
                holdout_only=True,
                dd_large_pct=dd_large,
                dd_medium_pct=dd_medium,
            )
        else:
            portfolio_compare["sim_primary"] = portfolio_compare.get("sim_sized") or {}

    if portfolio_compare.get("sim_sized") and portfolio_compare.get("ground_truth_chrono"):
        gt_pnl = float(portfolio_compare["ground_truth_chrono"].get("pnl_pct", 0))
        sim_pnl = float(portfolio_compare["sim_sized"].get("pnl_pct", 0))
        portfolio_compare["gt_capture_ratio"] = round(
            sim_pnl / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
            4,
        )
        portfolio_compare["gt_pnl_pct"] = gt_pnl
        portfolio_compare["sim_sized_pnl_pct"] = sim_pnl
        if portfolio_compare.get("sim_gt_model"):
            gtp = float(portfolio_compare["sim_gt_model"].get("pnl_pct", 0))
            portfolio_compare["gt_model_capture_ratio"] = round(
                gtp / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
                4,
            )
        if portfolio_compare.get("sim_causal_gt"):
            cgp = float(portfolio_compare["sim_causal_gt"].get("pnl_pct", 0))
            portfolio_compare["causal_gt_capture_ratio"] = round(
                cgp / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
                4,
            )
            portfolio_compare["sim_causal_gt_pnl_pct"] = cgp
        if portfolio_compare.get("sim_causal_hybrid"):
            chp = float(portfolio_compare["sim_causal_hybrid"].get("pnl_pct", 0))
            portfolio_compare["causal_hybrid_capture_ratio"] = round(
                chp / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
                4,
            )
            portfolio_compare["sim_causal_hybrid_pnl_pct"] = chp
        if portfolio_compare.get("sim_tier_enhanced"):
            tep = float(portfolio_compare["sim_tier_enhanced"].get("pnl_pct", 0))
            portfolio_compare["tier_enhanced_capture_ratio"] = round(
                tep / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
                4,
            )
            portfolio_compare["sim_tier_enhanced_pnl_pct"] = tep

    portfolio_compare["causal_gt_params"] = {}
    try:
        from deepcoin.ground_truth.causal_gt_calibrate import load_causal_gt_params

        portfolio_compare["causal_gt_params"] = load_causal_gt_params()
    except Exception:
        pass

    portfolio_compare["gt_allocation_analysis"] = gt_alloc_analysis
    portfolio_compare["causal_mode"] = {
        "gt_signal_causal": GT_SIGNAL_CAUSAL,
        "sim_causal_tier": SIM_CAUSAL_TIER,
        "note": "인과적: t 시점까지 데이터만 사용 (운영 정합)",
    }

    gt_portfolio: dict[str, Any] = {}
    if ANALYSIS_GT_CALIBRATION_JSON.is_file():
        cal = json.loads(ANALYSIS_GT_CALIBRATION_JSON.read_text(encoding="utf-8"))
        gt_portfolio = cal.get("final", {})
    else:
        from deepcoin.matching.gt_asset_calibration import (
            portfolio_asset_ratio,
        )

        gt_data_cal = load_ground_truth(resolve_ground_truth_file()) or {}
        trades = gt_data_cal.get("trades") or []
        mark_cal = (gt_data_cal.get("summary") or {}).get("mark_price")
        if trades:
            gt_portfolio = {
                "portfolio": portfolio_asset_ratio(trades, set(), mark_cal),
                "note": "캘리브레이션 미실행 — scripts/04_calibrate_gt_assets.py",
            }

    summaries = matched.get("all_rule_summaries") or matched.get("monitor_rules") or []
    leg_weight_check = summarize_leg_weights(gt_trades) if gt_trades else {}
    invalid_legs = [lid for lid, info in leg_weight_check.items() if not info.get("valid", True)]
    return {
        "label_mode": matched.get("label_mode"),
        "train_ratio": MATCH_TRAIN_RATIO,
        "holdout_ratio": MATCH_HOLDOUT_RATIO,
        "outcomes_rows": int(len(outcomes)),
        "walk_forward": wf_rows,
        "walk_forward_summary": wf_sum,
        "fee_stress_mult": SIM_FEE_STRESS_MULT,
        "fee_stress_by_rule": fee_stress,
        "live_order_cap_sim": live_cap,
        "go_no_go": go,
        "go_no_go_hybrid": go_hybrid,
        "go_no_go_option_c_phase2": go_option_c_phase2,
        "portfolio_compare": portfolio_compare,
        "gt_model": gt_data.get("model") or model_to_dict(default_model()),
        "gt_weight_policy": weight_policy_summary(default_model()),
        "gt_leg_weight_validation": {
            "legs": leg_weight_check,
            "invalid_leg_ids": invalid_legs,
            "all_valid": len(invalid_legs) == 0,
        },
        "monitor_rules": matched.get("monitor_rules", []),
        "gt_portfolio_calibration": gt_portfolio,
        "criteria": {
            "min_holdout_ev": SIM_GO_MIN_HOLDOUT_EV,
            "min_holdout_pf": SIM_GO_MIN_HOLDOUT_PF,
            "wf_positive_ratio": SIM_GO_WF_POSITIVE_RATIO,
            "wf_min_months": SIM_WALK_FORWARD_MIN_MONTHS,
            "hybrid_min_holdout_pnl_pct": SIM_HYBRID_MIN_HOLDOUT_PNL_PCT,
            "hybrid_max_mdd_pct": SIM_HYBRID_MAX_MDD_PCT,
            "option_c_target_pnl_pct": SIM_OPTION_C_TARGET_PNL_PCT,
            "option_c_phase2_target_pnl_pct": SIM_OPTION_C_PHASE2_TARGET_PNL_PCT,
            "option_c_phase2_fee_stress_ratio": SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO,
            "option_c_min_gt_capture": SIM_OPTION_C_MIN_GT_CAPTURE,
        },
    }


def write_simulation_html(report: dict[str, Any], out_path: Path) -> Path:
    """
    simulation_report.html 저장 (ground_truth 차트 동일 스타일).

    Args:
        report: build_simulation_report 결과.
        out_path: HTML 경로.

    Returns:
        out_path.
    """
    from deepcoin.matching.simulation_html import write_simulation_report_html

    return write_simulation_report_html(report, out_path)


def run_simulation_report(
    outcomes_path: Path | None = None,
    matched_path: Path | None = None,
) -> dict[str, Any]:
    """
    시뮬 리포트 생성·저장·요약 출력.

    Args:
        outcomes_path: fire_outcomes.csv.
        matched_path: matched_rules.json.

    Returns:
        report dict.
    """
    report = build_simulation_report(outcomes_path, matched_path)
    MATCHING_SIMULATION_JSON.parent.mkdir(parents=True, exist_ok=True)
    MATCHING_SIMULATION_JSON.write_text(
        json.dumps(report, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    write_simulation_html(report, MATCHING_SIMULATION_HTML)
    go = report["go_no_go"]["go"]
    go_h = report.get("go_no_go_hybrid") or {}
    pc_early = report.get("portfolio_compare") or {}
    print(f"[시뮬] 저장: {MATCHING_SIMULATION_JSON}")
    print(f"[시뮬] 저장: {MATCHING_SIMULATION_HTML}")
    print(f"[시뮬] Go/No-Go (규칙): {'GO' if go else 'NO-GO'}")
    print(
        f"[시뮬] Go/No-Go (hybrid tier): {'GO' if go_h.get('go') else 'NO-GO'} "
        f"· primary={pc_early.get('primary_sizing', '-')}"
    )
    for c in go_h.get("checks", []):
        mark = "OK" if c.get("pass") else "NG"
        opt = " (optional)" if c.get("optional") else ""
        print(f"  [hybrid {mark}] {c.get('name')}: {c.get('value', '-')}{opt}")
    go_p2 = report.get("go_no_go_option_c_phase2") or {}
    print(
        f"[시뮬] Option C 2차(+1000%): {'GO' if go_p2.get('go') else 'NO-GO'}"
    )
    for c in go_p2.get("checks", []):
        mark = "OK" if c.get("pass") else "NG"
        print(f"  [phase2 {mark}] {c.get('name')}: {c.get('value', '-')}")
    for c in report["go_no_go"].get("checks", []):
        mark = "OK" if c["pass"] else "NG"
        print(
            f"  [{mark}] {c['rule_id']}: holdout EV={c['holdout_ev']} "
            f"WF+={c['wf_positive_ratio']} fee2x EV={c['fee_stress_ev']}"
        )
    cal = report.get("gt_portfolio_calibration") or {}
    port = cal.get("portfolio") or {}
    pc = report.get("portfolio_compare") or {}
    if pc.get("gt_capture_ratio") is not None:
        print(
            f"[시뮬] GT 대비 sim_sized(전기간 복리): {pc.get('sim_sized_pnl_pct')}% "
            f"/ GT {pc.get('gt_pnl_pct')}% "
            f"(capture={pc.get('gt_capture_ratio'):.2%})"
        )
    if pc.get("gt_model_capture_ratio") is not None:
        print(
            f"[시뮬] GT 대비 sim_gt_model: "
            f"{pc.get('sim_gt_model', {}).get('pnl_pct')}% "
            f"(capture={pc.get('gt_model_capture_ratio'):.2%})"
        )
    if pc.get("sim_causal_gt_pnl_pct") is not None:
        scg = pc.get("sim_causal_gt") or {}
        print(
            f"[시뮬] GT 대비 sim_causal_gt(인과 leg): "
            f"{pc.get('sim_causal_gt_pnl_pct')}% "
            f"(capture={pc.get('causal_gt_capture_ratio', 0):.2%}, "
            f"legs={scg.get('leg_count', '-')}, trades={scg.get('trade_count', '-')})"
        )
    if pc.get("sim_causal_hybrid_pnl_pct") is not None:
        sch = pc.get("sim_causal_hybrid") or {}
        print(
            f"[시뮬] GT 대비 sim_causal_hybrid(monitor+DD tier): "
            f"{pc.get('sim_causal_hybrid_pnl_pct')}% "
            f"(capture={pc.get('causal_hybrid_capture_ratio', 0):.2%}, "
            f"MDD={sch.get('max_drawdown_pct', '-')}%)"
        )
    if pc.get("sim_primary"):
        sp = pc["sim_primary"]
        print(
            f"[시뮬] 권장 primary ({pc.get('primary_sizing')}): "
            f"{sp.get('pnl_pct')}% · MDD={sp.get('max_drawdown_pct', '-')}%"
        )
    ho_h = pc.get("sim_hybrid_holdout") or {}
    if ho_h.get("pnl_pct") is not None:
        print(
            f"[시뮬] hybrid holdout: {ho_h.get('pnl_pct')}% "
            f"({ho_h.get('initial_asset_krw')}→{ho_h.get('final_asset_krw')})"
        )
    if pc.get("sim_tier_enhanced_pnl_pct") is not None:
        ste = pc.get("sim_tier_enhanced") or {}
        ast = ste.get("alloc_stats") or {}
        print(
            f"[시뮬] GT 대비 sim_tier_enhanced(conviction tier): "
            f"{pc.get('sim_tier_enhanced_pnl_pct')}% "
            f"(capture={pc.get('tier_enhanced_capture_ratio', 0):.2%}, "
            f"large_buys={ast.get('large_tier_buy_count', '-')}, "
            f"avg_buy={ast.get('buy_amount_avg_krw', '-')})"
        )
    if pc.get("sim_sized", {}).get("max_drawdown_pct") is not None:
        print(
            f"[시뮬] sim_sized MDD: {pc['sim_sized']['max_drawdown_pct']}% "
            f"(GT MDD: {pc.get('ground_truth_chrono', {}).get('max_drawdown_pct')}%)"
        )
    if port.get("asset_ratio") is not None:
        met = cal.get("targets_met", port.get("target_met_90"))
        print(
            f"[시뮬] GT 총자산 대비 leg subset 비율: {port['asset_ratio']:.2%} "
            f"({port.get('legs_covered')}/{port.get('legs_total')} leg) "
            f"목표90%={'달성' if met else '미달'}"
        )
    return report
