#!/usr/bin/env python3
"""
专业交易员看板 v5 — 三层架构：宏观指数 → 板块分析 → 个股分析
增强：关注功能、板块详情、K线演示、个股详情优化
"""
import json, os, math
from collections import Counter, defaultdict
try:
    from pypinyin import lazy_pinyin, Style
    HAS_PINYIN = True
except ImportError:
    HAS_PINYIN = False

def safe_float(v):
    if isinstance(v, float):
        if math.isinf(v): return 99.99 if v > 0 else 0.0
        if math.isnan(v): return 0.0
    return v

def sanitize_data(obj):
    if isinstance(obj, float): return safe_float(obj)
    elif isinstance(obj, dict): return {k: sanitize_data(v) for k, v in obj.items()}
    elif isinstance(obj, list): return [sanitize_data(i) for i in obj]
    return obj

def safe_json_dumps(obj, **kw):
    return json.dumps(sanitize_data(obj), **kw)

def safe_str(v):
    v = safe_float(v)
    if isinstance(v, float): return str(round(v, 2))
    return str(v)

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
WORK_DIR = os.path.join(BASE_DIR, "data")
OUTPUT_DIR = BASE_DIR
RESULT_FILE = os.path.join(WORK_DIR, "scan_results_filtered.json")
INDUSTRY_FILE = os.path.join(WORK_DIR, "stock_industry_concepts_new.json")
BACKTEST_FILE = os.path.join(WORK_DIR, "backtest_results.json")
KLINE_FILE = os.path.join(WORK_DIR, "kline_data.json")
OUTPUT_HTML = os.path.join(OUTPUT_DIR, "A股K线指标扫描报告.html")

with open(RESULT_FILE, "r") as f:
    data = json.load(f)

industry_data = {}
if os.path.exists(INDUSTRY_FILE):
    with open(INDUSTRY_FILE, "r") as f:
        industry_data = json.load(f).get("stock_info", {})

# ===== 产业链上下游映射（幻方量化·板块轮动推演）=====
# 将行业归入 上游(资源/原料) / 中游(制造/加工) / 下游(应用/消费) / 服务(金融/交运) 四个环节
SECTOR_STAGE = {
    "煤炭开采": "上游", "焦炭Ⅱ": "上游", "贵金属": "上游", "小金属": "上游",
    "工业金属": "上游", "化学原料": "上游", "农化制品": "上游",
    "普钢": "中游", "特钢Ⅱ": "中游", "化学制品": "中游", "化学制药": "中游",
    "生物制品": "中游", "中药Ⅱ": "中游", "电力": "中游", "燃气Ⅱ": "中游",
    "通用设备": "中游", "汽车零部件": "中游", "通信设备": "中游", "航空装备Ⅱ": "中游",
    "食品加工": "中游", "白酒Ⅱ": "中游",
    "房地产开发": "下游", "白色家电": "下游", "消费电子": "下游",
    "医疗器械": "下游", "专业连锁Ⅱ": "下游", "影视娱乐": "下游",
    "证券Ⅱ": "金融", "多元金融": "金融", "港口航运": "交运", "航运港口": "交运",
    "铁路公路": "交运", "航空机场": "交运",
}

# 金融服务链 / 交运服务链细分标签（二级细分业务）
SECTOR_SUB = {
    "证券Ⅱ": "券商·投行·互联网金融",
    "多元金融": "信托·保险·期货·AMC",
    "港口航运": "港口码头·装卸仓储",
    "航运港口": "港口枢纽·航运物流",
    "铁路公路": "铁路运输·公路货运",
    "航空机场": "机场运营·航空客运",
}

# 产业链上下游传导关系（有向边 source → target）
CHAIN_EDGES = [
    ("煤炭开采", "电力"), ("煤炭开采", "普钢"), ("焦炭Ⅱ", "普钢"), ("焦炭Ⅱ", "特钢Ⅱ"),
    ("化学原料", "化学制品"), ("化学原料", "化学制药"), ("化学制品", "化学制药"), ("化学制品", "医疗器械"),
    ("工业金属", "通用设备"), ("工业金属", "白色家电"), ("工业金属", "消费电子"),
    ("小金属", "航空装备Ⅱ"), ("小金属", "消费电子"),
    ("普钢", "汽车零部件"), ("普钢", "房地产开发"), ("普钢", "白色家电"),
    ("特钢Ⅱ", "汽车零部件"), ("特钢Ⅱ", "航空装备Ⅱ"),
    ("通用设备", "房地产开发"), ("通信设备", "消费电子"),
    ("食品加工", "专业连锁Ⅱ"), ("白酒Ⅱ", "专业连锁Ⅱ"),
    # 交运服务链细分（陆路集疏运 → 港口 → 航运）
    ("铁路公路", "港口航运"), ("铁路公路", "航运港口"), ("港口航运", "航运港口"),
    # 金融服务链 → 实体传导（融资支持）
    ("证券Ⅱ", "房地产开发"), ("多元金融", "房地产开发"),
]

backtest_data = {}
market_trend = None
if os.path.exists(BACKTEST_FILE):
    with open(BACKTEST_FILE, "r") as f:
        bt_full = json.load(f)
        backtest_data = bt_full.get("backtest_results", {})
        market_trend = bt_full.get("market_trend", None)
        bt_signals = bt_full.get("trading_signals", {})
        bt_buy_signals = bt_full.get("buy_signals", [])
        bt_sell_signals = bt_full.get("sell_signals", [])
        backtest_date = bt_full.get("backtest_date", "")
else:
    bt_signals = {}; bt_buy_signals = []; bt_sell_signals = []; backtest_date = ""

kline_data = {}
if os.path.exists(KLINE_FILE):
    with open(KLINE_FILE, "r") as f:
        kline_data = json.load(f).get("stock_data", {})

BULLISH_PATTERNS = {
    "底部反转": ["出水芙蓉", "三金叉见底", "价托", "DIF上穿零线", "底部芝麻功", "熊牛转换点"],
    "趋势确认": ["多方炮", "老鸭头", "均线多头排列", "东方红大阳升", "进入2+3区间"],
    "突破加速": ["放量过头", "轻松过头", "巨量长阳", "会创新高还有新高"],
    "洗盘结束": ["底部芝麻功", "天眼地量", "压转托"],
}
BEARISH_PATTERNS = {
    "见顶信号": ["断头铡刀", "三死叉见顶", "价压", "DIF下穿零线"],
    "趋势恶化": ["空方炮", "均线空头排列", "五线顺下", "倒挂老鸭头"],
    "破位下跌": ["一阴下穿三角压", "死蜘蛛", "会创新低还有新低", "牛熊转换点"],
    "主力出货": ["量压", "阳阴墓碑", "托转压"],
}

def analyze_trading_signals(signals):
    signal_names = set(s["indicator"] for s in signals)
    bull = sum(1 for s in signals if s["type"] == "看涨")
    bear = sum(1 for s in signals if s["type"] == "看跌")
    sentiment = round((bull - bear) / max(len(signals), 1), 2)
    matched_bull = [{"pattern": n, "signals": [s for s in ps if s in signal_names]} for n, ps in BULLISH_PATTERNS.items() if any(s in signal_names for s in ps)]
    matched_bear = [{"pattern": n, "signals": [s for s in ps if s in signal_names]} for n, ps in BEARISH_PATTERNS.items() if any(s in signal_names for s in ps)]
    confidence = 50
    if sentiment >= 0:
        confidence += len(matched_bull) * 12 + bull * 2 - len(matched_bear) * 8
    else:
        confidence += len(matched_bear) * 12 + bear * 2 - len(matched_bull) * 8
    confidence = max(0, min(100, confidence))
    has_conflict = len(matched_bull) > 0 and len(matched_bear) > 0
    risk = "高" if (has_conflict or confidence < 35) else ("低" if (confidence >= 70 and abs(sentiment) >= 0.5) else "中")
    priority = 5 if (sentiment >= 0.6 and confidence >= 75 and len(signals) >= 10) else (4 if (sentiment >= 0.4 and confidence >= 65 and len(signals) >= 8) else (3 if (sentiment >= 0.2 and confidence >= 55) else (2 if confidence >= 45 else 1)))
    if sentiment >= 0.6 and priority >= 4: action, detail = "强烈关注", "多重看涨信号共振，建议重点关注突破确认后的介入机会"
    elif sentiment >= 0.3 and priority >= 3: action, detail = "重点关注", "看涨信号占优，建议观察量能配合，择机关注"
    elif sentiment <= -0.3 and priority >= 3: action, detail = "注意风险", "看跌信号主导，持仓者注意止损，未持仓者建议规避"
    elif sentiment <= -0.6: action, detail = "建议规避", "多重看跌信号共振，坚决回避"
    elif abs(sentiment) < 0.2: action, detail = "观望等待", "多空信号交织，建议等待方向明确后再操作"
    else: action, detail = "适度关注", "信号偏多但不够强烈，建议持续跟踪"
    return {"action": action, "action_detail": detail, "confidence": confidence, "risk": risk, "priority": priority,
            "sentiment_score": sentiment, "matched_bull": matched_bull, "matched_bear": matched_bear, "has_conflict": has_conflict,
            "pattern_descs": [f'{m["pattern"]}: {",".join(m["signals"])}' for m in (matched_bull + matched_bear)]}

indicator_counts = Counter()
signal_dates = Counter()
stocks_data = []
for s in data["top50"]:
    bull = sum(1 for sig in s["signals"] if sig["type"] == "看涨")
    bear = sum(1 for sig in s["signals"] if sig["type"] == "看跌")
    sentiment = round((bull - bear) / max(s["signal_count"], 1), 2)
    if sentiment >= 0.6: classification, cls_key = "强势看涨", "strong_bull"
    elif sentiment >= 0.2: classification, cls_key = "偏多", "bull"
    elif sentiment > -0.2: classification, cls_key = "中性", "neutral"
    elif sentiment > -0.6: classification, cls_key = "偏空", "bear"
    else: classification, cls_key = "强势看跌", "strong_bear"
    for sig in s["signals"]:
        indicator_counts[sig["indicator"]] += 1
        signal_dates[sig["date"]] += 1
    trading = analyze_trading_signals(s["signals"])
    ind_info = industry_data.get(s["code"], {})
    bt = backtest_data.get(s["code"], {})
    bt_sig = bt_signals.get(s["code"], {})
    stocks_data.append({
        "code": s["code"], "name": s["name"], "signal_count": s["signal_count"],
        "bullish": bull, "bearish": bear, "sentiment_score": sentiment,
        "classification": classification, "cls_key": cls_key,
        "latest_price": s["latest_price"], "pct_change": round(s["pct_change"], 2),
        "mkt_cap_yi": s.get("mkt_cap_yi", 0),
        "industry": ind_info.get("industry", "未知"), "concepts": ind_info.get("concepts", []),
        "signals": sorted(s["signals"], key=lambda x: x["date"]),
        "bt_win_rate": bt.get("overall_win_rate", 0), "bt_avg_return": bt.get("overall_avg_return", 0),
        "bt_has_data": bool(bt), "bt_wr_filtered": bt.get("overall_wr_filtered", 0),
        "bt_wr_3d": bt.get("overall_wr_3d", 0), "bt_wr_5d": bt.get("overall_wr_5d", 0),
        "bt_pf": bt.get("overall_pf", 0), "bt_exp": bt.get("overall_exp", 0),
        "bt_quant_score": bt.get("quant_score", 50), "bt_rsi": bt.get("current_rsi", 50),
        "bt_adx": bt.get("current_adx", 0), "bt_vol_ratio": bt.get("current_vol_ratio", 1.0),
        "bt_bb_pos": bt.get("current_bb_pos", 0.5), "bt_atr_pct": bt.get("current_atr_pct", 2.0),
        "trade_signal": bt_sig.get("signal", "观望"), "trade_reason": bt_sig.get("reason", ""),
        "entry_price": bt_sig.get("entry_price", 0), "stop_loss": bt_sig.get("stop_loss", 0),
        "target_price": bt_sig.get("target_price", 0), "position_size": bt_sig.get("position_size", 0),
        "wr_adjusted": bt_sig.get("wr_adjusted", 0), "wr_filtered": bt_sig.get("wr_filtered", 0),
        "holding_period": bt_sig.get("holding_period", "1-3天"), **trading,
    })

indicator_info = {}
for stock in data["top50"]:
    for sig in stock["signals"]:
        if sig["indicator"] not in indicator_info:
            indicator_info[sig["indicator"]] = {"section": sig["section"], "desc": sig["desc"], "type": sig["type"]}

sorted_indicators = sorted(indicator_counts.items(), key=lambda x: x[1], reverse=True)
bullish_total = sum(s["bullish"] for s in stocks_data)
bearish_total = sum(s["bearish"] for s in stocks_data)
industry_dist = Counter(s["industry"] for s in stocks_data if s["industry"] != "未知")
concept_dist = Counter()
for s in stocks_data:
    for c in s["concepts"]: concept_dist[c] += 1

sector_analysis = defaultdict(lambda: {"total": 0, "bullish": 0, "bearish": 0, "buy_signals": 0, "sell_signals": 0,
    "avg_sentiment": 0, "avg_confidence": 0, "avg_quant_score": 0, "avg_pct_change": 0,
    "avg_win_rate": 0, "bt_count": 0, "stocks": [], "max_sentiment": -99, "min_sentiment": 99})
for s in stocks_data:
    ind = s["industry"]
    if ind == "未知": continue
    sa = sector_analysis[ind]
    sa["total"] += 1; sa["bullish"] += s["bullish"]; sa["bearish"] += s["bearish"]
    sa["avg_sentiment"] += s["sentiment_score"]; sa["avg_confidence"] += s["confidence"]
    sa["avg_pct_change"] += s["pct_change"]
    sa["max_sentiment"] = max(sa["max_sentiment"], s["sentiment_score"])
    sa["min_sentiment"] = min(sa["min_sentiment"], s["sentiment_score"])
    if s["trade_signal"] == "买入": sa["buy_signals"] += 1
    if s["trade_signal"] == "卖出": sa["sell_signals"] += 1
    if s["bt_has_data"]:
        sa["avg_quant_score"] += s["bt_quant_score"]; sa["avg_win_rate"] += s["bt_win_rate"]; sa["bt_count"] += 1
    sa["stocks"].append({"code": s["code"], "name": s["name"], "sentiment": s["sentiment_score"],
                         "signal_count": s["signal_count"], "mkt_cap_yi": s["mkt_cap_yi"],
                         "trade_signal": s["trade_signal"], "bt_quant_score": s["bt_quant_score"]})

for ind in sector_analysis:
    n = sector_analysis[ind]["total"]; bn = max(sector_analysis[ind]["bt_count"], 1)
    sector_analysis[ind]["avg_sentiment"] = round(sector_analysis[ind]["avg_sentiment"] / n, 2)
    sector_analysis[ind]["avg_confidence"] = round(sector_analysis[ind]["avg_confidence"] / n, 0)
    sector_analysis[ind]["avg_pct_change"] = round(sector_analysis[ind]["avg_pct_change"] / n, 2)
    sector_analysis[ind]["avg_quant_score"] = round(sector_analysis[ind]["avg_quant_score"] / bn, 1)
    sector_analysis[ind]["avg_win_rate"] = round(sector_analysis[ind]["avg_win_rate"] / bn, 1)

sector_ranking = sorted(sector_analysis.items(), key=lambda x: (x[1]["avg_sentiment"], x[1]["buy_signals"]), reverse=True)

# ===== 幻方量化·板块轮动推演（多因子轮动模型）=====
def _clamp(v, lo, hi):
    return max(lo, min(hi, v))

rotation_data = []
for name, sa in sector_analysis.items():
    total = max(sa["total"], 1)
    momentum = sa["avg_pct_change"]
    sentiment = sa["avg_sentiment"]
    net_buy = sa["buy_signals"] - sa["sell_signals"]
    breadth = round(sa["bullish"] / total, 3) if total else 0.0
    quant = sa["avg_quant_score"] if sa["bt_count"] > 0 else 50.0
    winrate = sa["avg_win_rate"] if sa["bt_count"] > 0 else 50.0
    m = _clamp(50 + momentum * 5, 0, 100)      # 动量因子：当日涨跌幅
    s = _clamp(50 + sentiment * 50, 0, 100)    # 情绪因子
    n = _clamp(50 + net_buy * 22, 0, 100)      # 资金/信号净流入因子
    q = _clamp(quant, 0, 100)                  # 量化评分因子
    w = _clamp(winrate, 0, 100)                # 历史胜率因子
    score = round(0.28 * m + 0.22 * s + 0.16 * n + 0.20 * q + 0.14 * w, 1)
    if score >= 68: tier = "领跑主线"
    elif score >= 56: tier = "接力补涨"
    elif score >= 44: tier = "蓄势观察"
    elif score >= 34: tier = "动能衰减"
    else: tier = "退潮回避"
    rotation_data.append({
        "name": name, "total": total, "stage": SECTOR_STAGE.get(name, "其他"),
        "sub": SECTOR_SUB.get(name, ""),
        "score": score, "momentum": momentum, "sentiment": sentiment,
        "net_buy": net_buy, "breadth": breadth, "quant": round(q, 1),
        "winrate": round(w, 1), "tier": tier,
        "buy": sa["buy_signals"], "sell": sa["sell_signals"],
    })
rotation_data.sort(key=lambda x: x["score"], reverse=True)

lead = [r for r in rotation_data if r["tier"] == "领跑主线"]
follow = [r for r in rotation_data if r["tier"] == "接力补涨"]
fade = [r for r in rotation_data if r["tier"] in ("动能衰减", "退潮回避")]
lead_names = "、".join(r["name"] for r in lead[:4]) or "暂无明确领跑板块"
follow_names = "、".join(r["name"] for r in follow[:4]) or "暂无明确接力板块"
fade_names = "、".join(r["name"] for r in fade[:4]) or "暂无明确退潮板块"
top_sector = rotation_data[0] if rotation_data else None
if rotation_data:
    direction = f"资金由[{fade_names}]向[{lead_names}]迁移，呈{'主线强化' if top_sector['score'] >= 60 else '震荡分化'}轮动格局"
else:
    direction = "板块数据不足，暂无法推演轮动方向"
rotation_insight = {
    "lead": lead_names, "follow": follow_names, "fade": fade_names,
    "direction": direction,
    "top_score": round(top_sector["score"], 1) if top_sector else 0,
    "top_name": top_sector["name"] if top_sector else "--",
}

sector_by_name = {r["name"]: r for r in rotation_data}
chain_nodes = []
for r in rotation_data:
    chain_nodes.append({
        "name": r["name"], "stage": r["stage"], "sub": r.get("sub", ""), "total": r["total"],
        "score": r["score"], "tier": r["tier"], "momentum": r["momentum"],
        "sentiment": r["sentiment"], "buy": r["buy"], "sell": r["sell"],
    })
chain_edges = [{"source": a, "target": b} for a, b in CHAIN_EDGES if a in sector_by_name and b in sector_by_name]
chain_data = {"nodes": chain_nodes, "edges": chain_edges}

rotation_data_json = safe_json_dumps(rotation_data, ensure_ascii=False)
chain_data_json = safe_json_dumps(chain_data, ensure_ascii=False)
rotation_insight_json = safe_json_dumps(rotation_insight, ensure_ascii=False)

market_sentiment = round((bullish_total - bearish_total) / max(bullish_total + bearish_total, 1), 2)
action_dist = Counter(s["action"] for s in stocks_data)

# Build pinyin initials map for fuzzy search
pinyin_map = {}
if HAS_PINYIN:
    for s in stocks_data:
        name = s["name"]
        initials = ''.join(lazy_pinyin(name, style=Style.FIRST_LETTER))
        full_pinyin = ''.join(lazy_pinyin(name))
        pinyin_map[s["code"]] = initials + ' ' + full_pinyin
pinyin_map_json = safe_json_dumps(pinyin_map, ensure_ascii=False)

stocks_json = safe_json_dumps(stocks_data, ensure_ascii=False)
indicator_info_json = safe_json_dumps(indicator_info, ensure_ascii=False)
indicator_counts_json = safe_json_dumps(dict(sorted_indicators), ensure_ascii=False)
industry_dist_json = safe_json_dumps(dict(industry_dist.most_common()), ensure_ascii=False)
concept_dist_json = safe_json_dumps(dict(concept_dist.most_common(30)), ensure_ascii=False)
sector_data_json = safe_json_dumps([{"name": k, **v} for k, v in sector_ranking], ensure_ascii=False)

# ===== 需求4: 获取A股全市场板块轮动数据 =====
full_market_sectors = []
try:
    import requests as _req
    _sess = _req.Session()
    _sess.headers.update({'User-Agent': 'Mozilla/5.0', 'Referer': 'https://quote.eastmoney.com/'})
    # 东方财富行业板块API
    _url = 'http://push2.eastmoney.com/api/qt/clist/get'
    _params = {'pn': 1, 'pz': 100, 'po': 1, 'np': 1, 'fltt': 2, 'invt': 2,
               'fs': 'm:90+t:2', 'fields': 'f12,f14,f3,f20,f128,f136'}
    _resp = _sess.get(_url, params=_params, timeout=10)
    _data = _resp.json().get('data', {}).get('diff', [])
    for _b in _data:
        full_market_sectors.append({
            "name": _b.get('f14', ''),
            "pct_change": round(_b.get('f3', 0), 2),
            "amount": str(round(_b.get('f20', 0) / 1e8, 1)) + '亿' if _b.get('f20', 0) > 0 else '--',
            "lead_stock": _b.get('f128', '') or '--',
        })
    full_market_sectors.sort(key=lambda x: x['pct_change'], reverse=True)
    print(f"  全市场板块数据: {len(full_market_sectors)} 个板块")
except Exception as _e:
    print(f"  全市场板块数据获取失败: {_e}")
    full_market_sectors = []
full_market_sectors_json = safe_json_dumps(full_market_sectors, ensure_ascii=False)
action_dist_json = safe_json_dumps(dict(action_dist), ensure_ascii=False)
buy_signals_json = safe_json_dumps(bt_buy_signals, ensure_ascii=False)
sell_signals_json = safe_json_dumps(bt_sell_signals, ensure_ascii=False)
market_trend_json = safe_json_dumps(market_trend or {}, ensure_ascii=False)
kline_data_json = safe_json_dumps(kline_data, ensure_ascii=False)

bt_has_data_count = sum(1 for s in stocks_data if s["bt_has_data"])
avg_win_rate = round(sum(s["bt_win_rate"] for s in stocks_data if s["bt_has_data"]) / max(bt_has_data_count, 1), 1)
avg_wr_filtered = round(sum(s["bt_wr_filtered"] for s in stocks_data if s["bt_has_data"]) / max(bt_has_data_count, 1), 1)
avg_quant_score = round(sum(s["bt_quant_score"] for s in stocks_data if s["bt_has_data"]) / max(bt_has_data_count, 1), 1)
avg_pf = round(sum(safe_float(s["bt_pf"]) for s in stocks_data if s["bt_has_data"]) / max(bt_has_data_count, 1), 2)
avg_exp = round(sum(safe_float(s["bt_exp"]) for s in stocks_data if s["bt_has_data"]) / max(bt_has_data_count, 1), 2)

# ===== Self-Learning: aggregate indicator performance across all stocks =====
indicator_global_stats = defaultdict(lambda: {"total_triggers": 0, "total_wins": 0, "total_returns": [], "pf_list": [], "exp_list": [], "wr_filtered_list": [], "type": "", "desc": ""})
for code, bt in backtest_data.items():
    ind_stats = bt.get("indicator_stats", {})
    for ind_name, st in ind_stats.items():
        gs = indicator_global_stats[ind_name]
        gs["total_triggers"] += st.get("total", 0)
        gs["total_wins"] += st.get("wins", 0)
        gs["total_returns"].append(st.get("avg_return", 0))
        gs["pf_list"].append(st.get("profit_factor", 0))
        gs["exp_list"].append(st.get("expectancy", 0))
        gs["wr_filtered_list"].append(max(st.get("wr_rsi_filter", 0), st.get("wr_adx_filter", 0), st.get("wr_vol_filter", 0), st.get("wr_all_filter", 0)))
        if ind_name in indicator_info:
            gs["type"] = indicator_info[ind_name].get("type", "")
            gs["desc"] = indicator_info[ind_name].get("desc", "")

indicator_performance = []
for ind_name, gs in indicator_global_stats.items():
    if gs["total_triggers"] == 0:
        continue
    wr = round(gs["total_wins"] / gs["total_triggers"] * 100, 1)
    avg_ret = round(sum(gs["total_returns"]) / len(gs["total_returns"]), 2) if gs["total_returns"] else 0
    avg_pf = round(sum(gs["pf_list"]) / len(gs["pf_list"]), 2) if gs["pf_list"] else 0
    avg_exp = round(sum(gs["exp_list"]) / len(gs["exp_list"]), 2) if gs["exp_list"] else 0
    avg_wr_f = round(sum(gs["wr_filtered_list"]) / len(gs["wr_filtered_list"]), 1) if gs["wr_filtered_list"] else 0
    indicator_performance.append({
        "name": ind_name, "type": gs["type"], "desc": gs["desc"],
        "triggers": gs["total_triggers"], "win_rate": wr, "avg_return": avg_ret,
        "profit_factor": min(avg_pf, 99.99), "expectancy": avg_exp,
        "wr_filtered": avg_wr_f, "quality": "高" if wr >= 55 and avg_pf > 1.3 else ("中" if wr >= 48 else "低"),
    })
indicator_performance.sort(key=lambda x: x["win_rate"], reverse=True)

# Signal Quality Score per stock: weighted by indicator historical performance
for s in stocks_data:
    if not s["bt_has_data"]:
        s["signal_quality"] = 50
        continue
    bt = backtest_data.get(s["code"], {})
    ind_stats = bt.get("indicator_stats", {})
    current_inds = set(sig["indicator"] for sig in s["signals"])
    scores = []
    for ind_name in current_inds:
        if ind_name in ind_stats:
            st = ind_stats[ind_name]
            wr = st.get("win_rate", 50)
            pf = min(st.get("profit_factor", 1), 99.99)
            exp = st.get("expectancy", 0)
            score = wr * 0.5 + min(pf * 20, 30) + max(exp * 2, -20) + 20
            scores.append(score)
    s["signal_quality"] = round(sum(scores) / len(scores), 1) if scores else 50

# Learning insights
learning_insights = []
high_wr_inds = [i for i in indicator_performance if i["win_rate"] >= 55 and i["triggers"] >= 10]
low_wr_inds = [i for i in indicator_performance if i["win_rate"] < 45 and i["triggers"] >= 10]
if high_wr_inds:
    top3 = high_wr_inds[:3]
    learning_insights.append(f"高胜率指标TOP3: {'、'.join(i['name']+'('+str(i['win_rate'])+'%)' for i in top3)} — 优先关注含这些指标的个股")
if low_wr_inds:
    learning_insights.append(f"低胜率指标预警: {'、'.join(i['name']+'('+str(i['win_rate'])+'%)' for i in low_wr_inds[:3])} — 历史表现不佳，谨慎参考")
best_sectors = [s for s in stocks_data if s["bt_has_data"] and s["signal_quality"] >= 65]
if best_sectors:
    learning_insights.append(f"高信号质量个股({len(best_sectors)}只): {'、'.join(s['name'] for s in sorted(best_sectors, key=lambda x: x['signal_quality'], reverse=True)[:5])}")
avg_sq = round(sum(s["signal_quality"] for s in stocks_data if s["bt_has_data"]) / max(bt_has_data_count, 1), 1)
learning_insights.append(f"全市场平均信号质量分: {avg_sq} — {'优质区间' if avg_sq >= 60 else ('中等区间' if avg_sq >= 50 else '偏低区间')}")

# Risk metrics
total_buy = sum(1 for s in stocks_data if s["trade_signal"] in ("买入", "适度买入", "小仓买入", "轻仓买入", "轻仓关注"))
total_sell = sum(1 for s in stocks_data if s["trade_signal"] in ("建议清仓", "建议减仓", "注意风险"))
total_pos = sum(s["position_size"] for s in stocks_data if s["position_size"] > 0)
sector_concentration = {}
for s in stocks_data:
    if s["position_size"] > 0:
        sector_concentration[s["industry"]] = sector_concentration.get(s["industry"], 0) + s["position_size"]
risk_metrics = {
    "total_buy_signals": total_buy, "total_sell_signals": total_sell,
    "total_exposure": total_pos, "avg_position": round(total_pos / max(total_buy, 1), 1) if total_buy else 0,
    "sector_concentration": dict(sorted(sector_concentration.items(), key=lambda x: x[1], reverse=True)[:5]),
    "market_regime": ("多头" if market_trend and market_trend.get("trend") == "上行" else
                      "空头" if market_trend and market_trend.get("trend") in ("下行", "单边下行") else "震荡"),
    # 数据校验
    "validation": {
        "buy_signal_pct": round(total_buy / max(len(stocks_data), 1) * 100, 1),
        "sell_signal_pct": round(total_sell / max(len(stocks_data), 1) * 100, 1),
        "watch_pct": round((len(stocks_data) - total_buy - total_sell) / max(len(stocks_data), 1) * 100, 1),
        "max_single_position": max(s["position_size"] for s in stocks_data) if stocks_data else 0,
        "position_valid": all(0 <= s["position_size"] <= 100 for s in stocks_data),
        "entry_stop_valid": all(s["entry_price"] > s["stop_loss"] > 0 for s in stocks_data if s["entry_price"] > 0),
        "entry_target_valid": all(s["target_price"] > s["entry_price"] > 0 for s in stocks_data if s["entry_price"] > 0),
        "sector_concentration_pct": round(max(sector_concentration.values()) / max(total_pos, 1) * 100, 1) if sector_concentration else 0,
    },
}

indicator_performance_json = safe_json_dumps(indicator_performance[:30], ensure_ascii=False)
learning_insights_json = safe_json_dumps(learning_insights, ensure_ascii=False)
risk_metrics_json = safe_json_dumps(risk_metrics, ensure_ascii=False)

scan_date = data["scan_date"]; total_scanned = data["total_scanned"]; total_with_signals = data["total_with_signals"]
unique_indicators = len(indicator_counts)
print(f"Stocks: {len(stocks_data)}, Sectors: {len(sector_ranking)}, Buy: {len(bt_buy_signals)}")

