# -*- coding: utf-8 -*-
"""
weight_chart.py — 权重调优历史可视化（单页）
  - 权重变化折线图（可按因子筛选）
  - 因子胜率折线图
  - PnL柱状图
  - 权重变化记录表
  - 当前权重雷达图

python weight_chart.py [--open]
"""
import json, os, sys, webbrowser

BASE_DIR     = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # 项目根目录
REPORT_DIR   = os.path.join(BASE_DIR, "weight_reports")
HISTORY_FILE = os.path.join(REPORT_DIR, "weight_history.json")
OUTPUT_HTML  = os.path.join(REPORT_DIR, "weight_chart.html")

FK_ALL = ["TR","OB","TK","OI","FR","MP","VD","BTC","GM","IV","EX","LC","MR","NEWS"]
COLORS = {
    "TR":"#4e79a7","OB":"#f28e2b","TK":"#e15759","OI":"#76b7b2",
    "FR":"#59a14f","MP":"#edc948","VD":"#b07aa1","BTC":"#ff9da7",
    "GM":"#9c755f","IV":"#bab0ac","EX":"#d37295","LC":"#a0cbe8",
    "MR":"#ffbe7d","NEWS":"#8cd17d","SM":"#86bcb6",
}


def load_history():
    if not os.path.exists(HISTORY_FILE):
        return []
    raw = json.load(open(HISTORY_FILE, encoding="utf-8"))
    records = []
    for ts, rec in sorted(raw.items()):
        # 兼容旧格式 win_rates: {k: float} 和新格式 {k: {"wr":..,"n":..}}
        wr_raw = rec.get("win_rates", {})
        win_rates = {}
        for k, v in wr_raw.items():
            if isinstance(v, dict):
                win_rates[k] = v
            else:
                win_rates[k] = {"wr": float(v), "n": 0}
        records.append({
            "ts":       ts,
            "label":    ts[5:16],
            "dry_run":  rec.get("dry_run", False),
            "trades":   rec.get("trades", 0),
            "total_pnl":rec.get("total_pnl", 0),
            "weights":  rec.get("weights", {}),
            "win_rates":win_rates,
            "changes":  rec.get("changes", {}),
        })
    return records


