# -*- coding: utf-8 -*-
"""generate_charts.py - MySQL -> factor_detail_charts.html (Overview + Strategy Tabs)"""
import json, os, sys, math, urllib.request, re
from datetime import datetime, timezone, timedelta

_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, _ROOT)
OUT = os.path.join(_ROOT, "dashboard", "factor_detail_charts.html")
BJT = timezone(timedelta(hours=8))

import pymysql
from pymysql.cursors import DictCursor

DB = {'host':'127.0.0.1','port':3306,'user':'admin','password':'8ggtjny',
      'database':'coinglass_trader','charset':'utf8mb4','cursorclass':DictCursor,'connect_timeout':5}

FK = ['trend','orderbook','funding','taker','openinterest','maxpain',
      'volumedelta','bitcoin','gamma','impliedvol','exhaust','liquidation',
      'meanrevert','news','smartmoney','mtf','orderbook_liquidity','lowleverage','liquidation_exhaustion']
FN = {'trend':'趋势方向','orderbook':'订单簿失衡','funding':'资金费率',
      'taker':'主动成交','openinterest':'持仓量变化','maxpain':'期权痛点',
      'volumedelta':'成交量变化','bitcoin':'BTC相关性','gamma':'Gamma敞口',
      'impliedvol':'隐含波动率','exhaust':'Taker衰竭','liquidation':'清算冷却',
      'meanrevert':'均值回归','news':'新闻情绪','smartmoney':'聪明钱',
      'mtf':'多时间框架共振','orderbook_liquidity':'订单簿清算比','lowleverage':'低杠杆信号',
      'liquidation_exhaustion':'清算衰竭'}

W = {'trend':0.04,'orderbook':0.10,'funding':0.10,'taker':0.02,'openinterest':0.01,
     'maxpain':0.08,'volumedelta':0.03,'bitcoin':0.02,'gamma':0.02,'impliedvol':0.02,
     'exhaust':0.18,'liquidation':0.04,'meanrevert':0.01,'news':0.12,'smartmoney':0.03,
     'mtf':0.10,'orderbook_liquidity':0.08,'lowleverage':0.04,'liquidation_exhaustion':0.06}

def query(sql, params=None):
    conn = pymysql.connect(**DB)
    try:
        with conn.cursor() as cur: cur.execute(sql, params); return cur.fetchall()
    finally: conn.close()

def fmt_ts(ts):
    if hasattr(ts,'strftime'):
        if ts.tzinfo is None: ts = ts.replace(tzinfo=timezone.utc)
        return ts.astimezone(BJT).strftime('%Y-%m-%d %H:%M:%S')
    return str(ts)[:19]

def collect():
    rows = query("""SELECT price,total_score,direction,
        trend,orderbook,funding,taker,oi,maxpain,vol_delta,btc_corr,
        gamma,iv,exhaust,liq_cool,mean_revert,news,smart_money,
        mtf,ob_liq,low_lev,liq_ex,snapshot_at
        FROM factor_snapshots ORDER BY snapshot_at ASC LIMIT 2000""")
    prices,scores,timestamps,directions=[],[],[],[]
    factors={k:[] for k in FK}
    col_map = {'trend':'trend','orderbook':'orderbook','funding':'funding','taker':'taker',
        'oi':'openinterest','maxpain':'maxpain','vol_delta':'volumedelta','btc_corr':'bitcoin',
        'gamma':'gamma','iv':'impliedvol','exhaust':'exhaust','liq_cool':'liquidation',
        'mean_revert':'meanrevert','news':'news','smart_money':'smartmoney',
        'mtf':'mtf','ob_liq':'orderbook_liquidity','low_lev':'lowleverage','liq_ex':'liquidation_exhaustion'}
    for r in rows:
        p=float(r['price']or 0);s=float(r['total_score']or 0)
        if p==0:continue
        prices.append(p);scores.append(s);timestamps.append(fmt_ts(r['snapshot_at']))
        directions.append(r['direction']or'WAIT')
    for r in rows:
        for dbk,fk in col_map.items():
            factors[fk].append(float(r.get(dbk,0)or 0))

    eq=query("""SELECT equity,available,drawdown_pct,recorded_at
        FROM equity_history ORDER BY recorded_at ASC LIMIT 2000""")
    eqv=[float(e['equity'])for e in eq];eqt=[fmt_ts(e['recorded_at'])for e in eq]
    dd=[float(e.get('drawdown_pct',0)or 0)*100 for e in eq]

    # News events
    news = query("""SELECT snapshot_at,fng,news,news_summary,ds_analysis
        FROM factor_snapshots WHERE ds_analysis IS NOT NULL AND ds_analysis != ''
        ORDER BY id DESC LIMIT 30""")
    news_events = []; seen = set()
    for n in news:
        ds = (n.get('ds_analysis') or '')
        ai_m = re.search(r'AI.*?:([+-]?\d+\.?\d*)', ds)
        ai_s = float(ai_m.group(1)) if ai_m else 0
        bracket = re.findall(r'\[([^\]]+)\]', ds)
        key_fact = bracket[-1] if bracket else ''
        hr_m = re.search(r'~(\d+)h', ds); impact_h = int(hr_m.group(1)) if hr_m else 24
        ts = fmt_ts(n['snapshot_at'])[:16]; fng = n.get('fng') or 0
        hash_key = key_fact[:40]
        if hash_key and hash_key not in seen and len(key_fact)>10:
            seen.add(hash_key)
            news_events.append({'ts':ts,'score':ai_s,'fng':fng,'fact':key_fact,'hours':impact_h})
    news_events.sort(key=lambda x:-abs(x['score']))

    # Trade events for chart markers
    trades = query("""SELECT direction,entry_price,exit_price,pnl,pnl_pct,exit_reason,
        DATE_FORMAT(opened_at,'%Y-%m-%d %H:%i:%s') as opened,
        DATE_FORMAT(closed_at,'%Y-%m-%d %H:%i:%s') as closed
        FROM trades ORDER BY opened_at ASC LIMIT 200""")
    events = []
    for t in trades:
        if t['opened']:
            events.append({'ts':t['opened'],'type':'open','price':float(t['entry_price']),
                'dir':t['direction'],'label':t['direction'][0]+' @'+str(round(float(t['entry_price']),1))})
        if t['closed']:
            pnl = float(t.get('pnl',0)or 0)
            color = '#3fb950' if pnl>0 else '#f85149'
            events.append({'ts':t['closed'],'type':'close','price':float(t.get('exit_price',0)or 0),
                'pnl':pnl,'color':color,'label':(t.get('exit_reason','') or 'close')[:10]})

    n=len(prices);avg=sum(scores)/n if n else 0
    return {'ts':datetime.now(BJT).strftime('%Y-%m-%d %H:%M:%S')+' BJT','n':n,'avg':avg,
            'last_ts':timestamps[-1] if timestamps else '--',
            'p':prices,'s':scores,'t':timestamps,'d':directions,
            'f':factors,'fk':FK,'fn':FN,'ev':eqv,'et':eqt,'dd':dd,'news':news_events,'events':events}

