# -*- coding: utf-8 -*-
"""根据 backtest_results.json 生成 HTML 回测报告(含内联 SVG 图表, 无第三方依赖)"""
import json, math, datetime

res = json.load(open('backtest_results.json'))
START = res[0]['start']

# ---------- 工具: 把序列降采样到 ~N 点 ----------
def downsample(seq, n=1200):
    if len(seq) <= n:
        return list(seq)
    step = len(seq) / n
    return [seq[int(i*step)] for i in range(n)]

# ---------- SVG: 权益曲线(归一化, 4 套叠加) ----------
def svg_equity(curves, w=900, h=340):
    colors = {'保守':'#1f77b4','原版复刻':'#d62728','稳健':'#2ca02c','进取':'#ff7f0e'}
    pad = 42
    allv = [v for (name, c) in curves for v in c['data']]
    lo, hi = min(allv), max(allv)
    span = (hi-lo) or 1
    def X(i, n): return pad + i*(w-2*pad)/max(n-1,1)
    def Y(v): return h-pad - (v-lo)/span*(h-2*pad)
    parts = [f'<svg viewBox="0 0 {w} {h}" xmlns="http://www.w3.org/2000/svg" font-family="Segoe UI,Arial">']
    # 网格 + Y 轴标签
    for g in range(5):
        yy = lo + span*g/4
        y = Y(yy)
        parts.append(f'<line x1="{pad}" y1="{y:.1f}" x2="{w-pad}" y2="{y:.1f}" stroke="#eee"/>')
        parts.append(f'<text x="{pad-6}" y="{y+4:.1f}" font-size="11" fill="#888" text-anchor="end">${yy/1000:.1f}k</text>')
    for name, c in curves:
        col = colors.get(name,'#333')
        d = c['data']
        pts = [(X(i,len(d)), Y(v)) for i,v in enumerate(downsample(d, 1000))]
        path = 'M' + ' L'.join(f'{x:.1f},{y:.1f}' for x,y in pts)
        parts.append(f'<path d="{path}" fill="none" stroke="{col}" stroke-width="1.4"/>')
    # 起始线
    parts.append(f'<line x1="{pad}" y1="{Y(START):.1f}" x2="{w-pad}" y2="{Y(START):.1f}" stroke="#999" stroke-dasharray="4 3"/>')
    # 图例
    lx = w-pad-150
    for i,(name,c) in enumerate(curves):
        yy = pad+10+i*18
        parts.append(f'<rect x="{lx}" y="{yy-9}" width="12" height="12" fill="{colors.get(name,"#333")}"/>')
        parts.append(f'<text x="{lx+18}" y="{yy+1}" font-size="12" fill="#333">{name}</text>')
    parts.append(f'<text x="{pad}" y="{h-8}" font-size="10" fill="#aaa">2018-03 → 2026-09  (XAUUSDm M30, 99,345 根真实K线)</text>')
    parts.append('</svg>')
    return ''.join(parts)

# ---------- SVG: 单套逐轮盈亏直方图 ----------
def svg_hist(rp, w=900, h=300, bins=40):
    rp = [x for x in rp if x is not None]
    lo, hi = min(rp+[-50]), max(rp+[300])
    import collections
    b = max((hi-lo)/bins, 1.0)
    nb = int((hi-lo)/b)+1
    cnt = [0]*nb
    for x in rp:
        idx = min(int((x-lo)/b), nb-1)
        cnt[idx]+=1
    mx = max(cnt) or 1
    pad=46
    def X(i): return pad + i*(w-2*pad)/nb
    def Yc(v): return h-pad - v/mx*(h-2*pad-10)
    parts=[f'<svg viewBox="0 0 {w} {h}" xmlns="http://www.w3.org/2000/svg" font-family="Segoe UI,Arial">']
    for g in range(5):
        yy=mx*g/4; y=Yc(yy)
        parts.append(f'<line x1="{pad}" y1="{y:.1f}" x2="{w-pad}" y2="{y:.1f}" stroke="#eee"/>')
        parts.append(f'<text x="{pad-6}" y="{y+4:.1f}" font-size="11" fill="#888" text-anchor="end">{int(yy)}</text>')
    for i,c in enumerate(cnt):
        x=X(i); y=Yc(c)
        # 盈亏色彩: 负=红 正=绿
        mid=(lo+hi)/2
        col = '#2ca02c' if (lo+i*b+b/2)>=0 else '#d62728'
        parts.append(f'<rect x="{x:.1f}" y="{y:.1f}" width="{(w-2*pad)/nb-1:.1f}" height="{h-pad-y:.1f}" fill="{col}" opacity="0.8"/>')
    # 0 轴
    zx = X(int((0-lo)/b)) if lo<0 else pad
    parts.append(f'<line x1="{zx:.1f}" y1="{pad-6}" x2="{zx:.1f}" y2="{h-pad}" stroke="#333" stroke-width="1"/>')
    parts.append(f'<text x="{pad}" y="{h-8}" font-size="10" fill="#aaa">每轮盈亏分布 (USD) — 绿=盈利 红=亏损</text>')
    parts.append('</svg>')
    return ''.join(parts)