def main():
    open_browser = "--open" in sys.argv
    records = load_history()
    if not records:
        print("暂无历史数据，请先运行 weight_optimizer.py")
        return

    labels      = [r["label"] for r in records]
    pnl_list    = [r["total_pnl"] for r in records]
    trade_list  = [r["trades"] for r in records]

    # 各因子权重/胜率序列
    fw = {k: [r["weights"].get(k, 0) for r in records] for k in FK_ALL}
    wr = {k: [r["win_rates"].get(k, {}).get("wr", 0.5) for r in records] for k in FK_ALL if k != "NEWS"}
    wn = {k: [r["win_rates"].get(k, {}).get("n", 0)    for r in records] for k in FK_ALL if k != "NEWS"}

    # 最新权重（用于雷达图）
    last_w = records[-1]["weights"]

    # 变化记录（倒序）
    changes = []
    for r in records:
        for k, chg in r.get("changes", {}).items():
            if abs(chg) >= 0.001:
                old_w = round(r["weights"].get(k, 0) - chg, 4)
                new_w = r["weights"].get(k, 0)
                wr_v  = r["win_rates"].get(k, {}).get("wr", 0.5)
                n_v   = r["win_rates"].get(k, {}).get("n", 0)
                changes.append({
                    "ts": r["label"], "k": k,
                    "old": old_w, "new": new_w, "chg": round(chg, 4),
                    "wr": round(wr_v * 100, 1), "n": n_v,
                    "dr": r["dry_run"],
                })
    changes = list(reversed(changes))

    data_js = f"""
const TS = {json.dumps(labels)};
const FW = {json.dumps(fw)};
const WR = {json.dumps(wr)};
const WN = {json.dumps(wn)};
const PNL = {json.dumps(pnl_list)};
const TRADES = {json.dumps(trade_list)};
const LAST_W = {json.dumps(last_w)};
const CHANGES = {json.dumps(changes)};
const COLORS = {json.dumps(COLORS)};
const FK_ALL = {json.dumps(FK_ALL)};
const FK_NO_NEWS = {json.dumps([k for k in FK_ALL if k != "NEWS"])};
"""

    html = """<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<title>权重调优看板</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #0f0f1a; color: #ddd; font-family: 'Segoe UI',sans-serif; padding: 14px; font-size: 13px; }
h1  { font-size: 16px; color: #bbb; margin-bottom: 14px; border-bottom: 1px solid #222; padding-bottom: 8px; }
h2  { font-size: 13px; color: #888; margin-bottom: 8px; }
.section { background: #16162a; border-radius: 8px; padding: 14px; margin-bottom: 14px; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 14px; }
canvas { max-height: 260px; }
.filter-bar { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 10px; }
.btn { padding: 2px 8px; border-radius: 4px; border: 1px solid #333; background: #111;
       color: #aaa; cursor: pointer; font-size: 11px; transition: all .15s; }
.btn.on { border-color: currentColor; opacity: 1; }
.btn.off { opacity: 0.3; }
table { width: 100%; border-collapse: collapse; }
th { background: #1e1e35; color: #777; padding: 5px 8px; text-align: left;
     border-bottom: 1px solid #2a2a45; font-weight: normal; }
td { padding: 4px 8px; border-bottom: 1px solid #1e1e35; }
tr:hover td { background: #1c1c30; }
.up { color: #4ade80; } .dn { color: #f87171; } .nc { color: #666; }
.badge { font-size: 10px; padding: 1px 5px; border-radius: 3px;
         background: #2a2a20; color: #aaa8; margin-left: 4px; }
.stat-row { display: flex; gap: 20px; margin-bottom: 10px; flex-wrap: wrap; }
.stat { background: #1e1e35; border-radius: 6px; padding: 8px 14px; }
.stat .val { font-size: 18px; font-weight: bold; }
.stat .lbl { font-size: 11px; color: #666; margin-top: 2px; }
</style>
</head>
<body>
<h1>⚖️ 因子权重调优看板</h1>

<!-- 统计摘要 -->
<div class="section">
<h2>📊 总览</h2>
<div class="stat-row" id="stats-row"></div>
</div>

<!-- 权重变化趋势 -->
<div class="section">
<h2>📈 权重变化趋势（点击因子标签筛选）</h2>
<div class="filter-bar" id="fk-btns"></div>
<canvas id="weightChart"></canvas>
</div>

<!-- 胜率 + PnL -->
<div class="grid2">
  <div class="section">
    <h2>🎯 因子胜率趋势</h2>
    <canvas id="wrChart"></canvas>
  </div>
  <div class="section">
    <h2>💰 每次调优分析 PnL</h2>
    <canvas id="pnlChart"></canvas>
  </div>
</div>

<!-- 当前权重雷达 + 权重分布 -->
<div class="grid2">
  <div class="section">
    <h2>🕸 当前权重分布（雷达图）</h2>
    <canvas id="radarChart"></canvas>
  </div>
  <div class="section">
    <h2>📊 当前各因子权重</h2>
    <canvas id="barChart"></canvas>
  </div>
</div>

<!-- 变化记录表 -->
<div class="section">
<h2>📋 权重变化记录（含原因）</h2>
<table>
  <thead>
    <tr>
      <th>时间</th><th>因子</th><th>变化前</th><th>变化后</th>
      <th>变化量</th><th>胜率(样本)</th><th>备注</th>
    </tr>
  </thead>
  <tbody id="changelog-body"></tbody>
</table>
</div>

<!-- 当前权重详细表 -->
<div class="section">
<h2>⚙️ 当前权重明细</h2>
<table id="cur-weight-table">
  <thead><tr><th>因子</th><th>当前权重</th><th>胜率(最新)</th><th>样本数</th><th>状态</th></tr></thead>
  <tbody id="cur-weight-body"></tbody>
</table>
</div>

<script>
""" + data_js + """
// ── 统计摘要 ──────────────────────────────────────────────────
function stats() {
  let totalTrades = TRADES.reduce((a,b)=>a+b,0);
  let totalPnl    = PNL.reduce((a,b)=>a+b,0);
  let lastPnl     = PNL[PNL.length-1]||0;
  let nOpt        = TS.length;
  let lastWin     = Object.entries(WR).sort((a,b)=>{
    let la = a[1][a[1].length-1]||0.5, lb = b[1][b[1].length-1]||0.5;
    return lb-la;
  })[0];
  let row = document.getElementById("stats-row");
  [
    ["调优次数", nOpt+"次"],
    ["分析交易", totalTrades+"笔"],
    ["总PnL", (totalPnl>=0?"+":"")+totalPnl.toFixed(4)],
    ["最新PnL", (lastPnl>=0?"+":"")+lastPnl.toFixed(4)],
    ["最佳因子", lastWin ? lastWin[0]+"("+(lastWin[1][lastWin[1].length-1]*100).toFixed(0)+"%)" : "-"],
  ].forEach(([lbl,val])=>{
    let d = document.createElement("div"); d.className="stat";
    d.innerHTML = `<div class="val" style="color:${val.startsWith('-')?'#f87171':'#4ade80'}">${val}</div><div class="lbl">${lbl}</div>`;
    row.appendChild(d);
  });
}
stats();

// ── 权重趋势图 ────────────────────────────────────────────────
let activeFK = new Set(["BTC","GM","LC","TR","EX","OB","NEWS"]);
let wChart = new Chart(document.getElementById("weightChart").getContext("2d"), {
  type:"line",
  data:{labels:TS, datasets:[]},
  options:{
    responsive:true, maintainAspectRatio:false,
    plugins:{legend:{display:false}, tooltip:{callbacks:{
      label: ctx=>`${ctx.dataset.label}: ${(ctx.parsed.y*100).toFixed(2)}%`
    }}},
    scales:{
      x:{ticks:{color:"#555",maxTicksLimit:10},grid:{color:"#1e1e35"}},
      y:{ticks:{color:"#555",callback:v=>(v*100).toFixed(0)+"%"},grid:{color:"#1e1e35"},min:0}
    }
  }
});
function rebuildW() {
  wChart.data.datasets = FK_ALL.filter(k=>activeFK.has(k)).map(k=>({
    label:k, data:FW[k]||[],
    borderColor:COLORS[k]||"#aaa", backgroundColor:"transparent",
    borderWidth:2, pointRadius:3, tension:0.35
  }));
  wChart.update();
}
// 筛选按钮
let bar = document.getElementById("fk-btns");
FK_ALL.forEach(k=>{
  let btn = document.createElement("button");
  btn.className = "btn "+(activeFK.has(k)?"on":"off");
  btn.style.color = COLORS[k]||"#aaa";
  btn.textContent = k;
  btn.onclick = ()=>{
    if(activeFK.has(k)){ activeFK.delete(k); btn.className="btn off"; }
    else { activeFK.add(k); btn.className="btn on"; }
    btn.style.color = COLORS[k]||"#aaa";
    rebuildW();
  };
  bar.appendChild(btn);
});
rebuildW();

// ── 胜率趋势图 ────────────────────────────────────────────────
new Chart(document.getElementById("wrChart").getContext("2d"), {
  type:"line",
  data:{labels:TS, datasets:FK_NO_NEWS.map(k=>({
    label:k, data:WR[k]||[],
    borderColor:COLORS[k]||"#aaa", backgroundColor:"transparent",
    borderWidth:1.5, pointRadius:2, tension:0.3
  }))},
  options:{
    responsive:true, maintainAspectRatio:false,
    plugins:{legend:{labels:{color:"#666",boxWidth:10,font:{size:10}}}},
    scales:{
      x:{ticks:{color:"#555",maxTicksLimit:8},grid:{color:"#1e1e35"}},
      y:{min:0,max:1,ticks:{color:"#555",callback:v=>(v*100).toFixed(0)+"%"},grid:{color:"#1e1e35"},
         afterBuildTicks:ax=>{ax.ticks=[0,.25,.5,.75,1].map(v=>({value:v}));}}
    }
  }
});

// ── PnL 柱状图 ────────────────────────────────────────────────
new Chart(document.getElementById("pnlChart").getContext("2d"),{
  type:"bar",
  data:{labels:TS, datasets:[{
    label:"PnL", data:PNL,
    backgroundColor:PNL.map(v=>v>=0?"rgba(74,222,128,.55)":"rgba(248,113,113,.55)"),
    borderColor:PNL.map(v=>v>=0?"#4ade80":"#f87171"),
    borderWidth:1
  }]},
  options:{
    responsive:true,maintainAspectRatio:false,
    plugins:{legend:{display:false}},
    scales:{
      x:{ticks:{color:"#555",maxTicksLimit:8},grid:{color:"#1e1e35"}},
      y:{ticks:{color:"#555"},grid:{color:"#1e1e35"}}
    }
  }
});

// ── 雷达图（当前权重）─────────────────────────────────────────
let radarKeys = FK_ALL.filter(k=>k!=="SM");
new Chart(document.getElementById("radarChart").getContext("2d"),{
  type:"radar",
  data:{
    labels:radarKeys,
    datasets:[{
      label:"当前权重",
      data:radarKeys.map(k=>LAST_W[k]||0),
      borderColor:"#7eb8f7", backgroundColor:"rgba(126,184,247,.15)",
      pointBackgroundColor:radarKeys.map(k=>COLORS[k]||"#aaa"),
      borderWidth:2, pointRadius:4
    }]
  },
  options:{
    responsive:true,maintainAspectRatio:false,
    plugins:{legend:{labels:{color:"#888"}}},
    scales:{r:{
      ticks:{color:"#555",callback:v=>(v*100).toFixed(0)+"%", backdropColor:"transparent"},
      grid:{color:"#222"}, angleLines:{color:"#222"},
      pointLabels:{color:radarKeys.map(k=>COLORS[k]||"#aaa"), font:{size:11}}
    }}
  }
});

// ── 柱状图（当前权重排序）────────────────────────────────────
let sortedFK = [...FK_ALL].sort((a,b)=>(LAST_W[b]||0)-(LAST_W[a]||0));
new Chart(document.getElementById("barChart").getContext("2d"),{
  type:"bar",
  data:{
    labels:sortedFK,
    datasets:[{
      label:"权重",
      data:sortedFK.map(k=>LAST_W[k]||0),
      backgroundColor:sortedFK.map(k=>COLORS[k]||"#aaa"),
      borderRadius:4, borderSkipped:false
    }]
  },
  options:{
    responsive:true,maintainAspectRatio:false,indexAxis:"y",
    plugins:{legend:{display:false},tooltip:{callbacks:{
      label:ctx=>(ctx.parsed.x*100).toFixed(2)+"%"
    }}},
    scales:{
      x:{ticks:{color:"#555",callback:v=>(v*100).toFixed(0)+"%"},grid:{color:"#1e1e35"}},
      y:{ticks:{color:sortedFK.map(k=>COLORS[k]||"#aaa"),font:{size:11}},grid:{display:false}}
    }
  }
});

// ── 变化记录表 ────────────────────────────────────────────────
let tbody = document.getElementById("changelog-body");
if(CHANGES.length===0){
  tbody.innerHTML='<tr><td colspan="7" style="color:#444;text-align:center;padding:12px">暂无变化记录</td></tr>';
} else {
  CHANGES.forEach(c=>{
    let tr = document.createElement("tr");
    let up = c.chg>0, cls=up?"up":"dn", ar=up?"⬆️":"⬇️";
    let wr_cls = c.wr>=65?"up":(c.wr<=40?"dn":"nc");
    let reason = "";
    if(c.n < 15) reason = `样本不足(${c.n}笔)`;
    else if(c.wr>=70) reason = `强正效应，提权`;
    else if(c.wr>=60) reason = `正效应，小幅提权`;
    else if(c.wr<=30) reason = `强负效应，降权`;
    else if(c.wr<=40) reason = `负效应，小幅降权`;
    else reason = `胜率接近随机(${c.wr}%)，微调`;
    let drBadge = c.dr ? '<span class="badge">dry-run</span>' : '';
    tr.innerHTML=`
      <td style="color:#666">${c.ts}${drBadge}</td>
      <td style="color:${COLORS[c.k]||'#aaa'};font-weight:bold">${c.k}</td>
      <td>${(c.old*100).toFixed(2)}%</td>
      <td>${(c.new*100).toFixed(2)}%</td>
      <td class="${cls}">${ar} ${(Math.abs(c.chg)*100).toFixed(2)}%</td>
      <td class="${wr_cls}">${c.wr}% (n=${c.n})</td>
      <td style="color:#777">${reason}</td>`;
    tbody.appendChild(tr);
  });
}

// ── 当前权重明细表 ────────────────────────────────────────────
let cwBody = document.getElementById("cur-weight-body");
let lastRec = null;
// 找最新的 win_rates
""" + f"let lastWR = {json.dumps({k: {'wr': records[-1]['win_rates'].get(k,{}).get('wr',0.5) if records else 0.5, 'n': records[-1]['win_rates'].get(k,{}).get('n',0) if records else 0} for k in FK_ALL if k!='NEWS'})};" + """
FK_ALL.forEach(k=>{
  let w   = LAST_W[k]||0;
  let wri = lastWR[k]||{wr:0.5,n:0};
  let wr  = wri.wr||0.5, n=wri.n||0;
  let wr_cls = wr>=0.65?"up":(wr<=0.40?"dn":"nc");
  let status = "";
  if(n<15)           status = `<span style="color:#555">样本不足</span>`;
  else if(wr>=0.70)  status = `<span class="up">✅ 强正效应</span>`;
  else if(wr>=0.60)  status = `<span class="up">↑ 正效应</span>`;
  else if(wr<=0.30)  status = `<span class="dn">⚠️ 强负效应</span>`;
  else if(wr<=0.40)  status = `<span class="dn">↓ 负效应</span>`;
  else               status = `<span class="nc">— 中性</span>`;
  let tr = document.createElement("tr");
  tr.innerHTML=`
    <td style="color:${COLORS[k]||'#aaa'};font-weight:bold">${k}</td>
    <td>${(w*100).toFixed(2)}%
      <div style="display:inline-block;width:${Math.round(w*400)}px;height:6px;
           background:${COLORS[k]||'#aaa'};border-radius:3px;margin-left:6px;vertical-align:middle"></div>
    </td>
    <td class="${wr_cls}">${n>=15?(wr*100).toFixed(0)+"%" : "—"}</td>
    <td style="color:#555">${n}</td>
    <td>${status}</td>`;
  cwBody.appendChild(tr);
});
</script>
</body>
</html>"""

    with open(OUTPUT_HTML, "w", encoding="utf-8") as f:
        f.write(html)
    print(f"图表已生成: {OUTPUT_HTML}")
    if open_browser:
        webbrowser.open(f"file:///{OUTPUT_HTML.replace(chr(92),'/')}")
    return OUTPUT_HTML


if __name__ == "__main__":
    main()