def strat_b():
    try:
        r=urllib.request.urlopen('http://127.0.0.1:5050/api/s',timeout=2)
        return json.loads(r.read())
    except: return None

def get_correlation():
    try:
        from tools.correlation_hedge import analyze
        return analyze()
    except: return {}

JS = """(function(){
var E=window.echarts||echarts;if(!E)return;

function fmt(v){return v.length>=16?v.slice(5,16):v.slice(0,5)}
function z(arr){var m=arr.reduce(function(a,b){return a+b})/arr.length;
 var v=arr.reduce(function(a,b){return a+Math.pow(b-m,2)})/arr.length;
 var std=Math.sqrt(v)||1;return arr.map(function(x){return(x-m)/std})}

// Overview mini chart
(function(){
var c=E.init(document.getElementById('c0'));
var pz=z(D.p),sz=z(D.s);
c.setOption({grid:{left:50,right:15,top:10,bottom:20},
 tooltip:{trigger:'axis',backgroundColor:'rgba(22,27,34,.96)',borderColor:'#30363d',
  textStyle:{color:'#c9d1d9',fontSize:11}},
 xAxis:{type:'category',data:D.t,axisLabel:{show:false},axisLine:{show:false},axisTick:{show:false}},
 yAxis:{type:'value',axisLabel:{show:false},splitLine:{show:false},min:-4,max:4},
 series:[
  {name:'Price(Z)',type:'line',data:pz,smooth:true,symbol:'none',lineStyle:{color:'#58a6ff',width:1.2}},
  {name:'Score(Z)',type:'line',data:sz,smooth:true,symbol:'none',lineStyle:{color:'#d29922',width:1.8}}
 ],dataZoom:[]
});window.addEventListener('resize',function(){c.resize()})})();

// Equity overview mini (percent change from start)
(function(){
var c=E.init(document.getElementById('c0e'));
var base=D.ev[0]||1;
var ep=D.ev.map(function(v){return((v-base)/base*100)});
c.setOption({grid:{left:50,right:50,top:15,bottom:20},
 tooltip:{trigger:'axis',backgroundColor:'rgba(22,27,34,.96)',borderColor:'#30363d',
  textStyle:{color:'#c9d1d9',fontSize:11},
  formatter:function(ps){var i=ps[0].dataIndex;
   return'<b>'+D.et[i]+'</b><br/>Equity: $'+D.ev[i].toFixed(4)+'<br/>Change: '+ep[i].toFixed(4)+'%<br/>DD: '+(D.dd[i]||0).toFixed(4)+'%'}},
 xAxis:{type:'category',data:D.et,axisLabel:{show:false},axisLine:{show:false},axisTick:{show:false}},
 yAxis:[
  {type:'value',axisLabel:{show:false},splitLine:{show:false}},
  {type:'value',axisLabel:{show:false},splitLine:{show:false}}
 ],
 series:[
  {name:'Equity Change %',type:'line',data:ep,smooth:true,symbol:'none',
   lineStyle:{color:'#58a6ff',width:1.5},
   areaStyle:{color:new E.graphic.LinearGradient(0,0,0,1,
    [{offset:0,color:'rgba(88,166,255,.20)'},{offset:1,color:'rgba(88,166,255,0)'}])},yAxisIndex:0},
  {name:'Drawdown %',type:'line',data:D.dd,smooth:true,symbol:'none',
   lineStyle:{color:'#f85149',width:1.2},
   areaStyle:{color:new E.graphic.LinearGradient(0,0,0,1,
    [{offset:0,color:'rgba(248,81,73,.12)'},{offset:1,color:'rgba(248,81,73,0)'}])},yAxisIndex:1}
 ],dataZoom:[]
});window.addEventListener('resize',function(){c.resize()})})();

// Strategy A: Factor lines + Price/Score (factor_detail_charts style)
(function(){
var c=E.init(document.getElementById('c1'));
var pz=z(D.p),sz=z(D.s);
var fc={'trend':'#ff6b6b','orderbook':'#ffa502','funding':'#a55eea',
 'taker':'#2ed573','openinterest':'#1e90ff','maxpain':'#ff4757',
 'volumedelta':'#7bed9f','bitcoin':'#eccc68','gamma':'#e056fd',
 'impliedvol':'#686de0','exhaust':'#ff6348','liquidation':'#3ae374',
 'meanrevert':'#18dcff','news':'#ffd700','smartmoney':'#7d5fff',
 'mtf':'#ff9f43','orderbook_liquidity':'#00d2d3','lowleverage':'#f368e0',
 'liquidation_exhaustion':'#1dd1a1'};
var series=[
 {name:'Price',type:'line',data:D.p,smooth:true,symbol:'none',
  lineStyle:{color:'#58a6ff',width:2},yAxisIndex:0},
 {name:'Score',type:'line',data:D.s,smooth:true,symbol:'none',
  lineStyle:{color:'#d29922',width:2.5},yAxisIndex:1,
  markLine:{silent:true,symbol:'none',lineStyle:{width:1,type:'dashed'},
   data:[{yAxis:0.35,label:{formatter:'LONG',color:'#3fb950',fontSize:9},lineStyle:{color:'#3fb950'}},
         {yAxis:-0.35,label:{formatter:'SHORT',color:'#f85149',fontSize:9},lineStyle:{color:'#f85149'}},
         {yAxis:0,label:{formatter:'0',color:'#8b949e',fontSize:9},lineStyle:{color:'#8b949e'}}]}}
];
// Add all 18 factors as hidden series
D.fk.forEach(function(k){
  var fz=z(D.f[k]||[]);
  series.push({name:D.fn[k]||k,type:'line',data:fz,smooth:true,symbol:'none',
   lineStyle:{color:fc[k]||'#888',width:1},yAxisIndex:1,
   legendHoverLink:false});
});
c.setOption({grid:{left:60,right:70,top:15,bottom:65},
 legend:{type:'scroll',bottom:38,textStyle:{color:'#8b949e',fontSize:9},
  data:['Price','Score'].concat(D.fk.map(function(k){return D.fn[k]||k})),
  selected:{Price:true,Score:true}},
 tooltip:{trigger:'axis',backgroundColor:'rgba(22,27,34,.96)',borderColor:'#30363d',
  textStyle:{color:'#c9d1d9',fontSize:11}},
 xAxis:{type:'category',data:D.t,axisLabel:{color:'#8b949e',fontSize:9,formatter:fmt},axisLine:{lineStyle:{color:'#21262d'}}},
 yAxis:[{type:'value',name:'Price',nameTextStyle:{color:'#58a6ff'},
  axisLabel:{color:'#58a6ff',fontSize:10,formatter:function(v){return'$'+v.toFixed(0)}},
  splitLine:{lineStyle:{color:'#21262d',type:'dashed'}},position:'right'},
  {type:'value',name:'Score',nameTextStyle:{color:'#d29922'},
   axisLabel:{color:'#d29922',fontSize:10},splitLine:{show:false},min:-1,max:1}],
 series:series,
 dataZoom:[{type:'inside',start:0,end:100},
  {type:'slider',bottom:8,height:14,borderColor:'#21262d',backgroundColor:'#0d1117',
   dataBackground:{lineStyle:{color:'#8b949e'},areaStyle:{color:'rgba(139,148,158,.1)'}},
   textStyle:{color:'#8b949e'}}]
});window.addEventListener('resize',function(){c.resize()})})();

// Strategy A: Volatility sub-chart (ATR from factor price data)
(function(){
var c=E.init(document.getElementById('c1v'));
var atr=[],n=D.p.length,period=14;
for(var i=0;i<n;i++){
  if(i<period){atr.push(null);continue;}
  var tr=0;
  for(var j=Math.max(0,i-period+1);j<=i;j++){
    var h=D.p[j]*1.001,l=D.p[j]*0.999,pc=j>0?D.p[j-1]:D.p[j];
    tr+=Math.max(h-l,Math.abs(h-pc),Math.abs(l-pc));
  }
  atr.push((tr/period/D.p[i]*100).toFixed(4));
}
c.setOption({grid:{left:60,right:70,top:5,bottom:20},
 tooltip:{trigger:'axis',backgroundColor:'rgba(22,27,34,.96)',borderColor:'#30363d',
  textStyle:{color:'#c9d1d9',fontSize:11}},
 xAxis:{type:'category',data:D.t,axisLabel:{show:false},axisLine:{show:false},axisTick:{show:false}},
 yAxis:{type:'value',name:'ATR%',nameTextStyle:{color:'#8b949e'},
  axisLabel:{color:'#8b949e',fontSize:9},splitLine:{lineStyle:{color:'#21262d',type:'dashed'}}},
 series:[{name:'ATR(14)%',type:'line',data:atr,smooth:true,symbol:'none',
  lineStyle:{color:'#a371f7',width:1.5},
  areaStyle:{color:new E.graphic.LinearGradient(0,0,0,1,
   [{offset:0,color:'rgba(167,113,247,.12)'},{offset:1,color:'rgba(167,113,247,0)'}])}}],
 dataZoom:[{type:'inside',start:0,end:100}]
});window.addEventListener('resize',function(){c.resize()})})();

// Strategy A: Factor Heatmap
(function(){
var c=E.init(document.getElementById('c2'));
var kk=D.fk,nn=D.fn,ff=D.f,sd=[];
for(var ki=0;ki<kk.length;ki++){var k=kk[ki],arr=ff[k]||[];
 for(var vi=0;vi<arr.length&&vi<D.t.length;vi++)sd.push([vi,ki,arr[vi]])}
c.setOption({grid:{left:120,right:40,top:10,bottom:35},
 tooltip:{backgroundColor:'rgba(22,27,34,.96)',borderColor:'#30363d',textStyle:{color:'#c9d1d9',fontSize:11},
  formatter:function(p){var d=p.data;return(nn[kk[d[1]]]||kk[d[1]])+'<br/>Score:'+d[2].toFixed(4)+'<br/>'+D.t[d[0]]}},
 xAxis:{type:'category',data:D.t.map(fmt),axisLabel:{color:'#8b949e',fontSize:9},axisLine:{lineStyle:{color:'#21262d'}}},
 yAxis:{type:'category',data:kk.map(function(k){return nn[k]||k}),
  axisLabel:{color:'#c9d1d9',fontSize:9},axisLine:{show:false},axisTick:{show:false}},
 visualMap:{min:-1,max:1,calculable:true,orient:'vertical',right:0,top:'center',
  itemWidth:8,itemHeight:120,inRange:{color:['#f85149','#161b22','#3fb950']},
  text:['+1','-1'],textStyle:{color:'#8b949e',fontSize:9}},
 series:[{type:'heatmap',data:sd,itemStyle:{borderWidth:1,borderColor:'#0d1117'}}],
 dataZoom:[{type:'inside',start:0,end:100}]
});window.addEventListener('resize',function(){c.resize()})})();

// Strategy A: Equity (dual-axis: change% + DD%)
(function(){
var c=E.init(document.getElementById('c3'));
var base=D.ev[0]||1;
var ep=D.ev.map(function(v){return((v-base)/base*100)});
c.setOption({grid:{left:55,right:55,top:15,bottom:35},
 legend:{data:['Equity Change %','Drawdown %'],bottom:0,textStyle:{color:'#8b949e',fontSize:9}},
 tooltip:{trigger:'axis',backgroundColor:'rgba(22,27,34,.96)',borderColor:'#30363d',textStyle:{color:'#c9d1d9',fontSize:12},
  formatter:function(ps){var i=ps[0].dataIndex;return'<b>'+D.et[i]+'</b><br/>Equity: <b>$'+D.ev[i].toFixed(4)+'</b><br/>Change: '+ep[i].toFixed(4)+'%<br/>Drawdown: <b>'+(D.dd[i]||0).toFixed(4)+'%</b>'}},
 xAxis:{type:'category',data:D.et,axisLabel:{color:'#8b949e',fontSize:9,formatter:fmt},axisLine:{lineStyle:{color:'#21262d'}}},
 yAxis:[
  {type:'value',name:'Equity Change %',nameTextStyle:{color:'#58a6ff'},
   axisLabel:{color:'#58a6ff',fontSize:10,formatter:function(v){return v.toFixed(3)+'%'}},
   splitLine:{lineStyle:{color:'#21262d',type:'dashed'}}},
  {type:'value',name:'Drawdown %',nameTextStyle:{color:'#f85149'},
   axisLabel:{color:'#f85149',fontSize:10,formatter:function(v){return v.toFixed(3)+'%'}},
   splitLine:{show:false}}
 ],
 series:[
  {name:'Equity Change %',type:'line',data:ep,smooth:true,symbol:'none',yAxisIndex:0,
   lineStyle:{color:'#58a6ff',width:1.5},
   areaStyle:{color:new E.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(88,166,255,.15)'},{offset:1,color:'rgba(88,166,255,0)'}])}},
  {name:'Drawdown %',type:'line',data:D.dd,smooth:true,symbol:'none',yAxisIndex:1,
   lineStyle:{color:'#f85149',width:1.2},
   areaStyle:{color:new E.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(248,81,73,.10)'},{offset:1,color:'rgba(248,81,73,0)'}])}}
 ],dataZoom:[{type:'inside',start:0,end:100}]
});window.addEventListener('resize',function(){c.resize()})})();

// Strategy B: P1 + Adv
(function(){
var c=E.init(document.getElementById('c4'));
if(D.sb&&D.sb.mid){var sb=D.sb;
 c.setOption({grid:{left:50,right:50,top:15,bottom:35},
  legend:{data:['P1','Adv'],bottom:0,textStyle:{color:'#8b949e',fontSize:10}},
  tooltip:{trigger:'axis',backgroundColor:'rgba(22,27,34,.96)',borderColor:'#30363d'},
  xAxis:{type:'category',data:sb.tls||[],axisLabel:{color:'#8b949e',fontSize:9}},
  yAxis:{type:'value',name:'P1/Adv',nameTextStyle:{color:'#8b949e'},axisLabel:{color:'#8b949e',fontSize:9},
   splitLine:{lineStyle:{color:'#21262d',type:'dashed'}}},
  series:[
   {name:'P1',type:'line',data:sb.p1h||[],smooth:true,symbol:'none',lineStyle:{color:'#a371f7',width:2},
    markLine:{silent:true,symbol:'none',lineStyle:{width:1,type:'dashed'},
     data:[{yAxis:0.8,label:{formatter:'Entry',color:'#3fb950',fontSize:9},lineStyle:{color:'#3fb950'}},
           {yAxis:0.55,label:{formatter:'Watch',color:'#d29922',fontSize:9},lineStyle:{color:'#d29922'}}]}},
   {name:'Adv',type:'line',data:sb.advh||[],smooth:true,symbol:'none',lineStyle:{color:'#58a6ff',width:1.5},
    markLine:{silent:true,symbol:'none',lineStyle:{width:1,type:'dashed'},
     data:[{yAxis:1.0,label:{formatter:'Entry',color:'#3fb950',fontSize:9},lineStyle:{color:'#3fb950'}}]}}
  ],dataZoom:[{type:'inside',start:0,end:100}]
 });
}else{var h2=document.getElementById('sb-status');if(h2)h2.innerHTML='Waiting for WebSocket data...'}
window.addEventListener('resize',function(){c.resize()})})();

// Strategy B: Order Book Depth
(function(){
var c=E.init(document.getElementById('c5'));
if(D.sb&&D.sb.mid){
 var db=D.sb.db||[],da=D.sb.da||[];
 if(db.length||da.length){
  var bp=db.map(function(d){return d.p}),bv=db.map(function(d){return d.c});
  var ap=da.map(function(d){return d.p}),av=da.map(function(d){return d.c});
  c.setOption({grid:{left:55,right:15,top:15,bottom:35},
   tooltip:{trigger:'axis',backgroundColor:'rgba(22,27,34,.96)',borderColor:'#30363d',textStyle:{color:'#c9d1d9',fontSize:11}},
   xAxis:{type:'value',axisLabel:{color:'#8b949e',fontSize:9,formatter:function(v){return'$'+v.toFixed(0)}},splitLine:{lineStyle:{color:'#21262d',type:'dashed'}}},
   yAxis:{type:'value',name:'Depth',nameTextStyle:{color:'#8b949e'},axisLabel:{color:'#8b949e',fontSize:9}},
   series:[
    {name:'Bid',type:'line',data:bp.map(function(p,i){return[p,bv[i]]}),smooth:true,symbol:'none',
     lineStyle:{color:'#3fb950',width:1.5},areaStyle:{color:'rgba(63,185,80,.10)'},step:'end'},
    {name:'Ask',type:'line',data:ap.map(function(p,i){return[p,av[i]]}),smooth:true,symbol:'none',
     lineStyle:{color:'#f85149',width:1.5},areaStyle:{color:'rgba(248,81,73,.06)'},step:'end'}]});
 }else{document.getElementById('c5').innerHTML='<div style=display:flex;align-items:center;justify-content:center;height:100%;color:var(--muted)>Waiting for depth data...</div>'}
}else{document.getElementById('c5').innerHTML='<div style=display:flex;align-items:center;justify-content:center;height:100%;color:var(--muted)>Waiting for WebSocket...</div>'}
window.addEventListener('resize',function(){c.resize()})})();




})();"""