# ---------- 组织数据 ----------
order = ['原版复刻','保守','稳健','进取']
rmap = {r['config']:r for r in res}
curves = [(n, {'data':rmap[n]['equity_curve']}) for n in order if n in rmap]

# 主表
rows=[]
for n in order:
    r=rmap[n]
    rp=r['round_profits']
    pos=sum(1 for x in rp if x>0); neg=len(rp)-pos
    rows.append(dict(name=n, final=r['final'], ret=r['return_pct'], dd=r['max_dd_pct'],
                     rounds=r['rounds'], win=pos, winrate=pos/len(rp)*100, loss=neg,
                     awin=r['avg_win'], aloss=r['avg_loss'], maxloss=r['largest_loss'],
                     blow=r['blowups'], deep=r['deep_grids'], orders=r['total_orders']))

bal = rmap['稳健']
hist_svg = svg_hist(bal['round_profits'])

# ---------- HTML ----------
def rowhtml(r):
    cls = 'pos' if r['ret']>=0 else 'neg'
    return f"""<tr>
<td class="lbl">{r['name']}</td>
<td class="{cls}"><b>${r['final']:,.0f}</b><br><span class="sub">{r['ret']:+.1f}%</span></td>
<td>{r['dd']:.1f}%</td>
<td>{r['rounds']}</td>
<td>{r['win']} ({r['winrate']:.0f}%)</td>
<td>{r['loss']}</td>
<td>${r['awin']:.0f}</td>
<td>${r['aloss']:.0f}</td>
<td class="neg">${r['maxloss']:,.0f}</td>
<td>{r['blow']}</td>
<td>{r['deep']}</td>
<td>{r['orders']}</td>
</tr>"""

