#!/usr/bin/env python3
"""同一份 Polymarket 5m 数据，渲染到 TradingView Lightweight Charts。

复用 chart_btc5m.py 的抓取逻辑，只换前端引擎：
  - 蜡烛 / 成交量用 Lightweight Charts v4 原生 series
  - 支持 setData 全量 + update 增量（页面里带「回放」演示增量加载）
  - 库文件本地放置，无 CDN 依赖

用法:
  python chart_btc5m_tv.py --hours 2
  python chart_btc5m_tv.py --hours 6 --asset eth --out charts/eth5m_tv.html
"""
from __future__ import annotations

import argparse
import json
import os

from chart_btc5m import ROOT, build, set_proxy

HTML_TV = r"""<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Polymarket · BTC 5m · TradingView Lightweight Charts</title>
<style>
  :root{
    --bg:#0b1220; --panel:#111a2e; --line:#1e293b; --fg:#e2e8f0; --dim:#7c8aa5;
    --up:#22c55e; --dn:#ef4444;
  }
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--fg);
       font:13px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}
  .wrap{max-width:1280px;margin:0 auto;padding:18px 20px 36px}
  h1{font-size:17px;font-weight:600;margin:0 0 4px}
  .sub{color:var(--dim);font-size:12px}
  .sub code{background:#0e1728;border:1px solid var(--line);border-radius:4px;padding:1px 5px}
  .bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:14px 0 10px}
  .seg{display:flex;background:var(--panel);border:1px solid var(--line);border-radius:7px;overflow:hidden}
  .seg button{background:none;border:0;color:var(--dim);padding:6px 12px;cursor:pointer;font:inherit}
  .seg button.on{background:#1d2b47;color:var(--fg)}
  .seg button+button{border-left:1px solid var(--line)}
  .btn{background:var(--panel);border:1px solid var(--line);border-radius:7px;color:var(--fg);
       padding:6px 12px;cursor:pointer;font:inherit}
  .btn:hover{border-color:#2b3c5c}
  .hint{margin-left:auto;color:var(--dim);font-size:11px}
  #chart{height:560px;background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden}
  footer{margin-top:12px;color:var(--dim);font-size:11px;line-height:1.7}
  footer code{background:#0e1728;border:1px solid var(--line);border-radius:4px;padding:1px 5px}
</style>
</head>
<body>
<div class="wrap">
  <h1>Polymarket · BTC 5m 成交蜡烛</h1>
  <div class="sub">渲染引擎：TradingView Lightweight Charts v4.2.3（本地加载）· 数据：<code>data-api.polymarket.com/v2/trades</code></div>

  <div class="bar">
    <div class="seg" id="rangeSeg">
      <button data-r="15">15m</button>
      <button data-r="30" class="on">30m</button>
      <button data-r="60">1h</button>
      <button data-r="120">2h</button>
    </div>
    <div class="seg" id="viewSeg">
      <button data-v="5" class="on">5s</button>
      <button data-v="10">10s</button>
      <button data-v="30">30s</button>
      <button data-v="60">1m</button>
      <button data-v="300">5m</button>
    </div>
    <div class="seg" id="sideSeg">
      <button data-s="Up" class="on">Up</button>
      <button data-s="Down">Down</button>
    </div>
    <button class="btn" id="replay">▶ 回放（update 增量）</button>
    <span class="hint" id="hint"></span>
  </div>

  <div id="chart"></div>

  <footer>
    蜡烛由逐笔成交按桶聚合：open=桶内首笔、high/low=极值、close=末笔，柱状=成交量(shares)。<br>
    每 5 分钟市场边界有蓝色圆点标记；市场之间价格重置（0/1 → ~0.5），不是连续合约。<br>
    最后一档可能仍在进行中。数据快照时间见下方提示。
  </footer>
</div>

<script src="lightweight-charts.standalone.production.js"></script>
<script>
const DATA = __DATA__;
const STEP = DATA.step || 300;
const state = { bucket: 5, rangeMin: 30, side: 'Up' };

const el = document.getElementById('chart');
const chart = LightweightCharts.createChart(el, {
  autoSize: true,
  layout: {
    background: { type: 'solid', color: '#111a2e' },
    textColor: '#cbd5e1',
    fontSize: 11,
  },
  grid: {
    vertLines: { color: '#1b2740' },
    horzLines: { color: '#1b2740' },
  },
  rightPriceScale: { borderColor: '#1e293b', scaleMargins: { top: 0.02, bottom: 0.02 } },
  timeScale: {
    borderColor: '#1e293b',
    timeVisible: true,
    secondsVisible: true,
    rightOffset: 4,
    tickMarkFormatter: (t) => {
      if (typeof t !== 'number') return String(t);
      return new Date(t * 1000).toLocaleTimeString('zh-CN',
        { hour12: false, hour: '2-digit', minute: '2-digit' });
    },
  },
  localization: {
    locale: 'zh-CN',
    timeFormatter: (t) => new Date(t * 1000).toLocaleString('zh-CN', { hour12: false }),
  },
  crosshair: { mode: LightweightCharts.CrosshairMode.Normal },
});

const candleSeries = chart.addCandlestickSeries({
  upColor: '#22c55e', downColor: '#ef4444',
  borderUpColor: '#22c55e', borderDownColor: '#ef4444',
  wickUpColor: '#22c55e', wickDownColor: '#ef4444',
  priceFormat: { type: 'price', precision: 3, minMove: 0.001 },
  // 概率价格固定 0~1，不让成交量留白把轴撑到负数
  autoscaleInfoProvider: () => ({ priceRange: { minValue: 0, maxValue: 1 } }),
});

const volSeries = chart.addHistogramSeries({
  priceScaleId: '',
  priceFormat: { type: 'volume' },
  lastValueVisible: false,
  priceLineVisible: false,
});
volSeries.priceScale().applyOptions({ scaleMargins: { top: 0.86, bottom: 0 } });

/* ---------- 聚合 ---------- */
function buildRows() {
  const B = state.bucket;
  const perWin = Math.max(1, Math.round(STEP / B));
  const cut = DATA.generated - state.rangeMin * 60;
  const bars = [], vols = [], marks = [];
  for (const w of DATA.windows) {
    if (w.end <= cut) continue;
    const list = (w.trades && w.trades[state.side]) || [];
    let firstBar = true;
    for (let i = 0; i < perWin; i++) {
      const a = w.ts + i * B, b = a + B;
      let o = null, h = null, l = null, c = null, v = 0;
      for (const t of list) {
        if (t.t < a || t.t >= b) continue;
        if (o === null) { o = h = l = t.p; }
        if (t.p > h) h = t.p;
        if (t.p < l) l = t.p;
        c = t.p; v += t.s;
      }
      if (o === null) continue;  // 空桶不占位，时间轴照样连续
      bars.push({ time: a, open: o, high: h, low: l, close: c });
      vols.push({
        time: a, value: v,
        color: c >= o ? 'rgba(34,197,94,0.28)' : 'rgba(239,68,68,0.28)',
      });
      if (firstBar) {
        marks.push({ time: a, position: 'belowBar', color: '#38bdf8', shape: 'circle', size: 1 });
        firstBar = false;
      }
    }
  }
  return { bars, vols, marks };
}

function fmt(t) {
  return new Date(t * 1000).toLocaleString('zh-CN', { hour12: false });
}

function render() {
  stopReplay();
  const { bars, vols, marks } = buildRows();
  candleSeries.setData(bars);
  volSeries.setData(vols);
  candleSeries.setMarkers(marks);
  chart.timeScale().fitContent();
  document.getElementById('hint').textContent =
    `${bars.length} 根 · 每根 ${state.bucket}s · 快照 ${fmt(DATA.generated)}`;
}

/* ---------- 增量回放：演示 update() ---------- */
let timer = null;
const btn = document.getElementById('replay');
function stopReplay() {
  if (timer) { clearInterval(timer); timer = null; }
  btn.textContent = '▶ 回放（update 增量）';
}
btn.onclick = () => {
  if (timer) { stopReplay(); render(); return; }
  const { bars, vols, marks } = buildRows();
  candleSeries.setData([]);
  volSeries.setData([]);
  candleSeries.setMarkers(marks);
  let i = 0;
  btn.textContent = '⏸ 停止';
  timer = setInterval(() => {
    candleSeries.update(bars[i]);
    volSeries.update(vols[i]);
    if (++i >= bars.length) { stopReplay(); chart.timeScale().fitContent(); }
  }, 50);
};

/* ---------- 切档 ---------- */
function bind(segId, fn) {
  document.querySelectorAll('#' + segId + ' button').forEach(b => b.onclick = () => {
    document.querySelectorAll('#' + segId + ' button').forEach(x => x.classList.toggle('on', x === b));
    fn(b); render();
  });
}
bind('viewSeg', b => state.bucket = parseInt(b.dataset.v, 10));
bind('rangeSeg', b => state.rangeMin = parseInt(b.dataset.r, 10));
bind('sideSeg', b => state.side = b.dataset.s);

render();
</script>
</body>
</html>
"""


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--hours", type=float, default=2)
    ap.add_argument("--asset", default="btc", choices=["btc", "eth"])
    ap.add_argument("--workers", type=int, default=8)
    ap.add_argument("--out", default="")
    ap.add_argument("--proxy", default=os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy") or "http://127.0.0.1:10808")
    ap.add_argument("--no-proxy", action="store_true")
    args = ap.parse_args()

    set_proxy(None if args.no_proxy else args.proxy)
    data = build(args)
    payload = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
    out = args.out or os.path.join(ROOT, "charts", f"{args.asset}5m_tv.html")
    os.makedirs(os.path.dirname(out), exist_ok=True)
    with open(out, "w", encoding="utf-8", newline="\n") as f:
        f.write(HTML_TV.replace("__DATA__", payload))
    print("wrote", out, f"({os.path.getsize(out) / 1024:.1f} KB)")


if __name__ == "__main__":
    main()