def build_html(data, sb, corr=None):
    sb_data = None
    if sb:
        sb_data = {'mid':sb.get('mid'),'p1':sb.get('p1'),'adv':sb.get('adv'),
            'level':sb.get('level'),'p1h':sb.get('p1h',[]),'advh':sb.get('advh',[]),'tls':sb.get('tls',[]),
            'db':sb.get('db',[]),'da':sb.get('da',[]),'imb':sb.get('imb'),
            'iliq':sb.get('iliq'),'wall':sb.get('wall')}
    # Hedge HTML and corr time
    corr_time = corr.get('timestamp','--') if corr else '--'
    hedge_html = '<table style=\"width:100%;border-collapse:collapse;font-size:11px\"><tr style=\"color:var(--muted);font-size:10px\"><th style=\"padding:6px 10px;text-align:left\">Asset</th><th style=\"padding:6px 10px;text-align:center\">Corr to BTC</th><th style=\"padding:6px 10px;text-align:center\">Quality</th><th style=\"padding:6px 10px;text-align:center\">Weight %</th><th style=\"padding:6px 10px;text-align:center\">Direction</th></tr>'
    if corr:
        for h in (corr.get('hedge_suggestions',[]) or [])[:10]:
            q_cls = '#3fb950' if h['quality']=='Excellent' else '#d29922' if h['quality']=='Good' else '#f85149' if h['quality']=='Poor' else '#8b949e'
            hedge_html += '<tr style=\"border-bottom:1px solid rgba(255,255,255,.03)\"><td style=\"padding:6px 10px\">%s</td><td style=\"padding:6px 10px;text-align:center;font-family:SF Mono,Consolas,monospace\">%.3f</td><td style=\"padding:6px 10px;text-align:center;color:%s;font-weight:600\">%s</td><td style=\"padding:6px 10px;text-align:center;font-weight:600\">%.1f%%</td></tr>' % (h['label'], h['corr'], q_cls, h['quality'], h['weight'])
    if not corr or not corr.get('hedge_suggestions'):
        hedge_html += '<tr><td colspan=\"5\" style=\"text-align:center;color:var(--muted);padding:20px\">Waiting for correlation data...</td></tr>'
    hedge_html += '</table>'

    # Build corr panel HTML (appended after f-string to avoid template issues)
    corr_panel = ''
    if corr and corr.get('symbols'):
        corr_panel = '''<div class="tab-panel" id="panel-corr">
  <div class="card">
    <div class="card-hd"><span>Multi-Asset Correlation (1W 1H bars)</span><span class="extra">''' + corr_time + ''' BJT</span></div>
    <div class="chart" id="c6" style="height:420px"></div>
  </div>
  <div class="grid2">
    <div class="card"><div class="card-hd">Hedge Allocation vs BTC</div><div style="max-height:420px;overflow-y:auto">''' + hedge_html + '''</div></div>
    <div class="card"><div class="card-hd">Annualized Volatility</div><div class="chart" id="c7" style="height:400px"></div></div>
  </div>
</div>
'''
    else:
        corr_panel = '''<div class="tab-panel" id="panel-corr">
  <div class="card"><div class="card-hd">Multi-Asset Correlation</div>
  <div style="text-align:center;color:var(--muted);padding:80px">Loading correlation data...</div></div>
</div>
'''

    D = json.dumps({'p':data['p'],'s':data['s'],'t':data['t'],'d':data['d'],
        'f':data['f'],'fk':data['fk'],'fn':data['fn'],
        'ev':data['ev'],'et':data['et'],'dd':data['dd'],'sb':sb_data,
        'corr':corr if corr else None,
        'events':data.get('events',[]),},ensure_ascii=False)

    # Weights HTML (Chinese names, sorted high-low)
    weights_html = ''
    max_w = max(W.values())
    colors = {'exhaust':'#ff6348','news':'#ffd700','orderbook':'#ffa502','funding':'#a55eea',
        'mtf':'#ff9f43','maxpain':'#ff4757','orderbook_liquidity':'#00d2d3','liquidation_exhaustion':'#1dd1a1',
        'liquidation':'#3ae374','trend':'#ff6b6b','lowleverage':'#f368e0','smartmoney':'#7d5fff',
        'volumedelta':'#7bed9f','impliedvol':'#686de0','gamma':'#e056fd','bitcoin':'#eccc68',
        'taker':'#2ed573','openinterest':'#1e90ff','meanrevert':'#18dcff'}
    for k,w in sorted(W.items(), key=lambda x:-x[1]):
        fn = FN.get(k,k); c = colors.get(k,'#8b949e')
        weights_html += f'<tr><td style="padding:4px 10px;font-size:12px">{fn}</td><td style="padding:4px 10px;text-align:right;font-weight:600;color:var(--gold);font-family:SF Mono,Consolas,monospace">{w:.2f}</td><td style="padding:4px 10px"><div style="background:var(--border);height:6px;border-radius:3px;min-width:60px"><div style="background:{c};height:6px;border-radius:3px;width:{int(w/max_w*100)}%"></div></div></td></tr>'

    # News events HTML
    news_html = ''
    for i,n in enumerate(data['news'][:10]):
        cls = 'impact-high' if abs(n['score'])>0.7 else 'impact-mid' if abs(n['score'])>0.4 else 'impact-low'
        news_html += f'''<div class="news-row {cls}">
          <div class="news-rank">#{i+1}</div>
          <div class="news-body"><div class="news-fact">{n['fact']}</div>
          <div class="news-meta">{n['ts']} &middot; AI {n['score']:.2f} &middot; Impact ~{n['hours']}h &middot; FNG {n['fng']}</div></div>
          <div class="news-badge" style="background:{'#f85149' if n['score']<-0.5 else '#d29922' if n['score']<-0.2 else '#3fb950'}">{n['score']:.2f}</div></div>'''
    if not news_html: news_html = '<div style="text-align:center;color:var(--muted);padding:40px">Waiting for news data...</div>'

    sb_status = 'Connected' if sb and sb.get('mid') else 'Not connected'
    sb_mid = sb.get('mid','--') if sb else '--'
    sb_level = sb.get('level','--') if sb else '--'
    sb_p1 = float(sb.get('p1',0)or 0) if sb else 0
    sb_adv = float(sb.get('adv',0)or 0) if sb else 0
    sb_imb = float(sb.get('imb',0)or 0) if sb else 0
    sb_iliq = float(sb.get('iliq',0)or 0) if sb else 0
    sb_wall = float(sb.get('wall',0)or 0) if sb else 0

    # Liquidation events HTML - terminal feed style
    le_html = '<div style="font-size:11px"><div style="display:flex;padding:6px 14px;color:var(--muted);font-size:10px;border-bottom:1px solid var(--border)"><span style="width:40px">Side</span><span style="width:80px">Size</span><span style="width:80px">Price</span><span style="flex:1">Value</span></div>'
    if sb:
        le_list = sb.get('le',[]) or []
        for e in reversed(le_list[-20:]):
            side = e.get('s',''); sz = e.get('sz',0); px = e.get('px',0); v = sz*px
            cls = 'var(--green)' if side=='buy' else 'var(--red)'
            side_str = side.upper() if side else '?'
            bar_w = min(100, int(v/50000*100)) if v>0 else 0
            le_html += '<div style="display:flex;padding:3px 14px;border-bottom:1px solid rgba(255,255,255,.02);align-items:center;font-size:11px">'
            le_html += '<span style="width:40px;color:' + cls + ';font-weight:600">' + side_str + '</span>'
            le_html += '<span style="width:80px">' + str(sz) + '</span>'
            le_html += '<span style="width:80px">' + str(px) + '</span>'
            le_html += '<span style="flex:1;position:relative">$' + str(int(v)) + '<span style="position:absolute;left:0;top:0;height:100%;background:' + cls + ';opacity:0.08;width:' + str(bar_w) + '%"></span></span>'
            le_html += '</div>'
    if not le_html or '</div>' not in le_html.split('<div')[1:]:
        le_html = '<div style="text-align:center;color:var(--muted);padding:60px;font-size:14px">Waiting for real-time liquidation data...</div>'
    else:
        le_html += '</div>'

    return f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>BTC Quantitative Strategy</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