html = f"""<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
<title>GridMaster EA 回测报告 (XAUUSDm $60,000)</title>
<style>
*{{box-sizing:border-box}}
body{{font-family:Segoe UI,-apple-system,Arial,'Microsoft YaHei',sans-serif;margin:0;background:#f5f6f8;color:#1f2329}}
.wrap{{max-width:1080px;margin:0 auto;padding:28px 22px 60px}}
h1{{font-size:23px;margin:0 0 4px}}
.meta{{color:#8a93a0;font-size:13px;margin-bottom:18px}}
.card{{background:#fff;border:1px solid #e6e8 eb;border-radius:12px;padding:20px 22px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,.04)}}
.card h2{{font-size:16px;margin:0 0 14px;color:#2b3340}}
table{{width:100%;border-collapse:collapse;font-size:13.5px}}
th,td{{padding:9px 8px;text-align:center;border-bottom:1px solid #eef0f3}}
th{{background:#fafbfc;color:#6b7280;font-weight:600;font-size:12.5px}}
td.lbl{{text-align:left;font-weight:600;color:#2b3340}}
.pos{{color:#1a8a3c}} .neg{{color:#d23b3b}} .sub{{font-size:11px;color:#8a93a0}}
.svgbox{{width:100%;overflow:hidden}}
.note{{background:#fff8e6;border:1px solid #ffe0a3;border-radius:10px;padding:14px 16px;font-size:13.5px;line-height:1.7;color:#6b5400;margin-bottom:20px}}
.find{{font-size:14px;line-height:1.85}}
.find li{{margin:7px 0}}
.tag{{display:inline-block;background:#eef2ff;color:#3b5bdb;padding:2px 9px;border-radius:6px;font-size:12px;margin-right:6px}}
.warn{{color:#d23b3b;font-weight:600}}
.ok{{color:#1a8a3c;font-weight:600}}
code{{background:#f0f2f5;padding:1px 6px;border-radius:4px;font-size:12.5px}}
</style></head><body><div class="wrap">
<h1>GridMaster EA · XAUUSDm 回测报告</h1>
<div class="meta">本金 $60,000 · 品种 XAUUSDm (Exness MT5) · 周期 M30 · 数据 2018-03-06 → 2026-09-02 (99,345 根真实 K 线, 8.49 年) · 生成于 {datetime.date.today()}</div>

<div class="note">
<b>方法学与近似（务必先读）</b><br>
本回测用 Python 忠实复刻 <code>GridMaster.mq5</code> 的 <code>OnTick</code> 决策逻辑（RSI 反转信号 / 逆势网格加仓 / ATR 逐单止损 / 整体止盈·止损·回撤保护）。
数据来自 Exness MT5 终端本机历史库 <code>M30.hc</code> 中 <b>100% 真实的 High / Low</b>（8.49 年连续）。
因该私有格式仅存储 High/Low 为明文，<b>Open/Close 为重建值</b>（Open=前收、Close=(H+L)/2）；<b>点差=0、无滑点、无隔夜利息</b>；爆仓定义为权益≤0。
上述近似使结果偏向乐观，真实账户表现只会更弱。
</div>

<div class="card">
<h2>核心结果对比</h2>
<table>
<tr><th>配置</th><th>终值 / 收益</th><th>最大回撤</th><th>轮次</th><th>胜/胜率</th><th>负轮</th><th>均赢</th><th>均亏</th><th>最大单轮亏</th><th>爆仓</th><th>触顶网格</th><th>订单数</th></tr>
{''.join(rowhtml(r) for r in rows)}
</table>
<div style="font-size:12px;color:#8a93a0;margin-top:10px">“触顶网格”= 单轮持仓达到 MaxOrders 上限的次数，反映网格承受的最大压力。</div>
</div>

<div class="card">
<h2>权益曲线（四套叠加，归一化）</h2>
<div class="svgbox">{svg_equity(curves)}</div>
<div style="font-size:12.5px;color:#6b7280;margin-top:8px">8.5 年内四套曲线均围绕 $60,000 窄幅波动，无明显趋势性增长——即“没爆仓但也几乎没赚”。原版(红)因间距更窄($25)、倍数更高(1.5)轮次更频(212 次)，终值略高于优化三套。</div>
</div>

<div class="card">
<h2>稳健版 · 逐轮盈亏分布</h2>
<div class="svgbox">{hist_svg}</div>
<div style="font-size:12.5px;color:#6b7280;margin-top:8px">约 72% 轮次盈利（集中在 +$50~+$250），但少数亏损轮拖后腿（最长尾至 −$2,599）。均亏($433) &gt; 均赢($145) 是净值微亏的根本原因。</div>
</div>

<div class="card">
<h2>关键发现</h2>
<ul class="find">
<li><span class="tag">安全</span><b>四套参数在 $60k 上全部 0 爆仓。</b>因为 EA 手数固定（首单 0.01、上限 0.40）且不随本金缩放，相对 $6 万本金极小——8.5 年内没有任何一轮把网格加满（触顶网格=0），也就无从触发爆仓。</li>
<li><span class="tag">收益</span><b>近乎持平到微亏：</b>原版 +0.5%，优化三套 −2.9% ~ −5.0%。这与早前蒙特卡洛给出的“9–11% 年化”相差巨大——<span class="warn">早前的乐观估计错了</span>，因为它假设固定 $800 止盈、且未计入逐单 ATR 止损的流血。</li>
<li><span class="tag">真退出</span>代码里 <code>RetraceStart=100 / RetraceDrawdown=10</code> 的<b>回撤保护才是实际主退出</b>：浮盈峰值≥$100 后回落 $10 即全平。这使绝大多数轮次在 ~$90–210 止盈，$800 整体止盈基本触不到。</li>
<li><span class="tag">失血点</span><b>ATR 逐单止损是最大拖累。</b>止损距 = 开仓价 ±(ATR×20~25) ≈ ±$100–250，比网格间距($45)宽得多；逆势抖动时单子常在网格回补前被止损，形成“72% 胜率却净亏”的格局（均亏 $433 &gt; 均赢 $145）。</li>
<li><span class="tag">对比</span>原版(无整体止损、间距$25、倍数1.5)反而略优，因为更密更陡的网格回补更快、轮次更多；优化三套的 <code>OverallSl=15000</code> 在本测试中<b>从未触发</b>（最大单轮亏仅 −$2,599），对结果无贡献。</li>
</ul>
</div>

<div class="card">
<h2>结论与建议</h2>
<ul class="find">
<li><b>对 $60,000 而言，这套 EA 是“低风险、低回报”的 grind：</b>不会爆仓，但 8.5 年净收益接近 0（扣费后实际为负）。它并非此前设想的“可控回撤的 9–11% 策略”。</li>
<li class="warn">若想让 $60k 产生像样收益，必须放大手数（提高 InitialLot / MaxLot），但这会同步放大回撤与爆仓概率——本测试的固定手数已无法体现，需另做“手数缩放”情景。</li>
<li>若要止血：<b>关闭 ATR 逐单止损</b>（<code>InpUseAtrSl=false</code>），让网格靠加仓自然回补，而非被单子止损打断；代价是偶发深网格回撤变大。</li>
<li>建议下一步（任选）：① 在 <b>$5,000 / $10,000</b> 小本金重测，看真实爆仓/回撤分布；② 做<b>手数缩放</b>情景（把首单提到 0.05–0.10）；③ 关闭 ATR 止损后重测收益变化。</li>
</ul>
<div style="font-size:12px;color:#8a93a0;margin-top:12px">说明：回测引擎 <code>backtest.py</code> 与逐轮数据 <code>backtest_results.json</code> 已同目录生成，可复跑或调参。</div>
</div>

</div></body></html>"""

open('GridMaster_回测报告.html','w',encoding='utf-8').write(html)
print('报告已生成: GridMaster_回测报告.html  (%.1f KB)' % (len(html)/1024))