# ===================== HTML =====================
html = r'''<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark" style="background:#0a0e1a;color:#e8eaf0;">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="color-scheme" content="dark">
<script>document.documentElement.setAttribute('data-theme','dark');document.documentElement.style.background='#0a0e1a';document.documentElement.style.color='#e8eaf0';try{localStorage.setItem('stock-theme','dark');}catch(e){}var __tl=true;var __to=new MutationObserver(function(){if(__tl&&document.documentElement.getAttribute('data-theme')!=='dark')document.documentElement.setAttribute('data-theme','dark');});__to.observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']});setTimeout(function(){__tl=false;},3000);window.addEventListener('pageshow',function(e){if(e.persisted){__tl=true;document.documentElement.setAttribute('data-theme','dark');setTimeout(function(){__tl=false;},3000);}});</script>
<title>A股交易决策看板 — 专业交易员视角</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
:root,[data-theme="dark"]{--bg:#0a0e1a;--bg2:#131829;--bg3:#1d2340;--bg4:#283056;--ink:#e8eaf0;--muted:#7a82a3;--rule:#2a3050;--up:#ef4444;--up-rgb:239,68,68;--up-bg:rgba(239,68,68,0.12);--up-bd:rgba(239,68,68,0.3);--down:#22c55e;--down-rgb:34,197,94;--down-bg:rgba(34,197,94,0.12);--down-bd:rgba(34,197,94,0.3);--gold:#ffc83d;--blue:#4d9fff;--purple:#a855f7;--orange:#ff9500;--radius:14px;--bg-atmo:radial-gradient(1100px 700px at 85% -10%,rgba(77,159,255,.055),transparent 55%),radial-gradient(900px 600px at -5% 15%,rgba(168,85,247,.04),transparent 50%),radial-gradient(800px 500px at 50% 110%,rgba(255,200,61,.03),transparent 50%),var(--bg);--row-alt:rgba(127,137,163,.045);--heatmap-bd:rgba(255,255,255,.09)}
:root,[data-theme="dark"]{--shadow-sm:0 1px 3px rgba(0,0,0,.3),0 1px 2px rgba(0,0,0,.2);--shadow-md:0 4px 12px rgba(0,0,0,.35),0 2px 4px rgba(0,0,0,.2);--shadow-lg:0 12px 32px rgba(0,0,0,.4),0 4px 8px rgba(0,0,0,.2);--grad-card:linear-gradient(135deg,rgba(255,255,255,.03) 0%,rgba(255,255,255,0) 100%);--grad-nav:linear-gradient(180deg,rgba(19,24,41,.95) 0%,rgba(19,24,41,.85) 100%);--font-mono:'SF Mono','JetBrains Mono','Fira Code',ui-monospace,monospace;--font-sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Microsoft YaHei',sans-serif}
[data-theme="light"]{--bg:#f5f7fa;--bg2:#fff;--bg3:#eef1f6;--bg4:#dde3ed;--ink:#1a1f36;--muted:#6b7280;--rule:#e5e7eb;--up:#dc2626;--up-rgb:220,38,38;--up-bg:rgba(220,38,38,0.08);--up-bd:rgba(220,38,38,0.25);--down:#16a34a;--down-rgb:22,163,74;--down-bg:rgba(22,163,74,0.08);--down-bd:rgba(22,163,74,0.25);--gold:#d97706;--blue:#2563eb;--purple:#7c3aed;--orange:#ea580c;--bg-atmo:radial-gradient(1000px 600px at 85% -10%,rgba(37,99,235,.045),transparent 55%),var(--bg);--row-alt:rgba(26,31,54,.025);--heatmap-bd:rgba(255,255,255,.55)}
[data-theme="eye"]{--bg:#f0f6e8;--bg2:#e8f0dc;--bg3:#d8e5c8;--bg4:#c8dab4;--ink:#2d3b1a;--muted:#6b7d4e;--rule:#b8cca0;--up:#c41e3a;--up-rgb:196,30,58;--up-bg:rgba(196,30,58,0.1);--up-bd:rgba(196,30,58,0.25);--down:#2d8a4e;--down-rgb:45,138,78;--down-bg:rgba(45,138,78,0.1);--down-bd:rgba(45,138,78,0.25);--gold:#b8860b;--blue:#1e5fa8;--purple:#6b3fa0;--orange:#cc6600;--bg-atmo:var(--bg);--row-alt:rgba(45,59,26,.03);--heatmap-bd:rgba(255,255,255,.45)}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:var(--font-sans);background:var(--bg-atmo);color:var(--ink);line-height:1.6;overflow-x:hidden;font-feature-settings:'cv11','ss01';-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--bg4);border-radius:6px;border:2px solid transparent;background-clip:padding-box}
::-webkit-scrollbar-thumb:hover{background:var(--muted);border:2px solid transparent;background-clip:padding-box}
::selection{background:rgba(var(--up-rgb),.25)}
.container{max-width:1400px;margin:0 auto;padding:0 20px}
.top-nav{position:sticky;top:0;z-index:100;background:var(--grad-nav);border-bottom:1px solid var(--rule);padding:10px 0;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);box-shadow:var(--shadow-sm)}
.top-nav-inner{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}
.nav-brand{font-size:18px;font-weight:800;white-space:nowrap;letter-spacing:-.02em;background:linear-gradient(135deg,var(--ink) 0%,var(--up) 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.nav-brand span{color:var(--up);-webkit-text-fill-color:var(--up)}
.nav-links{display:flex;gap:4px;flex-wrap:wrap}
.nav-link{padding:6px 14px;border-radius:8px;font-size:13px;color:var(--muted);cursor:pointer;transition:all .25s cubic-bezier(.4,0,.2,1);text-decoration:none;display:flex;align-items:center;gap:4px;position:relative;overflow:hidden}
.nav-link:hover{background:var(--bg3);color:var(--ink);transform:translateY(-1px)}
.nav-link.active{background:var(--up-bg);color:var(--up);border:1px solid var(--up-bd);box-shadow:0 2px 8px rgba(var(--up-rgb),.15)}
.nav-link .badge-count{background:var(--up);color:#fff;border-radius:10px;padding:0 6px;font-size:10px;font-weight:700;min-width:18px;text-align:center;box-shadow:0 2px 6px rgba(var(--up-rgb),.4)}
.nav-right{display:flex;align-items:center;gap:10px}
.theme-switcher{display:flex;gap:4px}
.theme-btn{width:32px;height:32px;border-radius:8px;border:1px solid var(--rule);background:var(--bg3);cursor:pointer;font-size:14px;transition:all .25s cubic-bezier(.4,0,.2,1)}
.theme-btn:hover{transform:scale(1.1);border-color:var(--muted)}
.theme-btn.active{border-color:var(--up);background:var(--up-bg);box-shadow:0 2px 8px rgba(var(--up-rgb),.2)}
.search-box{display:flex;gap:6px}
.search-box input{background:var(--bg3);border:1px solid var(--rule);color:var(--ink);padding:6px 12px;border-radius:8px;font-size:13px;width:200px;outline:none;transition:all .25s;box-shadow:var(--shadow-sm)}
.search-box input:focus{border-color:var(--up);box-shadow:0 0 0 3px rgba(var(--up-rgb),.1)}
.search-box button{background:linear-gradient(135deg,var(--up),#dc2626);color:#fff;border:none;padding:6px 14px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;transition:all .25s;box-shadow:0 2px 6px rgba(var(--up-rgb),.3)}
.search-box button:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(var(--up-rgb),.4)}
.layer-header{display:flex;align-items:center;gap:12px;margin:32px 0 16px}
.page-section{display:none;animation:fadeIn .35s cubic-bezier(.4,0,.2,1)}
.page-section.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
.layer-num{width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:800;color:#fff;flex-shrink:0;box-shadow:var(--shadow-md)}
.l1 .layer-num{background:linear-gradient(135deg,var(--blue),var(--purple))}
.l2 .layer-num{background:linear-gradient(135deg,var(--gold),var(--orange))}
.l3 .layer-num{background:linear-gradient(135deg,var(--up),#ff6b6b)}
.l-ref .layer-num{background:var(--bg4)}
.l-watch .layer-num{background:linear-gradient(135deg,var(--gold),var(--up))}
.layer-title{font-size:22px;font-weight:800;letter-spacing:-.02em}
.layer-sub{font-size:13px;color:var(--muted);margin-top:2px;letter-spacing:.01em}
.card{background:var(--bg2);background-image:var(--grad-card);border:1px solid var(--rule);border-radius:var(--radius);padding:20px;margin-bottom:16px;box-shadow:var(--shadow-sm);transition:box-shadow .3s,border-color .3s}
.card:hover{box-shadow:var(--shadow-md),0 0 0 1px rgba(255,200,61,.1);border-color:var(--muted)}
.card-title{font-size:15px;font-weight:700;margin-bottom:14px;display:flex;align-items:center;gap:8px;letter-spacing:-.01em}
.card-title .bar{width:3px;height:16px;background:linear-gradient(180deg,var(--up),#ff6b6b);border-radius:2px}
.stat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}
.stat-item{background:var(--bg3);background-image:var(--grad-card);border-radius:10px;padding:14px;text-align:center;border:1px solid var(--rule);transition:all .25s cubic-bezier(.4,0,.2,1)}
.stat-item:hover{transform:translateY(-2px);border-color:var(--muted);box-shadow:var(--shadow-md)}
.stat-val{font-size:24px;font-weight:800;font-family:var(--font-mono);font-variant-numeric:tabular-nums}
.stat-val.up{color:var(--up)}.stat-val.down{color:var(--down)}.stat-val.gold{color:var(--gold)}.stat-val.blue{color:var(--blue)}
.stat-lbl{font-size:11px;color:var(--muted);margin-top:4px;letter-spacing:.03em;text-transform:uppercase}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
@media(max-width:900px){.grid-2,.grid-3{grid-template-columns:1fr}}
.chart-c{width:100%;height:280px}
.chart-c.tall{height:360px}
table{width:100%;border-collapse:collapse;font-size:13px}
th{background:var(--bg3);padding:8px 10px;text-align:left;font-weight:600;color:var(--muted);font-size:11px;white-space:nowrap;cursor:pointer;letter-spacing:.03em;text-transform:uppercase}
td{padding:7px 10px;border-top:1px solid var(--rule);white-space:nowrap;transition:background .15s}
tbody tr:nth-child(even){background:var(--row-alt)}
tbody tr:hover{background:var(--bg3)}
.pu{color:var(--up);font-weight:600}.pd{color:var(--down);font-weight:600}
.badge{display:inline-block;padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;letter-spacing:.02em}
.badge.buy{background:var(--up-bg);color:var(--up);border:1px solid var(--up-bd)}
.badge.sell{background:var(--down-bg);color:var(--down);border:1px solid var(--down-bd)}
.badge.watch{background:var(--bg4);color:var(--muted)}
.star-btn{background:none;border:none;cursor:pointer;font-size:16px;color:var(--bg4);transition:all .25s cubic-bezier(.4,0,.2,1);padding:2px}
.star-btn:hover{transform:scale(1.25) rotate(-10deg)}
.star-btn.active{color:var(--gold);text-shadow:0 0 8px rgba(255,200,61,.4)}
.sector-bar-row{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--rule);cursor:pointer;transition:all .25s cubic-bezier(.4,0,.2,1)}
.sector-bar-row:hover{background:var(--bg3);border-radius:8px;padding-left:8px;box-shadow:var(--shadow-sm)}
.sector-bar-row:last-child{border:none}
.sector-name{width:100px;font-weight:600;font-size:13px;flex-shrink:0}
.sector-bar-wrap{flex:1;height:24px;background:var(--bg3);border-radius:6px;position:relative;overflow:hidden}
.sector-bar-fill{height:100%;border-radius:6px;display:flex;align-items:center;padding:0 8px;font-size:11px;font-weight:600;color:#fff;transition:width .5s cubic-bezier(.4,0,.2,1)}
.sector-bar-fill.bull{background:linear-gradient(90deg,var(--up),rgba(var(--up-rgb),0.7))}
.sector-bar-fill.bear{background:linear-gradient(90deg,var(--down),rgba(var(--down-rgb),0.7))}
.sector-meta{width:140px;text-align:right;font-size:12px;color:var(--muted);flex-shrink:0}
.rotation-insight{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:12px;padding:12px 14px;border-radius:12px;background:linear-gradient(135deg,rgba(168,85,247,.08),rgba(77,159,255,.06));border:1px solid var(--rule)}
.ri-block{flex:1;min-width:170px}
.ri-label{font-size:11px;color:var(--muted);margin-bottom:4px}
.ri-val{font-size:13px;font-weight:600;line-height:1.5}
.ri-dir{flex-basis:100%;font-size:12px;color:var(--muted);line-height:1.6;border-top:1px dashed var(--rule);padding-top:8px}
.rot-tier{margin-bottom:10px}
.rot-tier-head{display:flex;align-items:center;gap:6px;font-size:13px;font-weight:700;margin-bottom:6px}
.rot-dot{width:8px;height:8px;border-radius:50%;display:inline-block}
.rot-tier-body{display:flex;flex-wrap:wrap;gap:6px}
.rot-chip{padding:3px 10px;border-radius:20px;border:1px solid var(--rule);font-size:12px;cursor:pointer;background:var(--bg2);transition:all .2s}
.rot-chip:hover{border-color:var(--blue);transform:translateY(-1px);box-shadow:var(--shadow-sm)}
.sig-card{background:var(--bg3);background-image:var(--grad-card);border-radius:10px;padding:14px;margin-bottom:8px;border-left:3px solid var(--up);transition:all .25s cubic-bezier(.4,0,.2,1)}
.sig-card:hover{transform:translateX(2px);box-shadow:var(--shadow-md)}
.sig-card.sell{border-left-color:var(--down)}
.sig-card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
.sig-card-name{font-weight:700;font-size:14px;letter-spacing:-.01em}
.sig-card-body{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;font-size:12px}
.sig-card-body .lbl{color:var(--muted);font-size:10px}
.sig-card-body .val{font-weight:700;font-family:var(--font-mono);font-variant-numeric:tabular-nums}
.matrix-wrap{overflow-x:auto}
.matrix{width:100%;border-collapse:collapse;font-size:12px;min-width:800px}
.matrix th{background:var(--bg3);padding:6px;text-align:center;font-size:10px;cursor:default;letter-spacing:.03em;text-transform:uppercase}
.matrix td{padding:5px;text-align:center}
.matrix-dot{width:12px;height:12px;border-radius:50%;display:inline-block;cursor:help;transition:transform .2s}
.matrix-dot:hover{transform:scale(1.3)}
.matrix-dot.up{background:var(--up);box-shadow:0 0 8px rgba(var(--up-rgb),0.5)}
.matrix-dot.down{background:var(--down);box-shadow:0 0 8px rgba(var(--down-rgb),0.5)}
.matrix-dot.neutral{background:var(--gold);box-shadow:0 0 8px rgba(255,200,61,0.5)}
.matrix-dot.none{background:var(--bg4)}
.heatmap-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px}
.heatmap-cell{border-radius:10px;padding:12px 8px;text-align:center;cursor:pointer;transition:all .25s cubic-bezier(.4,0,.2,1);border:1px solid var(--heatmap-bd);backdrop-filter:blur(2px)}
.heatmap-cell:hover{transform:scale(1.06);box-shadow:var(--shadow-lg);z-index:2;position:relative}
.multi-idx-table{width:100%;border-collapse:collapse;font-size:13px;min-width:680px}
.multi-idx-table th{background:var(--bg3);padding:10px 8px;text-align:center;font-size:11px;color:var(--muted);font-weight:600;border-bottom:1px solid var(--rule)}
.multi-idx-table td{padding:10px 8px;text-align:center;border-bottom:1px solid var(--rule);font-family:'SF Mono',monospace}
.multi-idx-table td.idx-name{font-weight:700;font-family:inherit;text-align:left}
.multi-idx-table tr:hover td{background:var(--bg3)}
.multi-idx-table .trend-tag{display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;font-family:inherit}
.multi-idx-table .trend-up{background:rgba(255,82,82,0.15);color:var(--up)}
.multi-idx-table .trend-down{background:rgba(48,217,181,0.15);color:var(--down)}
.multi-idx-table .trend-flat{background:var(--bg4);color:var(--muted)}
.regime-badge{padding:4px 14px;border-radius:12px;font-size:12px;font-weight:700;letter-spacing:1px}
.regime-bull{background:var(--up-bg);border:1px solid var(--up-bd);color:var(--up)}
.regime-bear{background:var(--down-bg);border:1px solid var(--down-bd);color:var(--down)}
.regime-range{background:var(--bg4);border:1px solid var(--rule);color:var(--muted)}
.trade-ready{display:inline-block;padding:2px 8px;border-radius:6px;font-size:10px;font-weight:700;background:var(--up);color:#fff;letter-spacing:1px;animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.6}}
.rr-bar{display:flex;align-items:center;gap:4px;font-size:11px}
.rr-bar .rr-fill{height:6px;border-radius:3px;background:linear-gradient(90deg,var(--down),var(--gold),var(--up))}
.sig-quality-badge{display:inline-block;padding:1px 8px;border-radius:8px;font-size:10px;font-weight:600}
.sig-q-high{background:var(--up-bg);color:var(--up)}
.sig-q-mid{background:rgba(255,200,61,0.12);color:var(--gold)}
.sig-q-low{background:var(--bg4);color:var(--muted)}
.insight-card{padding:10px 14px;background:var(--bg3);border-radius:10px;border-left:3px solid var(--gold);font-size:13px;line-height:1.7}
.insight-card .ic-label{color:var(--muted);font-size:11px;margin-bottom:2px}
.ticker-wrap{overflow:hidden;background:var(--bg2);background-image:var(--grad-card);border:1px solid var(--rule);border-radius:10px;padding:8px 0;margin-bottom:16px;position:relative;box-shadow:var(--shadow-sm)}
.ticker-wrap::before,.ticker-wrap::after{content:'';position:absolute;top:0;bottom:0;width:40px;z-index:2;pointer-events:none}
.ticker-wrap::before{left:0;background:linear-gradient(90deg,var(--bg2) 0%,transparent 100%)}
.ticker-wrap::after{right:0;background:linear-gradient(270deg,var(--bg2) 0%,transparent 100%)}
.ticker-track{display:inline-flex;gap:24px;white-space:nowrap;animation:scroll 60s linear infinite}
@keyframes scroll{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}
.ticker-item{display:inline-flex;align-items:center;gap:5px;font-size:13px;cursor:pointer;transition:color .2s}
.ticker-item:hover{color:var(--gold)}
.overlay{position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:200;display:none;align-items:center;justify-content:center;padding:20px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}
.overlay.active{display:flex;animation:fadeIn .25s ease}
.overlay-panel{background:var(--bg2);background-image:var(--grad-card);border:1px solid var(--rule);border-radius:16px;max-width:960px;width:100%;max-height:88vh;overflow-y:auto;padding:24px;position:relative;box-shadow:var(--shadow-lg);animation:slideUp .3s cubic-bezier(.4,0,.2,1)}
@keyframes slideUp{from{opacity:0;transform:translateY(20px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
.overlay-close{position:absolute;top:12px;right:12px;width:32px;height:32px;border-radius:8px;border:1px solid var(--rule);background:var(--bg3);color:var(--ink);cursor:pointer;font-size:16px;transition:all .2s}
.overlay-close:hover{background:var(--up-bg);border-color:var(--up-bd);color:var(--up);transform:rotate(90deg)}
.filter-bar{display:flex;gap:10px;align-items:center;margin-bottom:12px;flex-wrap:wrap}
.filter-bar input,.filter-bar select{background:var(--bg3);border:1px solid var(--rule);color:var(--ink);padding:7px 12px;border-radius:8px;font-size:13px;outline:none;transition:border-color .2s;box-shadow:var(--shadow-sm)}
.filter-bar input:focus,.filter-bar select:focus{border-color:var(--up)}
.btn{background:var(--bg3);border:1px solid var(--rule);color:var(--ink);padding:7px 14px;border-radius:8px;cursor:pointer;font-size:13px;transition:all .2s}
.btn:hover{background:var(--bg4);box-shadow:var(--shadow-sm)}
.footer{text-align:center;padding:24px 20px;color:var(--muted);font-size:12px;letter-spacing:.02em}
.disclaimer{background:var(--bg2);background-image:var(--grad-card);border:1px solid var(--rule);border-radius:10px;padding:16px;margin:16px 0;font-size:12px;color:var(--muted);line-height:1.8}
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-track{background:var(--bg)}
::-webkit-scrollbar-thumb{background:var(--bg4);border-radius:4px;transition:background .2s}
::-webkit-scrollbar-thumb:hover{background:var(--muted)}
.live-dot{width:6px;height:6px;border-radius:50%;background:var(--up);display:inline-block;animation:livePulse 1.5s ease-in-out infinite;box-shadow:0 0 6px rgba(var(--up-rgb),.5)}
@keyframes livePulse{0%,100%{opacity:1;box-shadow:0 0 6px rgba(var(--up-rgb),.5)}50%{opacity:.4;box-shadow:0 0 3px rgba(var(--up-rgb),.2)}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.6}}
/* Detail panel sections */
.dt-section{background:var(--bg3);background-image:var(--grad-card);border-radius:10px;padding:14px;margin-top:12px;border:1px solid var(--rule)}
.dt-section h4{font-size:13px;margin-bottom:10px;display:flex;align-items:center;gap:6px;letter-spacing:-.01em}
.dt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px}
.dt-cell{background:var(--bg2);border:1px solid var(--rule);border-radius:8px;padding:8px;text-align:center;transition:all .2s}
.dt-cell:hover{border-color:var(--muted);box-shadow:var(--shadow-sm)}
.dt-cell .v{font-size:16px;font-weight:700;font-family:var(--font-mono);font-variant-numeric:tabular-nums}
.dt-cell .l{font-size:10px;color:var(--muted);margin-top:2px;letter-spacing:.02em}
.sig-timeline{display:flex;flex-direction:column;gap:6px}
.sig-tl-item{display:flex;gap:8px;align-items:center;padding:6px 8px;border-radius:6px;background:var(--bg2);font-size:12px}
.sig-tl-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.concept-tag{background:var(--bg3);border:1px solid var(--rule);padding:3px 9px;border-radius:5px;font-size:12px;color:var(--blue);display:inline-block;margin:2px}
/* Indicator ref with SVG demo */
.ref-card{background:var(--bg3);border-radius:10px;padding:14px;border-left:3px solid var(--up)}
.ref-card.bear{border-left-color:var(--down)}
.ref-card.neutral{border-left-color:var(--gold)}
.ref-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}
.ref-name{font-weight:700;font-size:14px}
.ref-count{font-size:11px;color:var(--muted);background:var(--bg2);padding:2px 8px;border-radius:6px}
.ref-desc{font-size:12px;line-height:1.5;color:var(--muted)}
.ref-meta{display:flex;gap:8px;margin-top:6px;align-items:center}
.ref-svg{margin-top:8px;border-radius:6px;background:var(--bg2);padding:4px}
/* Watchlist analysis cards */
.watch-analysis{background:var(--bg3);background-image:var(--grad-card);border-radius:10px;padding:14px;margin-bottom:10px;border-left:3px solid var(--gold);transition:box-shadow .3s}
.watch-analysis:hover{box-shadow:var(--shadow-md)}
.watch-analysis-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px;margin-top:8px}
.watch-advice{background:var(--bg2);border:1px solid var(--rule);border-radius:8px;padding:10px;margin-top:8px;font-size:12px;line-height:1.6}
/* T+0 Opportunity */
.t0-box{background:linear-gradient(135deg,rgba(255,200,61,.06),rgba(239,68,68,.06));border:1px solid rgba(255,200,61,.3);border-radius:10px;padding:12px;margin-top:10px;box-shadow:var(--shadow-sm)}
.t0-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
.t0-badge{font-size:12px;font-weight:800;padding:3px 10px;border-radius:6px;letter-spacing:.02em}
.t0-badge.buy{background:rgba(239,68,68,.15);color:#ef4444;border:1px solid rgba(239,68,68,.4)}
.t0-badge.sell{background:rgba(34,197,94,.15);color:#22c55e;border:1px solid rgba(34,197,94,.4)}
.t0-badge.wait{background:rgba(125,135,165,.15);color:#9aa4c8;border:1px solid rgba(125,135,165,.35)}
.t0-metrics{display:grid;grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:6px;margin-bottom:8px}
.t0-metric{background:var(--bg2);border-radius:7px;padding:6px 8px;text-align:center;transition:all .2s}
.t0-metric:hover{transform:translateY(-1px);border-color:var(--muted)}
.t0-metric .v{font-size:13px;font-weight:700;font-family:var(--font-mono);font-variant-numeric:tabular-nums}
.t0-metric .l{font-size:9px;color:var(--muted);margin-top:2px}
.t0-action{background:var(--bg2);border:1px solid var(--rule);border-radius:8px;padding:10px;font-size:12px;line-height:1.7}
.t0-action b{color:var(--ink)}
.t0-points{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-top:8px}
.t0-point{text-align:center;background:var(--bg3);border-radius:6px;padding:6px;transition:all .2s}
.t0-point:hover{transform:translateY(-1px)}
.t0-point .v{font-size:13px;font-weight:700;font-family:var(--font-mono);font-variant-numeric:tabular-nums}
.t0-point .l{font-size:9px;color:var(--muted)}
.t0-chart{width:100%;height:180px;margin-top:8px}
.t0-grade{font-size:11px;font-weight:800;padding:2px 8px;border-radius:5px;letter-spacing:.02em}
.t0-grade.tg-A{background:rgba(239,68,68,.15);color:#ef4444;border:1px solid rgba(239,68,68,.4)}
.t0-grade.tg-B{background:rgba(255,200,61,.15);color:#e0a800;border:1px solid rgba(255,200,61,.4)}
.t0-grade.tg-C{background:rgba(125,135,165,.15);color:#9aa4c8;border:1px solid rgba(125,135,165,.35)}
.t0-score-wrap{margin-bottom:8px}
.t0-scorebar{height:7px;border-radius:4px;background:var(--bg4);overflow:hidden;margin-top:3px}
.t0-scorebar>div{height:100%;border-radius:4px;transition:width .6s cubic-bezier(.4,0,.2,1)}
.t0-factors{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px}
.t0-factor{font-size:10px;padding:1px 6px;border-radius:4px;background:var(--bg2);border:1px solid var(--rule)}
.t0-factor.on{border-color:rgba(239,68,68,.45);color:var(--up)}
.t0-factor.off{color:var(--muted);opacity:.55}
.t0-levels{display:grid;gap:3px;margin-bottom:8px}
.t0-level{display:flex;justify-content:space-between;align-items:center;font-size:12px;padding:3px 8px;background:var(--bg2);border:1px solid var(--rule);border-radius:5px}
.t0-level .nm{color:var(--muted);min-width:48px}
.t0-level .px{font-family:var(--font-mono);font-variant-numeric:tabular-nums}
.t0-level .dv{font-family:var(--font-mono);font-size:10px}
.t0-level.cur{background:linear-gradient(90deg,rgba(255,200,61,.12),rgba(239,68,68,.12));border-color:rgba(255,200,61,.35)}
.t0-batch{font-size:11px;color:var(--muted);margin-top:6px;padding-top:6px;border-top:1px dashed var(--rule)}
.t0-batch b{color:var(--gold)}
.t0-time{font-size:11px;color:var(--blue);margin-top:6px;line-height:1.6}
.t0-time b{color:var(--ink)}
.exec-step{display:flex;gap:8px;align-items:flex-start;font-size:12px;padding:7px 9px;background:var(--bg2);border-radius:7px;margin-top:6px;line-height:1.6}
.es-tag{font-size:10px;font-weight:700;padding:1px 8px;border-radius:4px;flex-shrink:0;min-width:34px;text-align:center}
/* ===== Sentiment Cycle (KPL-style) ===== */
.cycle-header{display:flex;justify-content:space-between;align-items:center;gap:10px}
.cycle-stage{font-size:26px;font-weight:800;padding:4px 16px;border-radius:10px}
.cycle-stage.s-bing{background:rgba(74,144,226,.15);color:#4d9fff;border:1px solid rgba(74,144,226,.4)}
.cycle-stage.s-huinuan{background:rgba(34,197,94,.12);color:#22c55e;border:1px solid rgba(34,197,94,.35)}
.cycle-stage.s-fajiao{background:rgba(255,200,61,.12);color:#ffc83d;border:1px solid rgba(255,200,61,.35)}
.cycle-stage.s-gaochao{background:rgba(239,68,68,.15);color:#ef4444;border:1px solid rgba(239,68,68,.4)}
.cycle-stage.s-fenqi{background:rgba(255,149,0,.15);color:#ff9500;border:1px solid rgba(255,149,0,.4)}
.cycle-stage.s-tuichao{background:rgba(34,197,94,.18);color:#16a34a;border:1px solid rgba(34,197,94,.45)}
.cycle-advice{margin-top:8px;padding:10px 12px;background:var(--bg2);border-radius:8px;font-size:12px;line-height:1.7;color:var(--ink)}
.cycle-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:10px}
.cycle-metric{background:var(--bg2);border-radius:8px;padding:8px;text-align:center}
.cycle-metric .v{font-size:16px;font-weight:700;font-family:'SF Mono',monospace}
.cycle-metric .l{font-size:10px;color:var(--muted);margin-top:2px}
/* Ladder */
.ladder-row{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;background:var(--bg2);margin-bottom:5px}
.ladder-lb{min-width:52px;text-align:center;font-weight:800;font-size:13px;border-radius:6px;padding:3px 6px;flex-shrink:0}
.ladder-lb.hi{background:rgba(239,68,68,.18);color:#ef4444}
.ladder-lb.mid{background:rgba(255,149,0,.15);color:#ff9500}
.ladder-lb.lo{background:rgba(125,135,165,.15);color:#9aa4c8}
.ladder-cnt{font-size:11px;color:var(--muted);min-width:44px;flex-shrink:0}
.ladder-stocks{display:flex;flex-wrap:wrap;gap:4px}
.ladder-stock{font-size:11px;padding:2px 8px;border-radius:5px;background:var(--bg3);cursor:pointer;color:var(--ink);border:1px solid transparent}
.ladder-stock:hover{border-color:var(--up)}
.ladder-stock .lb{color:#ef4444;font-weight:700}
/* Playbook */
.playbook-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-bottom:16px}
.pb-card{background:var(--bg2);background-image:var(--grad-card);border:1px solid var(--rule);border-radius:12px;padding:14px;transition:box-shadow .3s}
.pb-card:hover{box-shadow:var(--shadow-md)}
.pb-card h4{font-size:13px;margin-bottom:8px;display:flex;align-items:center;gap:6px;letter-spacing:-.01em}
.pb-row{display:flex;justify-content:space-between;font-size:12px;padding:4px 0;border-bottom:1px dashed var(--rule)}
.pb-row:last-child{border-bottom:none}
.pb-bar{height:8px;border-radius:4px;background:var(--bg4);overflow:hidden;margin-top:8px}
.pb-bar>div{height:100%;border-radius:4px;background:linear-gradient(90deg,#22c55e,#ffc83d,#ef4444)}
.pb-disc{font-size:12px;line-height:1.8;color:var(--muted)}
.pb-disc b{color:var(--ink)}
.pb-hot{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
.pb-hot a{font-size:12px;color:var(--blue);cursor:pointer;background:var(--bg3);padding:3px 10px;border-radius:6px;transition:all .2s}
.pb-hot a:hover{background:var(--bg4);transform:translateY(-1px)}
/* AI engine */
.ai-box{background:linear-gradient(135deg,rgba(168,85,247,.08),rgba(77,159,255,.08));border:1px solid rgba(168,85,247,.3);border-radius:10px;padding:14px;margin-top:12px;box-shadow:var(--shadow-sm)}
.ai-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}
.ai-verdict{font-size:15px;font-weight:800;padding:4px 14px;border-radius:8px;letter-spacing:.01em}
.ai-verdict.v-strong{background:rgba(239,68,68,.15);color:#ef4444;box-shadow:0 2px 8px rgba(239,68,68,.2)}
.ai-verdict.v-watch{background:rgba(255,200,61,.12);color:#ffc83d;box-shadow:0 2px 8px rgba(255,200,61,.15)}
.ai-verdict.v-wait{background:rgba(125,135,165,.15);color:#9aa4c8}
.ai-verdict.v-avoid{background:rgba(34,197,94,.15);color:#22c55e;box-shadow:0 2px 8px rgba(34,197,94,.15)}
.ai-chain{display:grid;gap:6px}
.ai-step{display:flex;gap:8px;align-items:flex-start;font-size:12px;padding:7px 9px;background:var(--bg2);border-radius:7px;line-height:1.6;transition:all .2s}
.ai-step:hover{background:var(--bg3);transform:translateX(2px)}
.ai-step .k{min-width:64px;font-weight:700;flex-shrink:0}
.ai-step .d{color:var(--muted);flex:1}
.ai-score-bar{display:flex;align-items:center;gap:8px;margin-top:10px}
.ai-score{font-size:24px;font-weight:800;font-family:var(--font-mono);font-variant-numeric:tabular-nums}
/* Confluence grades */
.grade-badge{display:inline-block;padding:2px 9px;border-radius:6px;font-size:11px;font-weight:800;letter-spacing:.02em}
.grade-A{background:rgba(239,68,68,.18);color:#ef4444;border:1px solid rgba(239,68,68,.45);box-shadow:0 2px 6px rgba(239,68,68,.15)}
.grade-B{background:rgba(255,200,61,.12);color:#ffc83d;border:1px solid rgba(255,200,61,.4)}
.grade-C{background:rgba(125,135,165,.12);color:#9aa4c8;border:1px solid rgba(125,135,165,.35)}
.confluence-factors{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}
.cf-tag{font-size:10px;padding:2px 7px;border-radius:4px;background:var(--bg2);border:1px solid var(--rule);transition:all .2s}
.cf-tag.on{border-color:rgba(239,68,68,.5);color:var(--up)}
.cf-tag.off{color:var(--muted);opacity:.6}
.sig-card.grade-C{opacity:.55}
/* Calibration */
.calib-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;margin-top:8px}
.calib-cell{background:var(--bg2);border-radius:8px;padding:10px;text-align:center;transition:all .2s}
.calib-cell:hover{border-color:var(--muted);box-shadow:var(--shadow-sm)}
.calib-cell .v{font-size:18px;font-weight:800;font-family:var(--font-mono);font-variant-numeric:tabular-nums}
.calib-cell .l{font-size:10px;color:var(--muted);margin-top:3px}
.track-row{display:flex;gap:8px;align-items:center;font-size:12px;padding:5px 8px;background:var(--bg2);border-radius:6px;margin-bottom:4px;transition:background .2s}
.track-row:hover{background:var(--bg3)}
</style>
</head>
<body style="background:#0a0e1a !important;color:#e8eaf0 !important;">

<!-- ===== Top Nav ===== -->
<div class="top-nav">
  <div class="container top-nav-inner">
    <div class="nav-brand">A股<span>交易决策看板</span></div>
    <div class="nav-links">
      <a class="nav-link active" onclick="scrollToSec('layer1',this)">市场全景</a>
      <a class="nav-link" onclick="scrollToSec('layer0',this)">交易信号</a>
      <a class="nav-link" onclick="scrollToSec('layer2',this)">板块轮动</a>
      <a class="nav-link" onclick="scrollToSec('layer3',this)">个股分析</a>
      <a class="nav-link" onclick="scrollToSec('layer4',this)">量化学习</a>
      <a class="nav-link" onclick="scrollToSec('layer-watch',this)">★ 我关注的 <span class="badge-count" id="watchCount">0</span></a>
      <a class="nav-link" onclick="scrollToSec('layer-ref',this)">指标参考</a>
    </div>
    <div class="nav-right">
      <div class="search-box" style="position:relative;">
        <input type="text" id="diagInput" placeholder="输入代码/名称/拼音首字母..." autocomplete="off">
        <button onclick="diagnoseStock()">诊断</button>
        <div id="diagSuggest" style="position:absolute;top:100%;left:0;right:60px;background:var(--bg2);border:1px solid var(--rule);border-radius:0 0 8px 8px;max-height:260px;overflow-y:auto;display:none;z-index:50;box-shadow:0 4px 12px rgba(0,0,0,0.3);"></div>
      </div>
      <div class="theme-switcher">
        <button class="theme-btn active" data-t="dark" title="暗色">🌙</button>
        <button class="theme-btn" data-t="light" title="亮色">☀️</button>
        <button class="theme-btn" data-t="eye" title="护眼">🌿</button>
      </div>
    </div>
  </div>
</div>

<!-- ===== Ticker ===== -->
<div class="container" style="margin-top:12px;"><div class="ticker-wrap"><div class="ticker-track" id="tickerTrack"></div></div></div>

<!-- ===== L0: 交易信号台 ===== -->
<div class="container l0 page-section" id="layer0">
  <div class="layer-header">
    <div class="layer-num" style="background:var(--up);">0</div>
    <div><div class="layer-title">交易信号台</div><div class="layer-sub">实盘操作中枢 · 买入信号 · 风险警示 · 仓位管理 · R/R比</div></div>
    <div style="margin-left:auto;display:flex;gap:12px;align-items:center;">
      <div class="regime-badge" id="regimeBadge">--</div>
      <div style="font-size:12px;color:var(--muted);"><span class="live-dot"></span> <span id="l0Update">--:--</span></div>
    </div>
  </div>
  <div class="card" style="margin-bottom:16px;">
    <div class="card-title"><span class="bar" style="background:var(--purple);"></span>今日操盘指南（AI引擎综合推演 · 实盘操作纪律）<span style="font-size:11px;color:var(--muted);margin-left:8px;">基于情绪周期 × 市场状态 × 信号共振自动生成</span></div>
    <div class="playbook-grid" id="playbookGrid"></div>
  </div>
  <div class="grid-2">
    <div class="card"><div class="card-title"><span class="bar" style="background:var(--up);"></span>买入信号 <span id="buyCount" style="color:var(--up);font-size:13px;">0只</span> <span style="font-size:11px;color:var(--muted);margin-left:8px;">入场/止损/目标/仓位/R-R</span> <label style="margin-left:auto;font-size:11px;color:var(--gold);cursor:pointer;user-select:none;"><input type="checkbox" id="onlyAB" onchange="renderSignalCenter()" style="vertical-align:-2px;margin-right:3px;">仅看A/B级共振</label></div><div id="buyList" style="max-height:520px;overflow-y:auto;"></div></div>
    <div class="card"><div class="card-title"><span class="bar" style="background:var(--down);"></span>风险警示 <span id="sellCount" style="color:var(--down);font-size:13px;">0只</span></div><div id="sellList" style="max-height:520px;overflow-y:auto;"></div></div>
  </div>
  <div class="grid-3" style="margin-top:16px;">
    <div class="card"><div class="card-title"><span class="bar" style="background:var(--gold);"></span>仓位与风险敞口</div><div id="riskPanel" class="chart-c" style="height:200px;"></div></div>
    <div class="card"><div class="card-title"><span class="bar" style="background:var(--blue);"></span>信号质量分布</div><div id="chartSigQual" class="chart-c" style="height:200px;"></div></div>
    <div class="card"><div class="card-title"><span class="bar" style="background:var(--purple);"></span>板块集中度</div><div id="chartSectorConc" class="chart-c" style="height:200px;"></div></div>
  </div>
</div>

<!-- ===== L1: 市场全景 ===== -->
<div class="container l1 page-section active" id="layer1">
  <div class="layer-header">
    <div class="layer-num">1</div>
    <div><div class="layer-title">市场全景</div><div class="layer-sub">大盘趋势 · 市场宽度 · 情绪温度</div></div>
    <div style="margin-left:auto;font-size:12px;color:var(--muted);"><span class="live-dot"></span> 实时更新 <span id="lastUpdate">--:--:--</span></div>
  </div>
  <div class="grid-2">
    <div class="card"><div class="card-title"><span class="bar"></span>多空偏向仪表盘<span style="font-size:10px;color:var(--muted);margin-left:6px;">信号(60%)+大盘(40%)</span></div><div id="chartGauge" class="chart-c"></div></div>
    <div class="card"><div class="card-title"><span class="bar"></span>市场宽度分析</div><div id="chartBreadth" class="chart-c"></div></div>
  </div>
  <div class="stat-grid" style="margin-top:16px;">
    <div class="stat-item"><div class="stat-val" id="mtIdx">--</div><div class="stat-lbl">上证指数</div></div>
    <div class="stat-item"><div class="stat-val" id="mtChg">--</div><div class="stat-lbl">日涨跌幅(%)</div></div>
    <div class="stat-item"><div class="stat-val gold" id="mtTrend">--</div><div class="stat-lbl">趋势判断</div></div>
    <div class="stat-item"><div class="stat-val blue" id="mtR20">--</div><div class="stat-lbl">20日涨跌(%)</div></div>
    <div class="stat-item"><div class="stat-val" id="mtMA5">--</div><div class="stat-lbl">MA5</div></div>
    <div class="stat-item"><div class="stat-val" id="mtMA20">--</div><div class="stat-lbl">MA20</div></div>
    <div class="stat-item"><div class="stat-val" id="mtMA60">--</div><div class="stat-lbl">MA60</div></div>
    <div class="stat-item"><div class="stat-val gold" id="mtSent">--</div><div class="stat-lbl">多空偏向</div></div>
  </div>
  <div class="grid-2" style="margin-top:16px;">
    <div class="card"><div class="card-title"><span class="bar" style="background:var(--gold);"></span>涨停情绪周期（开盘啦风格 · 实时涨停数据推演）<span style="font-size:11px;color:var(--muted);margin-left:8px;" id="cycleUpdate"></span></div><div id="cycleBox"></div></div>
    <div class="card"><div class="card-title"><span class="bar" style="background:var(--up);"></span>涨停梯队 · 连板天梯（点击股票查看）<span style="font-size:11px;color:var(--muted);margin-left:8px;">按连板高度分层</span></div><div id="ladderBox" style="max-height:420px;overflow-y:auto;"></div></div>
  </div>
  <div class="grid-3" style="margin-top:16px;">
    <div class="card"><div class="card-title"><span class="bar"></span>信号分布</div><div id="chartAction" class="chart-c" style="height:240px;"></div></div>
    <div class="card"><div class="card-title"><span class="bar"></span>指标触发频次 Top10 <span style="font-size:10px;color:var(--muted);margin-left:6px;">鼠标悬停查看指标说明</span></div><div id="chartInd" class="chart-c" style="height:240px;"></div></div>
    <div class="card"><div class="card-title"><span class="bar" style="background:var(--blue);"></span>指标说明参考</div><div id="indRefPanel" style="max-height:200px;overflow-y:auto;font-size:12px;line-height:1.8;color:var(--muted);"></div></div>
    <div class="card"><div class="card-title"><span class="bar"></span>回测胜率统计</div><div class="stat-grid" style="grid-template-columns:1fr 1fr;gap:8px;">
      <div class="stat-item"><div class="stat-val blue" id="sWR">--</div><div class="stat-lbl">平均胜率(%)</div></div>
      <div class="stat-item"><div class="stat-val up" id="sWF">--</div><div class="stat-lbl">量化过滤(%)</div></div>
      <div class="stat-item"><div class="stat-val gold" id="sQI">--</div><div class="stat-lbl">胜率提升(pp)</div></div>
      <div class="stat-item"><div class="stat-val" id="sPF">--</div><div class="stat-lbl">平均盈亏比</div></div>
      <div class="stat-item"><div class="stat-val blue" id="sEXP">--</div><div class="stat-lbl">期望收益(%)</div></div>
      <div class="stat-item"><div class="stat-val gold" id="sQS">--</div><div class="stat-lbl">量化评分</div></div>
    </div></div>
  </div>
</div>

<!-- ===== L2: 板块分析 ===== -->
<div class="container l2 page-section" id="layer2">
  <div class="layer-header">
    <div class="layer-num">2</div>
    <div><div class="layer-title">板块轮动</div><div class="layer-sub">幻方量化轮动推演 · 产业链上下游 · 板块强弱排名 · 信号分布 · 热力图</div></div>
  </div>
  <div class="card">
    <div class="card-title"><span class="bar" style="background:var(--purple);"></span>幻方量化 · 板块轮动推演<span style="margin-left:8px;font-size:10px;color:var(--muted);font-weight:400;">多因子轮动模型（动量 28% · 情绪 22% · 信号净流入 16% · 量化评分 20% · 胜率 14%）</span></div>
    <div class="rotation-insight" id="rotationInsight"></div>
    <div id="chartRotation" class="chart-c" style="height:340px;"></div>
    <div id="rotationLadder"></div>
  </div>
  <div class="card" style="margin-top:16px;border-left:3px solid var(--blue);">
    <div class="card-title"><span class="bar" style="background:var(--blue);"></span>A股全市场板块轮动 <span style="font-size:10px;color:var(--muted);margin-left:6px;">东方财富实时板块数据 · 覆盖全部行业板块</span></div>
    <div id="fullMarketRotation" style="max-height:500px;overflow-y:auto;"></div>
  </div>
  <div class="card">
    <div class="card-title"><span class="bar" style="background:var(--blue);"></span>产业链上下游关系<span style="margin-left:8px;font-size:10px;color:var(--muted);font-weight:400;">上游资源 → 中游制造 → 下游消费 → 金融 / 交运服务（点击节点查看板块，支持滚轮缩放/拖拽）</span></div>
    <div class="grid-2">
      <div class="card" style="background:transparent;box-shadow:none;padding:0;"><div id="chartChain" class="chart-c" style="height:520px;"></div></div>
      <div style="display:flex;flex-direction:column;gap:12px;">
        <div style="font-size:12px;color:var(--muted);line-height:1.7;">节点颜色代表板块轮动梯队：<b style="color:var(--up);">红=领跑主线</b>，<b style="color:var(--orange);">橙=接力补涨</b>，<b style="color:var(--muted);">灰=蓄势观察</b>，<b style="color:var(--blue);">蓝=动能衰减</b>，<b style="color:var(--down);">绿=退潮回避</b>。箭头表示产业链上下游的传导关系（上游资源→中游制造→下游消费），用于推演资金在产业链内部的轮动路径。右侧新增<b>金融链</b>（证券·多元金融 → 地产融资传导）与<b>交运链</b>（铁路公路 → 港口 → 航运集疏运）细分，节点悬停可查看细分业务。</div>
        <div class="card"><div class="card-title"><span class="bar" style="background:var(--gold);"></span>轮动推演结论</div><div id="rotationSummary" style="font-size:12px;line-height:1.8;color:var(--muted);"></div></div>
      </div>
    </div>
  </div>
  <div class="card"><div class="card-title"><span class="bar"></span>板块强弱排名（点击进入板块个股）</div><div id="sectorRanking"></div></div>
  <div class="grid-2">
    <div class="card"><div class="card-title"><span class="bar"></span>板块情绪热力图（点击进入）</div><div class="heatmap-grid" id="heatmapGrid"></div></div>
    <div class="card"><div class="card-title"><span class="bar"></span>板块买卖信号分布</div><div id="chartSecSig" class="chart-c"></div></div>
  </div>
  <div class="grid-2">
    <div class="card"><div class="card-title"><span class="bar"></span>热门概念分布（前20）</div><div id="chartConcept" class="chart-c tall"></div></div>
    <div class="card"><div class="card-title"><span class="bar"></span>行业分布统计</div><div id="chartIndustry" class="chart-c tall"></div></div>
  </div>
</div>

<!-- ===== L3: 个股分析 ===== -->
<div class="container l3 page-section" id="layer3">
  <div class="layer-header">
    <div class="layer-num">3</div>
    <div><div class="layer-title">个股分析</div><div class="layer-sub">量化因子 · 信号矩阵 · 个股排行（点击个股查看详情，★添加关注）</div></div>
  </div>
  <div class="card"><div class="card-title"><span class="bar"></span>量化因子雷达分析（Top 3）</div><div class="grid-3" id="radarGrid"></div></div>
  <div class="card"><div class="card-title"><span class="bar"></span>操作指标面板（实时交易参数）</div><div id="opPanel" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;"></div></div>
  <div class="card"><div class="card-title"><span class="bar"></span>信号矩阵（个股×指标交叉验证）</div><div class="matrix-wrap"><table class="matrix" id="sigMatrix"></table></div></div>
  <div class="grid-2">
    <div class="card"><div class="card-title"><span class="bar"></span>个股信号散点图</div><div id="chartScatter" class="chart-c tall"></div></div>
    <div class="card"><div class="card-title"><span class="bar"></span>量化评分 vs 回测胜率</div><div id="chartQScatter" class="chart-c tall"></div></div>
  </div>
  <div class="card">
    <div class="card-title"><span class="bar"></span>个股交易信号排行（Top 50）</div>
    <div class="filter-bar">
      <input type="text" id="searchInput" placeholder="搜索代码/名称...">
      <select id="sortBy">
        <option value="signal_count">按指标数量</option><option value="bt_win_rate">按回测胜率</option>
        <option value="bt_wr_filtered">按量化胜率</option><option value="bt_quant_score">按量化评分</option>
        <option value="bt_pf">按盈亏比</option><option value="position_size">按建议仓位</option>
        <option value="confidence">按置信度</option><option value="sentiment_score">按情绪评分</option>
        <option value="priority">按优先级</option><option value="mkt_cap_yi">按市值</option>
        <option value="pct_change">按涨跌幅</option>
        <option value="signal_quality">按信号质量</option>
      </select>
      <button class="btn" onclick="exportCSV()">导出CSV</button>
      <span style="font-size:12px;color:var(--muted);" id="filterInfo"></span>
    </div>
    <div style="overflow-x:auto;"><table>
      <thead><tr>
        <th>★</th><th>#</th><th>代码</th><th>名称</th><th class="sortable" data-key="signal_count">指标</th>
        <th>行业</th><th>信号</th><th class="sortable" data-key="bt_win_rate">胜率</th>
        <th class="sortable" data-key="bt_wr_filtered">量化胜率</th><th class="sortable" data-key="bt_quant_score">量化分</th>
        <th class="sortable" data-key="bt_pf">盈亏比</th><th class="sortable" data-key="position_size">仓位</th>
        <th class="sortable" data-key="confidence">置信度</th><th class="sortable" data-key="priority">星级</th>
        <th>风险</th><th class="sortable" data-key="sentiment_score">情绪</th>
        <th class="sortable" data-key="signal_quality">信号质量</th>
        <th class="sortable" data-key="mkt_cap_yi">市值(亿)</th><th class="sortable" data-key="latest_price">最新价</th>
        <th class="sortable" data-key="pct_change">涨跌幅</th>
      </tr></thead>
      <tbody id="stockTableBody"></tbody>
    </table></div>
  </div>
</div>

<!-- ===== L4: 量化学习中心 ===== -->
<div class="container l4 page-section" id="layer4">
  <div class="layer-header">
    <div class="layer-num" style="background:var(--purple);">4</div>
    <div><div class="layer-title">量化学习中心</div><div class="layer-sub">指标历史表现 · 自我进化 · 信号质量分层 · 学习洞察</div></div>
  </div>
  <div class="card" style="margin-bottom:16px;border-left:3px solid var(--purple);">
    <div class="card-title"><span class="bar" style="background:var(--purple);"></span>幻方量化策略看板 <span style="font-size:10px;color:var(--muted);margin-left:6px;">高胜率回测策略 · 可执行交易方案</span></div>
    <div id="quantStrategyPanel" style="display:grid;gap:12px;"></div>
  </div>
  <div class="card" style="margin-bottom:16px;">
    <div class="card-title"><span class="bar" style="background:var(--gold);"></span>学习洞察（基于全市场回测数据自动生成）</div>
    <div id="learningInsights" style="display:grid;gap:8px;"></div>
  </div>
  <div class="card" style="margin-bottom:16px;">
    <div class="card-title"><span class="bar" style="background:var(--purple);"></span>信号追踪与预测校准（自我学习引擎 · 实盘验证）<span style="font-size:11px;color:var(--muted);margin-left:8px;">记录每次A/B级信号 → 实盘兑现 → 校准共振评分权重</span></div>
    <div class="calib-grid" id="calibGrid"></div>
    <div style="margin-top:10px;max-height:260px;overflow-y:auto;" id="trackList"></div>
  </div>
  <div class="grid-2">
    <div class="card"><div class="card-title"><span class="bar"></span>指标历史胜率排行（点击查看详情）</div><div style="overflow-x:auto;max-height:420px;overflow-y:auto;"><table class="multi-idx-table" id="indPerfTable">
      <thead><tr><th>指标</th><th>类型</th><th>历史触发</th><th>胜率</th><th>过滤胜率</th><th>盈亏比</th><th>期望</th><th>质量</th></tr></thead>
      <tbody id="indPerfBody"></tbody>
    </table></div></div>
    <div class="card"><div class="card-title"><span class="bar"></span>指标胜率 vs 盈亏比散点图</div><div id="chartIndPerf" class="chart-c" style="height:420px;"></div></div>
  </div>
</div>

<!-- ===== 我关注的 ===== -->
<div class="container l-watch page-section" id="layer-watch">
  <div class="layer-header">
    <div class="layer-num" style="background:var(--gold);">★</div>
    <div><div class="layer-title">我关注的个股</div><div class="layer-sub">关注列表 · K线走势 · 多维分析 · 实时行情</div></div>
    <div style="margin-left:auto;font-size:12px;color:var(--muted);"><span class="live-dot"></span> 实时更新</div>
  </div>
  <div id="watchPageContent"></div>
</div>

<!-- ===== 指标参考 ===== -->
<div class="container l-ref page-section" id="layer-ref">
  <div class="layer-header">
    <div class="layer-num">i</div>
    <div><div class="layer-title">指标形态参考手册</div><div class="layer-sub">共__UNIQUE_INDICATORS__种指标形态 · 含K线图演示</div></div>
  </div>
  <div class="card"><div id="indicatorRefGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:12px;"></div></div>
  <div class="disclaimer"><strong>风险提示：</strong>本报告基于历史K线数据通过算法自动识别技术指标形态，所有交易建议均由程序根据信号组合自动生成，仅供参考学习用途，不构成任何投资建议。技术指标存在滞后性和误判可能，实际投资决策请结合基本面分析、市场环境及自身风险承受能力综合判断。股市有风险，投资需谨慎。</div>
  <div class="footer">数据来源：东方财富/腾讯财经API · 指标体系：《新生300天图解教程》 · 过滤条件：市值≥100亿+排除ST · 生成时间：__SCAN_DATE__</div>
</div>

<!-- ===== Overlays ===== -->
<div class="overlay" id="detailOverlay"><div class="overlay-panel" id="detailPanel"><button class="overlay-close" onclick="closeOverlay('detailOverlay')">&#10005;</button><div id="detailContent"></div></div></div>
<div class="overlay" id="sectorOverlay"><div class="overlay-panel" id="sectorPanel"><button class="overlay-close" onclick="closeOverlay('sectorOverlay')">&#10005;</button><div id="sectorContent"></div></div></div>
<div class="overlay" id="watchOverlay"><div class="overlay-panel" id="watchPanel"><button class="overlay-close" onclick="closeOverlay('watchOverlay')">&#10005;</button><div id="watchContent"></div></div></div>

<script>
// ===== Data =====
const scanData=__STOCKS_JSON__;
const pinyinMap=__PINYIN_MAP_JSON__;
const indicatorInfo=__INDICATOR_INFO_JSON__;
const indicatorCounts=__INDICATOR_COUNTS_JSON__;
const industryDist=__INDUSTRY_DIST_JSON__;
const conceptDist=__CONCEPT_DIST_JSON__;
const sectorData=__SECTOR_DATA_JSON__;
const rotationData=__ROTATION_DATA_JSON__;
const chainData=__CHAIN_DATA_JSON__;
const rotationInsight=__ROTATION_INSIGHT_JSON__;
const actionDist=__ACTION_DIST_JSON__;
const buySignals=__BUY_SIGNALS_JSON__;
const sellSignals=__SELL_SIGNALS_JSON__;
const marketTrend=__MARKET_TREND_JSON__;
const klineData=__KLINE_DATA_JSON__;
const avgWinRate=__AVG_WIN_RATE__;
const avgWrFiltered=__AVG_WR_FILTERED__;
const avgQuantScore=__AVG_QUANT_SCORE__;
const avgPf=__AVG_PF__;
const avgExp=__AVG_EXP__;
const indicatorPerf=__INDICATOR_PERF_JSON__;
const learningInsights=__LEARNING_INSIGHTS_JSON__;
const riskMetrics=__RISK_METRICS_JSON__;

// ===== Theme =====
let tc={up:'#ef4444',upRgb:'239,68,68',down:'#22c55e',downRgb:'34,197,94',gold:'#ffc83d',blue:'#4d9fff',purple:'#a855f7',orange:'#ff9500',bg:'#0a0e1a',bg2:'#131829',bg3:'#1d2340',bg4:'#283056',ink:'#e8eaf0',muted:'#7a82a3',rule:'#2a3050'};
const themeMap={
  dark:{up:'#ef4444',upRgb:'239,68,68',down:'#22c55e',downRgb:'34,197,94',gold:'#ffc83d',blue:'#4d9fff',purple:'#a855f7',orange:'#ff9500',bg:'#0a0e1a',bg2:'#131829',bg3:'#1d2340',bg4:'#283056',ink:'#e8eaf0',muted:'#7a82a3',rule:'#2a3050'},
  light:{up:'#dc2626',upRgb:'220,38,38',down:'#16a34a',downRgb:'22,163,74',gold:'#d97706',blue:'#2563eb',purple:'#7c3aed',orange:'#ea580c',bg:'#f5f7fa',bg2:'#fff',bg3:'#eef1f6',bg4:'#dde3ed',ink:'#1a1f36',muted:'#6b7280',rule:'#e5e7eb'},
  eye:{up:'#c41e3a',upRgb:'196,30,58',down:'#2d8a4e',downRgb:'45,138,78',gold:'#b8860b',blue:'#1e5fa8',purple:'#6b3fa0',orange:'#cc6600',bg:'#f0f6e8',bg2:'#e8f0dc',bg3:'#d8e5c8',bg4:'#c8dab4',ink:'#2d3b1a',muted:'#6b7d4e',rule:'#b8cca0'}
};
const chartInstances={};
// ===== Unified chart styling helpers =====
function hexA(hex,a){var h=(hex||'#888888').replace('#','');if(h.length===3)h=h[0]+h[0]+h[1]+h[1]+h[2]+h[2];var n=parseInt(h,16);return 'rgba('+((n>>16)&255)+','+((n>>8)&255)+','+(n&255)+','+a+')';}
function vgrad(c,a1,a2){return new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:hexA(c,a1==null?0.9:a1)},{offset:1,color:hexA(c,a2==null?0.22:a2)}]);}
function lgrad(c,a1,a2){return new echarts.graphic.LinearGradient(0,0,1,0,[{offset:0,color:hexA(c,a1==null?0.95:a1)},{offset:1,color:hexA(c,a2==null?0.4:a2)}]);}
function tipXL(extra){return Object.assign({backgroundColor:hexA(tc.bg2,.96),borderColor:tc.rule,borderWidth:1,padding:[10,14],textStyle:{color:tc.ink,fontSize:12},extraCssText:'border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.35);backdrop-filter:blur(6px);line-height:1.7;'},extra||{});}
function hbar(c,extra){return Object.assign({type:'bar',barWidth:'62%',itemStyle:{color:lgrad(c),borderRadius:[0,3,3,0],shadowColor:hexA(c,.28),shadowBlur:5},label:{show:true,position:'right',color:tc.muted,fontSize:10,fontFamily:"'SF Mono',monospace"}},extra||{});}
function pieSeries(data,extra){return Object.assign({type:'pie',data:data,itemStyle:{borderRadius:6,borderColor:tc.bg2,borderWidth:2},emphasis:{scale:true,scaleSize:6,itemStyle:{shadowBlur:16,shadowColor:'rgba(0,0,0,.35)'}},animationType:'scale',animationEasing:'cubicOut'},extra||{});}
function switchTheme(t){document.documentElement.setAttribute('data-theme',t);tc={...themeMap[t]};document.querySelectorAll('.theme-btn').forEach(b=>b.classList.toggle('active',b.dataset.t===t));try{localStorage.setItem('stock-theme',t);}catch(e){}reRenderCharts();}
document.querySelectorAll('.theme-btn').forEach(b=>b.addEventListener('click',()=>switchTheme(b.dataset.t)));
try{localStorage.setItem('stock-theme','dark');}catch(e){}
document.documentElement.setAttribute('data-theme','dark');
document.querySelectorAll('.theme-btn').forEach(b=>b.classList.toggle('active',b.dataset.t==='dark'));
function reRenderCharts(){Object.values(chartInstances).forEach(c=>{if(c&&c.dispose)c.dispose()});Object.keys(chartInstances).forEach(k=>delete chartInstances[k]);renderRiskDashboard();renderL1Charts();renderL2Charts();renderRotationQuadrant();renderChainGraph();renderL3Charts();renderRadarCharts();renderLearningCenter();renderTicker();renderPlaybook();renderCalibration();renderEmotionCycle();renderLadder();}

function scrollToSec(id,el){
  document.querySelectorAll('.page-section').forEach(s=>s.classList.remove('active'));
  document.getElementById(id).classList.add('active');
  document.querySelectorAll('.nav-link').forEach(l=>l.classList.remove('active'));if(el)el.classList.add('active');
  window.scrollTo(0,0);
  Object.values(chartInstances).forEach(c=>{if(c&&c.dispose)c.dispose()});
  Object.keys(chartInstances).forEach(k=>delete chartInstances[k]);
  var renderMap={
    'layer0':function(){renderRegimeBadge();renderRiskDashboard();renderSignalCenter();renderPlaybook();},
    'layer1':function(){renderMarketTrend();renderL1Charts();renderEmotionCycle();renderLadder();},
    'layer2':function(){renderRotationInsight();renderRotationSummary();renderRotationQuadrant();renderRotationLadder();renderChainGraph();renderSectorRanking();renderHeatmap();renderL2Charts();},
    'layer3':function(){renderRadarCharts();renderOpPanel();renderSignalMatrix();renderL3Charts();renderTable();},
    'layer4':function(){renderLearningCenter();renderCalibration();},
    'layer-watch':function(){renderWatchlistPage();},
    'layer-ref':function(){renderIndicatorRef();},
  };
  var fn=renderMap[id];
  if(fn){requestAnimationFrame(function(){requestAnimationFrame(fn);});}
}
function closeOverlay(id){document.getElementById(id).classList.remove('active');}
let _overlayZ=200;
function openOverlay(id){const el=document.getElementById(id);if(!el)return;_overlayZ++;el.style.zIndex=_overlayZ;el.classList.add('active');}
document.querySelectorAll('.overlay').forEach(o=>o.addEventListener('click',e=>{if(e.target===o)o.classList.remove('active')}));

// ===== Watchlist =====
let watchList=JSON.parse(localStorage.getItem('stock-watchlist')||'[]');
function toggleWatch(code,e){if(e)e.stopPropagation();const idx=watchList.indexOf(code);if(idx>-1)watchList.splice(idx,1);else watchList.push(code);localStorage.setItem('stock-watchlist',JSON.stringify(watchList));updateWatchCount();renderTable();renderSignalCenter();}
function isWatched(code){return watchList.includes(code);}
function updateWatchCount(){document.getElementById('watchCount').textContent=watchList.length;}
updateWatchCount();

function showWatchlist(el){scrollToSec('layer-watch',el);}
function renderWatchlistPage(){
  const watchedInTop50=scanData.filter(s=>watchList.includes(s.code));
  const watchedExtraCodes=watchList.filter(c=>!scanData.find(s=>s.code===c));
  if(watchedInTop50.length===0&&watchedExtraCodes.length===0){
    document.getElementById('watchPageContent').innerHTML='<h2 style="text-align:center;padding:40px;color:var(--muted);">暂无关注的个股<br><span style="font-size:14px;">点击个股列表中的 ★ 添加关注，或搜索全市场个股添加关注</span></h2>';
    return;
  }
  const watchedCount=watchedInTop50.length+watchedExtraCodes.length;
  let html='<h2 style="margin-bottom:16px;">★ 我关注的个股（'+watchedCount+'只）</h2>';
  // Summary stats (only for Top50 stocks with data)
  const wt=watchedInTop50;
  const avgS=wt.length>0?wt.reduce((s,x)=>s+x.sentiment_score,0)/wt.length:0;
  const avgC=wt.length>0?wt.reduce((s,x)=>s+x.confidence,0)/wt.length:0;
  const avgQ=wt.length>0?wt.reduce((s,x)=>s+(x.bt_has_data?x.bt_quant_score:0),0)/wt.length:0;
  const buys=wt.filter(s=>s.trade_signal==='买入').length;
  const sells=wt.filter(s=>s.trade_signal==='卖出').length;
  html+='<div class="stat-grid" style="margin-bottom:16px;">';
  html+='<div class="stat-item"><div class="stat-val gold">'+watchedCount+'</div><div class="stat-lbl">关注数量</div></div>';
  html+='<div class="stat-item" style="font-size:11px;"><div class="stat-val" style="color:var(--muted);font-size:14px;">Top50:'+wt.length+' 全市场:'+watchedExtraCodes.length+'</div><div class="stat-lbl">来源分布</div></div>';
  html+='<div class="stat-item"><div class="stat-val '+(avgS>=0?'up':'down')+'">'+avgS.toFixed(2)+'</div><div class="stat-lbl">平均情绪</div></div>';
  html+='<div class="stat-item"><div class="stat-val blue">'+Math.round(avgC)+'</div><div class="stat-lbl">平均置信度</div></div>';
  html+='<div class="stat-item"><div class="stat-val gold">'+avgQ.toFixed(0)+'</div><div class="stat-lbl">平均量化分</div></div>';
  html+='<div class="stat-item"><div class="stat-val up">'+buys+'</div><div class="stat-lbl">买入信号</div></div>';
  html+='<div class="stat-item"><div class="stat-val down">'+sells+'</div><div class="stat-lbl">风险警示</div></div>';
  html+='</div>';

  // Individual analysis for each watched stock (Top50)
  wt.forEach(s=>{
    const pc=s.pct_change>=0?'pu':'pd';
    html+='<div class="watch-analysis">';
    html+='<div style="display:flex;justify-content:space-between;align-items:center;cursor:pointer;" onclick="showDetail(\''+s.code+'\')">';
    html+='<div><span style="font-size:18px;font-weight:800;">'+s.name+'</span> <span style="color:var(--muted);font-size:14px;">'+s.code+'</span> <span class="badge '+s.cls_key+'">'+s.classification+'</span></div>';
    html+='<div><span class="'+pc+'" id="wlPrice_'+s.code+'" style="font-size:16px;font-weight:700;">'+s.latest_price.toFixed(2)+'</span> <span class="'+pc+'" id="wlPct_'+s.code+'">'+(s.pct_change>=0?'+':'')+s.pct_change.toFixed(2)+'%</span> <button class="star-btn active" onclick="toggleWatch(\''+s.code+'\');event.stopPropagation();renderWatchlistPage();">★</button></div>';
    html+='</div>';
    // Multi-dimensional analysis grid
    html+='<div class="watch-analysis-grid">';
    // Sentiment dimension
    html+='<div class="dt-cell"><div class="v '+(s.sentiment_score>=0?'up':'down')+'">'+(s.sentiment_score>=0?'+':'')+s.sentiment_score.toFixed(2)+'</div><div class="l">情绪评分</div></div>';
    html+='<div class="dt-cell"><div class="v">'+s.bullish+'/'+s.bearish+'</div><div class="l">看涨/看跌</div></div>';
    html+='<div class="dt-cell"><div class="v">'+s.confidence+'</div><div class="l">置信度</div></div>';
    html+='<div class="dt-cell"><div class="v">'+s.priority+'星</div><div class="l">优先级</div></div>';
    // Quant dimension
    if(s.bt_has_data){
      html+='<div class="dt-cell"><div class="v gold">'+s.bt_quant_score.toFixed(0)+'</div><div class="l">量化评分</div></div>';
      html+='<div class="dt-cell"><div class="v '+(s.bt_win_rate>=50?'up':'down')+'">'+s.bt_win_rate.toFixed(1)+'%</div><div class="l">回测胜率</div></div>';
      html+='<div class="dt-cell"><div class="v '+(s.bt_wr_filtered>=50?'up':'down')+'">'+s.bt_wr_filtered.toFixed(1)+'%</div><div class="l">量化过滤胜率</div></div>';
      html+='<div class="dt-cell"><div class="v">'+s.bt_pf.toFixed(2)+'</div><div class="l">盈亏比</div></div>';
      html+='<div class="dt-cell"><div class="v">'+s.bt_rsi.toFixed(0)+'</div><div class="l">RSI(14)</div></div>';
      html+='<div class="dt-cell"><div class="v">'+s.bt_adx.toFixed(0)+'</div><div class="l">ADX(14)</div></div>';
      html+='<div class="dt-cell"><div class="v">'+s.bt_vol_ratio.toFixed(2)+'</div><div class="l">量比</div></div>';
      html+='<div class="dt-cell"><div class="v">'+(s.bt_bb_pos*100).toFixed(0)+'%</div><div class="l">布林位置</div></div>';
      html+='<div class="dt-cell"><div class="v">'+s.bt_atr_pct.toFixed(2)+'%</div><div class="l">ATR波幅</div></div>';
    }
    html+='</div>';
    // Trading signal
    if(s.trade_signal==='买入'||s.trade_signal==='卖出'){
      html+='<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:8px;font-size:12px;">';
      html+='<div><span style="color:var(--muted);">信号</span> <b class="'+(s.trade_signal==='买入'?'pu':'pd')+'">'+s.trade_signal+'</b></div>';
      if(s.entry_price)html+='<div><span style="color:var(--muted);">入场</span> <b class="pu">'+s.entry_price.toFixed(2)+'</b></div>';
      if(s.stop_loss)html+='<div><span style="color:var(--muted);">止损</span> <b class="pd">'+s.stop_loss.toFixed(2)+'</b></div>';
      if(s.target_price)html+='<div><span style="color:var(--muted);">目标</span> <b style="color:var(--gold);">'+s.target_price.toFixed(2)+'</b></div>';
      html+='</div>';
    }
    // Actionable analysis
    html+='<div class="watch-advice">';
    // Technical analysis
    let techAdvice='';
    if(s.bt_has_data){
      if(s.bt_rsi<30)techAdvice+='RSI='+s.bt_rsi.toFixed(0)+'，处于超卖区间，存在技术性反弹需求；';
      else if(s.bt_rsi>70)techAdvice+='RSI='+s.bt_rsi.toFixed(0)+'，处于超买区间，追高风险加大；';
      else techAdvice+='RSI='+s.bt_rsi.toFixed(0)+'，处于正常区间；';
      if(s.bt_adx>25)techAdvice+='ADX='+s.bt_adx.toFixed(0)+'，趋势明确，适合趋势跟踪策略；';
      else techAdvice+='ADX='+s.bt_adx.toFixed(0)+'，趋势不明朗，适合区间操作；';
      if(s.bt_vol_ratio>1.5)techAdvice+='量比='+s.bt_vol_ratio.toFixed(2)+'，放量明显，资金关注度较高；';
      else if(s.bt_vol_ratio<0.5)techAdvice+='量比='+s.bt_vol_ratio.toFixed(2)+'，缩量明显，市场参与度低；';
      if(s.bt_bb_pos>0.8)techAdvice+='布林位置='+(s.bt_bb_pos*100).toFixed(0)+'%，接近上轨，注意回调风险；';
      else if(s.bt_bb_pos<0.2)techAdvice+='布林位置='+(s.bt_bb_pos*100).toFixed(0)+'%，接近下轨，关注支撑有效性；';
    }
    // Strategy advice
    let stratAdvice='';
    if(s.sentiment_score>=0.5&&s.confidence>=65)stratAdvice='多维度信号共振偏多，建议在突破确认后分批建仓，仓位控制在'+(s.position_size||30)+'%以内，严格止损在'+(s.stop_loss?s.stop_loss.toFixed(2):'入场价-ATR')+'。';
    else if(s.sentiment_score<=-0.3)stratAdvice='信号偏空，建议持仓者注意止损，未持仓者暂时规避，等待情绪反转信号。';
    else if(s.bt_has_data&&s.bt_quant_score>=60)stratAdvice='量化评分较高('+s.bt_quant_score.toFixed(0)+')，但情绪信号不够强烈，建议小仓位试探性参与，确认方向后加仓。';
    else strAdvice=stratAdvice='信号中性，建议持续观察，等待更明确的交易信号。';
    html+='<strong style="color:var(--gold);">技术面分析：</strong>'+techAdvice+'<br>';
    html+='<strong style="color:var(--up);">实战策略：</strong>'+stratAdvice;
    if(s.trade_reason)html+='<br><strong style="color:var(--blue);">信号解读：</strong>'+s.trade_reason;
    html+='</div>';
    // T+0 opportunity container
    html+='<div id="t0Container_'+s.code+'"><div style="text-align:center;padding:20px;color:var(--muted);font-size:12px;">T+0分析加载中...</div></div>';
    // K-line chart
    const kl=klineData[s.code]||{};const klData=kl.kline||[];
    if(klData.length>0)html+='<div id="wlKline_'+s.code+'" style="width:100%;height:440px;margin-top:8px;"></div>';
    html+='</div>';
  });
  // Non-Top50 watched stocks
  if(watchedExtraCodes.length>0){
    html+='<h3 style="margin:20px 0 12px;color:var(--gold);">全市场关注（'+watchedExtraCodes.length+'只）</h3>';
    watchedExtraCodes.forEach(code=>{
      html+='<div class="watch-analysis" id="wlExtra_'+code+'">';
      html+='<div style="display:flex;justify-content:space-between;align-items:center;cursor:pointer;" id="wlHeader_'+code+'" onclick="showDetailEx(\''+code+'\',\''+code+'\','+(code.startsWith('6')?1:0)+')">';
      html+='<div><span style="font-size:18px;font-weight:800;" id="wlName_'+code+'">'+code+'</span> <span class="badge watch" style="font-size:9px;">全市场</span> <span id="wlPrice_'+code+'" style="font-size:16px;font-weight:700;color:var(--muted);">--</span> <span id="wlPct_'+code+'" style="font-size:12px;color:var(--muted);">--</span></div>';
      html+='<button class="star-btn active" onclick="toggleWatch(\''+code+'\');event.stopPropagation();renderWatchlistPage();">★</button>';
      html+='</div>';
      html+='<div id="wlKline_'+code+'" style="width:100%;height:440px;margin-top:8px;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:13px;">加载K线数据中...</div>';
      html+='<div id="t0Container_'+code+'"><div style="text-align:center;padding:20px;color:var(--muted);font-size:12px;">T+0分析加载中...</div></div>';
      html+='</div>';
    });
  }
  document.getElementById('watchPageContent').innerHTML=html;
  // Render Top50 K-line charts
  wt.forEach(s=>{
    const kl=klineData[s.code]||{};const kline=kl.kline||[];
    if(kline.length>0)setTimeout(function(){renderKlineChart(kline,'wlKline_'+s.code);},100);
  });
  // Fetch fresh K-line for Top50 watched stocks from API (replaces static data)
  wt.forEach(async s=>{
    const prefix=s.code.startsWith('6')?'1':'0';
    const secid=prefix+'.'+s.code;
    try{
      const r=await jsonpRequest('https://push2his.eastmoney.com/api/qt/stock/kline/get?secid='+secid+'&fields1=f1,f2,f3,f4,f5,f6&fields2=f51,f52,f53,f54,f55,f56,f57,f58&klt=101&fqt=1&beg=0&end=20500101&lmt=120',8000);
      if(r&&r.data&&r.data.klines){
        const kline=r.data.klines.map(function(line){const p=line.split(',');return{date:p[0],open:parseFloat(p[1]),close:parseFloat(p[2]),high:parseFloat(p[3]),low:parseFloat(p[4]),volume:parseFloat(p[5]),ma5:0,ma10:0,ma20:0};});
        for(let i=0;i<kline.length;i++){kline[i].ma5=calcMA(kline.slice(Math.max(0,i-4),i+1).map(k=>k.close),5);kline[i].ma10=calcMA(kline.slice(Math.max(0,i-9),i+1).map(k=>k.close),10);kline[i].ma20=calcMA(kline.slice(Math.max(0,i-19),i+1).map(k=>k.close),20);}
        const dom=document.getElementById('wlKline_'+s.code);
        if(dom){const old=echarts.getInstanceByDom(dom);if(old)old.dispose();renderKlineChart(kline,'wlKline_'+s.code);}
      }
    }catch(e){}
  });
  // Fetch and render non-Top50 K-line charts + stock name
  watchedExtraCodes.forEach(async code=>{
    const mkt=code.startsWith('6')?1:0;
    const prefix=mkt===1?'1':'0';
    const secid=prefix+'.'+code;
    // Fetch quote for name + price
    try{
      const q=await jsonpRequest('https://push2.eastmoney.com/api/qt/stock/get?secid='+secid+'&fltt=2&fields=f43,f58,f170',5000);
      if(q&&q.data){const d=q.data;const nm=d.f58||code;const pr=d.f43||0;const pc=d.f170||0;
        const ne=document.getElementById('wlName_'+code);if(ne)ne.textContent=nm;
        const pe=document.getElementById('wlPrice_'+code);if(pe){pe.textContent=pr.toFixed(2);pe.style.color='var(--'+(pc>=0?'up':'down')+')';}
        const pce=document.getElementById('wlPct_'+code);if(pce){pce.textContent=(pc>=0?'+':'')+pc.toFixed(2)+'%';pce.style.color='var(--'+(pc>=0?'up':'down')+')';}
        const hd=document.getElementById('wlHeader_'+code);
        if(hd)hd.setAttribute('onclick','showDetailEx(\''+code+'\',\''+nm+'\','+mkt+')');}
    }catch(e){}
    // Fetch K-line
    try{
      const r=await jsonpRequest('https://push2his.eastmoney.com/api/qt/stock/kline/get?secid='+secid+'&fields1=f1,f2,f3,f4,f5,f6&fields2=f51,f52,f53,f54,f55,f56,f57,f58&klt=101&fqt=1&beg=0&end=20500101&lmt=120',8000);
      if(r&&r.data&&r.data.klines){
        const kline=r.data.klines.map(function(line){const p=line.split(',');return{date:p[0],open:parseFloat(p[1]),close:parseFloat(p[2]),high:parseFloat(p[3]),low:parseFloat(p[4]),volume:parseFloat(p[5]),ma5:0,ma10:0,ma20:0};});
        for(let i=0;i<kline.length;i++){kline[i].ma5=calcMA(kline.slice(Math.max(0,i-4),i+1).map(k=>k.close),5);kline[i].ma10=calcMA(kline.slice(Math.max(0,i-9),i+1).map(k=>k.close),10);kline[i].ma20=calcMA(kline.slice(Math.max(0,i-19),i+1).map(k=>k.close),20);}
        const dom=document.getElementById('wlKline_'+code);
        if(dom)renderKlineChart(kline,'wlKline_'+code);
      }
    }catch(e){const dom=document.getElementById('wlKline_'+code);if(dom)dom.textContent='K线数据加载失败';}
  });
  // Load T+0 opportunity analysis
  setTimeout(loadT0ForWatchlist,500);
}

// ===== T+0 Opportunity Engine =====
let t0Data={};
let t0Intraday={};
async function fetchIntradayData(code){
  const prefix=code.startsWith('6')?'1':'0';
  const secid=prefix+'.'+code;
  try{
    const r=await jsonpRequest('https://push2his.eastmoney.com/api/qt/stock/kline/get?secid='+secid+'&fields1=f1,f2,f3,f4,f5,f6&fields2=f51,f52,f53,f54,f55,f56,f57,f58&klt=1&fqt=1&beg=0&end=20500101&lmt=243',8000);
    if(r&&r.data&&r.data.klines){
      return r.data.klines.map(function(line){const p=line.split(',');return{time:p[0],open:parseFloat(p[1]),close:parseFloat(p[2]),high:parseFloat(p[3]),low:parseFloat(p[4]),volume:parseFloat(p[5])};});
    }
  }catch(e){}
  return null;
}
function analyzeT0(code,bars,rtPrice){
  if(!bars||bars.length<10)return null;
  const sd=scanData.find(x=>x.code===code)||{};
  const atrPct=sd.bt_atr_pct||2;
  const dailyVol=sd.bt_vol_ratio||1;
  var sumPV=0,sumVol=0;
  bars.forEach(function(b){sumPV+=b.close*b.volume;sumVol+=b.volume;});
  var vwap=sumVol>0?sumPV/sumVol:bars[bars.length-1].close;
  var closes=bars.map(function(b){return b.close;});
  function ma(arr,n){var a=arr.length>=n?arr.slice(arr.length-n):arr;return a.reduce(function(s,x){return s+x;},0)/a.length;}
  var ma5=ma(closes,5),ma10=ma(closes,10),ma20=ma(closes,20);
  var high=0,low=999999;
  bars.forEach(function(b){if(b.high>high)high=b.high;if(b.low<low)low=b.low;});
  var preClose=bars[0].open;
  var price=rtPrice||bars[bars.length-1].close;
  var amplitude=preClose>0?(high-low)/preClose*100:0;
  var posInRange=(high-low)>0.0001?(price-low)/(high-low):0.5;
  var priceVsVwapPct=vwap>0?(price-vwap)/vwap*100:0;
  var tail=bars.slice(-10);
  var momentumPct=tail.length>1?(tail[tail.length-1].close-tail[0].close)/tail[0].close*100:0;
  var avgVol=sumVol/bars.length;
  var r15=bars.slice(-15);
  var recentVol=r15.reduce(function(s,b){return s+b.volume;},0)/r15.length;
  var volRatio=avgVol>0?recentVol/avgVol:1;
  var atrAmt=price*atrPct/100;
  var s1=Math.max(low,vwap*0.995);
  var s2=Math.max(low-atrAmt*0.5,preClose-atrAmt);
  var r1=Math.min(high,vwap*1.005);
  var r2=Math.min(high+atrAmt*0.5,preClose+atrAmt);
  if(s1>s2){var t=s1;s1=s2;s2=t;}
  if(r1>r2){var u=r1;r1=r2;r2=u;}
  var support=s1,resistance=r1;
  var trendUp=ma5>=ma10&&ma10>=ma20&&momentumPct>0;
  var trendDn=ma5<=ma10&&ma10<=ma20&&momentumPct<0;
  // ==== 幻方风格多因子评分 0-100 ====
  var factors=[];var score=0;
  function fac(name,on,pts){factors.push({name:name,on:on,pts:pts});if(on)score+=pts;}
  // 因子1 振幅空间
  if(amplitude>=5&&amplitude<=8){fac('振幅空间',true,25);}
  else if(amplitude>=3&&amplitude<5){fac('振幅空间',true,20);}
  else if(amplitude>=2&&amplitude<3){fac('振幅空间',true,12);}
  else if(amplitude>8){fac('振幅过大',true,14);}
  else{fac('振幅不足',false,0);}
  // 因子2 方向确定
  if((trendUp&&priceVsVwapPct>=0)||(trendDn&&priceVsVwapPct<=0)){fac('方向确定',true,22);}
  else if(trendUp||trendDn){fac('方向形成',true,14);}
  else{fac('方向不明',false,0);}
  // 因子3 量能活性
  if(volRatio>=1.2&&volRatio<=3){fac('量能活性',true,18);}
  else if(volRatio>=0.8&&volRatio<1.2){fac('量能温和',true,10);}
  else if(volRatio>3){fac('量能过热',true,8);}
  else{fac('量能枯竭',false,0);}
  // 因子4 波动质量
  if(atrPct>=2&&atrPct<=4){fac('波动适配',true,15);}
  else if(atrPct>=1.5&&atrPct<2){fac('波动偏低',true,9);}
  else if(atrPct>4&&atrPct<=6){fac('波动偏高',true,9);}
  else{fac('波动极端',false,3);}
  // 因子5 位置质量
  if((trendUp&&posInRange<=0.45)||(trendDn&&posInRange>=0.55)){fac('位置占优',true,15);}
  else if(posInRange>=0.3&&posInRange<=0.7){fac('位置中性',true,8);}
  else{fac('位置不佳',false,3);}
  var grade=score>=70?'A':(score>=50?'B':'C');
  // ==== 方向与操作指令 ====
  var signal,badge,action,entry=null,target=null,stop=null,direction='观望';
  var batches='',timePlan='';
  if(amplitude<2){
    signal='不合做T';badge='wait';direction='观望';
    action='今日振幅仅'+amplitude.toFixed(2)+'%，波动空间不足，不适合日内回转。建议观望，等待放量扩大振幅后再做T。';
  }else if(trendUp&&posInRange<=0.55){
    signal='顺向做T';badge='buy';direction='顺向';
    entry=Math.max(s1,vwap*0.997);
    target=Math.min(r1,preClose*1.01);
    stop=Math.min(s2,entry-atrAmt*0.8);
    action='分时多头排列、价格贴近支撑，适合<b>顺向做T（先买后卖）</b>：在 '+entry.toFixed(2)+' 附近分批低吸，反弹至 '+target.toFixed(2)+' 附近卖出等量底仓摊低成本。';
    batches='两档低吸：首档现价下方 '+(price*0.998).toFixed(2)+'，回踩 '+(price*0.995).toFixed(2)+' 补第二档。';
    timePlan='<b>开盘</b>观察VWAP方向；<b>盘中</b>回踩不破MA10可低吸；<b>尾盘</b>若未反弹则当日止损，不隔夜加码。';
  }else if(trendDn&&posInRange>=0.45){
    signal='逆向做T';badge='sell';direction='逆向';
    entry=Math.min(r1,vwap*1.003);
    target=Math.max(s1,preClose*0.99);
    stop=Math.max(r2,entry+atrAmt*0.8);
    action='分时空头排列、价格贴近阻力，适合<b>逆向做T（先卖后买）</b>：在 '+entry.toFixed(2)+' 附近高抛底仓，回落至 '+target.toFixed(2)+' 附近买回赚取差价。';
    batches='两档高抛：首档现价上方 '+(price*1.002).toFixed(2)+'，冲高 '+(price*1.005).toFixed(2)+' 抛第二档。';
    timePlan='<b>开盘</b>冲高不追；<b>盘中</b>反弹至VWAP上方可高抛；<b>尾盘</b>回落至支撑再考虑回补。';
  }else if(amplitude>=3){
    signal='网格做T';badge='buy';direction='网格';
    var gs=Math.max(price*0.005,atrAmt*0.4);
    var gridLower=s2,gridUpper=r2;
    var gridCount=Math.floor((gridUpper-gridLower)/gs);
    gridCount=Math.max(3,Math.min(gridCount,10));
    entry=gridLower;target=gridUpper;stop=Math.max(s2*0.99,gridLower-gs);
    action='分时处于区间震荡，适合<b>网格做T</b>：在 ['+gridLower.toFixed(2)+','+gridUpper.toFixed(2)+'] 区间按约 '+gs.toFixed(2)+' 元/格（'+(gs/price*100).toFixed(1)+'%）高抛低吸，共约 '+gridCount+' 格。';
    batches='网格每格仓位≈底仓的'+(100/gridCount).toFixed(0)+'%，单格止损 '+Math.max(gs,price*0.008).toFixed(2)+' 元。';
    timePlan='<b>开盘</b>确定高低点；<b>盘中</b>区间上沿卖出、下沿买回；<b>尾盘</b>回收全部网格仓位。';
  }else{
    signal='观望';badge='wait';direction='观望';
    action='振幅'+amplitude.toFixed(2)+'%、方向与量能均不足以支撑日内回转，建议观望。支撑 '+s1.toFixed(2)+'，阻力 '+r1.toFixed(2)+'。';
  }
  var revExp=0,riskPct=0;
  if(entry&&target&&stop){
    if(badge==='buy'){revExp=(target-entry)/entry*100;riskPct=(entry-stop)/entry*100;}
    else if(badge==='sell'){revExp=(entry-target)/entry*100;riskPct=(stop-entry)/entry*100;}
    else{revExp=(target-entry)/entry*100;riskPct=0;}
  }
  return{signal:signal,badge:badge,action:action,direction:direction,score:score,grade:grade,factors:factors,
    vwap:vwap,amplitude:amplitude,momentumPct:momentumPct,volRatio:volRatio,atrPct:atrPct,
    support:support,resistance:resistance,s1:s1,s2:s2,r1:r1,r2:r2,
    preClose:preClose,high:high,low:low,price:price,priceVsVwapPct:priceVsVwapPct,
    entry:entry,target:target,stop:stop,batches:batches,timePlan:timePlan,revExp:revExp,riskPct:riskPct,
    dailyVol:dailyVol,bars:bars};
}
function renderT0Box(code,t0,prefix){
  if(!t0)return'';
  var pid=(prefix||'t0Chart_')+code;
  var scoreCol=t0.score>=70?'var(--up)':(t0.score>=50?'var(--gold)':'var(--muted)');
  var h='<div class="t0-box">';
  h+='<div class="t0-head"><div><b style="color:var(--gold);">T+0回转作战</b> <span style="font-size:10px;color:var(--muted);">幻方多因子·日内回转引擎</span></div><div style="display:flex;gap:6px;align-items:center;"><span class="t0-grade tg-'+t0.grade+'">'+t0.grade+'级机会</span><span class="t0-badge '+t0.badge+'">'+t0.signal+'</span></div></div>';
  h+='<div class="t0-score-wrap"><div style="display:flex;justify-content:space-between;font-size:10px;color:var(--muted);"><span>做T综合评分</span><span style="font-weight:800;color:'+scoreCol+';">'+t0.score+'分</span></div><div class="t0-scorebar"><div style="width:'+t0.score+'%;background:linear-gradient(90deg,var(--down),var(--gold),var(--up));"></div></div></div>';
  h+='<div class="t0-factors">'+(t0.factors||[]).map(function(f){return '<span class="t0-factor '+(f.on?'on':'off')+'">'+f.name+(f.on?' +'+f.pts:'')+'</span>';}).join('')+'</div>';
  var cur=t0.price;
  h+='<div class="t0-levels">';
  h+='<div class="t0-level"><span class="nm">阻力2</span><span class="px" style="color:var(--up);">'+t0.r2.toFixed(2)+'</span><span class="dv" style="color:var(--up);">+'+(cur>0?((t0.r2-cur)/cur*100):0).toFixed(1)+'%</span></div>';
  h+='<div class="t0-level"><span class="nm">阻力1</span><span class="px" style="color:var(--up);">'+t0.r1.toFixed(2)+'</span><span class="dv" style="color:var(--up);">+'+(cur>0?((t0.r1-cur)/cur*100):0).toFixed(1)+'%</span></div>';
  h+='<div class="t0-level cur"><span class="nm">现价</span><span class="px" style="color:var(--ink);font-weight:800;">'+cur.toFixed(2)+'</span><span class="dv" style="color:var(--muted);">VWAP '+t0.vwap.toFixed(2)+'</span></div>';
  h+='<div class="t0-level"><span class="nm">支撑1</span><span class="px" style="color:var(--down);">'+t0.s1.toFixed(2)+'</span><span class="dv" style="color:var(--down);">'+(cur>0?((t0.s1-cur)/cur*100):0).toFixed(1)+'%</span></div>';
  h+='<div class="t0-level"><span class="nm">支撑2</span><span class="px" style="color:var(--down);">'+t0.s2.toFixed(2)+'</span><span class="dv" style="color:var(--down);">'+(cur>0?((t0.s2-cur)/cur*100):0).toFixed(1)+'%</span></div>';
  h+='</div>';
  h+='<div class="t0-metrics">';
  h+='<div class="t0-metric"><div class="v '+(t0.priceVsVwapPct>=0?'pu':'pd')+'">'+(t0.priceVsVwapPct>=0?'+':'')+t0.priceVsVwapPct.toFixed(2)+'%</div><div class="l">偏离VWAP</div></div>';
  h+='<div class="t0-metric"><div class="v" style="color:var(--gold);">'+t0.vwap.toFixed(2)+'</div><div class="l">VWAP</div></div>';
  h+='<div class="t0-metric"><div class="v">'+t0.amplitude.toFixed(2)+'%</div><div class="l">振幅</div></div>';
  h+='<div class="t0-metric"><div class="v '+(t0.momentumPct>=0?'pu':'pd')+'">'+(t0.momentumPct>=0?'+':'')+t0.momentumPct.toFixed(2)+'%</div><div class="l">动量</div></div>';
  h+='<div class="t0-metric"><div class="v '+(t0.volRatio>=1?'pu':'pd')+'">'+t0.volRatio.toFixed(2)+'</div><div class="l">分时量比</div></div>';
  h+='<div class="t0-metric"><div class="v">'+t0.atrPct.toFixed(1)+'%</div><div class="l">ATR波幅</div></div>';
  h+='</div>';
  h+='<div class="t0-action">'+t0.action;
  if(t0.entry&&t0.target&&t0.stop){
    var lb=t0.badge==='sell';
    h+='<div class="t0-points">';
    h+='<div class="t0-point"><div class="v '+(lb?'pd':'pu')+'">'+t0.entry.toFixed(2)+'</div><div class="l">'+(lb?'高抛价':'低吸价')+'</div></div>';
    h+='<div class="t0-point"><div class="v '+(lb?'pd':'pu')+'">'+t0.target.toFixed(2)+'</div><div class="l">'+(lb?'低吸价':'高抛价')+'</div></div>';
    h+='<div class="t0-point"><div class="v pd">'+t0.stop.toFixed(2)+'</div><div class="l">止损</div></div>';
    h+='<div class="t0-point"><div class="v" style="color:var(--gold);">'+(t0.revExp>=0?'+':'')+t0.revExp.toFixed(2)+'%</div><div class="l">做T空间</div></div>';
    h+='</div>';
  }
  if(t0.batches)h+='<div class="t0-batch"><b>分批：</b>'+t0.batches+'</div>';
  h+='<div class="t0-time">'+t0.timePlan+'</div>';
  h+='</div>';
  h+='<div id="'+pid+'" class="t0-chart"></div>';
  h+='</div>';
  return h;
}
function renderT0Chart(code,t0,prefix){
  if(!t0||!t0.bars)return;
  var pid=(prefix||'t0Chart_')+code;
  var dom=document.getElementById(pid);
  if(!dom)return;
  var old=echarts.getInstanceByDom(dom);if(old)old.dispose();
  var data=t0.bars;
  var vwapLine=[];
  var sumPV=0,sumVol=0;
  data.forEach(function(b,i){
    sumPV+=b.close*b.volume;sumVol+=b.volume;
    vwapLine.push(sumVol>0?[i,sumPV/sumVol]:[i,b.close]);
  });
  var priceLine=data.map(function(b,i){return[i,b.close];});
  var g=echarts.init(dom);
  chartInstances[pid]=g;
  g.setOption({
    backgroundColor:'transparent',
    tooltip:tipXL({trigger:'axis'}),
    grid:{left:50,right:10,top:10,bottom:20},
    xAxis:{type:'value',show:false},
    yAxis:{type:'value',scale:true,axisLabel:{color:tc.muted,fontSize:10,fontFamily:"'SF Mono',monospace"},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},
    series:[
      {name:'价格',type:'line',data:priceLine,showSymbol:false,lineStyle:{color:tc.up,width:1.5},areaStyle:{color:vgrad(tc.up,.22,.02)},emphasis:{focus:'series',lineStyle:{width:2.5}}},
      {name:'VWAP',type:'line',data:vwapLine,showSymbol:false,lineStyle:{color:tc.gold,width:1.5,type:'dashed'},emphasis:{focus:'series',lineStyle:{width:2.5}}}
    ]
  });
}
async function loadT0ForWatchlist(){
  var codes=watchList.slice();
  for(var i=0;i<codes.length;i++){
    var code=codes[i];
    var bars=await fetchIntradayData(code);
    if(bars&&bars.length>10){
      t0Intraday[code]=bars;
      var rt=realtimeData[code];
      var rtPrice=rt?rt.price:bars[bars.length-1].close;
      t0Data[code]=analyzeT0(code,bars,rtPrice);
      var el=document.getElementById('t0Container_'+code);
      if(el&&t0Data[code]){
        el.innerHTML=renderT0Box(code,t0Data[code]);
        renderT0Chart(code,t0Data[code]);
      }
    }
  }
}
function refreshT0Prices(){
  Object.keys(t0Data).forEach(function(code){
    var rt=realtimeData[code];if(!rt)return;
    var bars=t0Intraday[code];if(!bars)return;
    var t0=analyzeT0(code,bars,rt.price);
    if(t0){
      t0Data[code]=t0;
      var el=document.getElementById('t0Container_'+code);
      if(el){el.innerHTML=renderT0Box(code,t0);renderT0Chart(code,t0);}
    }
  });
}
async function loadT0ForDetail(code){
  var box=document.getElementById('t0DetailBox');
  if(!box)return;
  box.innerHTML='<div style="text-align:center;padding:16px;color:var(--muted);font-size:12px;">T+0日内回转分析加载中...</div>';
  var bars=t0Intraday[code]||null;
  if(!bars){bars=await fetchIntradayData(code);if(bars&&bars.length>10)t0Intraday[code]=bars;}
  var rt=realtimeData[code];var rtPrice=rt?rt.price:null;
  if(!bars||bars.length<10){box.innerHTML='<div style="text-align:center;padding:16px;color:var(--muted);font-size:12px;">分时数据不足，无法生成T+0建议（非交易时段或数据源受限）</div>';return;}
  var t0=analyzeT0(code,bars,rtPrice);
  if(t0){box.innerHTML=renderT0Box(code,t0,'t0DetailChart_');renderT0Chart(code,t0,'t0DetailChart_');}
  else box.innerHTML='<div style="text-align:center;padding:16px;color:var(--muted);font-size:12px;">分时数据不足，无法生成T+0建议</div>';
}
function execPlanHtml(s){
  if(!s.entry_price||!s.stop_loss||!s.target_price)return'';
  var e=s.entry_price,sl=s.stop_loss,tg=s.target_price;
  var riskAmt=e-sl;
  var l1=(e+sl)/2;
  var l2=sl+riskAmt*0.3;
  var t1=e+riskAmt*1.0;
  var t2=Math.min(tg,e+riskAmt*2.0);
  var tr1=Math.max(e,e+riskAmt*0.5);
  var h='<div class="dt-section" style="border-left:3px solid var(--gold);"><h4><span style="width:3px;height:14px;background:var(--gold);border-radius:2px;display:inline-block;"></span>实操执行计划（交易员视角）</h4>';
  h+='<div class="exec-step"><span class="es-tag" style="background:var(--up-bg);color:var(--up);">建仓</span><span>三档分批：'+e.toFixed(2)+' 建30% → '+l1.toFixed(2)+' 补30% → '+l2.toFixed(2)+' 补40%。不追高、不满仓。</span></div>';
  h+='<div class="exec-step"><span class="es-tag" style="background:var(--down-bg);color:var(--down);">止损</span><span>跌破 '+sl.toFixed(2)+' 无条件离场，单笔亏损≤总资金2%。若回踩未破 '+tr1.toFixed(2)+' 可持有。</span></div>';
  h+='<div class="exec-step"><span class="es-tag" style="background:rgba(255,200,61,.12);color:var(--gold);">止盈</span><span>第一目标 '+t1.toFixed(2)+' 减半仓，第二目标 '+t2.toFixed(2)+' 清仓。跌破 '+tr1.toFixed(2)+' 后止损上移至成本价。</span></div>';
  h+='</div>';
  return h;
}

// ===== L1: Macro =====
function renderMarketTrend(){
  const mt=marketTrend||{};const idx=mt.index_close||0,chg=mt.index_change||0,r20=mt.ret_20d||0;
  const el=v=>document.getElementById(v);
  el('mtIdx').textContent=idx.toFixed(2);el('mtIdx').className='stat-val '+(chg>=0?'up':'down');
  el('mtChg').textContent=(chg>=0?'+':'')+chg.toFixed(2);el('mtChg').className='stat-val '+(chg>=0?'up':'down');
  el('mtTrend').textContent=mt.trend||'--';el('mtR20').textContent=(r20>=0?'+':'')+r20.toFixed(2);
  el('mtR20').className='stat-val '+(r20>=0?'up':'down');el('mtMA5').textContent=(mt.ma5||0).toFixed(2);
  el('mtMA20').textContent=(mt.ma20||0).toFixed(2);el('mtMA60').textContent=(mt.ma60||0).toFixed(2);
  const total=scanData.length;
	// 股票级统计: bullish>bearish 的股票数 vs bearish>bullish 的股票数
	const bullStocks=scanData.filter(s=>s.bullish>s.bearish).length;
	const bearStocks=scanData.filter(s=>s.bearish>s.bullish).length;
	// 信号偏向(股票级): (bullStocks-bearStocks)/total*100,含中性股票,不放大偏差
	const sigBias=((bullStocks-bearStocks)/total*100).toFixed(1);
	// 大盘指数加权: 取5大指数平均涨跌幅×10 作为市场分
	const ix=marketTrend&&marketTrend.indices?marketTrend.indices:[];var idxAvg=0;
	if(ix.length>0){for(var i=0;i<ix.length;i++)idxAvg+=(ix[i].index_change||0);idxAvg=idxAvg/ix.length;}
	// 综合情绪 = 信号偏向(60%) + 大盘指数(40%),指数涨跌幅按×10映射到仪表盘尺度
	const mktScore=Math.round(parseFloat(sigBias)*0.6+idxAvg*10*0.4);
	el('mtSent').textContent=(mktScore>=0?'+':'')+mktScore+'%';el('mtSent').className='stat-val '+(mktScore>=0?'up':'down');
	el('sWR').textContent=avgWinRate;el('sWF').textContent=avgWrFiltered;
  el('sQI').textContent=(avgWrFiltered-avgWinRate).toFixed(1);el('sPF').textContent=avgPf;
  el('sEXP').textContent=avgExp;el('sQS').textContent=avgQuantScore;
}
function renderMultiIndices(){
  const mt=marketTrend||{};const indices=mt.indices||[];const body=document.getElementById('multiIdxBody');
  if(!body)return;
  if(!indices.length){body.innerHTML='<tr><td colspan="8" style="color:var(--muted);">暂无多指数数据</td></tr>';return;}
  const cls=v=>v>0?'up':(v<0?'down':'');
  const sign=v=>(v>0?'+':'')+v.toFixed(2);
  const trendTag=t=>{const m={'上行':'trend-up','单边下行':'trend-down','下行':'trend-down','震荡':'trend-flat'};return '<span class="trend-tag '+(m[t]||'trend-flat')+'">'+t+'</span>';};
  body.innerHTML=indices.map(it=>{
    if(it.trend==='数据不足'){return '<tr><td class="idx-name">'+it.name+'</td><td colspan="7" style="color:var(--muted);">数据不足</td></tr>';}
    const chg=it.index_change||0,r20=it.ret_20d||0;
    return '<tr><td class="idx-name">'+it.name+'</td><td class="'+(chg>=0?'up':'down')+'">'+(it.index_close||0).toFixed(2)+'</td><td class="'+cls(chg)+'">'+sign(chg)+'</td><td class="'+cls(r20)+'">'+sign(r20)+'</td><td>'+(it.ma5||0).toFixed(2)+'</td><td>'+(it.ma20||0).toFixed(2)+'</td><td>'+(it.ma60||0).toFixed(2)+'</td><td>'+trendTag(it.trend||'震荡')+'</td></tr>';
  }).join('');
}
function renderL1Charts(){
  const g=echarts.init(document.getElementById('chartGauge'));chartInstances.gauge=g;
  const bullStocks=scanData.filter(s=>s.bullish>s.bearish).length;
  const bearStocks=scanData.filter(s=>s.bearish>s.bullish).length;
  const total=scanData.length;
  const sigBias=((bullStocks-bearStocks)/total*100);
  const ix=marketTrend&&marketTrend.indices?marketTrend.indices:[];var idxAvg=0;
  if(ix.length>0){for(var i=0;i<ix.length;i++)idxAvg+=(ix[i].index_change||0);idxAvg=idxAvg/ix.length;}
  const sv=Math.round(sigBias*0.6+idxAvg*10*0.4);
  g.setOption({backgroundColor:'transparent',series:[{type:'gauge',min:-100,max:100,startAngle:205,endAngle:-25,axisLine:{lineStyle:{width:18,color:[[-0.5,tc.down],[-0.2,tc.gold],[0.2,'#888888'],[0.5,tc.gold],[1,tc.up]]}},pointer:{length:'58%',width:4,itemStyle:{color:tc.ink}},anchor:{show:true,size:10,itemStyle:{color:tc.bg4,borderColor:tc.muted,borderWidth:2}},axisTick:{distance:-16,length:4,lineStyle:{color:tc.muted,width:1}},splitLine:{distance:-16,length:14,lineStyle:{color:tc.muted,width:1}},axisLabel:{color:tc.muted,distance:-28,fontSize:9,formatter:v=>v===0?'0':(v>0?'+'+v:''+v)},detail:{valueAnimation:true,color:sv>0?tc.up:(sv<0?tc.down:tc.ink),fontSize:30,fontWeight:700,fontFamily:"'SF Mono','JetBrains Mono',monospace",offsetCenter:[0,'35%'],formatter:v=>(v>0?'+':'')+v+'%'},title:{color:tc.muted,fontSize:12,offsetCenter:[0,'62%']},data:[{value:sv,name:'多空偏向'}]}]});
  const b=echarts.init(document.getElementById('chartBreadth'));chartInstances.breadth=b;
  const acts=Object.entries(actionDist);
  b.setOption({backgroundColor:'transparent',tooltip:tipXL({trigger:'item',formatter:'{b}: {c}只 ({d}%)'}),legend:{bottom:0,textStyle:{color:tc.muted,fontSize:11},itemWidth:12,itemHeight:8,icon:'roundRect'},series:[pieSeries(acts.map(([k,v])=>({value:v,name:k,itemStyle:{color:{'强烈关注':tc.up,'重点关注':'#ff6b6b','适度关注':tc.gold,'观望等待':tc.muted,'注意风险':'#88cc88','建议规避':tc.down}[k]||tc.muted}})),{radius:['42%','68%'],center:['50%','45%'],label:{formatter:'{b}\n{c}只',color:tc.ink,fontSize:11,lineHeight:16}})]});
  const a=echarts.init(document.getElementById('chartAction'));chartInstances.action=a;
  a.setOption({backgroundColor:'transparent',tooltip:tipXL({trigger:'item',formatter:'{b}: {c}只 ({d}%)'}),series:[pieSeries(acts.map(([k,v])=>({value:v,name:k,itemStyle:{color:{'强烈关注':tc.up,'重点关注':'#ff6b6b','适度关注':tc.gold,'观望等待':tc.muted,'注意风险':'#88cc88','建议规避':tc.down}[k]||tc.muted}})),{radius:['38%','62%'],center:['50%','48%'],label:{formatter:'{b}:{c}',color:tc.ink,fontSize:10}})]});
  const ind=echarts.init(document.getElementById('chartInd'));chartInstances.ind=ind;
  const t10=Object.entries(indicatorCounts).sort((a,b)=>b[1]-a[1]).slice(0,10);
  ind.setOption({backgroundColor:'transparent',tooltip:tipXL({trigger:'axis',axisPointer:{type:'shadow',shadowStyle:{color:hexA(tc.bg4,.35)}}}),grid:{left:80,right:36,top:10,bottom:20},xAxis:{type:'value',axisLabel:{color:tc.muted},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},yAxis:{type:'category',data:t10.map(e=>e[0]).reverse(),axisLabel:{color:tc.muted,fontSize:11},axisLine:{lineStyle:{color:tc.rule}},axisTick:{show:false}},series:[hbar(tc.blue,{data:t10.map(e=>e[1]).reverse()})]});
}

// ===== L2: Sector =====
function renderSectorRanking(){
  const max=Math.max(...sectorData.map(s=>Math.abs(s.avg_sentiment)));
  document.getElementById('sectorRanking').innerHTML=sectorData.map(s=>{
    const w=Math.abs(s.avg_sentiment)/max*100,cls=s.avg_sentiment>0?'bull':'bear';
    return '<div class="sector-bar-row" onclick="showSectorDetail(\''+s.name+'\')"><div class="sector-name">'+s.name+'</div><div class="sector-bar-wrap"><div class="sector-bar-fill '+cls+'" style="width:'+Math.max(w,5)+'%;">'+(s.avg_sentiment>0?'+':'')+s.avg_sentiment+'</div></div><div class="sector-meta">'+s.total+'只 | 买'+s.buy_signals+' 卖'+s.sell_signals+' | 量化'+s.avg_quant_score+'</div></div>';
  }).join('');
}
function renderHeatmap(){
  const max=Math.max(...sectorData.map(s=>Math.abs(s.avg_sentiment)));
  document.getElementById('heatmapGrid').innerHTML=sectorData.map(s=>{
    const r=Math.abs(s.avg_sentiment)/max,o=0.3+r*0.7;
    const c=s.avg_sentiment>0?'rgba('+tc.upRgb+','+o+')':'rgba('+tc.downRgb+','+o+')';
    const t=r>0.5?'#fff':tc.ink;
    return '<div class="heatmap-cell" style="background:'+c+';color:'+t+';" onclick="showSectorDetail(\''+s.name+'\')"><div style="font-weight:700;font-size:13px;">'+s.name+'</div><div style="font-size:11px;margin-top:4px;">'+s.total+'只 | 情绪'+(s.avg_sentiment>0?'+':'')+s.avg_sentiment+'</div><div style="font-size:10px;margin-top:2px;opacity:0.8;">买'+s.buy_signals+' 卖'+s.sell_signals+'</div></div>';
  }).join('');
}
function showSectorDetail(name){
  const sd=sectorData.find(s=>s.name===name);if(!sd)return;
  // Sort stocks by "龙头": signal_count desc, then sentiment desc, then mkt_cap desc
  const stocks=sd.stocks.sort((a,b)=>b.signal_count-a.signal_count||b.sentiment-a.sentiment||b.mkt_cap_yi-a.mkt_cap_yi);
  let html='<h2 style="margin-bottom:8px;">'+name+' 板块详情</h2>';
  html+='<div class="stat-grid" style="margin-bottom:16px;">';
  html+='<div class="stat-item"><div class="stat-val gold">'+sd.total+'</div><div class="stat-lbl">入选个股</div></div>';
  html+='<div class="stat-item"><div class="stat-val '+(sd.avg_sentiment>=0?'up':'down')+'">'+(sd.avg_sentiment>=0?'+':'')+sd.avg_sentiment+'</div><div class="stat-lbl">平均情绪</div></div>';
  html+='<div class="stat-item"><div class="stat-val up">'+sd.buy_signals+'</div><div class="stat-lbl">买入信号</div></div>';
  html+='<div class="stat-item"><div class="stat-val down">'+sd.sell_signals+'</div><div class="stat-lbl">风险警示</div></div>';
  html+='<div class="stat-item"><div class="stat-val blue">'+sd.avg_quant_score+'</div><div class="stat-lbl">平均量化分</div></div>';
  html+='<div class="stat-item"><div class="stat-val gold">'+sd.avg_win_rate+'%</div><div class="stat-lbl">平均胜率</div></div>';
  html+='</div>';
  html+='<div style="font-size:13px;color:var(--muted);margin-bottom:12px;">按板块龙头排序（指标数量→情绪→市值）</div>';
  html+='<table><thead><tr><th>排名</th><th>★</th><th>代码</th><th>名称</th><th>指标数</th><th>情绪</th><th>信号</th><th>量化分</th><th>市值(亿)</th><th>操作</th></tr></thead><tbody>';
  stocks.forEach((s,i)=>{
    const st=scanData.find(x=>x.code===s.code);
    const sigCls=st&&st.trade_signal==='买入'?'buy':(st&&st.trade_signal==='卖出'?'sell':'watch');
    html+='<tr><td>'+(i+1)+'</td><td><button class="star-btn '+(isWatched(s.code)?'active':'')+'" onclick="toggleWatch(\''+s.code+'\');showSectorDetail(\''+name+'\')">'+(isWatched(s.code)?'★':'☆')+'</button></td><td>'+s.code+'</td><td style="font-weight:600;cursor:pointer;" onclick="showDetail(\''+s.code+'\')">'+s.name+'</td><td>'+s.signal_count+'</td><td class="'+(s.sentiment>=0?'pu':'pd')+'">'+(s.sentiment>=0?'+':'')+s.sentiment.toFixed(2)+'</td><td><span class="badge '+sigCls+'">'+(st?st.trade_signal:'--')+'</span></td><td>'+(st&&st.bt_has_data?st.bt_quant_score.toFixed(0):'--')+'</td><td>'+s.mkt_cap_yi.toFixed(0)+'</td><td><button class="btn" style="padding:4px 10px;font-size:12px;" onclick="showDetail(\''+s.code+'\')">详情</button></td></tr>';
  });
  html+='</tbody></table>';
  document.getElementById('sectorContent').innerHTML=html;
  openOverlay('sectorOverlay');
}
function renderL2Charts(){
  const ss=echarts.init(document.getElementById('chartSecSig'));chartInstances.secSig=ss;
  const ts=sectorData.slice(0,10);
  ss.setOption({backgroundColor:'transparent',tooltip:tipXL({trigger:'axis',axisPointer:{type:'shadow',shadowStyle:{color:hexA(tc.bg4,.35)}}}),legend:{data:['买入','卖出','观望'],textStyle:{color:tc.muted,fontSize:11},bottom:0,itemWidth:12,itemHeight:8,icon:'roundRect'},grid:{left:80,right:20,top:20,bottom:40},xAxis:{type:'value',axisLabel:{color:tc.muted},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},yAxis:{type:'category',data:ts.map(s=>s.name),axisLabel:{color:tc.muted,fontSize:11},axisLine:{lineStyle:{color:tc.rule}},axisTick:{show:false}},series:[{name:'买入',type:'bar',stack:'a',data:ts.map(s=>s.buy_signals),itemStyle:{color:tc.up,opacity:.92},emphasis:{focus:'series'}},{name:'卖出',type:'bar',stack:'a',data:ts.map(s=>s.sell_signals),itemStyle:{color:tc.down,opacity:.92},emphasis:{focus:'series'}},{name:'观望',type:'bar',stack:'a',data:ts.map(s=>s.total-s.buy_signals-s.sell_signals),itemStyle:{color:tc.bg4,borderRadius:[0,3,3,0]},emphasis:{focus:'series'}}]});
  const c=echarts.init(document.getElementById('chartConcept'));chartInstances.concept=c;
  const tc20=Object.entries(conceptDist).slice(0,20);
  c.setOption({backgroundColor:'transparent',tooltip:tipXL({trigger:'axis',axisPointer:{type:'shadow',shadowStyle:{color:hexA(tc.bg4,.35)}}}),grid:{left:100,right:36,top:10,bottom:20},xAxis:{type:'value',axisLabel:{color:tc.muted},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},yAxis:{type:'category',data:tc20.map(e=>e[0]).reverse(),axisLabel:{color:tc.muted,fontSize:10},axisLine:{lineStyle:{color:tc.rule}},axisTick:{show:false}},series:[hbar(tc.purple,{data:tc20.map(e=>e[1]).reverse()})]});
  const i=echarts.init(document.getElementById('chartIndustry'));chartInstances.industry=i;
  const ti=Object.entries(industryDist);
  i.setOption({backgroundColor:'transparent',tooltip:tipXL({trigger:'axis',axisPointer:{type:'shadow',shadowStyle:{color:hexA(tc.bg4,.35)}}}),grid:{left:100,right:36,top:10,bottom:20},xAxis:{type:'value',axisLabel:{color:tc.muted},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},yAxis:{type:'category',data:ti.map(e=>e[0]).reverse(),axisLabel:{color:tc.muted,fontSize:10},axisLine:{lineStyle:{color:tc.rule}},axisTick:{show:false}},series:[hbar(tc.gold,{data:ti.map(e=>e[1]).reverse()})]});
}

// ===== 幻方量化·板块轮动推演 + 产业链上下游 =====
function tierColor(t){return ({'领跑主线':tc.up,'接力补涨':tc.orange,'蓄势观察':tc.muted,'动能衰减':tc.blue,'退潮回避':tc.down})[t]||tc.muted;}
function renderRotationInsight(){
  const dom=document.getElementById('rotationInsight');if(!dom||!rotationInsight)return;
  const r=rotationInsight;
  dom.innerHTML='<div class="ri-block"><div class="ri-label">轮动主线（领跑板块）</div><div class="ri-val" style="color:var(--up);">'+r.lead+'</div></div>'
    +'<div class="ri-block"><div class="ri-label">接力补涨（资金流入）</div><div class="ri-val" style="color:var(--orange);">'+r.follow+'</div></div>'
    +'<div class="ri-block"><div class="ri-label">退潮回避（资金流出）</div><div class="ri-val" style="color:var(--down);">'+r.fade+'</div></div>'
    +'<div class="ri-dir">推演方向：'+r.direction+'</div>';
}
function renderRotationSummary(){
  const dom=document.getElementById('rotationSummary');if(!dom)return;
  const top6=rotationData.slice(0,6);
  let h='<div style="margin-bottom:6px;">综合轮动分 <b style="color:var(--gold);">'+rotationInsight.top_name+'</b> 居首（'+rotationInsight.top_score+'分）。</div>';
  top6.forEach(function(r,i){
    h+='<div style="display:flex;gap:8px;align-items:center;padding:3px 0;"><span style="color:var(--muted);min-width:18px;">'+(i+1)+'</span><span class="rot-chip" style="border-color:'+hexA(tierColor(r.tier),.35)+';cursor:pointer;" onclick="showSectorDetail(\''+r.name+'\')">'+r.name+'</span><span style="color:'+tierColor(r.tier)+';font-weight:700;">'+r.score+'分</span><span style="color:'+(r.momentum>=0?'var(--up)':'var(--down)')+';min-width:64px;">'+(r.momentum>=0?'+':'')+r.momentum+'%</span><span style="color:var(--muted);">'+r.tier+'</span></div>';
  });
  h+='<div style="margin-top:6px;color:var(--muted);">提示：轮动分综合动量、情绪、信号净流入、量化评分与历史胜率，用于识别主线板块与轮动节奏，非投资建议。</div>';
  dom.innerHTML=h;
}
function renderRotationLadder(){
  const dom=document.getElementById('rotationLadder');if(!dom)return;
  const tiers=['领跑主线','接力补涨','蓄势观察','动能衰减','退潮回避'];
  dom.innerHTML=tiers.map(function(t){
    const list=rotationData.filter(function(r){return r.tier===t;});
    const c=tierColor(t);
    return '<div class="rot-tier"><div class="rot-tier-head" style="color:'+c+';"><span class="rot-dot" style="background:'+c+';"></span>'+t+' <span style="color:var(--muted);font-size:11px;font-weight:400;">('+list.length+'个板块)</span></div><div class="rot-tier-body">'+list.map(function(r){return '<span class="rot-chip" style="border-color:'+hexA(c,.35)+';" onclick="showSectorDetail(\''+r.name+'\')">'+r.name+'<b style="color:'+c+';margin-left:4px;">'+r.score+'</b></span>';}).join('')+'</div></div>';
  }).join('');
}
function renderRotationQuadrant(){
  const dom=document.getElementById('chartRotation');if(!dom)return;
  const old=echarts.getInstanceByDom(dom);if(old)old.dispose();
  const ch=echarts.init(dom);chartInstances.rotation=ch;
  const data=rotationData.map(function(r){return {name:r.name,value:[r.momentum,r.sentiment,r.total],netBuy:r.net_buy,buy:r.buy,sell:r.sell,score:r.score,tier:r.tier};});
  ch.setOption({
    backgroundColor:'transparent',
    tooltip:tipXL({formatter:function(p){const d=p.data;return '<b>'+d.name+'</b><br>轮动分：<b style="color:'+tierColor(d.tier)+';">'+d.score+'</b>（'+d.tier+'）<br>动量（当日涨跌幅）：'+(d.value[0]>=0?'+':'')+d.value[0]+'%<br>情绪：'+(d.value[1]>=0?'+':'')+d.value[1]+'<br>信号净流入：'+(d.netBuy>=0?'+':'')+d.netBuy+'（买'+d.buy+' 卖'+d.sell+'）<br>覆盖个股：'+d.value[2]+'只';}}),
    grid:{left:52,right:28,top:16,bottom:44},
    xAxis:{name:'动量（当日涨跌幅）',nameTextStyle:{color:tc.muted,fontSize:10},type:'value',axisLine:{lineStyle:{color:tc.rule}},axisLabel:{color:tc.muted,fontSize:10,formatter:'{value}%'},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},
    yAxis:{name:'情绪',nameTextStyle:{color:tc.muted,fontSize:10},type:'value',axisLine:{lineStyle:{color:tc.rule}},axisLabel:{color:tc.muted,fontSize:10},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},
    series:[{
      type:'scatter',data:data,
      symbolSize:function(v){return 14+Math.min(v[2]*6,28);},
      itemStyle:{color:function(p){return tierColor(p.data.tier);},opacity:.82,shadowBlur:8,shadowColor:'rgba(0,0,0,.28)'},
      label:{show:true,position:'right',formatter:'{b}',color:tc.ink,fontSize:10},
      emphasis:{focus:'series',label:{fontWeight:700}},
      markLine:{silent:true,symbol:'none',lineStyle:{color:tc.muted,type:'dashed',opacity:.4},label:{show:false},data:[{xAxis:0},{yAxis:0}]}
    }]
  });
  ch.off('click');ch.on('click',function(p){if(p.data&&p.data.name)showSectorDetail(p.data.name);});
}
function renderChainGraph(){
  const dom=document.getElementById('chartChain');if(!dom)return;
  const old=echarts.getInstanceByDom(dom);if(old)old.dispose();
  const ch=echarts.init(dom);chartInstances.chain=ch;
  const cols={上游:6,中游:28,下游:50,金融:72,交运:90};
  const byStage={};
  rotationData.forEach(function(r){(byStage[r.stage]=byStage[r.stage]||[]).push(r);});
  Object.keys(byStage).forEach(function(k){byStage[k].sort(function(a,b){return b.score-a.score;});});
  const nodes=rotationData.map(function(r){
    const list=byStage[r.stage]||[r];
    const idx=list.indexOf(r), n=list.length;
    const y=n<=1?50:Math.round(8+idx*(84/(n-1)));
    return {name:r.name,symbolSize:16+Math.min(r.total*5,22),x:cols[r.stage]||50,y:y,itemStyle:{color:tierColor(r.tier),opacity:.92,shadowBlur:8,shadowColor:'rgba(0,0,0,.3)',borderColor:hexA(tc.bg,1),borderWidth:1},label:{show:true,position:['下游','金融','交运'].indexOf(r.stage)>=0?'left':'right',color:tc.ink,fontSize:10}};
  });
  const edges=(chainData.edges||[]).map(function(e){return {source:e.source,target:e.target};});
  ch.setOption({
    backgroundColor:'transparent',
    tooltip:tipXL({formatter:function(p){if(p.dataType==='edge')return p.data.source+' → '+p.data.target+'（产业链传导）';const r=rotationData.find(function(x){return x.name===p.data.name;});if(!r)return p.data.name;return '<b>'+p.data.name+'</b><br>环节：'+r.stage+(r.sub?' · '+r.sub:'')+'<br>轮动分：<b style="color:'+tierColor(r.tier)+';">'+r.score+'</b>（'+r.tier+'）<br>覆盖个股：'+r.total+'只<br>动量：'+(r.momentum>=0?'+':'')+r.momentum+'% ｜ 情绪：'+(r.sentiment>=0?'+':'')+r.sentiment+'<br>买'+r.buy+' / 卖'+r.sell;}}),
    series:[{
      type:'graph',layout:'none',data:nodes,links:edges,roam:true,
      lineStyle:{color:hexA(tc.muted,.35),width:1,curveness:0.08},
      edgeSymbol:['none','arrow'],edgeSymbolSize:7,
      label:{show:true,formatter:'{b}'},
      emphasis:{focus:'adjacency',lineStyle:{width:2.2,color:tc.blue},label:{fontWeight:700}},
      scaleLimit:{min:0.5,max:3}
    }]
  });
  ch.off('click');ch.on('click',function(p){if(p.dataType==='node'&&p.data&&p.data.name)showSectorDetail(p.data.name);});
}

// ===== L3: Stocks =====
function renderSignalCenter(){
  const sqFor=s=>{const sd=scanData.find(x=>x.code===s.code);return sd?sd.signal_quality||50:50;};
  const sqBadge=sq=>{const cls=sq>=65?'sig-q-high':(sq>=50?'sig-q-mid':'sig-q-low');return '<span class="sig-quality-badge '+cls+'">SQ'+sq.toFixed(0)+'</span>';};
  const graded=buySignals.map(s=>({s:s,cf:confluenceInfo(s)}));
  const onlyAB=document.getElementById('onlyAB');
  const showList=onlyAB&&onlyAB.checked?graded.filter(x=>x.cf.grade!=='C'):graded;
  showList.sort((a,b)=>{const ga=a.cf.grade==='A'?0:(a.cf.grade==='B'?1:2),gb=b.cf.grade==='A'?0:(b.cf.grade==='B'?1:2);return ga!==gb?ga-gb:b.cf.score-a.cf.score;});
  const aCount=graded.filter(x=>x.cf.grade==='A').length,bCount=graded.filter(x=>x.cf.grade==='B').length,cCount=graded.filter(x=>x.cf.grade==='C').length;
  document.getElementById('buyCount').innerHTML=showList.length+'只 <span style="font-size:11px;">(A:'+aCount+' B:'+bCount+' C:'+cCount+')</span>';
  document.getElementById('sellCount').textContent=sellSignals.length+'只';
  document.getElementById('buyList').innerHTML=showList.length?showList.map(({s:s,cf:cf})=>{
    const pot=((s.target_price-s.entry_price)/s.entry_price*100).toFixed(1);
    const risk=((s.entry_price-s.stop_loss)/s.entry_price*100).toFixed(1);
    const rr=(risk>0?(pot/risk).toFixed(2):'--');
    const w=isWatched(s.code);const sq=sqFor(s);
    const sd=scanData.find(x=>x.code===s.code);
    const ai=sd?aiAnalyze(sd):null;
    const posAdj=cycleState?Math.round(s.position_size*(cf.grade==='A'?1:(cf.grade==='B'?0.6:0.3))):s.position_size;
    const factorTags=cf.factors.map(f=>'<span class="cf-tag '+(f.on?'on':'off')+'" title="'+f.name+(f.on?' ✓'+f.pts+'分':'')+'">'+f.name+(f.on?'✓':'')+'</span>').join('');
    return '<div class="sig-card grade-'+cf.grade+'" style="'+(cf.grade==='A'?'border-left:3px solid var(--up);':'')+'"><div class="sig-card-head"><span class="sig-card-name" style="cursor:pointer;" onclick="showDetail(\''+s.code+'\')">'+s.name+' <span style="color:var(--muted);font-size:12px;">'+s.code+'</span></span><div><button class="star-btn '+(w?'active':'')+'" onclick="toggleWatch(\''+s.code+'\')">'+(w?'★':'☆')+'</button> <span class="badge buy">'+s.signal+'</span> <span class="grade-badge grade-'+cf.grade+'">'+cf.grade+'级·共振'+cf.score+'</span>'+(ai?'<span class="ai-verdict '+ai.cls+'" style="font-size:11px;padding:2px 8px;">AI:'+ai.verdict+'('+ai.score+')</span>':'')+sqBadge(sq)+'</div></div><div class="sig-card-body"><div><div class="lbl">入场价</div><div class="val pu">'+s.entry_price.toFixed(2)+'</div></div><div><div class="lbl">止损价</div><div class="val pd">'+s.stop_loss.toFixed(2)+'</div></div><div><div class="lbl">目标价</div><div class="val" style="color:var(--gold);">'+s.target_price.toFixed(2)+'</div></div><div><div class="lbl">仓位</div><div class="val">'+posAdj+'%</div></div><div><div class="lbl">胜率</div><div class="val">'+s.win_rate.toFixed(1)+'%</div></div><div><div class="lbl">量化胜率</div><div class="val" style="color:var(--gold);">'+(s.wr_filtered||0).toFixed(1)+'%</div></div><div><div class="lbl">R/R比</div><div class="val" style="color:var(--gold);font-weight:700;">'+rr+'</div></div><div><div class="lbl">周期</div><div class="val" style="font-size:11px;">'+s.holding_period+'</div></div></div><div class="confluence-factors">'+factorTags+'</div><div class="rr-bar" style="margin-top:6px;"><span style="color:var(--down);font-size:10px;">风险-'+risk+'%</span><div style="flex:1;height:6px;border-radius:3px;background:var(--bg4);position:relative;overflow:hidden;"><div style="height:100%;width:'+Math.min(parseFloat(rr)/3*100,100)+'%;border-radius:3px;background:linear-gradient(90deg,var(--down),var(--gold),var(--up));"></div></div><span style="color:var(--up);font-size:10px;">收益+'+pot+'%</span></div>'+(ai?'<div class="watch-advice" style="margin-top:8px;padding:8px;border-color:rgba(168,85,247,.25);"><b style="color:var(--purple);">AI操作建议：</b>'+ai.advice+'</div>':'')+'<div style="margin-top:8px;font-size:11px;color:var(--muted);line-height:1.6;">'+s.reason+'</div></div>';
  }).join(''):'<div style="text-align:center;padding:30px 16px;color:var(--muted);font-size:13px;line-height:1.8;">'+(cycleState?('当前情绪周期 <b style="color:'+cycleState.st.color+';">'+cycleState.st.name+'</b>，'+graded.length+'个买入信号均未达到A/B级共振标准<br>（胜率保护机制：逆周期/弱共振信号自动过滤，防止低胜率交易）'):(graded.length+'个买入信号暂未完成共振评估（情绪数据加载中）'))+'<br><span style="font-size:11px;opacity:.7;">可取消勾选"仅看A/B级共振"查看全部信号</span></div>';
  document.getElementById('sellList').innerHTML=sellSignals.map(s=>{
    const w=isWatched(s.code);const sq=sqFor(s);
    return '<div class="sig-card sell"><div class="sig-card-head"><span class="sig-card-name" style="cursor:pointer;" onclick="showDetail(\''+s.code+'\')">'+s.name+' <span style="color:var(--muted);font-size:12px;">'+s.code+'</span></span><div><button class="star-btn '+(w?'active':'')+'" onclick="toggleWatch(\''+s.code+'\')">'+(w?'★':'☆')+'</button> <span class="badge sell">'+s.signal+'</span> '+sqBadge(sq)+'</div></div><div class="sig-card-body"><div><div class="lbl">入场价</div><div class="val">'+s.entry_price.toFixed(2)+'</div></div><div><div class="lbl">止损价</div><div class="val pd">'+s.stop_loss.toFixed(2)+'</div></div><div><div class="lbl">目标价</div><div class="val">'+s.target_price.toFixed(2)+'</div></div><div><div class="lbl">仓位</div><div class="val">'+s.position_size+'%</div></div><div><div class="lbl">胜率</div><div class="val">'+s.win_rate.toFixed(1)+'%</div></div><div><div class="lbl">量化分</div><div class="val">'+(s.quant_score||0).toFixed(0)+'</div></div><div><div class="lbl">盈亏比</div><div class="val">'+s.profit_factor.toFixed(2)+'</div></div><div><div class="lbl">持仓周期</div><div class="val" style="font-size:11px;">'+s.holding_period+'</div></div></div><div style="margin-top:8px;font-size:11px;color:var(--muted);line-height:1.6;">'+s.reason+'</div></div>';
  }).join('');
  trackSignals();
}
function renderRadarCharts(){
  const top=scanData.filter(s=>s.bt_has_data).sort((a,b)=>b.bt_quant_score-a.bt_quant_score).slice(0,3);
  document.getElementById('radarGrid').innerHTML=top.map((s,i)=>'<div><h4 style="margin-bottom:8px;">'+s.name+' ('+s.code+')</h4><div id="radar'+i+'" style="width:100%;height:260px;"></div></div>').join('');
  top.forEach((s,i)=>{
    const ch=echarts.init(document.getElementById('radar'+i));chartInstances['radar'+i]=ch;
    ch.setOption({backgroundColor:'transparent',tooltip:tipXL({trigger:'item'}),radar:{indicator:[{name:'RSI',max:100},{name:'ADX',max:100},{name:'量比',max:5},{name:'布林位置',max:1},{name:'ATR波幅%',max:10},{name:'量化评分',max:100}],center:['50%','52%'],radius:'68%',axisName:{color:tc.muted,fontSize:11},splitLine:{lineStyle:{color:tc.rule,type:'dashed'}},splitArea:{areaStyle:{color:[hexA(tc.bg2,.4),hexA(tc.bg3,.4)]}},axisLine:{lineStyle:{color:tc.rule}}},series:[{type:'radar',symbolSize:5,data:[{value:[s.bt_rsi||50,s.bt_adx||0,s.bt_vol_ratio||1,s.bt_bb_pos||0.5,s.bt_atr_pct||0,s.bt_quant_score||0],name:s.name,areaStyle:{color:'rgba('+tc.upRgb+',0.22)'},lineStyle:{color:tc.up,width:2.5},itemStyle:{color:tc.up,borderColor:tc.bg2,borderWidth:1.5,shadowColor:'rgba('+tc.upRgb+',.55)',shadowBlur:8}}],animationDuration:600}]});
  });
}
function renderOpPanel(){
  const buys=buySignals.slice(0,6);
  document.getElementById('opPanel').innerHTML=buys.map(s=>{
    const pot=((s.target_price-s.entry_price)/s.entry_price*100).toFixed(1);
    const risk=((s.entry_price-s.stop_loss)/s.entry_price*100).toFixed(1);
    const rr=(pot/risk).toFixed(2);
    return '<div style="background:var(--bg3);border-radius:10px;padding:14px;border-left:3px solid var(--up);"><div style="display:flex;justify-content:space-between;margin-bottom:8px;"><span style="font-weight:700;cursor:pointer;" onclick="showDetail(\''+s.code+'\')">'+s.name+' '+s.code+'</span><span class="badge buy">'+s.holding_period+'</span></div><div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;font-size:12px;"><div><span style="color:var(--muted);">入场</span><br><b style="color:var(--up);">'+s.entry_price.toFixed(2)+'</b></div><div><span style="color:var(--muted);">止损</span><br><b style="color:var(--down);">'+s.stop_loss.toFixed(2)+'</b></div><div><span style="color:var(--muted);">目标</span><br><b style="color:var(--gold);">'+s.target_price.toFixed(2)+'</b></div><div><span style="color:var(--muted);">仓位</span><br><b>'+s.position_size+'%</b></div><div><span style="color:var(--muted);">风险</span><br><b style="color:var(--down);">-'+risk+'%</b></div><div><span style="color:var(--muted);">收益</span><br><b style="color:var(--up);">+'+pot+'%</b></div></div><div style="margin-top:6px;font-size:11px;color:var(--muted);">盈亏比 '+rr+' | 胜率 '+s.win_rate.toFixed(0)+'% | 量化 '+(s.quant_score||0).toFixed(0)+'分</div></div>';
  }).join('');
}
function renderSignalMatrix(){
  const topInd=Object.entries(indicatorCounts).sort((a,b)=>b[1]-a[1]).slice(0,12).map(e=>e[0]);
  const topStocks=scanData.slice().sort((a,b)=>(b.signal_count||0)-(a.signal_count||0)).slice(0,15);
  let html='<thead><tr><th>股票</th>';topInd.forEach(ind=>{html+='<th style="writing-mode:vertical-lr;font-size:10px;">'+ind+'</th>';});html+='<th>合计</th></tr></thead><tbody>';
  topStocks.forEach(s=>{
    html+='<tr><td style="text-align:left;font-weight:600;cursor:pointer;" onclick="showDetail(\''+s.code+'\')">'+s.name+'</td>';let count=0;const indSet=new Set();const indType={};
    (s.signals||[]).forEach(sig=>{indSet.add(sig.indicator);indType[sig.indicator]=sig.type;});
    topInd.forEach(ind=>{const has=indSet.has(ind),st=indType[ind];let cls='none',title='';if(has){title=ind+'-'+st;cls=st==='看涨'?'up':(st==='看跌'?'down':'neutral');count++;}html+='<td title="'+title+'"><span class="matrix-dot '+cls+'"></span></td>';});
    html+='<td style="font-weight:700;color:var(--gold);">'+count+'</td></tr>';
  });
  html+='</tbody>';document.getElementById('sigMatrix').innerHTML=html;
}

// ===== L0: Trading Signal Board =====
function renderRegimeBadge(){
  const rm=riskMetrics||{};const regime=rm.market_regime||'震荡';
  const badge=document.getElementById('regimeBadge');
  if(!badge)return;
  const cls=regime==='多头'?'regime-bull':(regime==='空头'?'regime-bear':'regime-range');
  badge.className='regime-badge '+cls;
  badge.textContent='市场'+regime;
  const now=new Date();
  document.getElementById('l0Update').textContent=now.toLocaleTimeString('zh-CN');
}
function renderRiskDashboard(){
  // 数据校验
  const v=riskMetrics.validation||{};
  if(Object.keys(v).length>0){
    const issues=[];
    if(!v.position_valid)issues.push('仓位数据异常');
    if(!v.entry_stop_valid)issues.push('止损价异常');
    if(!v.entry_target_valid)issues.push('目标价异常');
    if(v.sector_concentration_pct>40)issues.push('行业集中度'+v.sector_concentration_pct+'%偏高');
    const validBox=document.createElement('div');
    validBox.style.cssText='font-size:11px;color:'+(issues.length?'var(--down)':'var(--down)')+';margin-top:8px;padding:6px 10px;border-radius:6px;background:rgba(255,255,255,.03);';
    validBox.innerHTML='<b>数据校验:</b> '+(issues.length?'⚠ '+issues.join(' · '):'✓ 全部通过')+
      ' | 买入占比'+v.buy_signal_pct+'% | 最大仓位'+v.max_single_position+'% | 行业集中度'+v.sector_concentration_pct+'%';
    const rp=document.getElementById('riskPanel');
    if(rp&&rp.parentNode){rp.parentNode.insertBefore(validBox,rp.nextSibling);}
  }
  const rm=riskMetrics||{};
  // Risk panel: gauge showing total exposure
  const rp=echarts.init(document.getElementById('riskPanel'));chartInstances.riskPanel=rp;
  const expo=rm.total_exposure||0;const maxExpo=300;
  rp.setOption({backgroundColor:'transparent',series:[{type:'gauge',min:0,max:maxExpo,startAngle:205,endAngle:-25,axisLine:{lineStyle:{width:16,color:[[0.3,tc.down],[0.6,tc.gold],[1,tc.up]]}},pointer:{length:'58%',width:4,itemStyle:{color:tc.ink}},anchor:{show:true,size:8,itemStyle:{color:tc.bg4,borderColor:tc.muted,borderWidth:2}},axisTick:{distance:-13,length:3,lineStyle:{color:tc.muted,width:1}},splitLine:{distance:-13,length:10,lineStyle:{color:tc.muted,width:1}},axisLabel:{color:tc.muted,distance:-24,fontSize:9},detail:{valueAnimation:true,color:expo>60?tc.up:(expo>30?tc.gold:tc.down),fontSize:24,fontWeight:700,fontFamily:"'SF Mono','JetBrains Mono',monospace",offsetCenter:[0,'38%'],formatter:'{value}%'},title:{color:tc.muted,fontSize:11,offsetCenter:[0,'62%']},data:[{value:expo,name:'总仓位敞口'}]}]});
  // Signal quality distribution
  const sq=echarts.init(document.getElementById('chartSigQual'));chartInstances.sigQual=sq;
  const buckets={'高(≥65)':0,'中(50-65)':0,'低(<50)':0};
  scanData.forEach(s=>{const v=s.signal_quality||50;if(v>=65)buckets['高(≥65)']++;else if(v>=50)buckets['中(50-65)']++;else buckets['低(<50)']++;});
  sq.setOption({backgroundColor:'transparent',tooltip:tipXL({trigger:'item',formatter:'{b}: {c}只 ({d}%)'}),legend:{bottom:0,textStyle:{color:tc.muted,fontSize:10},itemWidth:12,itemHeight:8,icon:'roundRect'},series:[pieSeries(Object.entries(buckets).map(([k,v])=>({value:v,name:k,itemStyle:{color:{'高(≥65)':tc.up,'中(50-65)':tc.gold,'低(<50)':tc.muted}[k]||tc.muted}})),{radius:['38%','62%'],center:['50%','45%'],label:{formatter:'{b}\n{c}只',color:tc.ink,fontSize:10,lineHeight:14}})]});
  // Sector concentration
  const sc=echarts.init(document.getElementById('chartSectorConc'));chartInstances.sectorConc=sc;
  const conc=Object.entries(rm.sector_concentration||{});
  sc.setOption({backgroundColor:'transparent',tooltip:tipXL({trigger:'axis',axisPointer:{type:'shadow',shadowStyle:{color:hexA(tc.bg4,.35)}}}),grid:{left:80,right:36,top:10,bottom:20},xAxis:{type:'value',axisLabel:{color:tc.muted,fontSize:9},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},yAxis:{type:'category',data:conc.map(e=>e[0]),axisLabel:{color:tc.muted,fontSize:10},axisLine:{lineStyle:{color:tc.rule}},axisTick:{show:false}},series:[hbar(tc.purple,{data:conc.map(e=>e[1])})]});
}

// ===== L4: Quantitative Learning Center =====
function renderLearningCenter(){
  // Learning insights
  const insights=learningInsights||[];
  document.getElementById('learningInsights').innerHTML=insights.map((t,i)=>'<div class="insight-card"><div class="ic-label">洞察 #'+(i+1)+'</div>'+t+'</div>').join('');
  // Indicator performance table
  const perf=indicatorPerf||[];
  const qTag=q=>{const c={'高':'trend-up','中':'trend-flat','低':'trend-down'}[q]||'trend-flat';return '<span class="trend-tag '+c+'">'+q+'</span>';};
  document.getElementById('indPerfBody').innerHTML=perf.map(p=>{
    return '<tr><td class="idx-name">'+p.name+'</td><td style="color:'+(p.type==='看涨'?'var(--up)':(p.type==='看跌'?'var(--down)':'var(--muted)'))+';">'+p.type+'</td><td>'+p.triggers+'</td><td class="'+(p.win_rate>=50?'up':'down')+'">'+p.win_rate+'%</td><td class="'+(p.wr_filtered>=50?'up':'down')+'">'+p.wr_filtered+'%</td><td>'+(p.profit_factor>=1.3?'<b style="color:var(--gold);">':'')+(p.profit_factor||0).toFixed(2)+(p.profit_factor>=1.3?'</b>':'')+'</td><td class="'+(p.expectancy>=0?'up':'down')+'">'+(p.expectancy>=0?'+':'')+(p.expectancy||0).toFixed(2)+'</td><td>'+qTag(p.quality)+'</td></tr>';
  }).join('');
  // Scatter: win rate vs profit factor
  const ip=echarts.init(document.getElementById('chartIndPerf'));chartInstances.indPerf=ip;
  ip.setOption({backgroundColor:'transparent',tooltip:tipXL({formatter:p=>p.data[2]+'<br/>胜率:'+p.data[0]+'% | 盈亏比:'+p.data[1]+'<br/>触发:'+p.data[3]+'次'}),grid:{left:50,right:30,top:30,bottom:40},xAxis:{type:'value',name:'胜率(%)',nameTextStyle:{color:tc.muted,fontSize:10},axisLabel:{color:tc.muted},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},yAxis:{type:'value',name:'盈亏比',nameTextStyle:{color:tc.muted,fontSize:10},axisLabel:{color:tc.muted},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},series:[{type:'scatter',symbolSize:p=>Math.sqrt(p[3])*3,data:perf.map(p=>{var c=p.win_rate>=55?tc.up:(p.win_rate>=48?tc.gold:tc.down);return{value:[p.win_rate,p.profit_factor,p.name,p.triggers],itemStyle:{color:c,opacity:.85,borderColor:hexA(c,.4),borderWidth:1,shadowColor:hexA(c,.5),shadowBlur:8}};}),label:{show:true,position:'right',formatter:p=>p.data[2],color:tc.muted,fontSize:9}}]});
}

function renderL3Charts(){
  const sc=echarts.init(document.getElementById('chartScatter'));chartInstances.scatter=sc;
  sc.setOption({backgroundColor:'transparent',tooltip:tipXL({formatter:p=>p.data[2]+'<br/>指标:'+p.data[0]+' 情绪:'+p.data[1]}),grid:{left:50,right:20,top:20,bottom:40},xAxis:{type:'value',name:'指标数量',nameTextStyle:{color:tc.muted},axisLabel:{color:tc.muted},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},yAxis:{type:'value',name:'情绪评分',nameTextStyle:{color:tc.muted},axisLabel:{color:tc.muted},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},series:[{type:'scatter',symbolSize:p=>p[0]*3,data:scanData.map(s=>{var c=s.sentiment_score>=0?tc.up:tc.down;return{value:[s.signal_count,s.sentiment_score,s.name],itemStyle:{color:c,opacity:.8,shadowColor:hexA(c,.5),shadowBlur:7}};})}]});
  const qs=echarts.init(document.getElementById('chartQScatter'));chartInstances.qs=qs;
  const bd=scanData.filter(s=>s.bt_has_data);
  qs.setOption({backgroundColor:'transparent',tooltip:tipXL({formatter:p=>p.data[2]+'<br/>量化:'+p.data[0]+' 胜率:'+p.data[1]+'% 盈亏比:'+p.data[3]}),grid:{left:50,right:20,top:20,bottom:40},xAxis:{type:'value',name:'量化评分',nameTextStyle:{color:tc.muted},axisLabel:{color:tc.muted},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},yAxis:{type:'value',name:'回测胜率(%)',nameTextStyle:{color:tc.muted},axisLabel:{color:tc.muted},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},series:[{type:'scatter',symbolSize:p=>Math.max(p[3]*8,8),data:bd.map(s=>({value:[s.bt_quant_score,s.bt_win_rate,s.name,s.bt_pf],itemStyle:{color:tc.blue,opacity:.8,shadowColor:hexA(tc.blue,.5),shadowBlur:7}}))}]});
}

// ===== Table =====
let curSort={key:'signal_count',desc:true},curFilter='';
function renderTable(){
  let f=scanData;if(curFilter){f=scanData.filter(s=>{const pinyin=pinyinMap[s.code]||'';return fuzzyMatch(s.code,curFilter)||fuzzyMatch(s.name,curFilter)||fuzzyMatch(pinyin,curFilter)||fuzzyMatch(s.industry,curFilter);});}
  f.sort((a,b)=>{let av=a[curSort.key],bv=b[curSort.key];if(typeof av==='number'&&typeof bv==='number')return curSort.desc?bv-av:av-bv;return curSort.desc?String(bv).localeCompare(String(av)):String(av).localeCompare(String(bv));});
  document.getElementById('filterInfo').textContent='显示 '+f.length+' / '+scanData.length+' 只';
  document.getElementById('stockTableBody').innerHTML=f.map((s,i)=>{
    const pc=s.pct_change>=0?'pu':'pd',sigCls=s.trade_signal==='买入'?'buy':(s.trade_signal==='卖出'?'sell':'watch'),w=isWatched(s.code);
    return '<tr><td><button class="star-btn '+(w?'active':'')+'" onclick="toggleWatch(\''+s.code+'\')">'+(w?'★':'☆')+'</button></td><td>'+(i+1)+'</td><td>'+s.code+'</td><td style="font-weight:600;cursor:pointer;" onclick="showDetail(\''+s.code+'\')">'+s.name+'</td><td>'+s.signal_count+'</td><td style="font-size:11px;color:var(--muted);">'+s.industry+'</td><td><span class="badge '+sigCls+'">'+s.trade_signal+'</span></td><td>'+(s.bt_has_data?s.bt_win_rate.toFixed(1)+'%':'--')+'</td><td>'+(s.bt_has_data?s.bt_wr_filtered.toFixed(1)+'%':'--')+'</td><td>'+(s.bt_has_data?s.bt_quant_score.toFixed(0):'--')+'</td><td>'+(s.bt_has_data?s.bt_pf.toFixed(2):'--')+'</td><td>'+(s.position_size||'--')+(s.position_size?'%':'')+'</td><td>'+s.confidence+'</td><td>'+('★'.repeat(s.priority))+('☆'.repeat(5-s.priority))+'</td><td>'+s.risk+'</td><td>'+(s.sentiment_score>=0?'+':'')+s.sentiment_score.toFixed(2)+'</td><td style="font-weight:700;color:'+(s.signal_quality>=65?'var(--up)':(s.signal_quality>=50?'var(--gold)':'var(--muted)'))+';">'+(s.signal_quality||50).toFixed(0)+'</td><td>'+s.mkt_cap_yi.toFixed(0)+'</td><td class="'+pc+'">'+s.latest_price.toFixed(2)+'</td><td class="'+pc+'">'+(s.pct_change>=0?'+':'')+s.pct_change.toFixed(2)+'%</td></tr>';
  }).join('');
}
document.getElementById('searchInput').addEventListener('input',e=>{curFilter=e.target.value;renderTable();});
document.getElementById('sortBy').addEventListener('change',e=>{curSort.key=e.target.value;curSort.desc=true;renderTable();});
document.querySelectorAll('.sortable').forEach(th=>th.addEventListener('click',()=>{const k=th.dataset.key;if(curSort.key===k)curSort.desc=!curSort.desc;else{curSort.key=k;curSort.desc=true;}renderTable();}));

// ===== Stock Detail (Enhanced) =====
function showDetail(code){
  const s=scanData.find(x=>x.code===code);if(!s)return;
  const pc=s.pct_change>=0?'pu':'pd',ps=s.pct_change>=0?'+':'';
  const kl=klineData[code]||{};const kline=kl.kline||[];
  const w=isWatched(s.code);
  // Signal timeline
  const tl=s.signals.map(sig=>{const cls=sig.type==='看涨'?'up':(sig.type==='看跌'?'down':'gold');return '<div class="sig-tl-item"><div class="sig-tl-dot" style="background:var(--'+cls+');"></div><span style="color:var(--muted);">'+sig.date+'</span><span class="badge '+(sig.type==='看涨'?'buy':(sig.type==='看跌'?'sell':'watch'))+'">'+sig.type+'</span><span style="font-weight:600;">'+sig.indicator+'</span><span style="color:var(--muted);font-size:11px;">'+sig.section+'</span></div>';}).join('');
  // Backtest table
  let btHtml='';
  if(s.bt_has_data){
    btHtml='<div class="dt-section"><h4><span style="width:3px;height:14px;background:var(--gold);border-radius:2px;display:inline-block;"></span>回测数据</h4>';
    btHtml+='<div class="dt-grid">';
    btHtml+='<div class="dt-cell"><div class="v '+(s.bt_win_rate>=50?'up':'down')+'">'+s.bt_win_rate.toFixed(1)+'%</div><div class="l">次日胜率</div></div>';
    btHtml+='<div class="dt-cell"><div class="v '+(s.bt_wr_3d>=50?'up':'down')+'">'+s.bt_wr_3d.toFixed(1)+'%</div><div class="l">3日胜率</div></div>';
    btHtml+='<div class="dt-cell"><div class="v '+(s.bt_wr_5d>=50?'up':'down')+'">'+s.bt_wr_5d.toFixed(1)+'%</div><div class="l">5日胜率</div></div>';
    btHtml+='<div class="dt-cell"><div class="v '+(s.bt_wr_filtered>=50?'up':'down')+'">'+s.bt_wr_filtered.toFixed(1)+'%</div><div class="l">量化过滤胜率</div></div>';
    btHtml+='<div class="dt-cell"><div class="v">'+s.bt_pf.toFixed(2)+'</div><div class="l">盈亏比</div></div>';
    btHtml+='<div class="dt-cell"><div class="v">'+s.bt_exp.toFixed(2)+'%</div><div class="l">期望收益</div></div>';
    btHtml+='<div class="dt-cell"><div class="v gold">'+s.bt_quant_score.toFixed(0)+'</div><div class="l">量化评分</div></div>';
    btHtml+='<div class="dt-cell"><div class="v">'+s.bt_rsi.toFixed(1)+'</div><div class="l">RSI(14)</div></div>';
    btHtml+='<div class="dt-cell"><div class="v">'+s.bt_adx.toFixed(1)+'</div><div class="l">ADX(14)</div></div>';
    btHtml+='<div class="dt-cell"><div class="v">'+s.bt_vol_ratio.toFixed(2)+'</div><div class="l">量比</div></div>';
    btHtml+='<div class="dt-cell"><div class="v">'+(s.bt_bb_pos*100).toFixed(0)+'%</div><div class="l">布林位置</div></div>';
    btHtml+='<div class="dt-cell"><div class="v">'+s.bt_atr_pct.toFixed(2)+'%</div><div class="l">ATR波幅</div></div>';
    btHtml+='</div></div>';
  }
  // Trading signal box
  let tradeHtml='';
  if(s.entry_price||s.stop_loss||s.target_price){
    const pot=s.target_price?((s.target_price-s.entry_price)/s.entry_price*100).toFixed(1):'--';
    const risk=s.stop_loss?((s.entry_price-s.stop_loss)/s.entry_price*100).toFixed(1):'--';
    tradeHtml='<div class="dt-section" style="border-left:3px solid var(--up);"><h4><span style="width:3px;height:14px;background:var(--up);border-radius:2px;display:inline-block;"></span>交易信号</h4>';
    tradeHtml+='<div style="display:flex;gap:8px;margin-bottom:8px;"><span class="badge '+(s.trade_signal==='买入'?'buy':(s.trade_signal==='卖出'?'sell':'watch'))+'">'+s.trade_signal+'</span><span style="color:var(--muted);font-size:12px;">持仓周期: '+s.holding_period+'</span></div>';
    tradeHtml+='<div class="dt-grid">';
    if(s.entry_price)tradeHtml+='<div class="dt-cell"><div class="v pu">'+s.entry_price.toFixed(2)+'</div><div class="l">入场价</div></div>';
    if(s.stop_loss)tradeHtml+='<div class="dt-cell"><div class="v pd">'+s.stop_loss.toFixed(2)+'</div><div class="l">止损价</div></div>';
    if(s.target_price)tradeHtml+='<div class="dt-cell"><div class="v" style="color:var(--gold);">'+s.target_price.toFixed(2)+'</div><div class="l">目标价</div></div>';
    if(s.position_size)tradeHtml+='<div class="dt-cell"><div class="v">'+s.position_size+'%</div><div class="l">建议仓位</div></div>';
    if(s.entry_price&&s.target_price)tradeHtml+='<div class="dt-cell"><div class="v pu">+'+pot+'%</div><div class="l">潜在收益</div></div>';
    if(s.entry_price&&s.stop_loss)tradeHtml+='<div class="dt-cell"><div class="v pd">-'+risk+'%</div><div class="l">风险幅度</div></div>';
    tradeHtml+='</div>';
    if(s.trade_reason)tradeHtml+='<div style="margin-top:8px;padding:10px;background:var(--bg2);border-radius:8px;font-size:12px;color:var(--muted);line-height:1.6;">'+s.trade_reason+'</div>';
    tradeHtml+='</div>';
  }
  // Pattern analysis
  let patHtml='';
  if(s.pattern_descs&&s.pattern_descs.length>0){
    patHtml='<div class="dt-section"><h4><span style="width:3px;height:14px;background:var(--purple);border-radius:2px;display:inline-block;"></span>信号组合分析</h4>';
    patHtml+=s.pattern_descs.map(p=>'<div style="padding:4px 0;font-size:12px;color:var(--muted);">'+p+'</div>').join('');
    patHtml+='</div>';
  }
  // Concepts
  let conHtml='';
  if(s.concepts&&s.concepts.length>0){
    conHtml='<div class="dt-section"><h4><span style="width:3px;height:14px;background:var(--blue);border-radius:2px;display:inline-block;"></span>所属概念板块</h4><div>'+s.concepts.map(c=>'<span class="concept-tag">'+c+'</span>').join('')+'</div></div>';
  }
  // K-line chart
  let kHtml='';
  if(kline.length>0)kHtml='<div class="dt-section"><h4><span style="width:3px;height:14px;background:var(--gold);border-radius:2px;display:inline-block;"></span>K线图（MA/BOLL + 成交量/MACD/KDJ）</h4><div id="klineChart" style="width:100%;height:460px;"></div></div>';
  // Build detail HTML
  document.getElementById('detailContent').innerHTML='<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center;"><div><h2 style="display:flex;align-items:center;gap:10px;">'+s.name+' <span style="color:var(--muted);font-size:16px;font-weight:400;">'+s.code+'</span> <span class="badge '+(s.cls_key==='strong_bull'||s.cls_key==='bull'?'buy':(s.cls_key==='strong_bear'||s.cls_key==='bear'?'sell':'watch'))+'">'+s.classification+'</span></h2><div style="display:flex;gap:20px;margin-top:8px;font-size:14px;"><span>最新价: <b class="'+pc+'" style="font-size:18px;">'+s.latest_price.toFixed(2)+'</b></span><span>涨跌幅: <b class="'+pc+'">'+ps+s.pct_change.toFixed(2)+'%</b></span><span>市值: <b>'+s.mkt_cap_yi.toFixed(0)+'亿</b></span><span>行业: <b>'+s.industry+'</b></span></div></div><div style="display:flex;flex-direction:column;align-items:flex-end;gap:8px;">'+(w?'<button style="background:var(--gold);color:#000;font-weight:700;padding:8px 20px;font-size:13px;border:none;border-radius:8px;cursor:pointer;" onclick="toggleWatch(\''+s.code+'\');showDetail(\''+s.code+'\')">★ 已关注</button>':'<button style="background:var(--bg3);color:var(--ink);font-weight:600;padding:8px 20px;font-size:13px;border:1px solid var(--rule);border-radius:8px;cursor:pointer;" onclick="toggleWatch(\''+s.code+'\');showDetail(\''+s.code+'\')">☆ 添加关注</button>')+'</div></div>'+tradeHtml+execPlanHtml(s)+'<div id="t0DetailBox"></div>'+aiAnalyzeHtml(s)+btHtml+patHtml+conHtml+'<div class="dt-section"><h4><span style="width:3px;height:14px;background:var(--up);border-radius:2px;display:inline-block;"></span>信号触发时间轴（'+s.signals.length+'个信号）</h4><div class="sig-timeline">'+tl+'</div></div>'+kHtml;
  openOverlay('detailOverlay');
  if(kline.length>0)setTimeout(()=>renderKlineChart(kline),100);
  loadT0ForDetail(s.code);
}
// ===== 副图指标配置系统 =====
function loadKlineCfg(){try{const s=localStorage.getItem('klineCfg');if(s){const c=JSON.parse(s);if(c&&Array.isArray(c.subs)&&c.subs.length>0)return c;}}catch(e){}return{count:3,subs:['VOL','MACD','KDJ'],boll:true};}
function saveKlineCfg(){try{localStorage.setItem('klineCfg',JSON.stringify(window._klineCfg));}catch(e){}}
window._klineCfg=loadKlineCfg();
const KF_ORDER=['VOL','MACD','KDJ','RSI','WR','OBV'];
const KF_LABEL={VOL:'成交量',MACD:'MACD',KDJ:'KDJ',RSI:'RSI',WR:'WR',OBV:'OBV'};
const SUB_IND={
  VOL:function(kline){return{title:'成交量',yAxis:{scale:true,position:'right',axisLabel:{color:tc.muted,fontSize:9,fontFamily:"'SF Mono',monospace",formatter:v=>v>=1e8?(v/1e8).toFixed(1)+'亿':(v>=1e4?(v/1e4).toFixed(0)+'万':v)}},series:[
    {name:'成交量',type:'bar',data:kline.map(k=>({value:k.volume,itemStyle:{color:k.close>=k.open?vgrad(tc.up,.85,.15):vgrad(tc.down,.85,.15),borderRadius:[2,2,0,0]}}))},
    {name:'MAVOL5',type:'line',data:calcVolMASeries(kline.map(k=>k.volume),5),showSymbol:false,lineStyle:{width:1,color:tc.gold}},
    {name:'MAVOL10',type:'line',data:calcVolMASeries(kline.map(k=>k.volume),10),showSymbol:false,lineStyle:{width:1,color:tc.blue}}
  ]};},
  MACD:function(kline){const m=calcMACDSeries(kline.map(k=>k.close));return{title:'MACD(12,26,9)',yAxis:{scale:true,position:'right',axisLabel:{color:tc.muted,fontSize:9,formatter:v=>+v.toFixed(2)}},series:[
    {name:'MACD',type:'bar',data:m.hist.map(v=>({value:v,itemStyle:{color:v>=0?tc.up:tc.down}}))},
    {name:'DIF',type:'line',data:m.dif,showSymbol:false,lineStyle:{width:1,color:tc.gold}},
    {name:'DEA',type:'line',data:m.dea,showSymbol:false,lineStyle:{width:1,color:tc.blue}}
  ]};},
  KDJ:function(kline){const k=calcKDJSeries(kline,9);return{title:'KDJ(9,3,3)',yAxis:{scale:true,position:'right',axisLabel:{color:tc.muted,fontSize:9}},series:[
    {name:'K',type:'line',data:k.K,showSymbol:false,lineStyle:{width:1,color:tc.gold}},
    {name:'D',type:'line',data:k.D,showSymbol:false,lineStyle:{width:1,color:tc.blue}},
    {name:'J',type:'line',data:k.J,showSymbol:false,lineStyle:{width:1,color:tc.purple}}
  ]};},
  RSI:function(kline){const c=kline.map(k=>k.close);return{title:'RSI(6/12/24)',yAxis:{min:0,max:100,position:'right',axisLabel:{color:tc.muted,fontSize:9}},series:[
    {name:'RSI6',type:'line',data:calcRSISeries(c,6),showSymbol:false,lineStyle:{width:1,color:tc.gold}},
    {name:'RSI12',type:'line',data:calcRSISeries(c,12),showSymbol:false,lineStyle:{width:1,color:tc.blue}},
    {name:'RSI24',type:'line',data:calcRSISeries(c,24),showSymbol:false,lineStyle:{width:1,color:tc.purple}}
  ]};},
  WR:function(kline){return{title:'WR(14)',yAxis:{min:0,max:100,position:'right',axisLabel:{color:tc.muted,fontSize:9}},series:[
    {name:'WR14',type:'line',data:calcWRSeries(kline,14),showSymbol:false,lineStyle:{width:1,color:tc.gold},markLine:{silent:true,symbol:'none',label:{show:false},data:[{yAxis:80,lineStyle:{color:hexA(tc.down,.4),type:'dashed'}},{yAxis:20,lineStyle:{color:hexA(tc.up,.4),type:'dashed'}}]}}
  ]};},
  OBV:function(kline){return{title:'OBV',yAxis:{scale:true,position:'right',axisLabel:{color:tc.muted,fontSize:9,fontFamily:"'SF Mono',monospace",formatter:v=>v>=1e8?(v/1e8).toFixed(1)+'亿':(v>=1e4?(v/1e4).toFixed(0)+'万':v)}},series:[
    {name:'OBV',type:'line',data:calcOBVSeries(kline),showSymbol:false,lineStyle:{width:1,color:tc.gold},areaStyle:{color:hexA(tc.gold,.08)}}
  ]};}
};
function buildKlineOption(kline,cfg){
  cfg=cfg||window._klineCfg;
  const dates=kline.map(k=>k.date);
  const count=Math.max(1,Math.min(4,cfg.count||cfg.subs.length));
  const subs=(cfg.subs||['VOL','MACD','KDJ']).slice(0,count);
  while(subs.length<count)subs.push('VOL');
  const series=[
    {name:'K线',type:'candlestick',data:kline.map(k=>[k.open,k.close,k.low,k.high]),xAxisIndex:0,yAxisIndex:0,barMaxWidth:16,itemStyle:{color:tc.up,color0:tc.down,borderColor:tc.up,borderColor0:tc.down,borderWidth:1}},
    {name:'MA5',type:'line',data:kline.map(k=>k.ma5||null),xAxisIndex:0,yAxisIndex:0,smooth:true,showSymbol:false,lineStyle:{width:1.2,color:tc.gold},emphasis:{focus:'series',lineStyle:{width:2.5}}},
    {name:'MA10',type:'line',data:kline.map(k=>k.ma10||null),xAxisIndex:0,yAxisIndex:0,smooth:true,showSymbol:false,lineStyle:{width:1.2,color:tc.blue},emphasis:{focus:'series',lineStyle:{width:2.5}}},
    {name:'MA20',type:'line',data:kline.map(k=>k.ma20||null),xAxisIndex:0,yAxisIndex:0,smooth:true,showSymbol:false,lineStyle:{width:1.2,color:tc.purple},emphasis:{focus:'series',lineStyle:{width:2.5}}}
  ];
  const legendData=['K线','MA5','MA10','MA20'];
  if(cfg.boll){const b=calcBollSeries(kline.map(k=>k.close),20);legendData.push('BOLL');series.push(
    {name:'BOLL',type:'line',data:b.mid,xAxisIndex:0,yAxisIndex:0,smooth:true,showSymbol:false,lineStyle:{width:1,color:tc.blue,opacity:.5},emphasis:{focus:'series'}},
    {name:'BOLL',type:'line',data:b.up,xAxisIndex:0,yAxisIndex:0,smooth:true,showSymbol:false,lineStyle:{width:1,color:tc.muted,type:'dashed',opacity:.6},emphasis:{focus:'series'}},
    {name:'BOLL',type:'line',data:b.low,xAxisIndex:0,yAxisIndex:0,smooth:true,showSymbol:false,lineStyle:{width:1,color:tc.muted,type:'dashed',opacity:.6},emphasis:{focus:'series'}}
  );}
  const grids=[{left:'8%',right:'14%',top:'12%',height:'27%'}];
  const subStart=42,subEnd=90,gap=1.5;
  const each=(subEnd-subStart-gap*(count-1))/count;
  for(let i=0;i<count;i++)grids.push({left:'8%',right:'14%',top:(subStart+i*(each+gap)).toFixed(1)+'%',height:each.toFixed(1)+'%'});
  const xAxis=[{type:'category',data:dates,gridIndex:0,axisLabel:{show:false},axisLine:{lineStyle:{color:tc.rule}},axisTick:{show:false}}];
  const yAxis=[{scale:true,gridIndex:0,position:'left',axisLabel:{color:tc.muted,fontSize:9,fontFamily:"'SF Mono',monospace"},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}},axisLine:{show:false}}];
  const titles=[];const zoomIdx=[0];
  for(let i=0;i<count;i++){
    const gi=i+1;
    xAxis.push({type:'category',data:dates,gridIndex:gi,axisLabel:(i===count-1)?{color:tc.muted,fontSize:9}:{show:false},axisLine:{lineStyle:{color:tc.rule}},axisTick:{show:false}});
    const ind=(SUB_IND[subs[i]]||SUB_IND.VOL)(kline);
    yAxis.push(Object.assign({gridIndex:gi,axisLine:{show:false},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},ind.yAxis));
    titles.push({text:ind.title,left:'1%',top:(subStart-5+i*(each+gap)).toFixed(1)+'%',textStyle:{color:tc.muted,fontSize:10,fontWeight:600}});
    ind.series.forEach(function(s){s.xAxisIndex=gi;s.yAxisIndex=gi;series.push(s);});
    zoomIdx.push(gi);
  }
  return{
    backgroundColor:'transparent',animation:false,
    legend:{data:legendData,textStyle:{color:tc.muted,fontSize:10},top:0,left:'center',itemWidth:12,itemHeight:7,icon:'roundRect',itemGap:12},
    title:titles,
    tooltip:{trigger:'axis',axisPointer:{type:'cross',link:[{xAxisIndex:'all'}],lineStyle:{color:tc.muted,type:'dashed',width:1},crossStyle:{color:tc.muted},label:{backgroundColor:tc.bg4,color:tc.ink,fontSize:10,borderColor:tc.rule,borderWidth:1,padding:[3,6]}},backgroundColor:hexA(tc.bg2,.96),borderColor:tc.rule,borderWidth:1,padding:[10,14],textStyle:{color:tc.ink,fontSize:12},extraCssText:'border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.35);backdrop-filter:blur(6px);line-height:1.7;'},
    grid:grids,xAxis:xAxis,yAxis:yAxis,
    dataZoom:[
      {type:'inside',xAxisIndex:zoomIdx,start:35,end:100},
      {type:'slider',xAxisIndex:zoomIdx,start:35,end:100,height:14,bottom:2,borderColor:tc.rule,backgroundColor:hexA(tc.bg3,.5),fillerColor:hexA(tc.blue,.18),handleStyle:{color:tc.bg4,borderColor:tc.muted},moveHandleStyle:{color:tc.bg4,opacity:.65},emphasis:{handleStyle:{borderColor:tc.blue},moveHandleStyle:{color:tc.blue}},dataBackground:{lineStyle:{color:tc.bg4},areaStyle:{color:hexA(tc.bg4,.3)}},selectedDataBackground:{lineStyle:{color:tc.blue},areaStyle:{color:hexA(tc.blue,.25)}},textStyle:{color:tc.muted}}
    ],
    series:series
  };
}
function renderKlineChart(kline,cid){
  const dom=document.getElementById(cid||'klineChart');if(!dom)return;
  const old=(echarts.getInstanceByDom)?echarts.getInstanceByDom(dom):null;
  const ch=old||echarts.init(dom);
  if(!window._kc)window._kc=[];window._kc.push(ch);
  dom._klineCtx={ch:ch,kline:kline};
  ch.setOption(buildKlineOption(kline,window._klineCfg),true);
  if(cid==='klineChart')renderKlineConfigBar(dom);
}
function renderKlineConfigBar(dom){
  let bar=dom._klineBar;
  if(!bar){bar=document.createElement('div');bar.style.cssText='display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:0 0 8px;font-size:12px;color:var(--muted);';dom.parentNode.insertBefore(bar,dom);dom._klineBar=bar;}
  const cfg=window._klineCfg;
  let h='<span>副图数量:</span>';
  for(let n=1;n<=4;n++){h+='<button data-kf-count="'+n+'" style="'+(cfg.count===n?'background:var(--gold);color:#000;':'background:var(--bg3);color:var(--ink);')+'border:none;border-radius:6px;padding:2px 10px;cursor:pointer;font-size:12px;font-weight:700;">'+n+'</button>';}
  h+='<span style="margin-left:6px;">副图指标:</span>';
  for(let i=0;i<cfg.count;i++){
    h+='<select data-kf-slot="'+i+'" style="background:var(--bg3);color:var(--ink);border:1px solid var(--rule);border-radius:6px;padding:2px 6px;font-size:12px;cursor:pointer;">';
    for(let j=0;j<KF_ORDER.length;j++){const key=KF_ORDER[j];h+='<option value="'+key+'"'+(cfg.subs[i]===key?' selected':'')+'>'+KF_LABEL[key]+'</option>';}
    h+='</select>';
  }
  h+='<label style="margin-left:6px;display:flex;align-items:center;gap:4px;cursor:pointer;"><input type="checkbox" data-kf-boll '+(cfg.boll?'checked':'')+' style="accent-color:var(--gold);cursor:pointer;"> BOLL叠加</label>';
  bar.innerHTML=h;
  bar.querySelectorAll('[data-kf-count]').forEach(function(b){b.onclick=function(){window._klineCfg.count=parseInt(b.getAttribute('data-kf-count'),10);while(window._klineCfg.subs.length<window._klineCfg.count)window._klineCfg.subs.push('VOL');window._klineCfg.subs=window._klineCfg.subs.slice(0,window._klineCfg.count);saveKlineCfg();rebuildKline(dom);renderKlineConfigBar(dom);};});
  bar.querySelectorAll('[data-kf-slot]').forEach(function(sel){sel.onchange=function(){window._klineCfg.subs[parseInt(sel.getAttribute('data-kf-slot'),10)]=sel.value;saveKlineCfg();rebuildKline(dom);};});
  const boll=bar.querySelector('[data-kf-boll]');if(boll)boll.onchange=function(){window._klineCfg.boll=boll.checked;saveKlineCfg();rebuildKline(dom);};
}
function rebuildKline(dom){const ctx=dom._klineCtx;if(ctx&&ctx.ch)ctx.ch.setOption(buildKlineOption(ctx.kline,window._klineCfg),true);}

// ===== Fuzzy Search =====
function fuzzyMatch(text,query){
  text=String(text).toLowerCase();query=query.toLowerCase();
  if(text.includes(query))return true;
  let ti=0;
  for(let qi=0;qi<query.length;qi++){
    ti=text.indexOf(query[qi],ti);
    if(ti===-1)return false;
    ti++;
  }
  return true;
}
function searchStocks(query){
  if(!query||query.trim().length<1)return[];
  query=query.trim();
  const results=[];
  for(const s of scanData){
    const pinyin=pinyinMap[s.code]||'';
    if(fuzzyMatch(s.code,query)||fuzzyMatch(s.name,query)||fuzzyMatch(pinyin,query)||fuzzyMatch(s.industry,query)){
      results.push(s);
    }
    if(results.length>=10)break;
  }
  return results;
}
// ===== Full Market Search via East Money API (JSONP to bypass CORS) =====
let apiSuggestTimer=null;
function jsonpRequest(url,timeout){
  return new Promise((resolve,reject)=>{
    const cbName='jsonp_cb_'+Date.now()+'_'+Math.floor(Math.random()*100000);
    const script=document.createElement('script');
    const timer=setTimeout(()=>{cleanup();reject(new Error('timeout'));},timeout||5000);
    function cleanup(){clearTimeout(timer);delete window[cbName];if(script.parentNode)script.parentNode.removeChild(script);}
    window[cbName]=function(data){cleanup();resolve(data);};
    script.onerror=function(){cleanup();reject(new Error('script error'));};
    const sep=url.indexOf('?')>=0?'&':'?';
    script.src=url+sep+'cb='+cbName;
    document.head.appendChild(script);
  });
}
async function searchAllStocks(query){
  try{
    const data=await jsonpRequest('https://searchapi.eastmoney.com/api/suggest/get?input='+encodeURIComponent(query)+'&type=14&token=D43BF722C8E33BDC906FB84D85E326E8',5000);
    if(data&&data.QuotationCodeTable&&data.QuotationCodeTable.Data){
      return data.QuotationCodeTable.Data.filter(s=>s.SecurityType=='2'||s.SecurityType=='1').map(s=>({
        code:s.Code,name:s.Name,mkt:parseInt(s.MktNum)||0,inTop50:false
      }));
    }
  }catch(e){console.log('API search skipped:',e.message);}
  return [];
}
// ===== Diagnosis =====
function diagnoseStock(){
  const input=document.getElementById('diagInput').value.trim();if(!input)return;
  const localResults=searchStocks(input);
  if(localResults.length>0){
    const s=localResults[0];
    addSearchHistory(s.code,s.name,1);
    showDetail(s.code);
    document.getElementById('diagInput').value='';
    document.getElementById('diagSuggest').style.display='none';
    return;
  }
  searchAllStocks(input).then(apiResults=>{
    if(apiResults.length>0){
      const r=apiResults[0];
      addSearchHistory(r.code,r.name,r.mkt);
      showDetailEx(r.code,r.name,r.mkt);
    }else{
      alert('未找到匹配的股票：'+input+'\\n请检查代码或名称是否正确。');
    }
  });
  document.getElementById('diagInput').value='';
  document.getElementById('diagSuggest').style.display='none';
}
// ===== Search History =====
let searchHistory=[];
try{searchHistory=JSON.parse(localStorage.getItem('stock-search-history')||'[]');}catch(e){searchHistory=[];}
function addSearchHistory(code,name,mkt){
  searchHistory=searchHistory.filter(h=>h.code!==code);
  searchHistory.unshift({code:code,name:name,mkt:mkt,time:Date.now()});
  if(searchHistory.length>10)searchHistory=searchHistory.slice(0,10);
  try{localStorage.setItem('stock-search-history',JSON.stringify(searchHistory));}catch(e){}
}
function clearSearchHistory(){
  searchHistory=[];
  try{localStorage.removeItem('stock-search-history');}catch(e){}
  var inp=document.getElementById('diagInput');
  if(inp&&!inp.value.trim())showDiagSuggestions();
}
function selectFromHistory(code,name,mkt){
  var s=scanData.find(x=>x.code===code);
  if(s){selectDiag(code);}else{selectDiagEx(code,name,mkt);}
}
function renderSearchHistory(){
  if(searchHistory.length===0)return'';
  var h='<div style="padding:6px 12px;font-size:11px;color:var(--muted);border-bottom:1px solid var(--rule);display:flex;justify-content:space-between;align-items:center;">搜索历史<span style="cursor:pointer;color:var(--muted);font-size:10px;" onclick="clearSearchHistory()">清除</span></div>';
  h+=searchHistory.map(function(item){
    return'<div style="padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--rule);display:flex;justify-content:space-between;align-items:center;" onmouseenter="this.style.background=\'var(--bg3)\'" onmouseleave="this.style.background=\'\'" onclick="selectFromHistory(\''+item.code+'\',\''+item.name.replace(/'/g,'')+'\','+item.mkt+')"><div><span style="font-weight:700;">'+item.name+'</span> <span style="color:var(--muted);font-size:12px;">'+item.code+'</span></div><div style="color:var(--muted);font-size:10px;">'+(item.mkt===1?'沪':'深')+'</div></div>';
  }).join('');
  return h;
}
function showDiagSuggestions(){
  const input=document.getElementById('diagInput').value.trim();
  const box=document.getElementById('diagSuggest');
  if(input.length<1){
    var histHtml=renderSearchHistory();
    if(histHtml){box.innerHTML=histHtml;box.style.display='block';}else{box.style.display='none';}
    return;
  }
  // Show local results immediately
  const localResults=searchStocks(input);
  const localCodes=new Set(localResults.map(s=>s.code));
  let html='';
  if(localResults.length>0){
    html+=localResults.map(s=>{
      const pc=s.pct_change>=0?'pu':'pd';
      return '<div style="padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--rule);display:flex;justify-content:space-between;align-items:center;" onmouseenter="this.style.background=\'var(--bg3)\'" onmouseleave="this.style.background=\'\'" onclick="selectDiag(\''+s.code+'\')"><div><span style="font-weight:700;">'+s.name+'</span> <span style="color:var(--muted);font-size:12px;">'+s.code+'</span> <span class="badge buy" style="font-size:9px;padding:1px 5px;">Top50</span></div><div><span class="'+pc+'" style="font-size:13px;font-weight:600;">'+s.latest_price.toFixed(2)+'</span> <span class="'+pc+'" style="font-size:11px;">'+(s.pct_change>=0?'+':'')+s.pct_change.toFixed(2)+'%</span></div></div>';
    }).join('');
  }
  box.innerHTML=html||'<div style="padding:10px;color:var(--muted);font-size:13px;">搜索中...</div>';
  box.style.display='block';
  // Debounced API search
  clearTimeout(apiSuggestTimer);
  apiSuggestTimer=setTimeout(async()=>{
    const apiResults=await searchAllStocks(input);
    const newResults=apiResults.filter(s=>!localCodes.has(s.code)).slice(0,7);
    if(newResults.length>0){
      const apiHtml=newResults.map(s=>{
        return '<div style="padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--rule);display:flex;justify-content:space-between;align-items:center;" onmouseenter="this.style.background=\'var(--bg3)\'" onmouseleave="this.style.background=\'\'" onclick="selectDiagEx(\''+s.code+'\',\''+s.name+'\','+s.mkt+')"><div><span style="font-weight:700;">'+s.name+'</span> <span style="color:var(--muted);font-size:12px;">'+s.code+'</span> <span class="badge watch" style="font-size:9px;padding:1px 5px;">全市场</span></div><div style="color:var(--muted);font-size:11px;">点击查看详情</div></div>';
      }).join('');
      box.innerHTML=html+apiHtml;
    }else if(localResults.length===0){
      box.innerHTML='<div style="padding:10px;color:var(--muted);font-size:13px;">无匹配结果</div>';
    }
  },300);
}
function selectDiag(code){
  const s=scanData.find(x=>x.code===code);
  if(s)addSearchHistory(code,s.name,1);
  showDetail(code);
  document.getElementById('diagInput').value='';
  document.getElementById('diagSuggest').style.display='none';
}
function selectDiagEx(code,name,mkt){
  addSearchHistory(code,name,mkt);
  showDetailEx(code,name,mkt);
  document.getElementById('diagInput').value='';
  document.getElementById('diagSuggest').style.display='none';
}
// ===== Detail for stocks NOT in Top50 =====
function fetchTencentQuote(code){
  return new Promise((resolve,reject)=>{
    const pfx=code.startsWith('6')?'sh':'sz';
    const varName='v_'+pfx+code;
    const script=document.createElement('script');
    script.charset='gbk';
    script.src='https://qt.gtimg.cn/q='+pfx+code;
    const timer=setTimeout(()=>{cleanup();reject(new Error('timeout'));},5000);
    function cleanup(){clearTimeout(timer);if(script.parentNode)script.parentNode.removeChild(script);}
    script.onload=function(){cleanup();const raw=window[varName];if(raw){const p=raw.split('~');resolve({price:parseFloat(p[3])||0,prevClose:parseFloat(p[4])||0,open:parseFloat(p[5])||0,vol:parseFloat(p[6])||0,pct:parseFloat(p[32])||0,high:parseFloat(p[33])||0,low:parseFloat(p[34])||0,amount:(parseFloat(p[37])||0)*1e4,pe:parseFloat(p[39])||0,mktcap:parseFloat(p[44])||0,pb:parseFloat(p[46])||0});}else{reject(new Error('no data'));}};
    script.onerror=function(){cleanup();reject(new Error('script error'));};
    document.head.appendChild(script);
  });
}
// ===== Technical Indicator Helpers =====
function calcMA(arr,p){if(arr.length<p)return 0;let s=0;for(let i=arr.length-p;i<arr.length;i++)s+=arr[i];return s/p;}
function calcRSI(closes,p){if(closes.length<p+1)return 50;let g=0,l=0;for(let i=closes.length-p;i<closes.length;i++){const d=closes[i]-closes[i-1];if(d>0)g+=d;else l-=d;}const ag=g/p,al=l/p;return ag+al===0?50:100*ag/(ag+al);}
function calcEMA(arr,p){if(arr.length===0)return 0;let ema=arr[0];const k=2/(p+1);for(let i=1;i<arr.length;i++)ema=arr[i]*k+ema*(1-k);return ema;}
function calcMACD(closes){const ema12=calcEMA(closes.slice(-60),12);const ema26=calcEMA(closes.slice(-60),26);const dif=ema12-ema26;const dea=dif*0.8;const hist=2*(dif-dea);return{dif,dea,hist};}
function calcBoll(closes,p){if(closes.length<p)return{upper:0,mid:0,lower:0,pos:50};const ma=calcMA(closes,p);let s=0;for(let i=closes.length-p;i<closes.length;i++)s+=Math.pow(closes[i]-ma,2);const sd=Math.sqrt(s/p);const last=closes[closes.length-1];return{upper:ma+2*sd,mid:ma,lower:ma-2*sd,pos:sd>0?Math.max(0,Math.min(100,(last-ma)/(2*sd)*100+50)):50};}
function calcKDJ(kline,n){if(kline.length<n)return{k:50,d:50,j:50};let k=50,d=50;for(let i=kline.length-n;i<kline.length;i++){let hh=kline[i].high,ll=kline[i].low;for(let j=Math.max(0,i-n+1);j<=i;j++){hh=Math.max(hh,kline[j].high);ll=Math.min(ll,kline[j].low);}const rsv=hh>ll?(kline[i].close-ll)/(hh-ll)*100:50;k=2/3*k+1/3*rsv;d=2/3*d+1/3*k;}return{k,d,j:3*k-2*d};}
function calcATR(kline,p){if(kline.length<p+1)return 0;let s=0;for(let i=kline.length-p;i<kline.length;i++){const tr=Math.max(kline[i].high-kline[i].low,Math.abs(kline[i].high-kline[i-1].close),Math.abs(kline[i].low-kline[i-1].close));s+=tr;}return s/p;}
function calcVolRatio(kline){if(kline.length<6)return 1;const today=kline[kline.length-1].volume;let s=0;for(let i=kline.length-6;i<kline.length-1;i++)s+=kline[i].volume;return s>0?today/(s/5):1;}
function calcADX(kline,p){if(kline.length<p*2+1)return 25;let dmP=0,dmM=0,tr=0;for(let i=kline.length-p;i<kline.length;i++){const up=kline[i].high-kline[i-1].high;const dn=kline[i-1].low-kline[i].low;const tr1=Math.max(kline[i].high-kline[i].low,Math.abs(kline[i].high-kline[i-1].close),Math.abs(kline[i].low-kline[i-1].close));tr+=tr1;if(up>dn&&up>0)dmP+=up;if(dn>up&&dn>0)dmM+=dn;}if(tr===0)return 25;const diP=100*dmP/tr,diM=100*dmM/tr;return diP+diM>0?100*Math.abs(diP-diM)/(diP+diM):0;}
// ===== 时间序列版技术指标（用于副图绘制）=====
function calcMACDSeries(closes){const dif=[],dea=[],hist=[];let e12=closes[0],e26=closes[0],d9=0;for(let i=0;i<closes.length;i++){if(i>0){e12=e12*11/13+closes[i]*2/13;e26=e26*25/27+closes[i]*2/27;}const d=e12-e26;if(i===0){d9=d;}else{d9=d9*0.8+d*0.2;}dif.push(+d.toFixed(3));dea.push(+d9.toFixed(3));hist.push(+((d-d9)*2).toFixed(3));}return{dif,dea,hist};}
function calcKDJSeries(kline,n){n=n||9;const K=[],D=[],J=[];let k=50,d=50;for(let i=0;i<kline.length;i++){let hh=-1e18,ll=1e18;for(let j=Math.max(0,i-n+1);j<=i;j++){hh=Math.max(hh,kline[j].high);ll=Math.min(ll,kline[j].low);}const rsv=hh>ll?(kline[i].close-ll)/(hh-ll)*100:50;k=k*2/3+rsv/3;d=d*2/3+k/3;K.push(+k.toFixed(2));D.push(+d.toFixed(2));J.push(+(3*k-2*d).toFixed(2));}return{K,D,J};}
function calcRSISeries(closes,n){n=n||14;const R=[];let g=0,l=0;for(let i=0;i<closes.length;i++){if(i===0){R.push(50);continue;}const d=closes[i]-closes[i-1];if(i<=n){if(d>0)g+=d;else l-=d;if(i===n)R.push(g+l===0?50:100*g/(g+l));else R.push(50);}else{g=(g*(n-1)+(d>0?d:0))/n;l=(l*(n-1)+(d<0?-d:0))/n;R.push(+((g+l===0?50:100*g/(g+l))).toFixed(2));}}return R;}
function calcBollSeries(closes,n){n=n||20;const up=[],mid=[],low=[];for(let i=0;i<closes.length;i++){const s=Math.max(0,i-n+1);let sum=0;for(let j=s;j<=i;j++)sum+=closes[j];const ma=sum/(i-s+1);let v=0;for(let j=s;j<=i;j++)v+=Math.pow(closes[j]-ma,2);const sd=Math.sqrt(v/(i-s+1));up.push(+(ma+2*sd).toFixed(3));mid.push(+ma.toFixed(3));low.push(+(ma-2*sd).toFixed(3));}return{up,mid,low};}
function calcVolMASeries(vols,p){const r=[];for(let i=0;i<vols.length;i++){const s=Math.max(0,i-p+1);let sum=0;for(let j=s;j<=i;j++)sum+=vols[j];r.push(+((sum/(i-s+1))).toFixed(0));}return r;}
function calcWRSeries(kline,n){n=n||14;const W=[];for(let i=0;i<kline.length;i++){const s=Math.max(0,i-n+1);let hh=-1e18,ll=1e18;for(let j=s;j<=i;j++){hh=Math.max(hh,kline[j].high);ll=Math.min(ll,kline[j].low);}W.push(hh>ll?+((hh-kline[i].close)/(hh-ll)*100).toFixed(2):50);}return W;}
function calcOBVSeries(kline){const O=[];let obv=0;for(let i=0;i<kline.length;i++){if(i===0)obv=kline[i].volume;else{if(kline[i].close>kline[i-1].close)obv+=kline[i].volume;else if(kline[i].close<kline[i-1].close)obv-=kline[i].volume;}O.push(obv);}return O;}
function detectExSignals(kl,rsi,macd,boll,kdj,adx,volRatio){
  const sigs=[];if(kl.length<5)return sigs;const last=kl.length-1;const t=kl[last],p=kl[last-1];
  if(kl.length>=20&&t.close>t.open&&t.close>kl[last].ma20&&volRatio>1.5)sigs.push({indicator:'出水芙蓉',type:'看涨',date:t.date,section:'实时',desc:'放量阳线突破20日均线'});
  if(macd.hist>0&&macd.dif>macd.dea)sigs.push({indicator:'MACD多头',type:'看涨',date:t.date,section:'实时',desc:'MACD柱状图为正，DIF在DEA之上'});
  if(macd.hist<0&&macd.dif<macd.dea)sigs.push({indicator:'MACD空头',type:'看跌',date:t.date,section:'实时',desc:'MACD柱状图为负，DIF在DEA之下'});
  if(rsi>70)sigs.push({indicator:'RSI超买',type:'看跌',date:t.date,section:'实时',desc:'RSI('+rsi.toFixed(1)+')超过70，短期有回调风险'});
  else if(rsi<30)sigs.push({indicator:'RSI超卖',type:'看涨',date:t.date,section:'实时',desc:'RSI('+rsi.toFixed(1)+')低于30，有反弹可能'});
  if(boll.pos>85)sigs.push({indicator:'布林上轨',type:'看跌',date:t.date,section:'实时',desc:'股价接近布林上轨('+boll.pos.toFixed(0)+'%)，注意回调'});
  else if(boll.pos<15)sigs.push({indicator:'布林下轨',type:'看涨',date:t.date,section:'实时',desc:'股价接近布林下轨('+boll.pos.toFixed(0)+'%)，关注反弹'});
  if(kdj.j>100)sigs.push({indicator:'KDJ超买',type:'看跌',date:t.date,section:'实时',desc:'J值('+kdj.j.toFixed(1)+')超过100'});
  else if(kdj.j<0)sigs.push({indicator:'KDJ超卖',type:'看涨',date:t.date,section:'实时',desc:'J值('+kdj.j.toFixed(1)+')低于0'});
  if(volRatio>2)sigs.push({indicator:'放量',type:'看涨',date:t.date,section:'实时',desc:'量比('+volRatio.toFixed(2)+')大于2，资金活跃'});
  if(adx>25)sigs.push({indicator:'趋势增强',type:'看涨',date:t.date,section:'实时',desc:'ADX('+adx.toFixed(1)+')大于25，趋势明显'});
  if(t.close>kl[last].ma5&&kl[last].ma5>kl[last].ma10&&kl[last].ma10>kl[last].ma20)sigs.push({indicator:'均线多头排列',type:'看涨',date:t.date,section:'实时',desc:'MA5>MA10>MA20，多头排列'});
  if(t.close<kl[last].ma5&&kl[last].ma5<kl[last].ma10&&kl[last].ma10<kl[last].ma20)sigs.push({indicator:'均线空头排列',type:'看跌',date:t.date,section:'实时',desc:'MA5<MA10<MA20，空头排列'});
  if(t.close<p.close&&t.volume>p.volume&&p.close>kl[last-2].close)sigs.push({indicator:'放量下跌',type:'看跌',date:t.date,section:'实时',desc:'今日放量下跌，注意风险'});
  if(t.close>maxO(kl,5)&&t.volume>p.volume)sigs.push({indicator:'平台突破',type:'看涨',date:t.date,section:'实时',desc:'突破5日平台高点且放量'});
  return sigs;
}
function maxO(kl,n){let m=0;for(let i=kl.length-n;i<kl.length-1;i++)m=Math.max(m,kl[i].high);return m;}
async function showDetailEx(code,name,mkt){
  const prefix=mkt===1?'1':'0';
  const secid=prefix+'.'+code;
  const w=isWatched(code);
  document.getElementById('detailContent').innerHTML='<div style="text-align:center;padding:60px;color:var(--muted);"><div style="font-size:24px;margin-bottom:12px;">'+name+' '+code+'</div><div style="font-size:14px;">正在加载个股数据...</div></div>';
  openOverlay('detailOverlay');
  // Fetch real-time quote
  let price=0,pct=0,open=0,high=0,low=0,vol=0,amount=0,pe=0,pb=0,mktcap=0;
  try{
    const data=await jsonpRequest('https://push2.eastmoney.com/api/qt/stock/get?secid='+secid+'&fltt=2&fields=f43,f44,f45,f46,f47,f48,f57,f58,f84,f85,f116,f117,f162,f167,f169,f170',5000);
    if(data&&data.data){const d=data.data;price=d.f43||0;pct=d.f170||0;open=d.f46||0;high=d.f44||0;low=d.f45||0;vol=d.f47||0;amount=d.f48||0;mktcap=(d.f116||0)/1e8;pe=d.f162||0;pb=d.f167||0;}
    if(price===0)throw new Error('empty data');
  }catch(e){
    try{const q=await fetchTencentQuote(code);price=q.price;pct=q.pct;open=q.open;high=q.high;low=q.low;vol=q.vol;amount=q.amount;pe=q.pe;pb=q.pb;mktcap=q.mktcap;}catch(e2){}
  }
  // Fetch K-line data
  let kline=[];
  try{
    const klData=await jsonpRequest('https://push2his.eastmoney.com/api/qt/stock/kline/get?secid='+secid+'&fields1=f1,f2,f3,f4,f5,f6&fields2=f51,f52,f53,f54,f55,f56,f57,f58&klt=101&fqt=1&beg=0&end=20500101&lmt=120',8000);
    if(klData&&klData.data&&klData.data.klines){
      kline=klData.data.klines.map(function(line){const p=line.split(',');return{date:p[0],open:parseFloat(p[1]),close:parseFloat(p[2]),high:parseFloat(p[3]),low:parseFloat(p[4]),volume:parseFloat(p[5]),ma5:0,ma10:0,ma20:0};});
      for(let i=0;i<kline.length;i++){const s5=kline.slice(Math.max(0,i-4),i+1).map(k=>k.close);const s10=kline.slice(Math.max(0,i-9),i+1).map(k=>k.close);const s20=kline.slice(Math.max(0,i-19),i+1).map(k=>k.close);kline[i].ma5=calcMA(s5,5);kline[i].ma10=calcMA(s10,10);kline[i].ma20=calcMA(s20,20);}
    }
  }catch(e){console.log('K-line fetch failed:',e.message);}
  // Calculate indicators
  const closes=kline.map(k=>k.close);
  const rsi=calcRSI(closes,14);
  const macd=calcMACD(closes);
  const boll=calcBoll(closes,20);
  const kdj=calcKDJ(kline,9);
  const adx=calcADX(kline,14);
  const atr=calcATR(kline,14);
  const volRatio=calcVolRatio(kline);
  const atrPct=kline.length>0&&kline[kline.length-1].close>0?(atr/kline[kline.length-1].close*100):0;
  const signals=detectExSignals(kline,rsi,macd,boll,kdj,adx,volRatio);
  const bull=signals.filter(s=>s.type==='看涨').length;
  const bear=signals.filter(s=>s.type==='看跌').length;
  const sentiment=bull-bear;
  const cls=sentiment>2?'strong_bull':(sentiment>0?'bull':(sentiment<-2?'strong_bear':(sentiment<0?'bear':'neutral')));
  const clsName=sentiment>2?'强势看涨':(sentiment>0?'看涨':(sentiment<-2?'强势看跌':(sentiment<0?'看跌':'中性')));
  const clsBadge=cls==='strong_bull'||cls==='bull'?'buy':(cls==='strong_bear'||cls==='bear'?'sell':'watch');
  const pc=pct>=0?'pu':'pd';
  const ps=pct>=0?'+':'';
  const watchBtn=w?'<button style="background:var(--gold);color:#000;font-weight:700;padding:8px 20px;font-size:13px;border:none;border-radius:8px;cursor:pointer;" onclick="toggleWatch(\''+code+'\');showDetailEx(\''+code+'\',\''+name+'\','+mkt+')">★ 已关注</button>':'<button style="background:var(--bg3);color:var(--ink);font-weight:600;padding:8px 20px;font-size:13px;border:1px solid var(--rule);border-radius:8px;cursor:pointer;" onclick="toggleWatch(\''+code+'\');showDetailEx(\''+code+'\',\''+name+'\','+mkt+')">☆ 添加关注</button>';
  // Signal timeline
  const tl=signals.map(function(sig){const c=sig.type==='看涨'?'up':(sig.type==='看跌'?'down':'gold');return '<div class="sig-tl-item"><div class="sig-tl-dot" style="background:var(--'+c+');"></div><span style="color:var(--muted);">'+sig.date+'</span><span class="badge '+(sig.type==='看涨'?'buy':(sig.type==='看跌'?'sell':'watch'))+'">'+sig.type+'</span><span style="font-weight:600;">'+sig.indicator+'</span><span style="color:var(--muted);font-size:11px;">'+sig.section+'</span></div>';}).join('');
  let html='<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center;"><div><h2 style="display:flex;align-items:center;gap:10px;">'+name+' <span style="color:var(--muted);font-size:16px;font-weight:400;">'+code+'</span> <span class="badge '+clsBadge+'">'+clsName+'</span> <span class="badge watch">全市场</span></h2><div style="display:flex;gap:20px;margin-top:8px;font-size:14px;"><span>最新价: <b class="'+pc+'" style="font-size:18px;">'+price.toFixed(2)+'</b></span><span>涨跌幅: <b class="'+pc+'">'+ps+pct.toFixed(2)+'%</b></span><span>市值: <b>'+(mktcap>0?mktcap.toFixed(0)+'亿':'--')+'</b></span><span>PE: <b>'+pe.toFixed(1)+'</b></span><span>PB: <b>'+pb.toFixed(2)+'</b></span></div></div>'+watchBtn+'</div>';
  // Trading signal box
  const entryPrice=price;
  const stopLoss=atr>0?price-2*atr:0;
  const targetPrice=atr>0?price+3*atr:0;
  const pot=targetPrice>0?((targetPrice-entryPrice)/entryPrice*100).toFixed(1):'--';
  const risk=stopLoss>0?((entryPrice-stopLoss)/entryPrice*100).toFixed(1):'--';
  html+='<div class="dt-section" style="border-left:3px solid var(--up);"><h4><span style="width:3px;height:14px;background:var(--up);border-radius:2px;display:inline-block;"></span>交易信号</h4>';
  html+='<div style="display:flex;gap:8px;margin-bottom:8px;"><span class="badge '+(sentiment>0?'buy':(sentiment<0?'sell':'watch'))+'">'+(sentiment>0?'买入':(sentiment<0?'卖出':'观望'))+'</span><span style="color:var(--muted);font-size:12px;">信号强度: '+sentiment+' (多'+bull+'/空'+bear+')</span></div>';
  html+='<div class="dt-grid">';
  html+='<div class="dt-cell"><div class="v pu">'+entryPrice.toFixed(2)+'</div><div class="l">入场价</div></div>';
  if(stopLoss>0)html+='<div class="dt-cell"><div class="v pd">'+stopLoss.toFixed(2)+'</div><div class="l">止损价</div></div>';
  if(targetPrice>0)html+='<div class="dt-cell"><div class="v" style="color:var(--gold);">'+targetPrice.toFixed(2)+'</div><div class="l">目标价</div></div>';
  html+='<div class="dt-cell"><div class="v">50%</div><div class="l">建议仓位</div></div>';
  if(targetPrice>0)html+='<div class="dt-cell"><div class="v pu">+'+pot+'%</div><div class="l">潜在收益</div></div>';
  if(stopLoss>0)html+='<div class="dt-cell"><div class="v pd">-'+risk+'%</div><div class="l">风险幅度</div></div>';
  html+='</div>';
  html+='<div style="margin-top:8px;padding:10px;background:var(--bg2);border-radius:8px;font-size:12px;color:var(--muted);line-height:1.6;">基于ATR('+atr.toFixed(2)+')动态计算止损/目标价位。入场价取最新价，止损=2倍ATR，目标=3倍ATR，盈亏比约1.5:1。</div>';
  html+='</div>';
  html+=execPlanHtml({entry_price:entryPrice,stop_loss:stopLoss,target_price:targetPrice});
  html+='<div id="t0DetailBox"></div>';
  // Real-time quote
  html+='<div class="dt-section"><h4><span style="width:3px;height:14px;background:var(--gold);border-radius:2px;display:inline-block;"></span>实时行情</h4><div class="dt-grid">';
  html+='<div class="dt-cell"><div class="v '+pc+'">'+price.toFixed(2)+'</div><div class="l">最新价</div></div>';
  html+='<div class="dt-cell"><div class="v">'+open.toFixed(2)+'</div><div class="l">今开</div></div>';
  html+='<div class="dt-cell"><div class="v pu">'+high.toFixed(2)+'</div><div class="l">最高</div></div>';
  html+='<div class="dt-cell"><div class="v pd">'+low.toFixed(2)+'</div><div class="l">最低</div></div>';
  html+='<div class="dt-cell"><div class="v">'+(vol/10000).toFixed(0)+'万</div><div class="l">成交量(手)</div></div>';
  html+='<div class="dt-cell"><div class="v">'+(amount/1e8).toFixed(2)+'亿</div><div class="l">成交额</div></div>';
  html+='</div></div>';
  // Technical indicators
  html+='<div class="dt-section"><h4><span style="width:3px;height:14px;background:var(--gold);border-radius:2px;display:inline-block;"></span>技术指标</h4><div class="dt-grid">';
  html+='<div class="dt-cell"><div class="v '+(rsi>=70?'pd':(rsi<=30?'pu':''))+'">'+rsi.toFixed(1)+'</div><div class="l">RSI(14)</div></div>';
  html+='<div class="dt-cell"><div class="v '+(macd.hist>0?'pu':'pd')+'">'+macd.dif.toFixed(2)+'</div><div class="l">MACD-DIF</div></div>';
  html+='<div class="dt-cell"><div class="v '+(macd.hist>0?'pu':'pd')+'">'+macd.hist.toFixed(2)+'</div><div class="l">MACD柱</div></div>';
  html+='<div class="dt-cell"><div class="v '+(boll.pos>85?'pd':(boll.pos<15?'pu':''))+'">'+boll.pos.toFixed(0)+'%</div><div class="l">布林位置</div></div>';
  html+='<div class="dt-cell"><div class="v '+(kdj.j>100?'pd':(kdj.j<0?'pu':''))+'">'+kdj.j.toFixed(1)+'</div><div class="l">KDJ-J</div></div>';
  html+='<div class="dt-cell"><div class="v '+(adx>25?'pu':'')+'">'+adx.toFixed(1)+'</div><div class="l">ADX(14)</div></div>';
  html+='<div class="dt-cell"><div class="v '+(volRatio>1.5?'pu':'')+'">'+volRatio.toFixed(2)+'</div><div class="l">量比</div></div>';
  html+='<div class="dt-cell"><div class="v">'+atrPct.toFixed(2)+'%</div><div class="l">ATR波幅</div></div>';
  if(kline.length>0){const last=kline[kline.length-1];html+='<div class="dt-cell"><div class="v">'+last.ma5.toFixed(2)+'</div><div class="l">MA5</div></div>';html+='<div class="dt-cell"><div class="v">'+last.ma10.toFixed(2)+'</div><div class="l">MA10</div></div>';html+='<div class="dt-cell"><div class="v">'+last.ma20.toFixed(2)+'</div><div class="l">MA20</div></div>';}
  html+='</div></div>';
  // Valuation
  html+='<div class="dt-section"><h4><span style="width:3px;height:14px;background:var(--blue);border-radius:2px;display:inline-block;"></span>估值数据</h4><div class="dt-grid">';
  html+='<div class="dt-cell"><div class="v">'+pe.toFixed(1)+'</div><div class="l">市盈率(PE)</div></div>';
  html+='<div class="dt-cell"><div class="v">'+pb.toFixed(2)+'</div><div class="l">市净率(PB)</div></div>';
  html+='<div class="dt-cell"><div class="v gold">'+(mktcap>0?mktcap.toFixed(0)+'亿':'--')+'</div><div class="l">总市值</div></div>';
  html+='</div></div>';
  // Signal timeline
  html+='<div class="dt-section"><h4><span style="width:3px;height:14px;background:var(--up);border-radius:2px;display:inline-block;"></span>信号触发时间轴（'+signals.length+'个信号）</h4>';
  html+='<div class="sig-timeline">'+(tl||'<div style="padding:8px;color:var(--muted);font-size:12px;">暂无触发信号</div>')+'</div></div>';
  // K-line chart
  if(kline.length>0)html+='<div class="dt-section"><h4><span style="width:3px;height:14px;background:var(--gold);border-radius:2px;display:inline-block;"></span>K线图（MA/BOLL + 成交量/MACD/KDJ）</h4><div id="klineChart" style="width:100%;height:460px;"></div></div>';
  // Watch analysis
  html+='<div class="dt-section" style="border-left:3px solid var(--gold);"><h4><span style="width:3px;height:14px;background:var(--gold);border-radius:2px;display:inline-block;"></span>关注分析</h4>';
  html+='<div class="watch-advice"><strong style="color:var(--gold);">关注说明：</strong>该股票不在当前Top50指标触发列表中，数据通过实时API获取并计算。<br><strong style="color:var(--up);">操作建议：</strong>'+(w?'该股已在关注列表中，可在导航栏"我关注的"查看详细分析。':'点击上方"添加关注"按钮，将该股加入关注列表。')+'</div></div>';
  document.getElementById('detailContent').innerHTML=html;
  openOverlay('detailOverlay');
  if(kline.length>0)setTimeout(function(){renderKlineChart(kline);},100);
  loadT0ForDetail(code);
}
document.addEventListener('DOMContentLoaded',function(){
  const inp=document.getElementById('diagInput');
  if(inp){
    inp.addEventListener('input',showDiagSuggestions);
    inp.addEventListener('keydown',function(e){if(e.key==='Enter'){e.preventDefault();diagnoseStock();}});
    inp.addEventListener('blur',function(){setTimeout(function(){document.getElementById('diagSuggest').style.display='none';},200);});
    inp.addEventListener('focus',function(){if(inp.value.trim().length>=1)showDiagSuggestions();});
  }
});

// ===== Ticker =====
function renderTicker(){
  const items=IDX_SECIDS.map(secid=>{
    const code=secid.split('.')[1];const name=IDX_MAP[code];
    const rt=realtimeIndices[code];
    let price=0,pct=0;
    if(rt){price=rt.price;pct=rt.pct;}
    else if(marketTrend&&marketTrend.indices){
      const idx=marketTrend.indices.find(i=>i.name===name);
      if(idx){price=idx.index_close||0;pct=idx.index_change||0;}
    }
    const pc=pct>=0?'up':'down';
    return '<span class="ticker-item" style="cursor:pointer;" onclick="showIndexDetail(\''+code+'\')"><span style="font-weight:600;">'+name+'</span><span style="color:var(--'+pc+');">'+(price||0).toFixed(2)+'</span><span style="color:var(--'+pc+');font-size:11px;">'+(pct>=0?'+':'')+(pct||0).toFixed(2)+'%</span></span>';
  }).join('');
  document.getElementById('tickerTrack').innerHTML=items+items;
}
async function showIndexDetail(code){
  const name=IDX_MAP[code]||code;
  const secid=IDX_SECIDS.find(s=>s.endsWith('.'+code));
  if(!secid)return;
  const rt=realtimeIndices[code]||{};
  let price=rt.price||0,pct=rt.pct||0;
  let ma5=0,ma10=0,ma20=0,ma60=0,ret20d=0,trend='--';
  if(marketTrend&&marketTrend.indices){
    const idx=marketTrend.indices.find(i=>i.name===name);
    if(idx){ma5=idx.ma5||0;ma10=idx.ma10||0;ma20=idx.ma20||0;ma60=idx.ma60||0;ret20d=idx.ret_20d||0;trend=idx.trend||'--';}
  }
  document.getElementById('detailContent').innerHTML='<div style="text-align:center;padding:60px;color:var(--muted);">正在加载指数数据...</div>';
  openOverlay('detailOverlay');
  let kline=[];
  try{
    const r=await jsonpRequest('https://push2his.eastmoney.com/api/qt/stock/kline/get?secid='+secid+'&fields1=f1,f2,f3,f4,f5,f6&fields2=f51,f52,f53,f54,f55,f56,f57,f58&klt=101&fqt=1&beg=0&end=20500101&lmt=120',8000);
    if(r&&r.data&&r.data.klines){
      kline=r.data.klines.map(function(line){const p=line.split(',');return{date:p[0],open:parseFloat(p[1]),close:parseFloat(p[2]),high:parseFloat(p[3]),low:parseFloat(p[4]),volume:parseFloat(p[5]),ma5:0,ma10:0,ma20:0};});
      for(let i=0;i<kline.length;i++){kline[i].ma5=calcMA(kline.slice(Math.max(0,i-4),i+1).map(k=>k.close),5);kline[i].ma10=calcMA(kline.slice(Math.max(0,i-9),i+1).map(k=>k.close),10);kline[i].ma20=calcMA(kline.slice(Math.max(0,i-19),i+1).map(k=>k.close),20);}
    }
  }catch(e){}
  const pc=pct>=0?'pu':'pd',ps=pct>=0?'+':'';
  const trendTag={'上行':'trend-up','下行':'trend-down','震荡':'trend-flat'}[trend]||'trend-flat';
  let html='<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">';
  html+='<div><span style="font-size:24px;font-weight:800;">'+name+'</span> <span style="color:var(--muted);font-size:14px;">'+code+'</span> <span class="badge watch">指数</span></div>';
  html+='<div><span class="'+pc+'" style="font-size:28px;font-weight:700;">'+(price||0).toFixed(2)+'</span> <span class="'+pc+'" style="font-size:16px;">'+ps+(pct||0).toFixed(2)+'%</span></div>';
  html+='</div>';
  html+='<div class="dt-grid" style="margin-bottom:16px;">';
  html+='<div class="dt-cell"><div class="v">'+(ma5||0).toFixed(2)+'</div><div class="l">MA5</div></div>';
  html+='<div class="dt-cell"><div class="v">'+(ma10||0).toFixed(2)+'</div><div class="l">MA10</div></div>';
  html+='<div class="dt-cell"><div class="v">'+(ma20||0).toFixed(2)+'</div><div class="l">MA20</div></div>';
  html+='<div class="dt-cell"><div class="v">'+(ma60||0).toFixed(2)+'</div><div class="l">MA60</div></div>';
  html+='<div class="dt-cell"><div class="v '+(ret20d>=0?'up':'down')+'">'+(ret20d>=0?'+':'')+(ret20d||0).toFixed(2)+'%</div><div class="l">20日涨跌</div></div>';
  html+='<div class="dt-cell"><div class="v"><span class="trend-tag '+trendTag+'">'+trend+'</span></div><div class="l">趋势</div></div>';
  html+='</div>';
  html+='<div class="dt-section"><div class="dt-section-title">趋势分析</div>';
  if(price>ma5&&ma5>ma20)html+='<p>当前价格在MA5和MA20之上，短期趋势向上，多头排列。</p>';
  else if(price<ma5&&ma5<ma20)html+='<p>当前价格在MA5和MA20之下，短期趋势向下，空头排列。</p>';
  else html+='<p>均线交叉，趋势不明朗，处于震荡格局。</p>';
  if(ma5>ma20)html+='<p>MA5('+ma5.toFixed(2)+') > MA20('+ma20.toFixed(2)+')，短期均线在长期均线之上。</p>';
  else html+='<p>MA5('+ma5.toFixed(2)+') < MA20('+ma20.toFixed(2)+')，短期均线在长期均线之下。</p>';
  html+='</div>';
  if(kline.length>0)html+='<div class="dt-section"><div class="dt-section-title">K线走势（MA/BOLL + 成交量/MACD/KDJ）</div><div id="klineChart" style="width:100%;height:460px;"></div></div>';
  document.getElementById('detailContent').innerHTML=html;
  if(kline.length>0)setTimeout(function(){renderKlineChart(kline);},100);
}

// ===== CSV =====
function exportCSV(){
  const _h=['代码','名称','指标数§信号触发总数','看涨§看涨信号数量','看跌§看跌信号数量','情绪§多空偏向值(-1~1)','交易信号§回测+量化综合信号','入场价§建议入场价格','止损价§ATR动态止损','目标价§ATR动态目标','仓位§建议仓位%','胜率§原始回测胜率','量化胜率§过滤后胜率','盈亏比§盈亏比PF','量化评分§多因子综合评分','RSI§相对强弱(30-65优)','ADX§趋势强度(>20有趋势)','量比§量比(1.2-3优)','布林位置§布林带位置(0-1)','ATR波幅%§平均真实波幅','置信度§信号置信度','风险§风险等级','优先级§操作优先级','行业§所属行业','市值(亿)','最新价','涨跌幅%'];
  const rows=scanData.map(s=>[s.code,s.name,s.signal_count,s.bullish,s.bearish,s.sentiment_score,s.trade_signal,s.entry_price,s.stop_loss,s.target_price,s.position_size,s.bt_win_rate,s.bt_wr_filtered,s.bt_pf,s.bt_quant_score,s.bt_rsi,s.bt_adx,s.bt_vol_ratio,s.bt_bb_pos,s.bt_atr_pct,s.confidence,s.risk,s.priority,s.industry,s.mkt_cap_yi,s.latest_price,s.pct_change]);
  const csv=[h,...rows].map(r=>r.map(c=>'"'+c+'"').join(',')).join('\n');
  const blob=new Blob(['\ufeff'+csv],{type:'text/csv;charset=utf-8'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='stocks.csv';a.click();
}

// ===== Indicator Reference =====
function renderIndicatorRef(){
  const entries=Object.entries(indicatorCounts).sort((a,b)=>b[1]-a[1]);
  document.getElementById('indicatorRefGrid').innerHTML=entries.map(([name,count])=>{
    const info=indicatorInfo[name];if(!info)return'';
    const cls=info.type==='看涨'?'':(info.type==='看跌'?'bear':'neutral');
    return '<div class="ref-card '+cls+'"><div class="ref-head"><span class="ref-name">'+name+'</span><span class="ref-count">'+count+'次</span></div><div class="ref-desc">'+info.desc+'</div><div class="ref-meta"><span class="badge '+(info.type==='看涨'?'buy':(info.type==='看跌'?'sell':'watch'))+'">'+info.type+'</span><span style="font-size:11px;color:var(--muted);">'+info.section+'</span></div></div>';
  }).join('');
}

// ===== Real-time Index Data =====
let realtimeIndices={};
let indexKlineLoaded=false;
const IDX_SECIDS=['1.000001','0.399001','0.399006','1.000688','0.899050'];
const IDX_MAP={'000001':'上证指数','399001':'深证成指','399006':'创业板指','000688':'科创50','899050':'北证50'};
async function fetchRealtimeIndices(){
  try{
    const data=await jsonpRequest('https://push2.eastmoney.com/api/qt/ulist.np/get?fltt=2&fields=f2,f3,f4,f12,f14&secids='+IDX_SECIDS.join(','),8000);
    if(data&&data.data&&data.data.diff){
      data.data.diff.forEach(item=>{realtimeIndices[item.f12]={price:item.f2,pct:item.f3,name:item.f14};});
      updateIndexDisplay();
    }
  }catch(e){console.log('Index realtime fetch skipped:',e.message);}
}
async function fetchIndexKlines(){
  if(indexKlineLoaded)return;
  for(const secid of IDX_SECIDS){
    const code=secid.split('.')[1];const name=IDX_MAP[code];
    try{
      const r=await jsonpRequest('https://push2his.eastmoney.com/api/qt/stock/kline/get?secid='+secid+'&fields1=f1,f2,f3,f4,f5,f6&fields2=f51,f52,f53,f54,f55,f56,f57,f58&klt=101&fqt=1&beg=0&end=20500101&lmt=120',8000);
      if(r&&r.data&&r.data.klines){
        const closes=r.data.klines.map(function(line){return parseFloat(line.split(',')[2]);});
        if(!marketTrend)marketTrend={};
        if(!marketTrend.indices)marketTrend.indices=[];
        let idx=marketTrend.indices.find(i=>i.name===name);
        if(!idx){idx={name:name,code:secid,index_close:0,index_change:0,trend:'震荡'};marketTrend.indices.push(idx);}
        idx.ma5=closes.length>=5?closes.slice(-5).reduce((a,b)=>a+b,0)/5:0;
        idx.ma10=closes.length>=10?closes.slice(-10).reduce((a,b)=>a+b,0)/10:0;
        idx.ma20=closes.length>=20?closes.slice(-20).reduce((a,b)=>a+b,0)/20:0;
        idx.ma60=closes.length>=60?closes.slice(-60).reduce((a,b)=>a+b,0)/60:0;
        idx.ret_20d=closes.length>=21?((closes[closes.length-1]-closes[closes.length-21])/closes[closes.length-21]*100):0;
      }
    }catch(e){console.log('Index kline fetch failed for',name,e.message);}
  }
  indexKlineLoaded=true;updateIndexDisplay();
}
function updateIndexDisplay(){
  if(!marketTrend)marketTrend={indices:[]};
  if(!marketTrend.indices)marketTrend.indices=[];
  Object.keys(IDX_MAP).forEach(code=>{
    const name=IDX_MAP[code];const rt=realtimeIndices[code];
    let idx=marketTrend.indices.find(i=>i.name===name);
    if(!idx){idx={name:name,code:'',index_close:0,index_change:0,trend:'震荡',ma5:0,ma10:0,ma20:0,ma60:0,ret_20d:0};marketTrend.indices.push(idx);}
    if(rt){idx.index_close=rt.price;idx.index_change=rt.pct;
      if(idx.ma5&&idx.ma20){
        if(rt.price>idx.ma5&&idx.ma5>idx.ma20)idx.trend='上行';
        else if(rt.price<idx.ma5&&idx.ma5<idx.ma20)idx.trend='下行';
        else idx.trend='震荡';
      }
    }
  });
  const sh=marketTrend.indices.find(i=>i.name==='上证指数');
  if(sh){marketTrend.index_close=sh.index_close;marketTrend.index_change=sh.index_change;
    marketTrend.ma5=sh.ma5;marketTrend.ma20=sh.ma20;marketTrend.ma60=sh.ma60;
    marketTrend.ret_20d=sh.ret_20d;marketTrend.trend=sh.trend;}
  const active=document.querySelector('.page-section.active');
  if(active&&active.id==='layer1'){renderMarketTrend();}
  renderTicker();
  const lu=document.getElementById('lastUpdate');
  if(lu)lu.textContent=new Date().toLocaleTimeString('zh-CN');
}

// ===== Real-time Polling =====
let realtimeData={};
async function fetchRealtimeQuotes(){
  const stockCodes=scanData.map(s=>{const p=s.code.startsWith('6')?'1':'0';return p+'.'+s.code;});
  const extraCodes=watchList.filter(c=>!scanData.find(s=>s.code===c)).map(c=>{const p=c.startsWith('6')?'1':'0';return p+'.'+c;});
  const allCodes=[...stockCodes,...extraCodes].join(',');
  try{const data=await jsonpRequest('https://push2.eastmoney.com/api/qt/ulist.np/get?fltt=2&fields=f2,f3,f12,f14&secids='+allCodes,8000);if(data&&data.data&&data.data.diff){data.data.diff.forEach(item=>{realtimeData[item.f12]={price:item.f2,pct:item.f3};});updateRealtimeDisplay();}}catch(e){console.log('Realtime fetch skipped:',e.message);}
}
function updateRealtimeDisplay(){
  let u=0;
  scanData.forEach(s=>{const rt=realtimeData[s.code];if(rt){s.latest_price=rt.price;s.pct_change=rt.pct;u++;}});
  watchList.filter(c=>!scanData.find(s=>s.code===c)).forEach(c=>{if(realtimeData[c])u++;});
  if(u>0){
    renderTicker();renderTable();evaluateTracked();renderPlaybook();
    const active=document.querySelector('.page-section.active');
    if(active&&active.id==='layer-watch'){updateWatchlistPrices();refreshT0Prices();}
    if(active&&active.id==='layer4'){renderCalibration();}
    document.getElementById('lastUpdate').textContent=new Date().toLocaleTimeString('zh-CN');
  }
}
function updateWatchlistPrices(){
  watchList.forEach(code=>{
    const rt=realtimeData[code];if(!rt)return;
    const pe=document.getElementById('wlPrice_'+code);
    const pce=document.getElementById('wlPct_'+code);
    const color='var(--'+(rt.pct>=0?'up':'down')+')';
    if(pe){pe.textContent=rt.price.toFixed(2);pe.style.color=color;}
    if(pce){pce.textContent=(rt.pct>=0?'+':'')+rt.pct.toFixed(2)+'%';pce.style.color=color;}
  });
}

// ===== Sentiment Cycle Engine (KPL-style) =====
const CYCLE_STAGES={
  bing:{name:'冰点期',cls:'s-bing',color:'#4d9fff',advice:'涨停寥寥、连板断层，市场缺乏赚钱效应。<b>策略：空仓观望为主</b>，仅跟踪率先逆势放量的首板，等待转暖信号（涨停回升至40家+、出现3连板）再介入。',pos:12},
  huinuan:{name:'回暖期',cls:'s-huinuan',color:'#22c55e',advice:'涨停回升、出现空间板，资金开始试错。<b>策略：轻仓(2-3成)参与率先启动的主线龙头</b>，首板/2板低吸为主，快进快出，不见合力不重仓。',pos:32},
  fajiao:{name:'发酵期',cls:'s-fajiao',color:'#ffc83d',advice:'主线明确、梯队完整、封板质量高。<b>策略：聚焦核心主线顺势加仓(5-7成)</b>，持有龙头为主，分歧低吸为辅；3板以上不追高，等分时回踩确认。',pos:55},
  gaochao:{name:'高潮期',cls:'s-gaochao',color:'#ef4444',advice:'涨停潮、高位板密集、情绪亢奋。<b>策略：持股为主勿追板(仓位上限7成)</b>，挂好移动止盈；警惕量能极致+高位炸板=见顶前兆，备好撤退计划。',pos:78},
  fenqi:{name:'分歧期',cls:'s-fenqi',color:'#ff9500',advice:'炸板率抬升、高位板松动，多空剧烈换手。<b>策略：只做核心龙头的弱转强低吸(降半仓)</b>，非核心杂毛一律回避；断板/尾盘漏单坚决走。',pos:62},
  tuichao:{name:'退潮期',cls:'s-tuichao',color:'#16a34a',advice:'跌停增多、涨停骤减、高位股批量补跌。<b>策略：清仓防守，绝不抄底高位股</b>；等待冰点信号（连板高度≤2、跌停>涨停）出现后再谈新周期。',pos:18}
};
let emoPools={zt:[],dt:[],zb:[],ok:false};
let cycleState=null;
function getSH(){const rt=realtimeIndices['000001'];if(rt&&rt.pct!==undefined)return rt.pct;return (marketTrend&&marketTrend.index_change)||0;}
function getRegime(){const rm=riskMetrics||{};return rm.market_regime||'震荡';}
async function fetchEmotionPools(){
  const base='https://push2ex.eastmoney.com/';
  const ut='ut=7eea3edcaed734bea9cbfc24409ed989&dpt=wz.ztzt';
  try{
    const [zt,dt,zb]=await Promise.all([
      jsonpRequest(base+'getTopicZTPool?'+ut+'&Pageindex=0&pagesize=500&sort=fbt%3Aasc',9000),
      jsonpRequest(base+'getTopicDTPool?'+ut+'&Pageindex=0&pagesize=100&sort=fund%3Aasc',9000),
      jsonpRequest(base+'getTopicZBPool?'+ut+'&Pageindex=0&pagesize=300&sort=fbt%3Aasc',9000)
    ]);
    if(zt&&zt.data&&zt.data.pool)emoPools.zt=zt.data.pool;else emoPools.zt=[];
    if(dt&&dt.data&&dt.data.pool)emoPools.dt=dt.data.pool;else emoPools.dt=[];
    if(zb&&zb.data&&zb.data.pool)emoPools.zb=zb.data.pool;else emoPools.zb=[];
    emoPools.ok=emoPools.zt.length>0||emoPools.dt.length>0;
    computeEmotionCycle();
    renderEmotionCycle();renderLadder();renderPlaybook();renderSignalCenter();
  }catch(e){console.log('Emotion pools fetch skipped:',e.message);
    // Fallback: use market trend to create basic cycleState
    const mt=marketTrend||{};const shPct=mt.index_change||0;
    let fStage='huinuan';let fScore=40;
    if(mt.trend==='上行'){fStage='fajiao';fScore=65;}
    else if(mt.trend==='下行'||mt.is_downtrend){fStage='bing';fScore=25;}
    else if(mt.is_severe_downtrend){fStage='tuichao';fScore=15;}
    if(!cycleState){cycleState={score:fScore,stage:fStage,st:CYCLE_STAGES[fStage],ztC:0,dtC:0,zbC:0,zhaBanLv:0,height:0,lbRate:0,fundTotal:0,shPct:shPct};}
    renderEmotionCycle();renderLadder();renderPlaybook();renderSignalCenter();}
}
function computeEmotionCycle(){
  const ztC=emoPools.zt.length,dtC=emoPools.dt.length,zbC=emoPools.zb.length;
  let height=0,lbC=0,fundTotal=0;
  emoPools.zt.forEach(s=>{const lb=s.lbc||1;if(lb>height)height=lb;if(lb>=2)lbC++;fundTotal+=(s.fund||0);});
  const zhaBanLv=(ztC+zbC)>0?zbC/(ztC+zbC)*100:0;
  const lbRate=ztC>0?lbC/ztC*100:0;
  const shPct=getSH();
  let score=Math.min(ztC/100,1)*32+Math.min(height/8,1)*18+(1-Math.min(zhaBanLv/50,1))*12+Math.min(lbRate/60,1)*14+Math.max(0,Math.min((shPct+1.5)/3.5,1))*14-Math.min(dtC/20,1)*8;
  score=Math.max(0,Math.min(100,Math.round(score)));
  let stage;
  if(dtC>=15||shPct<=-1.5)stage='tuichao';
  else if(zhaBanLv>=35&&score>=45)stage='fenqi';
  else if(score>=68||height>=7)stage='gaochao';
  else if(score>=48)stage='fajiao';
  else if(score>=30)stage='huinuan';
  else stage='bing';
  cycleState={score:score,stage:stage,st:CYCLE_STAGES[stage],ztC:ztC,dtC:dtC,zbC:zbC,zhaBanLv:zhaBanLv,height:height,lbRate:lbRate,fundTotal:fundTotal/1e8,shPct:shPct};
}
function renderEmotionCycle(){
  const box=document.getElementById('cycleBox');if(!box)return;
  if(!cycleState){box.innerHTML='<div style="text-align:center;padding:40px;color:var(--muted);font-size:13px;">正在获取实时涨停数据（东财涨停池/跌停池/炸板池）...</div>';return;}
  const c=cycleState,st=c.st;
  const cu=document.getElementById('cycleUpdate');if(cu)cu.textContent='更新 '+new Date().toLocaleTimeString('zh-CN');
  let m='<div class="cycle-header"><div class="cycle-stage '+st.cls+'">'+st.name+'</div><div style="text-align:right;"><div style="font-size:28px;font-weight:800;color:'+st.color+';">'+c.score+'<span style="font-size:12px;color:var(--muted);"> /100</span></div><div style="font-size:11px;color:var(--muted);">情绪温度计</div></div></div>';
  m+='<div class="pb-bar" style="margin-top:10px;"><div style="width:'+c.score+'%;"></div></div>';
  const idx=['bing','tuichao','huinuan','fajiao','fenqi','gaochao'];
  m+='<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--muted);margin-top:4px;">'+idx.map(k=>'<span style="'+(k===c.stage?'color:'+CYCLE_STAGES[k].color+';font-weight:700;':'')+'">'+CYCLE_STAGES[k].name+'</span>').join('')+'</div>';
  m+='<div class="cycle-metrics">';
  m+='<div class="cycle-metric"><div class="v" style="color:var(--up);">'+c.ztC+'</div><div class="l">涨停</div></div>';
  m+='<div class="cycle-metric"><div class="v" style="color:var(--down);">'+c.dtC+'</div><div class="l">跌停</div></div>';
  m+='<div class="cycle-metric"><div class="v" style="color:var(--orange);">'+c.zbC+'</div><div class="l">炸板</div></div>';
  m+='<div class="cycle-metric"><div class="v" style="color:'+(c.zhaBanLv<25?'var(--up)':(c.zhaBanLv<40?'var(--gold)':'var(--down)'))+';">'+c.zhaBanLv.toFixed(0)+'%</div><div class="l">炸板率</div></div>';
  m+='<div class="cycle-metric"><div class="v" style="color:var(--gold);">'+c.height+'板</div><div class="l">空间高度</div></div>';
  m+='<div class="cycle-metric"><div class="v">'+c.lbRate.toFixed(0)+'%</div><div class="l">连板率</div></div>';
  m+='<div class="cycle-metric"><div class="v" style="color:'+(c.shPct>=0?'var(--up)':'var(--down)')+';">'+(c.shPct>=0?'+':'')+c.shPct.toFixed(2)+'%</div><div class="l">上证</div></div>';
  m+='<div class="cycle-metric"><div class="v">'+c.fundTotal.toFixed(0)+'亿</div><div class="l">封单总额</div></div>';
  m+='</div>';
  m+='<div class="cycle-advice"><b style="color:'+st.color+';">【'+st.name+'·操作指引】</b>'+st.advice+'</div>';
  box.innerHTML=m;
}
function renderLadder(){
  const box=document.getElementById('ladderBox');if(!box)return;
  if(!emoPools.ok){box.innerHTML='<div style="text-align:center;padding:40px;color:var(--muted);font-size:13px;">正在获取涨停梯队数据...</div>';return;}
  const groups={};
  emoPools.zt.forEach(s=>{const lb=s.lbc||1;if(!groups[lb])groups[lb]=[];groups[lb].push(s);});
  const lbs=Object.keys(groups).map(Number).sort((a,b)=>b-a);
  if(!lbs.length){box.innerHTML='<div style="text-align:center;padding:40px;color:var(--muted);">今日暂无涨停数据</div>';return;}
  let html='';
  lbs.forEach(lb=>{
    const arr=groups[lb].sort((a,b)=>(b.fund||0)-(a.fund||0));
    const cls=lb>=5?'hi':(lb>=3?'mid':'lo');
    const stocks=arr.slice(0,10).map(s=>{
      const days=s.zttj&&s.zttj.days?s.zttj.days+'天'+(s.zttj.ct||lb)+'板':'';
      const tip=s.n+' '+s.c+' | '+(days||lb+'连板')+' | 封单'+((s.fund||0)/1e8).toFixed(1)+'亿 | '+(s.hybk||'')+(s.zbc?' | 炸板'+s.zbc+'次':'');
      return '<span class="ladder-stock" title="'+tip+'" onclick="showZtInfo(\''+s.c+'\')">'+s.n+(lb>=3?'<span class="lb">'+lb+'</span>':'')+'</span>';
    }).join('');
    html+='<div class="ladder-row"><span class="ladder-lb '+cls+'">'+lb+'板</span><span class="ladder-cnt">'+arr.length+'只</span><div class="ladder-stocks">'+stocks+(arr.length>10?'<span style="font-size:10px;color:var(--muted);align-self:center;">+'+(arr.length-10)+'</span>':'')+'</div></div>';
  });
  box.innerHTML=html;
}
function showZtInfo(code){
  const s=scanData.find(x=>x.code===code);
  if(s){showDetail(code);return;}
  const z=emoPools.zt.find(x=>x.c===code)||emoPools.zb.find(x=>x.c===code);
  if(!z)return;
  const price=(z.p||0)/1000;
  const days=z.zttj&&z.zttj.days?z.zttj.days+'天'+z.zttj.ct+'板':(z.lbc||1)+'连板';
  let html='<div style="padding:8px 4px;"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;"><div><span style="font-size:22px;font-weight:800;">'+z.n+'</span> <span style="color:var(--muted);">'+z.c+'</span></div><span class="badge buy">'+days+'</span></div>';
  html+='<div class="dt-grid">';
  html+='<div class="dt-cell"><div class="v pu">'+price.toFixed(2)+'</div><div class="l">现价</div></div>';
  html+='<div class="dt-cell"><div class="v pu">'+(z.zdp||0).toFixed(2)+'%</div><div class="l">涨跌幅</div></div>';
  html+='<div class="dt-cell"><div class="v">'+((z.fund||0)/1e8).toFixed(2)+'亿</div><div class="l">封单资金</div></div>';
  html+='<div class="dt-cell"><div class="v">'+(z.zbc||0)+'</div><div class="l">炸板次数</div></div>';
  html+='<div class="dt-cell"><div class="v gold">'+(z.hybk||'--')+'</div><div class="l">所属板块</div></div>';
  html+='<div class="dt-cell"><div class="v">'+(z.hs||0).toFixed(1)+'%</div><div class="l">换手率</div></div>';
  html+='</div>';
  if(cycleState)html+='<div class="watch-advice">当前情绪周期：<b style="color:'+cycleState.st.color+';">'+cycleState.st.name+'('+cycleState.score+'分)</b> — '+cycleState.st.advice+'</div>';
  html+='<div style="margin-top:10px;font-size:11px;color:var(--muted);">该股不在Top50扫描池中，如需完整AI推演请在搜索框添加关注后查看。</div></div>';
  document.getElementById('detailContent').innerHTML=html;openOverlay('detailOverlay');
}

// ===== Trading Playbook =====
function suggestPosition(){
  if(!cycleState)return {pos:30,txt:'情绪数据加载中，默认保守仓位'};
  const c=cycleState;let pos=Math.round(c.score*0.9);
  const reg=getRegime();
  if(reg==='空头')pos=Math.round(pos*0.4);
  else if(reg==='震荡')pos=Math.round(pos*0.75);
  pos=Math.max(0,Math.min(80,pos));
  let txt;
  if(c.stage==='gaochao')txt='情绪高潮，可持股但不再新增追高仓位';
  else if(c.stage==='fajiao')txt='顺势阶段，围绕主线龙头分批配置';
  else if(c.stage==='fenqi')txt='分歧期降杠杆，只保留核心龙头';
  else if(c.stage==='tuichao')txt='退潮期以现金为王，严控回撤';
  else if(c.stage==='huinuan')txt='回暖试错期，小仓位验证手感';
  else txt='冰点期观望为主，保留弹药等右侧';
  if(reg==='空头')txt+='；注意：大盘处于空头趋势，整体仓位需再打折扣';
  return {pos:pos,txt:txt};
}
function renderPlaybook(){
  const grid=document.getElementById('playbookGrid');if(!grid)return;
  const reg=getRegime();const c=cycleState;const sp=suggestPosition();
  const mt=marketTrend||{};
  // Card 1: market state
  let h='<div class="pb-card"><h4><span style="width:3px;height:14px;background:var(--blue);border-radius:2px;display:inline-block;"></span>市场状态</h4>';
  h+='<div class="pb-row"><span>大盘趋势</span><b style="color:'+(reg==='多头'?'var(--up)':(reg==='空头'?'var(--down)':'var(--gold)'))+';">'+reg+'</b></div>';
  h+='<div class="pb-row"><span>上证 MA5 / MA20</span><b>'+(mt.ma5?mt.ma5.toFixed(0):'--')+' / '+(mt.ma20?mt.ma20.toFixed(0):'--')+'</b></div>';
  h+='<div class="pb-row"><span>情绪周期</span><b style="color:'+(c?c.st.color:'var(--muted)')+';">'+(c?c.st.name+'('+c.score+'分)':'加载中')+'</b></div>';
  h+='<div class="pb-row"><span>涨停/跌停/炸板</span><b>'+(c?c.ztC+' / '+c.dtC+' / '+c.zbC:'--')+'</b></div>';
  h+='<div class="pb-row"><span>空间高度</span><b>'+(c?c.height+'板':'--')+'</b></div></div>';
  // Card 2: position
  h+='<div class="pb-card"><h4><span style="width:3px;height:14px;background:var(--gold);border-radius:2px;display:inline-block;"></span>仓位建议</h4>';
  h+='<div style="font-size:34px;font-weight:800;color:'+(sp.pos>=50?'var(--up)':(sp.pos>=25?'var(--gold)':'var(--down)'))+';">'+sp.pos+'<span style="font-size:13px;color:var(--muted);">%上限</span></div>';
  h+='<div class="pb-bar"><div style="width:'+sp.pos+'%;"></div></div>';
  h+='<div class="pb-disc" style="margin-top:8px;">'+sp.txt+'</div></div>';
  // Card 3: discipline
  h+='<div class="pb-card"><h4><span style="width:3px;height:14px;background:var(--down);border-radius:2px;display:inline-block;"></span>操作纪律（铁律）</h4><div class="pb-disc">';
  h+='1. <b>止损铁律</b>：破位即走，单笔亏损不超过总资金2%，绝不扛单补仓。<br>';
  h+='2. <b>仓位约束</b>：单票不超过'+(sp.pos>=50?'20':(sp.pos>=25?'15':'10'))+'%总仓位，同板块不超过2只。<br>';
  h+='3. <b>周期纪律</b>：'+(c?('<b style="color:'+c.st.color+';">'+c.st.name+'</b>，'+(c.stage==='gaochao'?'只止盈不开新仓':(c.stage==='fajiao'?'顺势持有，回踩加仓':(c.stage==='fenqi'?'弱转强才做，断板即走':(c.stage==='tuichao'?'空仓等待，不抄底':'轻仓试错，严格止损'))))):'按信号分级执行')+'。<br>';
  h+='4. <b>复盘要求</b>：每日收盘后核对信号兑现情况，学习中心自动校准胜率权重。</div></div>';
  // Card 4: today focus
  h+='<div class="pb-card"><h4><span style="width:3px;height:14px;background:var(--up);border-radius:2px;display:inline-block;"></span>今日焦点（A级共振优选）</h4><div class="pb-disc">';
  const aSigs=buySignals.map(s=>({s:s,cf:confluenceInfo(s)})).filter(x=>x.cf.grade==='A').sort((a,b)=>b.cf.score-a.cf.score).slice(0,3);
  if(aSigs.length){
    aSigs.forEach(x=>{
      const s=x.s;const cf=x.cf;
      h+='<div style="margin-bottom:6px;"><b style="color:var(--up);cursor:pointer;" onclick="showDetail(\''+s.code+'\')">'+s.name+'</b> <span class="grade-badge grade-'+cf.grade+'">共振'+cf.score+'分</span><br><span style="font-size:11px;">入场 '+s.entry_price.toFixed(2)+' · 止损 '+s.stop_loss.toFixed(2)+' · 目标 '+s.target_price.toFixed(2)+' · 建议仓 '+Math.min(s.position_size,sp.pos>=50?20:15)+'%</span></div>';
    });
  }else{
    h+='<div style="color:var(--muted);">当前无A级共振信号'+(c&&emoPools.ok?'（情绪周期'+c.st.name+'，信号需等待共振确认）':'（情绪数据加载中，稍候重试）')+'</div>';
    const bSigs=buySignals.map(s=>({s:s,cf:confluenceInfo(s)})).filter(x=>x.cf.grade==='B').sort((a,b)=>b.cf.score-a.cf.score).slice(0,2);
    bSigs.forEach(x=>{const s=x.s;h+='<div style="margin-bottom:6px;"><b style="color:var(--gold);cursor:pointer;" onclick="showDetail(\''+s.code+'\')">'+s.name+'</b> <span class="grade-badge grade-B">B级观察</span> <span style="font-size:11px;">入场 '+s.entry_price.toFixed(2)+' / 止损 '+s.stop_loss.toFixed(2)+'</span></div>';});
  }
  h+='</div></div>';
  grid.innerHTML=h;
}

// ===== Confluence Engine (win-rate booster) =====
let calibFactor=1.0;
function confluenceInfo(sig){
  const sd=scanData.find(x=>x.code===sig.code)||{};
  const sq=sd.signal_quality||50;
  const wrf=sig.wr_filtered||0;
  const pot=sig.target_price?((sig.target_price-sig.entry_price)/sig.entry_price*100):0;
  const risk=sig.stop_loss?((sig.entry_price-sig.stop_loss)/sig.entry_price*100):0;
  const rr=risk>0?pot/risk:0;
  const vol=sd.bt_vol_ratio||1;
  const adx=sd.bt_adx||0;
  const reg=getRegime();
  const stage=cycleState?cycleState.stage:null;
  const sec=sectorData.find(x=>x.name===sd.industry);
  const secSent=sec?sec.avg_sentiment:0;
  const factors=[];let score=0;
  const f=(name,on,pts)=>{factors.push({name:name,on:on,pts:pts});if(on)score+=pts;};
  f('量化胜率≥55',wrf>=55,wrf>=60?16:12);
  f('信号质量≥60',sq>=60,sq>=70?14:10);
  f('盈亏比≥1.5',rr>=1.5,rr>=2?16:12);
  f('大盘多头',reg==='多头',reg==='多头'?14:7);
  f('情绪顺周期',stage?['fajiao','gaochao','huinuan'].includes(stage):true,stage?(['fajiao','gaochao'].includes(stage)?12:(stage==='huinuan'?8:0)):5);
  f('量能确认',vol>=1.2&&vol<=3,8);
  f('趋势ADX≥20',adx>=20,8);
  f('板块共振',secSent>=0.15,10);
  score=Math.round(score*calibFactor);
  score=Math.max(0,Math.min(100,score));
  const grade=score>=75?'A':(score>=58?'B':'C');
  return {score:score,grade:grade,factors:factors,rr:rr};
}

// ===== 需求1: 涨跌分布 + 今日炒什么 + T+0机会 =====
function renderUpDownDist(){
  const el=document.getElementById('upDownDist');if(!el)return;
  let bull=0,bear=0,neutral=0;
  scanData.forEach(s=>{if(s.sentiment_score>=0.2)bull++;else if(s.sentiment_score<=-0.2)bear++;else neutral++;});
  const total=Math.max(scanData.length,1);
  const bullPct=(bull/total*100).toFixed(1),bearPct=(bear/total*100).toFixed(1),neuPct=(neutral/total*100).toFixed(1);
  el.innerHTML='<div style="display:flex;height:32px;border-radius:8px;overflow:hidden;margin-bottom:10px;">'+
    '<div style="width:'+bullPct+'%;background:var(--up);display:flex;align-items:center;justify-content:center;font-size:12px;color:#fff;font-weight:600;">'+bull+' 涨 '+bullPct+'%</div>'+
    '<div style="width:'+neuPct+'%;background:var(--bg4);display:flex;align-items:center;justify-content:center;font-size:11px;color:var(--muted);">'+neutral+' 平</div>'+
    '<div style="width:'+bearPct+'%;background:var(--down);display:flex;align-items:center;justify-content:center;font-size:12px;color:#fff;font-weight:600;">'+bear+' 跌 '+bearPct+'%</div>'+
    '</div><div style="display:flex;justify-content:space-between;font-size:11px;color:var(--muted);"><span>看涨 '+bull+'只</span><span>中性 '+neutral+'只</span><span>看跌 '+bear+'只</span><span>合计 '+total+'只</span></div>';
}
function renderTodayHot(){
  const el=document.getElementById('todayHot');if(!el)return;
  // 分析板块强度和概念热度
  let h='';
  // 板块方向
  const topSec=sectorData.slice().sort((a,b)=>b.avg_sentiment-a.avg_sentiment).slice(0,5);
  if(topSec.length){h+='<div style="margin-bottom:8px;"><b style="color:var(--up);">板块方向：</b>';topSec.forEach(s=>{h+='<span style="display:inline-block;padding:2px 8px;margin:2px;border-radius:6px;background:'+((s.avg_sentiment>=0)?'var(--up-bg)':'var(--down-bg)')+';color:'+((s.avg_sentiment>=0)?'var(--up)':'var(--down)')+';font-size:12px;">'+s.name+' '+s.avg_sentiment.toFixed(2)+'</span>';});h+='</div>';}
  // 买入信号方向
  const buys=buySignals.slice().sort((a,b)=>b.win_rate-a.win_rate);
  if(buys.length){h+='<div style="margin-bottom:8px;"><b style="color:var(--gold);">信号精选：</b>';buys.slice(0,5).forEach(s=>{h+='<span style="display:inline-block;padding:2px 8px;margin:2px;border-radius:6px;background:var(--up-bg);color:var(--up);font-size:12px;">'+s.name+' 胜率'+s.win_rate.toFixed(0)+'%</span>';});h+='</div>';}
  // 概念方向
  const concepts={};scanData.forEach(s=>{s.concepts.forEach(c=>{if(!concepts[c])concepts[c]=0;concepts[c]++;});});
  const topCon=Object.entries(concepts).sort((a,b)=>b[1]-a[1]).slice(0,5);
  if(topCon.length){h+='<div><b style="color:var(--blue);">热门概念：</b>';topCon.forEach(c=>{h+='<span style="display:inline-block;padding:2px 8px;margin:2px;border-radius:6px;background:rgba(77,159,255,.1);color:var(--blue);font-size:12px;">'+c[0]+' ('+c[1]+')</span>';});h+='</div>';}
  el.innerHTML=h||'<div style="color:var(--muted);text-align:center;padding:20px;">暂无数据</div>';
}
function renderT0List(){
  const el=document.getElementById('t0List');if(!el)return;
  // T+0机会：高换手率 + 日内波动大 + 量比>1.5
  const t0=scanData.filter(s=>{
    const vol=s.bt_vol_ratio||1;const atr=s.bt_atr_pct||2;const rsi=s.bt_rsi||50;
    return vol>=1.2&&atr>=2.5&&rsi>=40&&rsi<=70;
  }).sort((a,b)=>(b.bt_vol_ratio||0)-(a.bt_vol_ratio||0)).slice(0,10);
  if(!t0.length){el.innerHTML='<div style="color:var(--muted);text-align:center;padding:20px;">暂无符合条件的T+0标的</div>';return;}
  el.innerHTML='<table style="width:100%;"><thead><tr><th>代码</th><th>名称</th><th>量比</th><th>ATR波幅</th><th>RSI</th><th>布林位置</th><th>建议</th></tr></thead><tbody>'+
    t0.map(s=>{
      const bb=((s.bt_bb_pos||0.5)*100).toFixed(0);
      const advice=s.bt_rsi<50?'低吸高抛':'持股待涨';
      return '<tr><td>'+s.code+'</td><td style="cursor:pointer;color:var(--up);" onclick="showDetail(\''+s.code+'\')">'+s.name+'</td><td class="pu">'+(s.bt_vol_ratio||0).toFixed(2)+'</td><td>'+(s.bt_atr_pct||0).toFixed(2)+'%</td><td>'+(s.bt_rsi||0).toFixed(0)+'</td><td>'+bb+'%</td><td style="font-size:11px;color:var(--gold);">'+advice+'</td></tr>';
    }).join('')+'</tbody></table>';
}
// ===== 需求4: 全市场板块轮动 =====
function renderFullMarketRotation(){
  const el=document.getElementById('fullMarketRotation');if(!el)return;
  if(!window._fullMarketSectors){el.innerHTML='<div style="color:var(--muted);text-align:center;padding:20px;">全市场板块数据加载中...</div>';return;}
  const data=window._fullMarketSectors;
  el.innerHTML='<table style="width:100%;font-size:12px;"><thead><tr><th>板块</th><th>涨跌幅</th><th>领涨股</th><th>成交额</th><th>强度</th></tr></thead><tbody>'+
    data.map(s=>{
      const pct=s.pct_change||0;
      const cls=pct>=0?'pu':'pd';
      const str=pct>=2?'🔥强势':pct>=0?'📈偏强':pct>=-2?'📉偏弱':'❄️弱势';
      return '<tr><td style="font-weight:600;">'+s.name+'</td><td class="'+cls+'">'+(pct>=0?'+':'')+pct.toFixed(2)+'%</td><td style="font-size:11px;">'+(s.lead_stock||'--')+'</td><td>'+(s.amount||'--')+'</td><td>'+str+'</td></tr>';
    }).join('')+'</tbody></table>';
}
// ===== 需求5: 量化策略看板 =====
function renderQuantStrategy(){
  const el=document.getElementById('quantStrategyPanel');if(!el)return;
  // 基于回测数据生成策略
  const buys=buySignals.filter(s=>s.win_rate>=50&&s.profit_factor>=1.2);
  if(!buys.length){el.innerHTML='<div style="color:var(--muted);text-align:center;padding:20px;">暂无符合条件的策略信号</div>';return;}
  // 按策略类型分组
  const strategies={};
  buys.forEach(s=>{
    const sq=scanData.find(x=>x.code===s.code)||{};
    const ind=sq.industry||'未知';
    if(!strategies[ind])strategies[ind]={stocks:[],avgWr:0,avgPf:0};
    strategies[ind].stocks.push(s);
    strategies[ind].avgWr+=s.win_rate;
    strategies[ind].avgPf+=s.profit_factor;
  });
  Object.keys(strategies).forEach(k=>{strategies[k].avgWr/=strategies[k].stocks.length;strategies[k].avgPf/=strategies[k].stocks.length;});
  const sorted=Object.entries(strategies).sort((a,b)=>b[1].avgWr-a[1].avgWr);
  el.innerHTML=sorted.map(([ind,d])=>{
    const grade=d.avgWr>=60?'A':d.avgWr>=52?'B':'C';
    const gradeColor=grade==='A'?'var(--up)':grade==='B'?'var(--gold)':'var(--muted)';
    return '<div style="background:var(--bg3);border-radius:10px;padding:14px;border-left:3px solid '+gradeColor+';">'+
      '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">'+
      '<div><b style="font-size:14px;">'+ind+'板块策略</b> <span style="font-size:11px;color:'+gradeColor+';margin-left:8px;">'+grade+'级</span></div>'+
      '<div style="font-size:12px;color:var(--muted);">平均胜率 '+d.avgWr.toFixed(1)+'% | 盈亏比 '+d.avgPf.toFixed(2)+'</div></div>'+
      '<div style="display:flex;flex-wrap:wrap;gap:6px;">'+
      d.stocks.map(s=>'<span style="display:inline-block;padding:3px 8px;border-radius:6px;background:var(--up-bg);color:var(--up);font-size:11px;cursor:pointer;" onclick="showDetail(\''+s.code+'\')">'+s.name+' '+s.signal+'</span>').join('')+
      '</div><div style="margin-top:8px;font-size:11px;color:var(--muted);">策略逻辑：板块内多只个股触发看涨信号+回测胜率≥50%+盈亏比≥1.2 → 板块级共振买入</div></div>';
  }).join('');
}
// ===== 需求6: 指标说明参考面板 =====
function renderIndRef(){
  const el=document.getElementById('indRefPanel');if(!el)return;
  const inds=Object.entries(indicatorCounts).sort((a,b)=>b[1]-a[1]);
  if(!inds.length){el.innerHTML='暂无指标数据';return;}
  const info=indicatorInfo||{};
  el.innerHTML=inds.map(([name,cnt])=>{
    const d=info[name]||{};
    return '<div style="padding:4px 0;border-bottom:1px solid var(--rule);"><b style="color:var(--ink);">'+name+'</b> <span style="color:var(--muted);font-size:11px;">['+(d.type||'')+']</span> — '+(d.desc||'')+ '</div>';
  }).join('');
}

// ===== AI Deep-Analysis Engine =====
function aiAnalyze(s){
  const adx=s.bt_adx||0,rsi=s.bt_rsi||50,vol=s.bt_vol_ratio||1,bb=s.bt_bb_pos||0.5,atr=s.bt_atr_pct||2;
  const sec=sectorData.find(x=>x.name===s.industry);
  const secSent=sec?sec.avg_sentiment:0;
  const stage=cycleState?cycleState.stage:null;
  const sq=s.signal_quality||50;
  const steps=[];
  const dim=(k,d,sc)=>{steps.push({k:k,d:d,score:sc});};
  // 1 Trend
  if(adx>=30)dim('趋势强度','ADX='+adx.toFixed(0)+'，趋势强劲，顺势操作胜率最高',90);
  else if(adx>=20)dim('趋势强度','ADX='+adx.toFixed(0)+'，趋势正在形成，可积极参与',70);
  else dim('趋势强度','ADX='+adx.toFixed(0)+'，盘整格局，等待方向选择',42);
  // 2 Momentum
  if(rsi>=50&&rsi<=70)dim('动量状态','RSI='+rsi.toFixed(0)+'，多头动量健康，持有为主',85);
  else if(rsi>70&&rsi<=80)dim('动量状态','RSI='+rsi.toFixed(0)+'，偏强但临近超买，注意冲高节奏',60);
  else if(rsi>80)dim('动量状态','RSI='+rsi.toFixed(0)+'，超买区，追高风险大',38);
  else if(rsi>=40)dim('动量状态','RSI='+rsi.toFixed(0)+'，动量中性，需量能配合',52);
  else dim('动量状态','RSI='+rsi.toFixed(0)+'，弱势区间，反弹以减仓思路对待',32);
  // 3 Volume
  if(vol>=1.2&&vol<=3)dim('量能确认','量比='+vol.toFixed(2)+'，温和放量，资金介入迹象明显',85);
  else if(vol>3&&vol<=5)dim('量能确认','量比='+vol.toFixed(2)+'，放量偏大，防冲高回落',62);
  else if(vol>5)dim('量能确认','量比='+vol.toFixed(2)+'，天量过热，警惕情绪透支',40);
  else if(vol>=0.8)dim('量能确认','量比='+vol.toFixed(2)+'，量能温和，突破需补量',55);
  else dim('量能确认','量比='+vol.toFixed(2)+'，缩量整理，方向未明',38);
  // 4 Bollinger
  if(bb>=0.5&&bb<=0.85)dim('价格位置','布林位置'+(bb*100).toFixed(0)+'%，处于主升通道，趋势与位置俱佳',85);
  else if(bb>0.85&&bb<=0.98)dim('价格位置','布林位置'+(bb*100).toFixed(0)+'%，接近上轨，强势但注意节奏',60);
  else if(bb>0.98)dim('价格位置','布林位置'+(bb*100).toFixed(0)+'%，上轨之外，短期过热',38);
  else if(bb>=0.3)dim('价格位置','布林位置'+(bb*100).toFixed(0)+'%，中轨附近，方向待选择',55);
  else dim('价格位置','布林位置'+(bb*100).toFixed(0)+'%，下轨低位，左侧区域需耐心',45);
  // 5 Volatility
  if(atr<3)dim('波动风险','ATR波幅'+atr.toFixed(1)+'%，波动可控，适合标准仓位',85);
  else if(atr<5)dim('波动风险','ATR波幅'+atr.toFixed(1)+'%，中等波动，仓位适当降低',65);
  else if(atr<8)dim('波动风险','ATR波幅'+atr.toFixed(1)+'%，高波动，建议减半仓位并放宽容忍',42);
  else dim('波动风险','ATR波幅'+atr.toFixed(1)+'%，极端波动，仅适合极小仓位博弈',25);
  // 6 Sector
  if(secSent>=0.15)dim('板块共振','所属板块['+s.industry+']平均情绪+'+secSent.toFixed(2)+'，板块合力向上',85);
  else if(secSent<=-0.15)dim('板块共振','所属板块['+s.industry+']平均情绪'+secSent.toFixed(2)+'，板块走弱形成拖累',30);
  else dim('板块共振','所属板块['+s.industry+']情绪中性，缺乏板块合力',55);
  // 7 Cycle
  const goodSt=['fajiao','gaochao','huinuan'];
  if(goodSt.includes(stage))dim('情绪周期','当前'+(cycleState?cycleState.st.name:'')+'，赚钱效应正循环，个股胜率天然加成',85);
  else if(stage==='fenqi')dim('情绪周期','当前分歧期，仅核心龙头有持续性，需精挑细选',58);
  else dim('情绪周期','当前'+(cycleState?cycleState.st.name:'')+'，逆周期环境，等周期转暖再介入',32);
  // 8 Signal quality
  if(sq>=65)dim('信号质量','历史回测质量分'+sq.toFixed(0)+'，指标组合可靠度高',88);
  else if(sq>=50)dim('信号质量','历史回测质量分'+sq.toFixed(0)+'，中等可靠，需共振确认',58);
  else dim('信号质量','历史回测质量分'+sq.toFixed(0)+'，历史表现不佳，谨慎对待',35);
  const weights=[.15,.12,.13,.10,.08,.12,.15,.15];
  let total=0;steps.forEach((st,i)=>{total+=st.score*weights[i];});
  total=Math.round(total);
  let verdict,cls,advice;
  const stageTxt=cycleState?cycleState.st.name:'当前周期';
  if(total>=72){verdict='强烈关注';cls='v-strong';advice='多维共振向好，若'+stageTxt+'延续，可按计划分批介入：入场参考 '+(s.entry_price?s.entry_price.toFixed(2):'--')+'，止损 '+(s.stop_loss?s.stop_loss.toFixed(2):'--')+'，目标 '+(s.target_price?s.target_price.toFixed(2):'--')+'。';}
  else if(total>=58){verdict='重点关注';cls='v-watch';advice='多数维度偏多但存在短板('+steps.filter(x=>x.score<50).map(x=>x.k).join('/')+')，建议等待量价齐升确认后再介入，或以半仓试探。';}
  else if(total>=45){verdict='观望等待';cls='v-wait';advice='多空交织，安全边际不足。放入自选观察，等待关键维度（量能/板块共振）转好。';}
  else{verdict='建议回避';cls='v-avoid';advice='多项维度转弱，尤其'+steps.slice().sort((a,b)=>a.score-b.score)[0].k+'拖累明显，不建议参与，持仓者反弹减仓。';}
  return {score:total,verdict:verdict,cls:cls,steps:steps,advice:advice};
}
function aiAnalyzeHtml(s){
  const ai=aiAnalyze(s);
  let h='<div class="ai-box"><div class="ai-head"><div><span style="font-weight:800;color:var(--purple);">AI深度推演</span> <span style="font-size:10px;color:var(--muted);">规则×量化融合引擎 · 8维推理链</span></div><span class="ai-verdict '+ai.cls+'">'+ai.verdict+'</span></div>';
  h+='<div class="ai-chain">';
  ai.steps.forEach(st=>{
    const col=st.score>=70?'var(--up)':(st.score>=50?'var(--gold)':'var(--down)');
    h+='<div class="ai-step"><span class="k" style="color:'+col+';">'+st.k+'</span><span class="d">'+st.d+'<b style="color:'+col+';float:right;">'+st.score+'</b></span></div>';
  });
  h+='</div>';
  h+='<div class="ai-score-bar"><span style="font-size:11px;color:var(--muted);">综合评分</span><span class="ai-score" style="color:'+(ai.score>=72?'var(--up)':(ai.score>=58?'var(--gold)':(ai.score>=45?'var(--muted)':'var(--down)')))+';">'+ai.score+'</span><div style="flex:1;height:8px;border-radius:4px;background:var(--bg4);overflow:hidden;"><div style="height:100%;width:'+ai.score+'%;border-radius:4px;background:linear-gradient(90deg,var(--down),var(--gold),var(--up));"></div></div></div>';
  h+='<div class="watch-advice" style="border-color:rgba(168,85,247,.3);"><b>操作结论：</b>'+ai.advice+'</div></div>';
  return h;
}

// ===== Signal Tracker & Calibration (self-learning v2) =====
let signalTracker={};
try{signalTracker=JSON.parse(localStorage.getItem('signal-tracker-v1')||'{}');}catch(e){signalTracker={};}
function saveTracker(){try{localStorage.setItem('signal-tracker-v1',JSON.stringify(signalTracker));}catch(e){}}
function trackSignals(){
  const today=new Date().toISOString().slice(0,10);
  let changed=false;
  buySignals.forEach(s=>{
    const cf=confluenceInfo(s);
    if(cf.grade==='C')return;
    const rec=signalTracker[s.code];
    if(rec&&rec.date===today){if(rec.score!==cf.score||rec.grade!==cf.grade){rec.score=cf.score;rec.grade=cf.grade;changed=true;}return;}
    if(rec&&rec.date!==today&&!rec.resolved){rec.resolved=true;rec.status='superseded';changed=true;}
    signalTracker[s.code]={code:s.code,name:s.name,date:today,entry:s.entry_price,stop:s.stop_loss,target:s.target_price,pred:s.wr_filtered||s.win_rate||50,grade:cf.grade,score:cf.score,resolved:false,status:'open'};
    changed=true;
  });
  if(changed)saveTracker();
}
function evaluateTracked(){
  const codes=Object.keys(signalTracker);let changed=false;
  codes.forEach(code=>{
    const r=signalTracker[code];if(r.resolved)return;
    const rt=realtimeData[code];const cur=rt?rt.price:null;
    if(cur!==null){
      if(cur>=r.target){r.resolved=true;r.status='win';r.exit=cur;changed=true;}
      else if(cur<=r.stop){r.resolved=true;r.status='lose';r.exit=cur;changed=true;}
    }
    const days=(Date.now()-new Date(r.date).getTime())/86400000;
    if(!r.resolved&&days>7){r.resolved=true;r.status=cur!==null?(cur>r.entry?'win':'lose'):'expired';changed=true;}
  });
  if(changed)saveTracker();
  // calibration factor
  const resolved=Object.values(signalTracker).filter(r=>r.resolved&&(r.status==='win'||r.status==='lose'));
  if(resolved.length>=5){
    const realized=resolved.filter(r=>r.status==='win').length/resolved.length*100;
    const predAvg=resolved.reduce((a,r)=>a+r.pred,0)/resolved.length;
    if(predAvg>0){
      const bias=realized/predAvg;
      calibFactor=Math.max(0.9,Math.min(1.1,bias));
    }
  }
}
function renderCalibration(){
  const grid=document.getElementById('calibGrid');if(!grid)return;
  const all=Object.values(signalTracker);
  const resolved=all.filter(r=>r.resolved&&(r.status==='win'||r.status==='lose'));
  const wins=resolved.filter(r=>r.status==='win').length;
  const realized=resolved.length?wins/resolved.length*100:0;
  const predAvg=resolved.length?resolved.reduce((a,r)=>a+r.pred,0)/resolved.length:0;
  const drift=resolved.length?realized-predAvg:0;
  let h='<div class="calib-cell"><div class="v blue">'+all.length+'</div><div class="l">累计追踪信号</div></div>';
  h+='<div class="calib-cell"><div class="v">'+resolved.length+'</div><div class="l">已了结</div></div>';
  h+='<div class="calib-cell"><div class="v '+(realized>=55?'up':(realized>=45?'':'down'))+'">'+(resolved.length?realized.toFixed(1)+'%':'--')+'</div><div class="l">实盘兑现胜率</div></div>';
  h+='<div class="calib-cell"><div class="v gold">'+(predAvg?predAvg.toFixed(1)+'%':'--')+'</div><div class="l">预测平均胜率</div></div>';
  h+='<div class="calib-cell"><div class="v '+(drift>=0?'up':'down')+'">'+(resolved.length?(drift>=0?'+':'')+drift.toFixed(1)+'pp':'--')+'</div><div class="l">预测偏差</div></div>';
  h+='<div class="calib-cell"><div class="v" style="color:var(--purple);">'+calibFactor.toFixed(2)+'x</div><div class="l">共振评分校准系数</div></div>';
  grid.innerHTML=h;
  const list=document.getElementById('trackList');
  if(!list)return;
  const sorted=all.sort((a,b)=>(a.resolved===b.resolved)?(b.date>a.date?1:-1):(a.resolved?1:-1)).slice(0,20);
  const stTag={win:'<span class="badge buy">达标</span>',lose:'<span class="badge sell">止损</span>',open:'<span class="badge watch">进行中</span>',expired:'<span class="badge watch">过期</span>',superseded:'<span class="badge watch">已更新</span>'};
  list.innerHTML=sorted.map(r=>{
    const cur=realtimeData[r.code]?realtimeData[r.code].price:null;
    const pct=cur!==null&&r.entry?((cur-r.entry)/r.entry*100):null;
    return '<div class="track-row"><span style="min-width:70px;font-weight:700;cursor:pointer;" onclick="showDetail(\''+r.code+'\')">'+r.name+'</span><span class="grade-badge grade-'+r.grade+'">'+r.grade+'级 '+r.score+'分</span><span style="color:var(--muted);min-width:76px;">'+r.date+'</span><span style="min-width:130px;color:var(--muted);">入'+r.entry.toFixed(2)+'/止'+r.stop.toFixed(2)+'/目'+r.target.toFixed(2)+'</span>'+(pct!==null?'<span style="color:'+(pct>=0?'var(--up)':'var(--down)')+';min-width:56px;">'+(pct>=0?'+':'')+pct.toFixed(1)+'%</span>':'<span style="min-width:56px;color:var(--muted);">--</span>')+stTag[r.status]+'</div>';
  }).join('')||'<div style="color:var(--muted);font-size:12px;padding:8px;">暂无追踪记录 — 首次打开后自动记录当日A/B级买入信号，并在后续访问中自动验证兑现情况。</div>';
}

// ===== Init =====
renderRegimeBadge();renderRiskDashboard();renderSignalCenter();
renderMarketTrend();renderL1Charts();renderRotationInsight();renderRotationSummary();renderRotationQuadrant();renderRotationLadder();renderChainGraph();renderSectorRanking();renderHeatmap();renderL2Charts();
renderRadarCharts();renderOpPanel();renderSignalMatrix();renderL3Charts();
renderLearningCenter();renderWatchlistPage();renderTable();renderIndicatorRef();renderTicker();
renderEmotionCycle();renderLadder();renderPlaybook();renderCalibration();
fetchRealtimeQuotes();fetchRealtimeIndices();fetchIndexKlines();fetchEmotionPools();
setInterval(fetchRealtimeQuotes,30000);setInterval(fetchRealtimeIndices,30000);setInterval(fetchEmotionPools,60000);
setInterval(function(){var a=document.querySelector('.page-section.active');if(a&&a.id==='layer-watch'&&watchList.length>0)loadT0ForWatchlist();},120000);
</script>
</body>
</html>'''