<style>
:root{{--bg:#0a0e14;--card:#11161e;--card2:#161d28;--border:#1e293b;--text:#e2e8f0;--muted:#64748b;--green:#22c55e;--red:#ef4444;--blue:#3b82f6;--gold:#f59e0b;--purple:#a855f7}}
*{{margin:0;padding:0;box-sizing:border-box}}
body{{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Noto Sans SC,sans-serif;font-size:13px;line-height:1.4}}
::-webkit-scrollbar{{width:3px}}::-webkit-scrollbar-thumb{{background:var(--border)}}
.header{{display:flex;align-items:center;justify-content:space-between;padding:10px 24px;background:var(--card);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100}}
.header .brand{{font-size:15px;font-weight:700;letter-spacing:.3px}}
.header .brand .dot{{display:inline-block;width:8px;height:8px;background:var(--green);border-radius:50%;margin-right:10px;animation:pulse 2s infinite;box-shadow:0 0 6px var(--green)}}
@keyframes pulse{{0%,100%{{opacity:1;transform:scale(1)}}50%{{opacity:.4;transform:scale(.8)}}}}
.header .meta{{display:flex;gap:20px;color:var(--muted);font-size:11px}}
.header .meta b{{color:var(--text);font-weight:600}}
.tabs{{display:flex;gap:2px;background:var(--card);border-bottom:1px solid var(--border);padding:0 24px}}
.tab-btn{{padding:10px 22px;font-size:12px;font-weight:600;color:var(--muted);cursor:pointer;background:none;border:none;border-bottom:2px solid transparent;transition:all .2s;font-family:inherit;letter-spacing:.3px}}
.tab-btn:hover{{color:var(--text);background:rgba(59,130,246,.05)}}
.tab-btn.active{{color:var(--blue);border-bottom-color:var(--blue)}}
.tab-panel{{display:none;padding:12px 16px;max-width:1800px;margin:0 auto}}
.tab-panel.active{{display:block}}
.card{{background:var(--card);border:1px solid var(--border);border-radius:10px;margin-bottom:10px;overflow:hidden;transition:border-color .2s}}
.card:hover{{border-color:rgba(59,130,246,.15)}}
.card-hd{{padding:10px 18px;border-bottom:1px solid var(--border);font-size:11px;font-weight:600;color:var(--muted);letter-spacing:.8px;display:flex;justify-content:space-between;align-items:center;text-transform:uppercase}}
.card-hd .extra{{font-weight:400;font-size:10px;letter-spacing:0;text-transform:none}}
.chart{{width:100%}}
.grid2{{display:grid;grid-template-columns:1fr 1fr;gap:10px}}
.grid3{{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}}
@media(max-width:1100px){{.grid2,.grid3{{grid-template-columns:1fr}}}}
.kpi-tile{{padding:16px 20px;background:linear-gradient(135deg,var(--card) 0%,var(--card2) 100%);border:1px solid var(--border);border-radius:10px;text-align:center;position:relative;overflow:hidden}}
.kpi-tile::before{{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--blue),transparent);opacity:.4}}
.kpi-tile .label{{font-size:10px;color:var(--muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:1px;font-weight:500}}
.kpi-tile .value{{font-size:28px;font-weight:800;letter-spacing:-1px;line-height:1.1}}
.kpi-tile .sub{{font-size:10px;color:var(--muted);margin-top:4px}}
.news-row{{display:flex;align-items:flex-start;gap:10px;padding:10px 14px;border-bottom:1px solid var(--border);transition:background .2s}}
.news-row:hover{{background:rgba(59,130,246,.04)}}
.news-rank{{font-size:13px;font-weight:700;color:var(--muted);min-width:26px;text-align:center;line-height:1.5}}
.news-body{{flex:1}}.news-fact{{font-size:12px;line-height:1.5;color:var(--text);margin-bottom:2px}}
.news-meta{{font-size:10px;color:var(--muted)}}
.news-badge{{font-size:12px;font-weight:700;color:#fff;padding:3px 10px;border-radius:4px;min-width:46px;text-align:center;font-family:SF Mono,Consolas,monospace;flex-shrink:0}}
.impact-high{{border-left:3px solid var(--red)}}.impact-mid{{border-left:3px solid var(--gold)}}.impact-low{{border-left:3px solid var(--green)}}
.footer{{text-align:center;padding:10px;color:var(--muted);font-size:10px;border-top:1px solid var(--border)}}
</style>
</head>
<body>

<div class="header">
  <div class="brand"><span class="dot"></span>BTC Quantitative Strategy</div>
  <div class="meta">Data <b>{data['n']} points</b> | Avg Score <b>{data['avg']:.3f}</b> | Updated <b>{data.get('last_ts','--')}</b></div>
</div>

<div class="tabs">
  <button class="tab-btn active" onclick="switchTab('overview')">Overview</button>
  <button class="tab-btn" onclick="switchTab('a')">Strategy A · 18-Factor</button>
  
  <a href="/corr" class="tab-btn" style="text-decoration:none">Correlation &amp; Hedge</a>
  <button class="tab-btn" onclick="switchTab('b')">Strategy B · Liquidation</button>
</div>

<!-- OVERVIEW TAB -->
<div class="tab-panel active" id="panel-overview">
  <div class="grid3">
    <div class="kpi-tile"><div class="label">Current Price</div><div class="value" style="color:var(--blue)">${data.get('latest_price','--'):.2f}</div><div class="sub">BTC-USDT-SWAP</div></div>
    <div class="kpi-tile"><div class="label">Total Score</div><div class="value" style="color:{'#f85149' if data['avg']<-0.2 else '#3fb950' if data['avg']>0.2 else '#d29922'}">{data.get('latest_score',data['avg']):.3f}</div><div class="sub">Direction: {data.get('latest_dir','WAIT')}</div></div>
    <div class="kpi-tile"><div class="label">Equity</div><div class="value" style="color:var(--green)">${data.get('latest_equity',0):.4f}</div><div class="sub">Drawdown {data.get('latest_dd',0)*100:.2f}%</div></div>
  </div>
  <div class="grid3">
    <div class="kpi-tile"><div class="label">Fear & Greed</div><div class="value" style="color:{'#f85149' if data['fng']<30 else '#d29922' if data['fng']<55 else '#3fb950'}">{data['fng']}</div><div class="sub">{'Fear' if data['fng']<30 else 'Neutral' if data['fng']<55 else 'Greed'}</div></div>
    <div class="kpi-tile"><div class="label">News Score</div><div class="value" style="color:{'#f85149' if data['news_score']<-0.3 else '#3fb950' if data['news_score']>0.3 else '#d29922'}">{data['news_score']:.2f}</div><div class="sub">AI Sentiment</div></div>
    <div class="kpi-tile"><div class="label">Data Freshness</div><div class="value" style="color:var(--green);font-size:16px">{data.get('last_ts','--')}</div><div class="sub">BJT UTC+8</div></div>
  </div>
  <div class="grid2">
    <div class="card">
      <div class="card-hd"><span>Price & Score (Z-Score Normalized)</span><span class="extra">Blue=Price Gold=Score</span></div>
      <div class="chart" id="c0" style="height:240px"></div>
    </div>
    <div class="card">
      <div class="card-hd"><span>Equity & Drawdown (Z-Score Normalized)</span></div>
      <div class="chart" id="c0e" style="height:240px"></div>
    </div>
  </div>
  <div class="grid2">
    <div class="card">
      <div class="card-hd">Impact Events</div>
      <div style="max-height:300px;overflow-y:auto">{news_html}</div>
    </div>
    <div class="card">
      <div class="card-hd">Factor Weights (High to Low)</div>
      <div style="padding:8px 12px;max-height:300px;overflow-y:auto">
        <table style="width:100%;border-collapse:collapse">{weights_html}</table>
      </div>
    </div>
  </div>
</div>

<!-- STRATEGY A TAB -->
<div class="tab-panel" id="panel-a">
  <div class="card">
    <div class="card-hd"><span>Price & Score · Z-Score Normalized</span><span class="extra">Blue=Price Gold=Score Dashed=Thresholds</span></div>
    <div class="chart" id="c1" style="height:380px"></div>
    <div class="chart" id="c1v" style="height:120px"></div>
  </div>
  <div class="grid2">
    <div class="card">
      <div class="card-hd">18-Factor Heatmap</div>
      <div class="chart" id="c2" style="height:320px"></div>
    </div>
    <div class="card">
      <div class="card-hd">Equity & Drawdown (Z-Score Normalized)</div>
      <div class="chart" id="c3" style="height:340px"></div>
    </div>
  </div>
</div>

<!-- STRATEGY B TAB -->
<div class="tab-panel" id="panel-b">
  <!-- Hero signal indicator -->
  <div class="card" style="border-left:4px solid {'var(--green)' if sb_p1>0.8 else 'var(--gold)' if sb_p1>0.55 else 'var(--muted)'};margin-bottom:10px">
    <div style="padding:16px 20px;display:flex;align-items:center;gap:24px">
      <div style="text-align:center;min-width:120px">
        <div style="font-size:10px;color:var(--muted);letter-spacing:1px;margin-bottom:4px">SIGNAL LEVEL</div>
        <div style="font-size:36px;font-weight:800;color:{'var(--green)' if sb_p1>0.8 else 'var(--gold)' if sb_p1>0.55 else 'var(--muted)'}">{sb_level}</div>
      </div>
      <div style="flex:1;display:flex;gap:24px">
        <div><div style="font-size:10px;color:var(--muted)">P1</div><div style="font-size:24px;font-weight:700;font-family:SF Mono,Consolas,monospace">{sb_p1:.3f}</div><div style="font-size:9px;color:var(--muted)">threshold: 0.55/0.80</div></div>
        <div><div style="font-size:10px;color:var(--muted)">Adv</div><div style="font-size:24px;font-weight:700;font-family:SF Mono,Consolas,monospace">{sb_adv:.2f}</div><div style="font-size:9px;color:var(--muted)">threshold: 1.0</div></div>
        <div><div style="font-size:10px;color:var(--muted)">Mid</div><div style="font-size:24px;font-weight:700;font-family:SF Mono,Consolas,monospace;color:var(--blue)">${sb_mid}</div><div style="font-size:9px;color:var(--muted)">BTC-USDT-SWAP</div></div>
        <div><div style="font-size:10px;color:var(--muted)">Imbalance</div><div style="font-size:24px;font-weight:700;font-family:SF Mono,Consolas,monospace;color:{'var(--green)' if sb_imb>0 else 'var(--red)'}">{sb_imb:+.3f}</div><div style="font-size:9px;color:var(--muted)">Bid/Ask</div></div>
        <div><div style="font-size:10px;color:var(--muted)">I_liq</div><div style="font-size:24px;font-weight:700;font-family:SF Mono,Consolas,monospace">${sb_iliq:.0f}/s</div><div style="font-size:9px;color:var(--muted)">Intensity</div></div>
        <div><div style="font-size:10px;color:var(--muted)">Wall</div><div style="font-size:24px;font-weight:700;font-family:SF Mono,Consolas,monospace">{sb_wall:.0f}</div><div style="font-size:9px;color:var(--muted)">Thickness</div></div>
      </div>
    </div>
  </div>

  <div class="grid2">
    <div class="card">
      <div class="card-hd"><span>Order Book Depth (Bid/Ask)</span><span class="extra" style="color:'var(--muted)'">Real-time from OKX WebSocket</span></div>
      <div class="chart" id="c5" style="height:380px"></div>
    </div>
    <div class="card">
      <div class="card-hd"><span>P1 + Adv Signal Trend</span><span class="extra" id="sb-status">{sb_status}</span></div>
      <div class="chart" id="c4" style="height:380px"></div>
    </div>
  </div>

  <div class="card">
    <div class="card-hd"><span>Recent Liquidation Events (real-time)</span></div>
    <div style="max-height:320px;overflow-y:auto;font-family:SF Mono,Consolas,monospace">
      {le_html}
    </div>
  </div>
</div>



<div class="footer">Coinglass Quantitative Strategy · MySQL · BJT (UTC+8) · Auto-refresh each cycle</div>

<script>
function switchTab(name){{
  document.querySelectorAll('.tab-btn').forEach(function(b){{b.classList.remove('active')}});
  document.querySelectorAll('.tab-panel').forEach(function(p){{p.classList.remove('active')}});
  document.getElementById('panel-'+name).classList.add('active');
  document.querySelectorAll('.tab-btn').forEach(function(b){{
    var t=b.textContent.toLowerCase();
    if((name==='overview'&&t.indexOf('overview')!==-1)||(name==='a'&&t.indexOf('18-factor')!==-1)||(name==='b'&&t.indexOf('liquidation')!==-1)||(name==='corr'&&t.indexOf('correlation')!==-1))b.classList.add('active')
  }});
  setTimeout(function(){{
    var ec=window.echarts||echarts;
    ['c0','c0e','c1','c2','c3','c4','c5'].forEach(function(id){{
      var dom=document.getElementById(id);if(dom){{var inst=ec.getInstanceByDom(dom);if(inst)inst.resize()}}
    }})
  }},150);
}}

const D = ''' + D + ''';
''' + JS + '''
</script>
</body>
</html>'''


def main():
    print('Collecting data...')
    data = collect()
    latest = query("""SELECT price,total_score,direction,fng,news FROM factor_snapshots ORDER BY id DESC LIMIT 1""")
    if latest:
        r = latest[0]
        data['latest_price'] = float(r.get('price',0)or 0)
        data['latest_score'] = float(r.get('total_score',0)or 0)
        data['latest_dir'] = r.get('direction','WAIT')
        data['fng'] = int(r.get('fng',0)or 0)
        data['news_score'] = float(r.get('news',0)or 0)
    else:
        data['latest_price'] = 0; data['latest_score'] = 0; data['latest_dir'] = 'WAIT'
        data['fng'] = 28; data['news_score'] = -1.0

    eq_last = query("""SELECT equity,drawdown_pct FROM equity_history ORDER BY id DESC LIMIT 1""")
    data['latest_equity'] = float(eq_last[0].get('equity',0)or 0) if eq_last else 0
    data['latest_dd'] = float(eq_last[0].get('drawdown_pct',0)or 0) if eq_last else 0

    sb = strat_b()
    corr = get_correlation()
    print(f'  A: {data["n"]} points, score {data["avg"]:.3f}, {len(data["news"])} news')
    print(f'  B: {"online" if sb and sb.get("mid") else "offline"}')
    print(f'  Corr: {len(corr.get("symbols",[]))} assets')

    html = build_html(data, sb, corr)
    os.makedirs(os.path.dirname(OUT), exist_ok=True)
    with open(OUT, 'w', encoding='utf-8') as f: f.write(html)
    print(f'  -> {OUT} ({os.path.getsize(OUT)/1024:.0f} KB)')


if __name__ == '__main__':
    main()