replacements = {
    "__SCAN_DATE__": scan_date,
    "__UNIQUE_INDICATORS__": str(unique_indicators),
    "__STOCKS_JSON__": stocks_json,
    "__PINYIN_MAP_JSON__": pinyin_map_json,
    "__INDICATOR_INFO_JSON__": indicator_info_json,
    "__INDICATOR_COUNTS_JSON__": indicator_counts_json,
    "__INDUSTRY_DIST_JSON__": industry_dist_json,
    "__CONCEPT_DIST_JSON__": concept_dist_json,
    "__SECTOR_DATA_JSON__": sector_data_json,
    "__ROTATION_DATA_JSON__": rotation_data_json,
    "__CHAIN_DATA_JSON__": chain_data_json,
    "__ROTATION_INSIGHT_JSON__": rotation_insight_json,
    "__ACTION_DIST_JSON__": action_dist_json,
    "__BUY_SIGNALS_JSON__": buy_signals_json,
    "__SELL_SIGNALS_JSON__": sell_signals_json,
    "__MARKET_TREND_JSON__": market_trend_json,
    "__AVG_WIN_RATE__": safe_str(avg_win_rate),
    "__AVG_WR_FILTERED__": safe_str(avg_wr_filtered),
    "__AVG_QUANT_SCORE__": safe_str(avg_quant_score),
    "__AVG_PF__": safe_str(avg_pf),
    "__AVG_EXP__": safe_str(avg_exp),
    "__KLINE_DATA_JSON__": kline_data_json,
    "__INDICATOR_PERF_JSON__": indicator_performance_json,
    "__LEARNING_INSIGHTS_JSON__": learning_insights_json,
    "__RISK_METRICS_JSON__": risk_metrics_json,
}
for key, val in replacements.items():
    html = html.replace(key, val)

os.makedirs(OUTPUT_DIR, exist_ok=True)
with open(OUTPUT_HTML, "w", encoding="utf-8") as f:
    f.write(html)
print(f"Report generated: {OUTPUT_HTML}")
print(f"File size: {os.path.getsize(OUTPUT_HTML)/1024:.1f} KB")
