#!/usr/bin/env python3
"""
专业交易员看板 v5 — 三层架构：宏观指数 → 板块分析 → 个股分析
增强：关注功能、板块详情、K线演示、个股详情优化
"""
import json, os, math, datetime, time
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)

def _num(v):
    try:
        return float(v)
    except (ValueError, TypeError):
        return 0.0

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

# 产业链每条边的传导逻辑说明（source→target: 传导含义 + 交易推演）
CHAIN_EDGE_NOTES = {
    ("煤炭开采", "电力"): "煤价是火电最大成本项（约占50-60%）。煤涨电未涨=电力毛利被压缩（利空电力）；煤跌电稳=电力盈利修复。",
    ("煤炭开采", "普钢"): "焦煤占炼钢成本约30%。煤涨推高钢厂成本，钢价能否顺价决定钢企毛利，顺价成功则共振上涨。",
    ("焦炭Ⅱ", "普钢"): "焦炭是炼钢第二大原料。焦炭提涨落地=成本推动钢价；钢厂减产则焦炭需求回落（反向压制）。",
    ("焦炭Ⅱ", "特钢Ⅱ"): "特钢对焦炭价格敏感度低于普钢，成本传导更依赖高端产品提价能力，观察订单价差。",
    ("化学原料", "化学制品"): "基础化工品（纯碱/烯烃等）是制品的直接原料。原料涨→制品涨价时滞约1-2季度，先压制后传导。",
    ("化学原料", "化学制药"): "原料药（API）成本受基础化工影响。原料涨压制制剂毛利，具备提价权的创新药企免疫。",
    ("化学制品", "化学制药"): "中间体→原料药→制剂的供应链传导。中间体涨价利好一体化药企，利空纯制剂厂商。",
    ("化学制品", "医疗器械"): "医用耗材（高分子材料）成本传导。集采品种无法顺价=成本上涨直接侵蚀利润。",
    ("工业金属", "通用设备"): "铜铝占机械制造直接材料成本约20-40%。金属涨价时，有订单储备的龙头可顺价，小厂承压。",
    ("工业金属", "白色家电"): "铜占家电成本约15-20%。铜涨空调涨不了价=家电毛利收缩；铜跌=盈利弹性释放。",
    ("工业金属", "消费电子"): "铜箔/铝壳用于消费电子。金属涨价对低端机型压力大，高端机型（苹果链）议价能力强。",
    ("小金属", "航空装备Ⅱ"): "钛材/高温合金是军航核心材料。小金属涨价利好军工材料环节（稀土/钛材），总装环节影响小。",
    ("小金属", "消费电子"): "稀土磁材用于声学/马达。涨价利好磁材股，成本压力由整机厂消化。",
    ("普钢", "汽车零部件"): "钢材占零部件成本约40-60%。钢涨时给主机厂供货的 Tier1 年度锁价，毛利被动收缩。",
    ("普钢", "房地产开发"): "螺纹钢是建筑主体用钢。地产开工强=钢需强（钢→地产是需求验证方向）。",
    ("普钢", "白色家电"): "板材（冷轧）用于家电外壳。钢涨压制家电毛利，尤其白电价格战时期无法顺价。",
    ("特钢Ⅱ", "汽车零部件"): "特钢（齿轮钢/轴承钢）决定零部件品质。特钢涨=高端零部件成本上行，技术壁垒高的企业可顺价。",
    ("特钢Ⅱ", "航空装备Ⅱ"): "高温合金/高强钢是军机结构件核心。军品定价成本加成模式下，特钢涨价可部分传导。",
    ("通用设备", "房地产开发"): "工程机械/电梯需求滞后地产开工约6-12个月。地产放松先利好设备订单预期。",
    ("通信设备", "消费电子"): "通信模组/射频前端是手机核心部件。5G换机周期启动=通信设备与消费电子共振。",
    ("食品加工", "专业连锁Ⅱ"): "上游食材成本决定餐饮/商超毛利。原料降价=连锁餐饮盈利弹性（逆向受益）。",
    ("白酒Ⅱ", "专业连锁Ⅱ"): "白酒是商超/烟酒店核心利润品。白酒批价坚挺=渠道利润厚，连锁动销活跃。",
    ("铁路公路", "港口航运"): "铁路疏港是港口集疏运一环。大宗运量上行时铁路与港口吞吐量同向共振。",
    ("铁路公路", "航运港口"): "公铁联运直接对接港口枢纽。货运量改善同时利好两端（量价共振）。",
    ("港口航运", "航运港口"): "同类枢纽（不同细分口径），吞吐量与费率同步变动，观察集装箱/散货结构。",
    ("证券Ⅱ", "房地产开发"): "地产融资（债券/股权）经券商投行落地。地产政策松=券商投行收入+地产融资改善双击。",
    ("多元金融", "房地产开发"): "信托/AMC参与地产纾困与并购融资。地产风险出清阶段，多元金融业务量放大。",
}

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)
sector_stage_json = safe_json_dumps(SECTOR_STAGE, ensure_ascii=False)
sector_sub_json = safe_json_dumps(SECTOR_SUB, ensure_ascii=False)
chain_edge_info_json = safe_json_dumps(
    [{"source": a, "target": b, "note": n} for (a, b), n in CHAIN_EDGE_NOTES.items()],
    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
    import urllib3
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    _sess = _req.Session()
    _sess.verify = False
    _sess.headers.update({'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', 'Referer': 'https://quote.eastmoney.com/'})
    _api_hosts = [
        'https://push2delay.eastmoney.com/api/qt/clist/get',
        'https://push2.eastmoney.com/api/qt/clist/get',
        'http://80.push2.eastmoney.com/api/qt/clist/get',
    ]
    _seen_bk = set()
    for _po in (1, 0):
        _params = {'pn': 1, 'pz': 100, 'po': _po, 'np': 1, 'fltt': 2, 'invt': 2, 'fid': 'f3',
                   'fs': 'm:90+t:2', 'fields': 'f12,f14,f3,f20,f21,f128,f136,f140'}
        for _url in _api_hosts:
            try:
                _resp = _sess.get(_url, params=_params, timeout=15)
                _data = _resp.json().get('data', {}).get('diff', [])
                if not _data:
                    continue
                for _b in _data:
                    _c = _b.get('f12', '')
                    if _c in _seen_bk:
                        continue
                    _seen_bk.add(_c)
                    full_market_sectors.append({
                        "code": _c,
                        "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 '--',
                        "mkt_cap_yi": round(_b.get('f21', 0) / 1e8, 1) if _b.get('f21', 0) > 0 else 0,
                        "lead_stock": _b.get('f128', '') or '--',
                        "lead_code": _b.get('f140', '') or '',
                    })
                break
            except Exception:
                continue
    full_market_sectors.sort(key=lambda x: x['pct_change'], reverse=True)
    print(f"  全市场板块数据: {len(full_market_sectors)} 个板块")
    # 抓取热门题材的成分股作为"机会个股"
    hot_sector_stocks = {}
    for _hs in full_market_sectors[:6]:
        _bk = _hs.get('code', '')
        _sn = _hs.get('name', '')
        if not _bk or not _sn:
            continue
        try:
            _mem_params = {'pn': 1, 'pz': 6, 'po': 1, 'np': 1, 'fltt': 2, 'invt': 2, 'fid': 'f3',
                           'fs': 'b:' + _bk, 'fields': 'f12,f14,f3,f6'}
            _mresp = _sess.get('https://push2delay.eastmoney.com/api/qt/clist/get', params=_mem_params, timeout=15)
            _mdiff = _mresp.json().get('data', {}).get('diff', []) or []
            _mstocks = []
            for _m in _mdiff[:5]:
                _mstocks.append({
                    "code": _m.get('f12', ''), "name": _m.get('f14', ''),
                    "pct_change": round(_m.get('f3', 0), 2),
                    "amount": str(round(_m.get('f6', 0) / 1e8, 1)) + '亿' if _m.get('f6', 0) > 0 else '--',
                })
            if _mstocks:
                hot_sector_stocks[_sn] = _mstocks
        except Exception:
            continue
    print(f"  热门题材机会个股: {len(hot_sector_stocks)} 个热门题材")
except Exception as _e:
    print(f"  全市场板块数据获取失败: {_e}")
    full_market_sectors = []
    hot_sector_stocks = {}
full_market_sectors_json = safe_json_dumps(full_market_sectors, ensure_ascii=False)
hot_sector_stocks_json = safe_json_dumps(hot_sector_stocks, ensure_ascii=False)

# ===== 需求2: 全市场涨跌分布数据（分页抓全市场，精确统计涨/跌/平/停牌） =====
market_breadth = {}
try:
    _b_fs = 'm:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048'
    _b_hosts = [
        'https://push2delay.eastmoney.com/api/qt/clist/get',
        'https://push2.eastmoney.com/api/qt/clist/get',
        'http://80.push2.eastmoney.com/api/qt/clist/get',
    ]
    _b_all = []
    _b_total = 0
    for _b_url in _b_hosts:
        try:
            _first = _sess.get(_b_url, params={'pn': 1, 'pz': 100, 'po': 1, 'np': 1, 'fltt': 2, 'invt': 2,
                                               'fs': _b_fs, 'fields': 'f3,f2,f292'}, timeout=20)
            _first_data = _first.json().get('data', {})
            _b_total = _first_data.get('total', 0)
            _b_all = list(_first_data.get('diff', []) or [])
            _page = 1
            while _b_total > 0 and len(_b_all) < _b_total and _page <= 100:
                _page += 1
                _pg = _sess.get(_b_url, params={'pn': _page, 'pz': 100, 'po': 1, 'np': 1, 'fltt': 2, 'invt': 2,
                                                'fs': _b_fs, 'fields': 'f3,f2,f292'}, timeout=20)
                _pg_diff = _pg.json().get('data', {}).get('diff', []) or []
                if not _pg_diff:
                    break
                _b_all.extend(_pg_diff)
            if _b_total > 0 and len(_b_all) >= int(_b_total * 0.9):
                break
        except Exception:
            continue
    _up_cnt = 0
    _down_cnt = 0
    _flat_cnt = 0
    _suspended_cnt = 0
    _delisted_cnt = 0
    # 涨跌幅区间直方图（左跌→右涨，13 档）
    _hist = [0] * 13
    def _bucket(fv):
        if fv == 0:
            return 6
        if fv < 0:
            if fv <= -7: return 0
            if fv <= -5: return 1
            if fv <= -3: return 2
            if fv <= -2: return 3
            if fv <= -1: return 4
            return 5
        if fv >= 7: return 12
        if fv >= 5: return 11
        if fv >= 3: return 10
        if fv >= 2: return 9
        if fv >= 1: return 8
        return 7
    for _s in _b_all:
        _st = _s.get('f292')
        if _st == 7 or _st == '7':
            _delisted_cnt += 1
            continue
        _v = _s.get('f3', 0)
        try:
            _fv = float(_v)
        except (ValueError, TypeError):
            _suspended_cnt += 1
            continue
        if _fv > 0:
            _up_cnt += 1
        elif _fv < 0:
            _down_cnt += 1
        else:
            _flat_cnt += 1
        _hist[_bucket(_fv)] += 1
    _hist_labels = ["≤-7%", "-7~-5%", "-5~-3%", "-3~-2%", "-2~-1%", "-1~0%", "平盘", "0~1%", "1~2%", "2~3%", "3~5%", "5~7%", "≥7%"]
    _hist_json = [{"name": _hist_labels[i], "value": _hist[i]} for i in range(13)]
    _real_total = max(_b_total - _delisted_cnt, _up_cnt + _down_cnt + _flat_cnt + _suspended_cnt)
    market_breadth = {
        "total": _real_total, "up": _up_cnt, "down": _down_cnt,
        "flat": _flat_cnt, "suspended": _suspended_cnt,
        "limit_up": 0, "limit_down": 0,
        "hist": _hist_json,
    }
    print(f"  全市场涨跌: 总{_real_total} 涨{_up_cnt} 跌{_down_cnt} 平{_flat_cnt} 停牌{_suspended_cnt} 退市忽略{_delisted_cnt}")
except Exception as _e:
    print(f"  全市场涨跌数据获取失败: {_e}")
    market_breadth = {}
market_breadth_json = safe_json_dumps(market_breadth, ensure_ascii=False)

# ===== 需求2: 场内T+0 ETF基金数据 =====
t0_etf_list = []
try:
    # T+0 ETF: 跨境ETF、债券ETF、黄金ETF、商品ETF等支持当日回转交易（货币ETF波动≈0已剔除）
    # fs=全市场场内基金（沪m:1+t:9 + 深m:0+t:10）按成交额降序；旧板块MK0024~MK0405只含黄金ETF+LOF
    _e_params = {'pn': 1, 'pz': 100, 'po': 1, 'np': 1, 'fltt': 2, 'invt': 2, 'fid': 'f6',
                 'fs': 'm:1+t:9,m:0+t:10',
                 'fields': 'f12,f14,f2,f3,f6,f8,f152'}
    _e_data = []
    for _eurl in ('https://push2delay.eastmoney.com/api/qt/clist/get',
                  'https://push2.eastmoney.com/api/qt/clist/get'):
        try:
            _e_resp = _sess.get(_eurl, params=_e_params, timeout=15)
            _e_data = _e_resp.json().get('data', {}).get('diff', [])
            if _e_data:
                break
        except Exception:
            continue
    for _e in _e_data:
        _code = str(_e.get('f12', ''))
        _name = str(_e.get('f14', ''))
        _fv = _e.get('f3', 0)
        # 停牌/退市ETF的 f3 为 '-'，直接跳过
        if not isinstance(_fv, (int, float)):
            continue
        _pct = round(float(_fv), 2)
        _amount = _num(_e.get('f6', 0))
        _turnover = _num(_e.get('f8', 0))
        _price = _num(_e.get('f2', 0))
        _name_up = _name.upper()
        # 货币ETF净值化运作、日内振幅≈0，永无做T机会，排除避免霸占候选名额
        if any(k in _name for k in ('货币', '添益', '日利', '理财', '保证金', '现金')):
            continue
        _etf_type = ''
        if '黄金' in _name or 'GOLD' in _name_up or _code.startswith('518'):
            _etf_type = '黄金ETF'
        elif '债' in _name or '短融' in _name or '国开' in _name or '国债' in _name or '政金' in _name or _code.startswith('511'):
            _etf_type = '债券ETF'
        elif '原油' in _name or '豆粕' in _name or '有色' in _name or '能源' in _name or '商品' in _name or '白银' in _name or '化工' in _name:
            _etf_type = '商品ETF'
        elif ('跨境' in _name or '纳指' in _name or '恒生' in _name or '日经' in _name or '标普' in _name or '德国' in _name or '法国' in _name
              or '港股' in _name or '中概' in _name or 'QDII' in _name_up or '海外' in _name or '东南亚' in _name or '沙特' in _name
              or '印度' in _name or '美国' in _name or _code.startswith('513') or _code.startswith('1596')):
            _etf_type = '跨境ETF'
        if _etf_type and _amount > 1e7 and _price > 0:
            _strategy = ''
            if _etf_type == '债券ETF':
                _strategy = '日内波段：利用债券ETF低波动特性，在盘中低点买入、高点卖出，日收益0.1-0.3%；适合大资金低风险套利'
            elif _etf_type == '跨境ETF':
                _strategy = '隔日溢价套利：关注夜间海外市场走势，次日开盘顺势操作；若溢价>2%可做空溢价回落；波动大设2%止损'
            elif _etf_type == '黄金ETF':
                _strategy = '趋势跟踪：跟随国际金价波动，RSI<30低吸、RSI>70高抛；地缘事件驱动时可加大仓位；止损3%'
            elif _etf_type == '商品ETF':
                _strategy = '事件驱动：关注大宗商品价格和库存变化，突破前日高点买入、跌破前日低点止损；趋势行情可留仓过夜'
            t0_etf_list.append({
                "code": _code, "name": _name, "price": _price,
                "pct_change": _pct, "amount": str(round(_amount / 1e8, 1)) + '亿',
                "_amount_num": _amount,
                "turnover": round(_turnover, 1) if _turnover else 0,
                "type": _etf_type, "strategy": _strategy,
            })
    # 类型内按成交额降序，再按类型配额挑选（跨境10+黄金3+商品3+债券4），与前端JSONP逻辑一致
    _rank_t = {'跨境ETF': 0, '黄金ETF': 1, '商品ETF': 1, '债券ETF': 2}
    t0_etf_list.sort(key=lambda x: (_rank_t.get(x.get('type'), 3), -x.get('_amount_num', 0)))
    _quota = {'跨境ETF': 10, '黄金ETF': 3, '商品ETF': 3, '债券ETF': 4}
    _picked = {}
    _final = []
    for _x in t0_etf_list:
        _t = _x.get('type')
        if _picked.get(_t, 0) < _quota.get(_t, 0):
            _picked[_t] = _picked.get(_t, 0) + 1
            _final.append(_x)
    t0_etf_list = _final[:20]
    print(f"  T+0 ETF: {len(t0_etf_list)} 只")
except Exception as _e:
    print(f"  T+0 ETF数据获取失败: {_e}")
    t0_etf_list = []
t0_etf_list_json = safe_json_dumps(t0_etf_list, ensure_ascii=False)

# ===== 需求3: 涨停池数据（Python端fallback，全市场分页抓取） =====
emo_pools_data = {"zt": [], "dt": [], "zb": [], "ok": False}
try:
    _today_yyyymmdd = datetime.datetime.now().strftime('%Y%m%d')
    _pool_cfg = [
        ("getTopicZTPool", "fbt%3Aasc", 500, "zt"),
        ("getTopicDTPool", "fund%3Aasc", 200, "dt"),
        ("getTopicZBPool", "fbt%3Aasc", 300, "zb"),
    ]
    for _kind, _sort, _ps, _key in _pool_cfg:
        try:
            _pool, _idx = [], 0
            while _idx < 8:  # 最多翻8页，覆盖全市场极端行情
                _u = f'https://push2ex.eastmoney.com/{_kind}?ut=7eea3edcaed734bea9cbfc24409ed989&dpt=wz.ztzt&Pageindex={_idx}&pagesize={_ps}&sort={_sort}&date={_today_yyyymmdd}'
                _r = _sess.get(_u, timeout=15)
                _d = _r.json()
                _p = _d.get('data', {}).get('pool', [])
                if not _p:
                    break
                _pool.extend(_p)
                _tc = _d.get('data', {}).get('tc', 0)
                if _tc and len(_pool) >= _tc:
                    break
                if len(_p) < _ps:
                    break
                _idx += 1
            if _pool:
                emo_pools_data[_key] = _pool
                emo_pools_data["ok"] = True
        except Exception:
            pass
    print(f"  涨停池: ZT={len(emo_pools_data['zt'])} DT={len(emo_pools_data['dt'])} ZB={len(emo_pools_data['zb'])}")
except Exception as _e:
    print(f"  涨停池数据获取失败: {_e}")
emo_pools_json = safe_json_dumps(emo_pools_data, ensure_ascii=False)
# 大V动态情绪数据（微博52198/唐史主任司马迁/狂龙十八段）
weibo_dav_data = {"ok": False, "davs": [], "overall_sentiment": "neutral", "overall_score": 0,
                  "total_bull": 0, "total_bear": 0, "total_neutral": 0, "total_posts": 0,
                  "fetch_time": "", "error": "数据未生成"}
_dav_file = os.path.join(WORK_DIR, "weibo_dav_data.json")
if os.path.exists(_dav_file):
    try:
        with open(_dav_file, "r") as f:
            weibo_dav_data = json.load(f)
        print(f"  大V情绪: {weibo_dav_data.get('overall_sentiment','N/A')} posts={weibo_dav_data.get('total_posts',0)}")
    except Exception as _e:
        print(f"  大V情绪数据读取失败: {_e}")
weibo_dav_json = safe_json_dumps(weibo_dav_data, ensure_ascii=False)

# ===== 经济事件日历数据 =====
import datetime as _dt
_econ_events = [
    # 2026年8月
    {"date":"2026-08-26","time":"20:30","title":"美国7月PCE/核心PCE","cat":"宏观","importance":"high","country":"美国",
     "desc":"美联储锚定通胀指标，预计核心PCE环比+0.2%","forecast":"+0.2%","prior":"+0.2%",
     "impact":"neutral","impact_text":"若低于预期=降息预期升=利好债市+成长股；高于预期=鹰派=利空",
     "markets":["美股","债券","外汇"],"status":"pending"},
    {"date":"2026-08-26","time":"20:30","title":"美国二季度GDP修正值","cat":"宏观","importance":"medium","country":"美国",
     "desc":"二季度实际GDP修正值，首次预估值+2.4%","forecast":"+2.4%","prior":"+2.4%",
     "impact":"neutral","impact_text":"数据若下修=经济放缓担忧=利好降息预期；上修=经济强劲=鹰派",
     "markets":["美股","债券","外汇"],"status":"pending"},
    {"date":"2026-08-27","time":"22:00","title":"密歇根大学消费者信心终值","cat":"宏观","importance":"low","country":"美国",
     "desc":"8月消费者信心指数终值","forecast":"90.2","prior":"90.2",
     "impact":"neutral","impact_text":"消费者信心走强=消费股利好；走弱=经济衰退担忧",
     "markets":["美股"],"status":"pending"},
    {"date":"2026-08-29","time":"20:30","title":"美国7月个人收入/支出","cat":"宏观","importance":"medium","country":"美国",
     "desc":"个人收入和支出数据，反映消费动能","forecast":"+0.3%","prior":"+0.3%",
     "impact":"neutral","impact_text":"收入增长=消费预期利好；支出放缓=经济动能减弱",
     "markets":["美股","债券"],"status":"pending"},
    # 9月
    {"date":"2026-09-01","time":"09:00","title":"中国8月官方PMI","cat":"宏观","importance":"high","country":"中国",
     "desc":"制造业+非制造业PMI，荣枯线50","forecast":"49.5","prior":"49.3",
     "impact":"bullish","impact_text":"若回升至50以上=经济企稳=利好A股周期股+港股；低于预期=政策加码预期",
     "markets":["A股","港股","商品"],"status":"pending"},
    {"date":"2026-09-02","time":"09:45","title":"中国8月财新制造业PMI","cat":"宏观","importance":"medium","country":"中国",
     "desc":"财新制造业PMI初值/终值","forecast":"49.8","prior":"49.8",
     "impact":"neutral","impact_text":"与官方PMI互补，偏中小盘。50以上=制造业扩张",
     "markets":["A股","港股"],"status":"pending"},
    {"date":"2026-09-05","time":"20:30","title":"美国8月非农就业","cat":"重磅","importance":"high","country":"美国",
     "desc":"非农就业人口变动+失业率，每月最重要的数据之一","forecast":"+18万","prior":"+20.6万",
     "impact":"bullish","impact_text":"就业走弱=降息预期升=利好美股科技+A股外资流入；就业超预期=鹰派=利空",
     "markets":["美股","债券","外汇","商品"],"status":"pending"},
    {"date":"2026-09-10","time":"09:30","title":"中国8月CPI/PPI","cat":"宏观","importance":"high","country":"中国",
     "desc":"消费者物价指数+生产者物价指数","forecast":"CPI+0.6%","prior":"CPI+0.5%",
     "impact":"neutral","impact_text":"CPI回升+PPI转正=通胀预期=利好资源股；CPI走低=宽松预期=利好债市",
     "markets":["A股","债券","商品"],"status":"pending"},
    {"date":"2026-09-11","time":"20:30","title":"美国8月CPI","cat":"重磅","importance":"high","country":"美国",
     "desc":"消费者物价指数同比/环比，9月议息前最重要通胀数据","forecast":"+3.4%","prior":"+3.4%",
     "impact":"bullish","impact_text":"CPI低于预期=9月降息预期确认=利好美股+A股+黄金+债券；超预期=利空",
     "markets":["美股","债券","外汇","商品","A股"],"status":"pending"},
    {"date":"2026-09-12","time":"20:30","title":"美国8月PPI","cat":"宏观","importance":"medium","country":"美国",
     "desc":"生产者物价指数，CPI的前瞻指标","forecast":"+2.3%","prior":"+2.3%",
     "impact":"neutral","impact_text":"PPI低于预期=通胀降温=利好；高于预期=成本推升=利空消费",
     "markets":["美股","债券"],"status":"pending"},
    {"date":"2026-09-15","time":"09:20","title":"中国MLF续作/逆回购","cat":"宏观","importance":"medium","country":"中国",
     "desc":"央行MLF操作+公开市场逆回购，关注利率变动","forecast":"待定","prior":"待定",
     "impact":"neutral","impact_text":"MLF降息=宽松信号=利好A股债市；缩量续作=流动性收紧",
     "markets":["A股","债券"],"status":"pending"},
    {"date":"2026-09-16","time":"09:30","title":"中国8月经济数据","cat":"宏观","importance":"high","country":"中国",
     "desc":"工业增加值+社零+固定资产投资","forecast":"待定","prior":"待定",
     "impact":"neutral","impact_text":"数据走强=经济企稳=利好A股；走弱=政策刺激预期",
     "markets":["A股","港股","商品"],"status":"pending"},
    {"date":"2026-09-17","time":"02:00","title":"美联储FOMC议息会议(第一天)","cat":"重磅","importance":"high","country":"美国",
     "desc":"美联储9月议息会议第一天","forecast":"待定","prior":"5.25-5.50%",
     "impact":"neutral","impact_text":"9月会议是下半年最关键会议之一。市场预计可能启动首次降息",
     "markets":["美股","债券","外汇","商品","A股","港股"],"status":"pending"},
    {"date":"2026-09-18","time":"02:00","title":"美联储利率决议+点阵图+发布会","cat":"重磅","importance":"high","country":"美国",
     "desc":"利率决议+经济预测+点阵图+鲍威尔新闻发布会","forecast":"待定","prior":"5.25-5.50%",
     "impact":"bullish","impact_text":"若降息=利好全球股市+债券+黄金+A股外资流入；维持不变但鸽派=偏利好；鹰派=利空",
     "markets":["美股","债券","外汇","商品","A股","港股"],"status":"pending"},
    {"date":"2026-09-20","time":"11:00","title":"日本央行利率决议","cat":"重磅","importance":"medium","country":"日本",
     "desc":"日本央行货币政策会议","forecast":"待定","prior":"0.25%",
     "impact":"neutral","impact_text":"日银加息=日元升值=全球套息交易平仓风险；维持不变=风险偏好稳定",
     "markets":["外汇","美股","港股"],"status":"pending"},
    {"date":"2026-09-26","time":"20:30","title":"美国8月PCE/核心PCE","cat":"重磅","importance":"high","country":"美国",
     "desc":"美联储9月会议后的首份通胀数据，验证降息决策","forecast":"待定","prior":"待定",
     "impact":"neutral","impact_text":"若PCE继续回落=降息路径确认=利好；反弹=鹰派重来=利空",
     "markets":["美股","债券","外汇","商品"],"status":"pending"},
]
_econ_cal_data = {
    "ok": True,
    "fetch_time": _dt.datetime.now().strftime("%Y-%m-%d %H:%M"),
    "events": _econ_events,
    "summary": {
        "total": len(_econ_events),
        "high_impact": sum(1 for e in _econ_events if e["importance"] == "high"),
        "bullish": sum(1 for e in _econ_events if e["impact"] == "bullish"),
        "bearish": sum(1 for e in _econ_events if e["impact"] == "bearish"),
        "neutral": sum(1 for e in _econ_events if e["impact"] == "neutral"),
        "next_major": "9月5日 非农 / 9月11日 CPI / 9月18日 FOMC",
    },
}
econ_cal_json = safe_json_dumps(_econ_cal_data, ensure_ascii=False)
# 用涨停/跌停池数量回填涨跌分布的涨停/跌停家数
if isinstance(market_breadth, dict) and emo_pools_data.get("ok"):
    market_breadth["limit_up"] = len(emo_pools_data.get("zt", []))
    market_breadth["limit_down"] = len(emo_pools_data.get("dt", []))
    market_breadth_json = safe_json_dumps(market_breadth, 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">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<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}
*{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;padding-bottom:68px}
::-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}
.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}
.metric-info-btn{margin-left:auto;flex-shrink:0;width:20px;height:20px;border-radius:50%;border:1px solid var(--rule);background:var(--bg3);color:var(--muted);font-size:12px;line-height:18px;text-align:center;cursor:pointer;font-weight:700;transition:all .2s;user-select:none}
.metric-info-btn:hover{background:var(--blue);border-color:var(--blue);color:#fff;transform:scale(1.12)}
.dav-tab-btn,.dav-filter-btn,.dav-src-btn{font-size:11px;padding:3px 10px;border-radius:5px;border:1px solid var(--bd);background:var(--bg2);color:var(--muted);cursor:pointer;transition:all .2s}
.dav-tab-btn:hover,.dav-filter-btn:hover,.dav-src-btn:hover{color:var(--ink);border-color:var(--purple)}
.dav-tab-btn.active,.dav-filter-btn.active,.dav-src-btn.active{background:var(--purple);border-color:var(--purple);color:#fff;font-weight:600}
.dav-filter-btn[data-filter="bullish"].active{background:var(--up);border-color:var(--up)}
.dav-filter-btn[data-filter="bearish"].active{background:var(--down);border-color:var(--down)}
.dav-filter-btn[data-filter="neutral"].active{background:var(--bg4);border-color:var(--bg4);color:var(--ink)}
.dav-src-btn[data-src="weibo"].active{background:#ef4444;border-color:#ef4444}
.dav-src-btn[data-src="xueqiu"].active{background:#3b82f6;border-color:#3b82f6}
.dav-src-btn[data-src="wechat"].active{background:#22c55e;border-color:#22c55e}
.mi-row{display:flex;gap:12px;padding:10px 0;border-top:1px solid var(--rule);font-size:12.5px;line-height:1.75;color:var(--ink)}
.mi-row:first-of-type{border-top:none}
.mi-key{flex-shrink:0;min-width:64px;font-weight:700;color:var(--blue)}
.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}
/* 产业链泳道布局 */
.chain-lanes{display:flex;gap:12px;align-items:stretch;min-width:860px}
.chain-lane{flex:1 1 0;min-width:165px;background:var(--bg3);border-radius:10px;padding:10px;display:flex;flex-direction:column;gap:8px}
.chain-lane-head{padding-bottom:8px}
.chain-lane-title{font-size:14px;font-weight:800}
.chain-lane-sub{font-size:10px;color:var(--muted);margin-top:1px}
.chain-lane-avg{font-size:16px;font-weight:800;margin-top:3px}
.chain-lane-body{display:flex;flex-direction:column;gap:8px}
.chain-node{background:var(--bg2);border-radius:8px;padding:8px 10px;cursor:pointer;transition:transform .15s ease,box-shadow .15s ease}
.chain-node:hover{transform:translateY(-2px);box-shadow:0 6px 16px rgba(0,0,0,.35)}
.chain-node-name{font-size:13px;font-weight:700}
.chain-node-meta{font-size:10px;color:var(--muted);margin-top:2px}
.chain-dots{margin-top:5px;display:flex;gap:4px;flex-wrap:wrap}
.chain-dot{width:9px;height:9px;border-radius:50%;display:inline-block;cursor:help;border:1px solid rgba(255,255,255,.25)}
.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)}
.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{position:fixed;bottom:12px;left:50%;transform:translateX(-50%);z-index:95;background:var(--bg2);background-image:var(--grad-card);border:1px solid var(--rule);border-radius:999px;padding:6px 10px;box-shadow:0 8px 26px rgba(0,0,0,.35);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);max-width:calc(100vw - 20px)}
.ticker-track{display:flex;flex-wrap:nowrap;align-items:center;gap:2px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.ticker-track::-webkit-scrollbar{display:none}
.ticker-item{display:flex;flex-direction:row;align-items:baseline;gap:7px;font-size:13px;cursor:pointer;transition:all .2s;padding:6px 13px;border-radius:999px;border:1px solid transparent;white-space:nowrap;flex:0 0 auto}
.ticker-item:hover{color:var(--gold);background:var(--bg3)}
.ticker-item.active{border-color:var(--gold);background:var(--bg3)}
.ticker-item .tk-name{font-weight:600;font-size:12px;color:var(--muted)}
.ticker-item .tk-price{font-size:14px;font-weight:700;font-family:'SF Mono','JetBrains Mono',monospace}
.ticker-item .tk-pct{font-size:12px;font-weight:600}
/* ===== 模块化看板 ===== */
/* 一行 1~3 个模块，随视口宽度自适应：≥1500px 三列 / ≥1000px 两列 / 更窄单列 */
.mod-grid{display:grid;grid-template-columns:1fr;gap:16px;align-items:stretch}
@media(min-width:1000px){.mod-grid{grid-template-columns:repeat(2,1fr)}}
@media(min-width:1500px){.mod-grid{grid-template-columns:repeat(3,1fr)}}
.module-wrap{grid-column:auto;min-width:0;display:flex;flex-direction:column}
.module-wrap[hidden]{display:none}
.module-wrap>.card{flex:1;display:flex;flex-direction:column;width:100%}
.module-wrap>.card>.card-title{flex-shrink:0}
.module-wrap .chart-c{flex:1;height:auto;min-height:280px}
.module-wrap .gm-grid{flex:1;align-content:center}
.module-wrap .stat-grid{flex:1;align-content:center}
.module-wrap>.card>#cycleBox,.module-wrap>.card>#ladderBox,.module-wrap>.card>#todayHot,.module-wrap>.card>#t0List,.module-wrap>.card>#volBox,.module-wrap>.card>#upDownDist{flex:1;min-height:300px}
.module-wrap>.card>#cycleBox{max-height:680px;overflow-y:auto}
.module-wrap>.card>#ladderBox{max-height:640px}
.module-wrap>.card>#todayHot,.module-wrap>.card>#t0List{max-height:520px}
.module-wrap>.card>#volBox{max-height:560px;overflow-y:auto}
/* 模块拖拽 */
.module-wrap[draggable=true]{cursor:grab}
.module-wrap.dragging{opacity:.35;cursor:grabbing}
.module-wrap.drop-mark-before{box-shadow:-4px 0 0 -1px var(--gold)}
.module-wrap.drop-mark-after{box-shadow:4px 0 0 -1px var(--gold)}
.module-wrap.drop-mark-top{box-shadow:0 -4px 0 -1px var(--gold)}
.module-wrap.drop-mark-bottom{box-shadow:0 4px 0 -1px var(--gold)}
.lp-row[draggable=true]{cursor:grab}
.lp-row.dragging{opacity:.4}
.lp-row.drop-mark-before{box-shadow:-3px 0 0 -1px var(--gold)}
.lp-row.drop-mark-after{box-shadow:3px 0 0 -1px var(--gold)}
.lp-row{display:flex;align-items:center;gap:8px;padding:7px 10px;border-bottom:1px solid var(--rule);font-size:13px}
.lp-row:hover{background:var(--bg3)}
.lp-row .lp-name{font-weight:600;min-width:150px}
.lp-row .lp-desc{font-size:11px;color:var(--muted);flex:1;line-height:1.4}
.lp-row input[type=checkbox]{accent-color:var(--gold);width:15px;height:15px;cursor:pointer}
.lp-btn{background:var(--bg3);color:var(--ink);border:1px solid var(--rule);border-radius:5px;width:24px;height:24px;cursor:pointer;font-size:12px;line-height:1;flex-shrink:0}
.lp-btn:hover{border-color:var(--gold);color:var(--gold)}
.lp-cat{font-size:11px;color:var(--gold);font-weight:700;padding:10px 10px 4px;letter-spacing:1px}
.lp-off .lp-name{color:var(--muted)}
.gm-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;padding:10px}
.gm-cell{background:var(--bg3);border-radius:10px;padding:10px 12px;border:1px solid var(--rule)}
.gm-cell .nm{font-size:11px;color:var(--muted)}
.gm-cell .px{font-size:16px;font-weight:700;font-family:'SF Mono','JetBrains Mono',monospace;margin-top:2px}
.gm-cell .ct{font-size:11px;margin-top:1px}
.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:1}
/* 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;">

<!-- ===== 用户登录弹窗 ===== -->
<div id="loginOverlay" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(10,14,26,0.85);backdrop-filter:blur(8px);align-items:center;justify-content:center;">
  <div style="background:var(--bg2);border-radius:20px;padding:40px 36px;width:380px;max-width:90vw;border:1px solid var(--rule);box-shadow:0 20px 60px rgba(0,0,0,.5);">
    <div style="text-align:center;margin-bottom:28px;">
      <div style="width:56px;height:56px;border-radius:14px;background:linear-gradient(135deg,var(--purple),var(--gold));margin:0 auto 14px;display:flex;align-items:center;justify-content:center;font-size:26px;">📊</div>
      <div style="font-size:20px;font-weight:800;color:var(--ink);">欢迎使用交易看板</div>
      <div style="font-size:13px;color:var(--muted);margin-top:4px;">首次使用请设置用户名</div>
    </div>
    <div style="margin-bottom:20px;">
      <input id="loginUsername" type="text" placeholder="输入用户名（字母/数字/中文）" maxlength="20" style="width:100%;padding:12px 16px;font-size:14px;border-radius:10px;border:1px solid var(--rule);background:var(--bg3);color:var(--ink);outline:none;box-sizing:border-box;" onkeydown="if(event.key==='Enter')doLogin()">
      <div id="loginError" style="font-size:12px;color:var(--up);margin-top:6px;display:none;"></div>
    </div>
    <button onclick="doLogin()" style="width:100%;padding:12px;font-size:15px;font-weight:700;border-radius:10px;border:none;background:linear-gradient(135deg,var(--purple),var(--blue));color:#fff;cursor:pointer;transition:all .25s;">进入看板</button>
    <div style="font-size:11px;color:var(--muted);margin-top:14px;text-align:center;line-height:1.6;">用户名将关联你的关注列表和模拟交易<br>数据永久保存在服务器，换设备登录也能恢复</div>
  </div>
</div>
<!-- ===== 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('layer-tpo',this)">四度空间</a>
      <a class="nav-link" onclick="scrollToSec('layer2',this)">板块轮动</a>
      <a class="nav-link" onclick="scrollToSec('layer0',this)">交易信号</a>
      <a class="nav-link" onclick="scrollToSec('layer-paper',this)">模拟交易</a>
      <a class="nav-link" onclick="scrollToSec('layer-watch',this)">★ 我关注的 <span class="badge-count" id="watchCount">0</span></a>
    </div>
    <div class="nav-right" style="display:flex;align-items:center;gap:10px;">
      <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:150;box-shadow:0 4px 12px rgba(0,0,0,0.3);"></div>
      </div>
      <div id="userBadge" style="display:none;align-items:center;gap:6px;background:var(--bg2);border:1px solid var(--rule);border-radius:20px;padding:4px 12px;cursor:pointer;flex-shrink:0;" onclick="showUserMenu()">
        <span style="width:8px;height:8px;border-radius:50%;background:var(--down);"></span>
        <span id="userBadgeName" style="font-size:12px;font-weight:600;color:var(--ink);"></span>
      </div>
    </div>
  </div>
</div>

<!-- ===== Ticker（悬浮页面底部的固定指数条） ===== -->
<div class="ticker-wrap"><div class="ticker-track" id="tickerTrack"></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">买入信号 · 风险警示 · 仓位管理 · 因子雷达 · 信号矩阵 · 个股排行</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="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: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" 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="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="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 title="自选">★</th><th>#</th><th title="股票代码">代码</th><th title="股票名称">名称</th><th class="sortable" data-key="signal_count" title="信号触发总数">指标</th>
        <th title="所属行业板块">行业</th><th title="回测+量化综合信号">信号</th><th class="sortable" data-key="bt_win_rate" title="原始回测胜率">胜率</th>
        <th class="sortable" data-key="bt_wr_filtered" title="量化过滤后胜率">量化胜率</th><th class="sortable" data-key="bt_quant_score" title="多因子综合评分">量化分</th>
        <th class="sortable" data-key="bt_pf" title="盈亏比PF">盈亏比</th><th class="sortable" data-key="position_size" title="建议仓位%">仓位</th>
        <th class="sortable" data-key="confidence" title="信号置信度">置信度</th><th class="sortable" data-key="priority" title="操作优先级">星级</th>
        <th title="风险等级">风险</th><th class="sortable" data-key="sentiment_score" title="多空偏向值(-1~1)">情绪</th>
        <th class="sortable" data-key="signal_quality" title="信号质量SQ评分">信号质量</th>
        <th class="sortable" data-key="mkt_cap_yi" title="总市值(亿元)">市值(亿)</th><th class="sortable" data-key="latest_price" title="最新成交价">最新价</th>
        <th class="sortable" data-key="pct_change" title="当日涨跌幅%">涨跌幅</th>
      </tr></thead>
      <tbody id="stockTableBody"></tbody>
    </table></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;display:flex;align-items:center;gap:10px;">
      <button class="btn" style="padding:4px 12px;font-size:12px;" onclick="openLayoutPanel()" title="自定义模块与顺序">⚙ 自定义看板</button>
      <div style="font-size:12px;color:var(--muted);"><span class="live-dot"></span> 实时更新 <span id="lastUpdate">--:--:--</span></div>
    </div>
  </div>
  <div class="mod-grid" id="modGrid">
    <div class="module-wrap w8" data-mod="idxtpo"><div class="card"><div class="card-title"><span class="bar" style="background:var(--gold);"></span>大盘指数 · TPO四度空间<span style="font-size:10px;color:var(--muted);margin-left:6px;">Market Profile 价值区/POC分析</span><span class="metric-info-btn" onclick="showMetricInfo('tpo')">i</span></div><div id="idxTpoTabs" style="padding:6px 8px 2px;"></div><div id="idxTpoChart" style="width:100%;height:340px;"></div><div id="idxTpoAdvice" style="padding:4px 8px 8px;"></div><div id="idxTpoPeriod" style="padding:4px 8px 10px;"></div></div></div>
    <div class="module-wrap w6" data-mod="gauge"><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><span class="metric-info-btn" onclick="showMetricInfo('gauge')">i</span></div><div id="chartGauge" class="chart-c"></div></div></div>
    <div class="module-wrap w6" data-mod="breadth"><div class="card"><div class="card-title"><span class="bar"></span>市场宽度分析<span style="font-size:10px;color:var(--muted);margin-left:6px;">A股全市场涨跌家数</span><span class="metric-info-btn" onclick="showMetricInfo('breadth')">i</span></div><div id="chartBreadth" class="chart-c"></div></div></div>
    <div class="module-wrap w6" data-mod="emotion"><div class="card"><div class="card-title"><span class="bar" style="background:var(--gold);"></span>涨停情绪周期（A股全市场 · 实时涨停数据推演）<span style="font-size:11px;color:var(--muted);margin-left:8px;" id="cycleUpdate"></span><span class="metric-info-btn" onclick="showMetricInfo('emotion')">i</span></div><div id="cycleBox"></div></div></div>
    <div class="module-wrap w6" data-mod="ladder"><div class="card"><div class="card-title"><span class="bar" style="background:var(--up);"></span>涨停梯队 · 连板天梯（A股全市场 · 点击股票查看）<span style="font-size:11px;color:var(--muted);margin-left:8px;">按连板高度分层</span><span class="metric-info-btn" onclick="showMetricInfo('ladder')">i</span></div><div id="ladderBox" style="overflow-y:auto;"></div></div></div>
    <div class="module-wrap w4" data-mod="updown"><div class="card"><div class="card-title"><span class="bar" style="background:var(--up);"></span>全市场涨跌幅分布<span style="font-size:10px;color:var(--muted);margin-left:6px;">实时全A股涨跌幅区间直方图</span><span class="metric-info-btn" onclick="showMetricInfo('updown')">i</span></div><div id="upDownDist" style="padding:8px 0;"></div></div></div>
    <div class="module-wrap w4" data-mod="todayhot"><div class="card"><div class="card-title"><span class="bar" style="background:var(--gold);"></span>今天炒什么<span style="font-size:10px;color:var(--muted);margin-left:6px;">全市场热门方向</span><span class="metric-info-btn" onclick="showMetricInfo('todayhot')">i</span></div><div id="todayHot" style="padding:8px 0;overflow-y:auto;"></div></div></div>
    <div class="module-wrap" data-mod="marketpulse" style="grid-column:1/-1;"><div class="card"><div class="card-title"><span class="bar" style="background:var(--up);"></span>市场异动监测<span style="font-size:10px;color:var(--muted);margin-left:6px;">板块异动 · 量比异动 · 资金异动 · 领涨领跌</span><span class="metric-info-btn" onclick="showMetricInfo('marketpulse')">i</span></div><div id="marketPulseBox" style="padding:8px 12px;"></div></div></div>
    <div class="module-wrap w6" data-mod="momentum"><div class="card"><div class="card-title"><span class="bar" style="background:var(--purple);"></span>蓄势待发<span style="font-size:10px;color:var(--muted);margin-left:6px;">高评分+缩量整理+即将突破</span><span class="metric-info-btn" onclick="showMetricInfo('momentum')">i</span></div><div id="momentumBox" style="padding:8px 0;max-height:420px;overflow-y:auto;"></div></div></div>
    <div class="module-wrap w6" data-mod="stockt0"><div class="card"><div class="card-title"><span class="bar" style="background:var(--blue);"></span>个股做T参考<span style="font-size:10px;color:var(--muted);margin-left:6px;">日内支撑压力 · T0买卖点 · 量价配合</span><span class="metric-info-btn" onclick="showMetricInfo('stockt0')">i</span></div><div id="stockT0Box" style="padding:8px 0;max-height:420px;overflow-y:auto;"></div></div></div>
    <div class="module-wrap" data-mod="econcal" style="grid-column:1/-1;"><div class="card"><div class="card-title"><span class="bar" style="background:var(--gold);"></span>重磅事件日历<span style="font-size:10px;color:var(--muted);margin-left:6px;">未来30天 · 利好利空研判 · 参考华尔街见闻日历</span><span class="metric-info-btn" onclick="showMetricInfo('econcal')">i</span></div><div id="econCalBox" style="padding:8px 12px;"></div></div></div>
    <div class="module-wrap w4" data-mod="t0etf"><div class="card"><div class="card-title"><span class="bar" style="background:var(--blue);"></span>场内T+0 ETF机会<span style="font-size:10px;color:var(--muted);margin-left:6px;">支持当日回转交易</span><span class="metric-info-btn" onclick="showMetricInfo('t0')">i</span></div><div id="t0List" style="padding:8px 0;overflow-y:auto;"></div></div></div>
    <div class="module-wrap w6" data-mod="turnover"><div class="card"><div class="card-title"><span class="bar" style="background:var(--blue);"></span>两市成交额监控<span style="font-size:10px;color:var(--muted);margin-left:6px;">沪深分时累计 · 对比昨日同期</span><span class="metric-info-btn" onclick="showMetricInfo('turnover')">i</span></div><div id="turnoverChart" style="width:100%;flex:1;min-height:220px;"></div><div id="turnoverStats" style="padding:4px 8px 8px;flex-shrink:0;"></div></div></div>
    <div class="module-wrap w6" data-mod="moneyflow"><div class="card"><div class="card-title"><span class="bar" style="background:var(--up);"></span>主力资金流向<span style="font-size:10px;color:var(--muted);margin-left:6px;">板块主力净流入排行</span><span class="metric-info-btn" onclick="showMetricInfo('moneyflow')">i</span></div><div id="moneyflowChart" style="width:100%;flex:1;min-height:300px;"></div></div></div>
    <div class="module-wrap w4" data-mod="volmonitor"><div class="card"><div class="card-title"><span class="bar" style="background:var(--purple);"></span>波动率监控<span style="font-size:10px;color:var(--muted);margin-left:6px;">日内已实现波动</span><span class="metric-info-btn" onclick="showMetricInfo('vol')">i</span></div><div id="volBox" style="padding:8px;"></div></div></div>
    <div class="module-wrap w8" data-mod="idxfly"><div class="card"><div class="card-title"><span class="bar" style="background:var(--gold);"></span>全球市场 · 隔夜与亚太<span style="font-size:10px;color:var(--muted);margin-left:6px;">美股/港股/日经/人民币</span><span class="metric-info-btn" onclick="showMetricInfo('global')">i</span></div><div id="globalMkt" class="gm-grid"></div></div></div>
    <div class="module-wrap w6" data-mod="rsgrid"><div class="card"><div class="card-title"><span class="bar"></span>指数相对强度<span style="font-size:10px;color:var(--muted);margin-left:6px;">当日/5日/20日对比</span><span class="metric-info-btn" onclick="showMetricInfo('rs')">i</span></div><div id="rsChart" style="width:100%;flex:1;min-height:300px;"></div></div></div>
    <div class="module-wrap w6" data-mod="factorstyle"><div class="card"><div class="card-title"><span class="bar" style="background:var(--purple);"></span>风格因子表现<span style="font-size:10px;color:var(--muted);margin-left:6px;">大小盘/成长防御轮动</span><span class="metric-info-btn" onclick="showMetricInfo('factor')">i</span></div><div id="factorChart" style="width:100%;flex:1;min-height:300px;"></div></div></div>
    <div class="module-wrap w4" data-mod="sigdist"><div class="card"><div class="card-title"><span class="bar"></span>信号分布<span class="metric-info-btn" onclick="showMetricInfo('signal')">i</span></div><div id="chartAction" class="chart-c"></div></div></div>
    <div class="module-wrap w4" data-mod="indtop"><div class="card"><div class="card-title"><span class="bar"></span>指标触发频次 Top10 <span style="font-size:10px;color:var(--muted);margin-left:6px;">鼠标悬停查看指标说明</span><span class="metric-info-btn" onclick="showMetricInfo('indtop')">i</span></div><div id="chartInd" class="chart-c"></div></div></div>
    <div class="module-wrap w4" data-mod="btwin"><div class="card"><div class="card-title"><span class="bar"></span>回测胜率统计<span class="metric-info-btn" onclick="showMetricInfo('backtest')">i</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 class="module-wrap" data-mod="davsenti" style="grid-column:1/-1;"><div class="card"><div class="card-title"><span class="bar" style="background:var(--purple);"></span>大V动态 · 情绪风向标<span style="font-size:10px;color:var(--muted);margin-left:6px;">微博/雪球/微信大V发言情绪分析</span><span class="metric-info-btn" onclick="showMetricInfo('davsenti')">i</span></div><div id="davSentiBox" style="padding:8px 12px 12px;"></div></div></div>
  </div>
</div>

<!-- ===== 四度空间（Market Profile 深度分析 · 独立菜单） ===== -->
<div class="container l-tpo page-section" id="layer-tpo">
  <div class="layer-header">
    <div class="layer-num" style="background:var(--gold);">MP</div>
    <div><div class="layer-title">四度空间 · Market Profile</div><div class="layer-sub">短/中/长期三级时间价格机会分析 · 从交易可执行性角度给出买卖点、止损、目标、仓位</div></div>
    <div style="margin-left:auto;font-size:12px;color:var(--muted);"><span class="live-dot"></span> 实时计算</div>
  </div>
  <div class="card" style="margin-bottom:16px;border-left:3px solid var(--gold);">
    <div class="card-title"><span class="bar" style="background:var(--gold);"></span>方法论解读 · 什么是四度空间</div>
    <div id="mpMethodBox" style="line-height:1.9;font-size:13px;color:var(--ink);"></div>
  </div>
  <div class="card" style="margin-bottom:16px;">
    <div class="card-title"><span class="bar"></span>分析对象 <span style="font-size:10px;color:var(--muted);margin-left:6px;">支持指数 / 板块 / 个股四度空间分析</span></div>
    <div id="mpObjTypeTabs" style="padding:4px 8px 2px;"></div>
    <div id="mpTabs" style="padding:4px 8px 4px;"></div>
    <div id="mpAnalysisBox"></div>
  </div>
  <div class="card" style="margin-bottom:16px;border-left:3px solid var(--up);">
    <div class="card-title"><span class="bar" style="background:var(--up);"></span>可执行交易建议（四度空间自动扫描） <span style="font-size:10px;color:var(--muted);margin-left:6px;">筛选失衡向上/突破价值区的板块与个股</span></div>
    <div id="mpSuggestBox"></div>
  </div>
</div>

<!-- ===== 看板配置面板 ===== -->
<div class="overlay" id="layoutOverlay">
  <div class="overlay-panel" style="max-width:680px;">
    <button class="ov-close" onclick="closeOverlay('layoutOverlay')">✕</button>
    <h2 style="margin-bottom:4px;">⚙ 自定义看板模块</h2>
    <div style="font-size:12px;color:var(--muted);margin-bottom:14px;line-height:1.7;">勾选要展示的模块，用 ↑↓ 或<b>直接拖拽</b>调整顺序（本列表与页面网格均可拖拽，配置自动保存在本机浏览器）。默认布局按交易员动线编排：<b>风控状态 → 情绪主线 → 交易机会 → 资金验证 → 统计反馈</b>；页面上按住任意模块拖到目标位置即可完成排序。</div>
    <div id="layoutPanelBody" style="max-height:56vh;overflow-y:auto;"></div>
    <div style="display:flex;gap:10px;margin-top:16px;">
      <button class="btn" style="background:var(--gold);color:#1a1a1a;border:none;" onclick="resetLayout()">恢复默认布局</button>
      <button class="btn" onclick="closeOverlay('layoutOverlay')">完成</button>
    </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">A股全市场实时轮动推演 · 产业链传导逻辑 · 全市场板块热力图</div></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(--purple);"></span>板块轮动推演（A股全市场）<span style="margin-left:8px;font-size:10px;color:var(--muted);font-weight:400;">全市场行业板块 · 动量（当日涨跌幅）+ 成交强度双因子 · 实时刷新</span></div>
    <div class="rotation-insight" id="rotationInsight"></div>
    <div id="chartRotation" class="chart-c" style="height:340px;"></div>
    <div id="rotationLadder"></div>
    <div class="card" style="margin-top:12px;background:var(--bg2);"><div class="card-title"><span class="bar" style="background:var(--blue);"></span>全市场板块轮动明细 <span style="font-size:10px;color:var(--muted);margin-left:6px;font-weight:400;">东方财富实时板块数据 · 展示涨幅前20</span></div><div id="fullMarketRotationSummary" style="margin-bottom:10px;"></div><div id="fullMarketRotation" style="max-height:520px;overflow-y:auto;"></div></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 id="chainInsight" style="margin-bottom:12px;"></div>
    <div id="chainLanes" style="overflow-x:auto;"></div>
    <div style="font-size:12px;color:var(--muted);line-height:1.7;margin-top:10px;">卡片左侧色条代表板块轮动梯队：<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 style="color:var(--up);">红=共振上涨</b>，<b style="color:var(--gold);">金=待传导</b>，<b style="color:var(--down);">绿=成本挤压</b>，<b style="color:var(--blue);">蓝=成本红利</b>），悬停圆点可查看该条传导的完整逻辑与交易推演。</div>
  </div>
  <div class="card"><div class="card-title"><span class="bar"></span>板块情绪云图（A股全市场）<div style="margin-left:auto;display:flex;gap:6px;align-items:center;"><select id="hmDim" onchange="renderHeatmap()" style="font-size:11px;padding:3px 8px;border-radius:6px;border:1px solid var(--rule);background:var(--bg3);color:var(--ink);"><option value="mktCapYi">按市值</option><option value="pct">按涨跌幅</option><option value="amtYi">按成交额</option></select><select id="hmCount" onchange="renderHeatmap()" style="font-size:11px;padding:3px 8px;border-radius:6px;border:1px solid var(--rule);background:var(--bg3);color:var(--ink);"><option value="30">前30</option><option value="50" selected>前50</option><option value="80">前80</option><option value="0">全部</option></select></div></div><div id="heatmapGrid" style="height:440px;width:100%;"></div><div id="heatmapLegend" style="display:flex;align-items:center;justify-content:center;gap:0;height:24px;margin-top:4px;border-radius:4px;overflow:hidden;font-size:10px;"></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="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 title="指标名称">指标</th><th title="看涨/看跌/中性">类型</th><th title="历史触发次数">历史触发</th><th title="原始回测胜率">胜率</th><th title="量化过滤后胜率">过滤胜率</th><th title="盈亏比PF">盈亏比</th><th title="期望收益">期望</th><th title="信号质量评级">质量</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-paper page-section" id="layer-paper">
  <div class="layer-header">
    <div class="layer-num" style="background:var(--gold);">P</div>
    <div><div class="layer-title">模拟交易 & 模拟组合</div><div class="layer-sub">观测信号推荐效果 · 买入/卖出/撤单/持仓 · 多组合对比vs基准（本地保存）</div></div>
    <div style="margin-left:auto;font-size:12px;color:var(--muted);"><span class="live-dot"></span> 挂单自动撮合 · 止盈止损自动执行</div>
  </div>
  <div class="card" style="margin-bottom:16px;border-left:3px solid var(--gold);">
    <div class="card-title"><span class="bar" style="background:var(--gold);"></span>模拟账户概览 <span style="font-size:10px;color:var(--muted);margin-left:6px;">佣金万2.5(最低5元) · 卖出印花税0.05% · T+1</span></div>
    <div id="paperAccountBox"></div>
  </div>
  <div class="card" style="margin-bottom:16px;">
    <div class="card-title"><span class="bar" style="background:var(--up);"></span>下单面板（买卖） <span style="font-size:10px;color:var(--muted);margin-left:6px;">市价单立即成交 · 限价单挂单等待自动撮合</span></div>
    <div id="paperOrderBox"></div>
  </div>
  <div class="grid-2" style="margin-bottom:16px;">
    <div class="card"><div class="card-title"><span class="bar" style="background:var(--blue);"></span>持仓（浮动盈亏实时）</div><div id="paperPositionBox" style="max-height:380px;overflow-y:auto;"></div></div>
    <div class="card"><div class="card-title"><span class="bar" style="background:var(--muted);"></span>挂单（可撤单）</div><div id="paperPendingBox" style="max-height:380px;overflow-y:auto;"></div></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:10px;color:var(--muted);margin-left:6px;">按信号来源分组 · 胜率/盈亏比/期望</span></div>
    <div id="paperStatsBox"></div>
  </div>
  <div class="card" style="margin-bottom:16px;">
    <div class="card-title"><span class="bar" style="background:var(--gold);"></span>交易分析 <span style="font-size:10px;color:var(--muted);margin-left:6px;">权益曲线 · 回撤 · 盈亏分布</span></div>
    <div id="paperAnalysisBox"></div>
  </div>
  <div class="card" style="margin-bottom:16px;">
    <div class="card-title"><span class="bar" style="background:var(--muted);"></span>成交记录</div>
    <div id="paperTradeBox" style="max-height:300px;overflow-y:auto;"></div>
  </div>
  <div class="card" style="margin-bottom:16px;border-left:3px solid var(--up);">
    <div class="card-title"><span class="bar" style="background:var(--up);"></span>模拟组合管理 <span style="font-size:10px;color:var(--muted);margin-left:6px;">组合增删改 · 成分股配置 · 多组合对比vs上证</span></div>
    <div id="paperPortfolioBox"></div>
  </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>
<div class="overlay" id="infoOverlay"><div class="overlay-panel" style="max-width:560px;"><button class="overlay-close" onclick="closeOverlay('infoOverlay')">&#10005;</button><div id="infoContent"></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__;
window._fullMarketSectors=__FULL_MARKET_SECTORS_JSON__;
window._hotSectorStocks=__HOT_SECTOR_STOCKS_JSON__;
window._marketBreadth=__MARKET_BREADTH_JSON__;
window._t0EtfList=__T0_ETF_LIST_JSON__;
window._emoPoolsData=__EMO_POOLS_JSON__;
window._davSentiData=__DAV_SENTI_JSON__;
window._econCalData=__ECON_CAL_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 chartInstances={};
// 安全初始化图表：init 前先 dispose 容器上的旧实例。
// 原因：ECharts5 对已有实例的 DOM 重复 init 会直接返回旧实例；若此前 innerHTML='' 已移除其 canvas，
// 渲染会发生在离屏 canvas 上，页面图表消失（涨跌分布 60s 刷新即触发）。
function initChartSafe(domId,instKey){
  const dom=document.getElementById(domId);if(!dom)return null;
  const old=echarts.getInstanceByDom(dom);
  if(old){try{old.dispose();}catch(e){}}
  const ch=echarts.init(dom);
  if(instKey)chartInstances[instKey]=ch;
  return ch;
}
// ===== 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 reRenderCharts(){Object.values(chartInstances).forEach(c=>{if(c&&c.dispose)c.dispose()});Object.keys(chartInstances).forEach(k=>delete chartInstances[k]);renderRiskDashboard();renderL1Charts();renderBreadthChart();renderUpDownDist();renderTodayHot();renderT0List();renderL2Charts();renderRotationInsight();renderRotationQuadrant();renderRotationLadder();renderChainInsight();renderChainGraph();renderFullMarketRotation();renderRadarCharts();renderQuantStrategy();renderLearningCenter();renderTicker();renderPlaybook();renderCalibration();renderEmotionCycle();renderLadder();renderMarketPulse();renderMomentum();renderEconCal();if(modVisible('turnover'))fetchTurnover();if(modVisible('moneyflow'))fetchMoneyFlow();if(modVisible('volmonitor'))fetchVolMonitor();if(modVisible('idxfly'))fetchGlobalMarkets();if(modVisible('rsgrid'))fetchRSGrid();if(modVisible('factorstyle'))fetchFactorStyle();applyLayout();}

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();renderRadarCharts();renderOpPanel();renderSignalMatrix();renderTable();renderCalibration();},
    'layer1':function(){renderMarketTrend();renderL1Charts();renderIdxTpo();renderUpDownDist();renderTodayHot();renderT0List();renderEmotionCycle();renderLadder();renderDavSenti();renderMarketPulse();renderMomentum();renderStockT0();renderEconCal();},
    'layer-tpo':function(){renderTpoLayer();},
    'layer2':function(){refreshFullMarketRotation();renderHeatmap();renderL2Charts();},
    'layer4':function(){renderQuantStrategy();renderLearningCenter();},
    'layer-paper':function(){renderPaperAll();startPaperLoop();},
    'layer-watch':function(){renderWatchlistPage();},
  };
  var fn=renderMap[id];
  if(fn){requestAnimationFrame(function(){requestAnimationFrame(fn);});}
}
// layer2 尺寸变化时重渲图表：覆盖窗口 resize（产业链图节点坐标依赖容器宽高）与初始隐藏态 0 宽渲染
(function(){
  var _rt=null;
  try{
    var l2=document.getElementById('layer2');
    if(l2&&window.ResizeObserver){
      var ro=new ResizeObserver(function(entries){
        var w=entries[0].contentRect.width;
        clearTimeout(_rt);
        if(w>100){_rt=setTimeout(function(){renderRotationQuadrant();renderChainGraph();renderL2Charts();},150);}
      });
      ro.observe(l2);
    }
  }catch(e){}
})();
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')}));

// ===== 指标说明弹窗（各看板指标） =====
const METRIC_INFO={
  'gauge':{title:'多空偏向仪表盘',def:'实时量化全市场多空力量的综合情绪仪表，读数范围 -100 ~ +100。',calc:'信号偏向(60%) + 大盘指数涨跌(40%)：信号偏向 = (看涨家数 - 看跌家数) ÷ 总家数 × 100；指数分 = 上证/深证成指/创业板指/科创50/北证50 五大指数平均涨跌幅 × 10，再映射到仪表盘刻度。',read:'读数 >0 表示多头占优、<0 表示空头占优，绝对值越大倾向越强。配套统计卡同步展示上证指数、20日涨跌、MA5/MA20/MA60 均线以辅助判断趋势方向。'},
  'breadth':{title:'市场宽度分析',def:'通过全市场上涨/下跌家数衡量涨跌参与的广度，判断行情是否健康、能否持续。',calc:'统计全A股当日上涨、下跌、平盘、停牌家数及占比，并结合涨停/跌停家数（优先取实时涨停池）。上涨家数 > 下跌家数 1.5 倍判定为多头主导；反之空头主导；介于其间为多空均衡。',read:'指数上涨而宽度不足（赚指数不赚钱）往往是顶部信号；指数回调但宽度尚可通常只是洗盘。宽度是趋势延续的必要条件。'},
  'emotion':{title:'涨停情绪周期',def:'基于A股全市场实时涨停池/跌停池/炸板池推演的短线情绪温度计（0 - 100 分）。',calc:'评分由涨停家数、连板空间高度、炸板率、连板率、上证涨跌、跌停家数加权合成。阶段依次为 冰点 → 退潮 → 回暖 → 发酵 → 高潮 → 分歧，反映市场赚钱效应的循环节律。',read:'发酵/高潮期顺势做多主线龙头，分歧期只保留核心龙头，退潮/冰点期以现金为王、严控回撤。数据覆盖沪深京全A股市场。'},
  'ladder':{title:'涨停梯队 · 连板天梯',def:'按连板高度对全A股涨停股分层，直接反映资金接力意愿与市场空间高度。',calc:'按 1板/2板/3板… 分组，展示各高度家数与代表个股（按封单资金降序）。连板高度与家数共同体现赚钱效应强弱。',read:'5 板以上（梯队高点）是主线龙头与情绪标杆；高位股集体断板往往预示情绪退潮。数据覆盖A股全市场。'},
  'updown':{title:'全市场涨跌幅分布',def:'全A股当日涨跌幅的 13 档直方图，一眼看清市场涨跌结构。',calc:'将全部个股涨跌幅归入 ≤-7% 至 ≥7% 的 13 个区间并计数，同时统计上涨/下跌/平盘/停牌家数，剔除退市整理股。',read:'右偏（多数上涨）为普涨行情、左偏为普跌。结合涨停/跌停家数可判断情绪极端程度。'},
  'todayhot':{title:'今天炒什么',def:'聚合全市场最强题材方向、最弱板块与量化信号精选的机会个股。',calc:'取全市场涨幅居前板块，匹配其成分机会个股并标注回测胜率与信号质量；同时列出弱势板块与按胜率排序的量化精选信号。',read:'题材与量化共振的个股更具持续性，胜率徽标越高代表历史回测表现越稳。'},
  't0':{title:'场内T+0 ETF机会',def:'支持当日回转交易（当日买入当日可卖出）的ETF，用幻方多因子模型淘日内做T机会。',calc:'覆盖跨境/债券/黄金/商品ETF（货币ETF波动≈0已剔除）。按类型分级阈值：跨境及格线1.0%、黄金/商品0.6%、债券0.2%，最低价差同步分级（0.25%/0.20%/0.12%）。五因子评分：振幅空间、方向确定、量能活性、波动质量、位置质量，综合给出 A/B/C 级机会、预计胜率与具体买卖点。',read:'每条机会均给出低吸/高抛价、止损价与操作依据；非交易时段或波动不足时展示候选池概览。'},
  'marketpulse':{title:'市场异动监测',def:'实时扫描全市场板块与个股异动，包括急涨急跌板块、量比突变、资金异动。',calc:'基于东财实时板块数据（涨跌幅排序Top/Bottom）、个股量比（vol_ratio）、成交额突变、信号质量评分综合推演。板块异动阈值：涨幅≥2%为急涨、≤-2%为急跌；量比≥2.5为放量异动。',read:'板块急涨+资金流入=主线确认；板块急跌+放量=退潮预警；量比突变+价格低位=资金吸筹信号。'},
  'momentum':{title:'蓄势待发',def:'从Top50扫描池中筛选高评分但尚未放量突破的个股，锁定即将启动的交易机会。',calc:'筛选条件：信号质量≥60分 + 量化评分≥50分 + 当日涨跌幅在[-1%,3%]区间 + 量比在0.8~2.0之间（温和放量未爆发）+ ADX≥20（趋势形成中）。按综合蓄势得分排序。',read:'蓄势得分=信号质量×0.4+量化评分×0.3+量能温和度×0.2+趋势强度×0.1。得分越高越接近突破点，关注放量突破信号。'},
  'stockt0':{title:'个股做T参考',def:'从Top50个股中筛选适合日内做T的标的，提供日内支撑压力位和T0买卖点。',calc:'筛选条件：成交额≥3亿（流动性保障）+ 量比≥1.5（活跃度）+ 日内振幅≥2%（做T空间）。通过分时数据计算VWAP、支撑压力位、量价配合，给出顺向/逆向/网格做T建议。',read:'支撑位=日内分时密集低点，压力位=日内分时密集高点；VWAP为加权均价，价格在VWAP下方适合顺向做T（先买后卖），上方适合逆向做T（先卖后买）。'},
  'econcal':{title:'重磅事件日历',def:'参考华尔街见闻日历，梳理未来30天重磅经济事件，涵盖美联储议息、CPI/PPI/PCE/非农等关键数据，给出利好利空研判。',calc:'事件数据来源：东方财富财经快讯+雪球经济日历+公开预期。利好利空研判基于：数据好坏与市场预期的偏差、事件对流动性预期的影响、对风险偏好的传导路径。',read:'绿色=利好（降息/数据超预期偏弱利好债市+成长股）、红色=利空（通胀超预期/鹰派发声）、黄色=中性/待定。重点关注美联储议息和非农/CPI/PCE三大数据。'},
  'signal':{title:'信号分布',def:'A股全市场个股操作信号的分类占比，反映当前整体操作倾向。',calc:'分页拉取全部A股实时行情（涨跌幅/换手率/量比），实时因子近似评分 = 50基准 + 涨跌幅×6 + 换手率加分(封顶) + 量比确认/背离修正，归入 强烈关注/重点关注/适度关注/观望等待/注意风险/建议规避 六档。数据源不可用时降级为 Top50 扫描池。',read:'强烈关注占比高说明多头机会多；建议规避占比高说明市场风险加大。全市场口径比 Top50 池更能反映真实市场结构。'},
  'indtop':{title:'指标触发频次 Top10',def:'全市场个股当日触发最多的技术指标排行。',calc:'统计扫描池中每只个股当日触发的技术指标并累加，取触发次数前10。',read:'高频指标代表当前市场的主流交易逻辑（如 MACD 金叉、放量突破等），单个指标含义可参考「量化学习中心」的指标说明。'},
  'backtest':{title:'回测胜率统计',def:'扫描池量化策略的历史回测表现汇总。',calc:'统计所有具备回测数据个股的平均胜率、量化过滤后胜率、胜率提升、平均盈亏比、期望收益与量化评分。',read:'量化过滤后胜率高于原始胜率说明多因子过滤有效；期望收益为正才值得长期参与。'},
  'turnover':{title:'两市成交额监控',def:'沪深两市当日分时累计成交额曲线，与昨日同期对比。',calc:'上证(1.000001)+深证(0.399001) 东财分时接口累计成交额逐分钟相加，昨日同期取昨日同一分钟的两市累计值。',read:'较昨日同期放量 >15% 为显著放量（情绪升温，题材持续性更好）；显著缩量则谨慎追高。量价配合是确认主线成色的核心。'},
  'moneyflow':{title:'主力资金流向',def:'A股行业板块当日主力净流入排行（流入Top8 + 流出Top8）。',calc:'东财板块接口按 f62 主力净流入额排序（超大单+大单净额），单位亿元。',read:'主线板块需资金流入配合才有持续性；高位板块大额流出是退潮信号。注意：主力净流入为逐笔大单统计，仅作方向参考。'},
  'vol':{title:'波动率监控',def:'基于上证指数分时数据计算的日内已实现波动率（年化）。',calc:'对上证指数当日分钟收盘价取对数收益率，标准差 × √(240分钟×252日) 年化；展示近5个交易日对比。',read:'>18% 高波动（降仓位、收紧止损）；12-18% 中波动（正常）；<12% 低波动（可适度放开）。波动率抬升时优先做确定性高的方向。'},
  'global':{title:'全球市场·隔夜与亚太',def:'隔夜美股（纳指/标普/道指）、亚太（恒生/日经）与离岸人民币实时状态。',calc:'东财全球指数接口实时行情，美股为隔夜收盘涨跌，恒生/日经为当日盘中。',read:'隔夜美股大跌或离岸人民币快速贬值通常压制A股开盘情绪；科技股风向看纳指，外资流向看人民币汇率。'},
  'rs':{title:'指数相对强度',def:'五大指数当日/5日/20日区间涨跌幅对比，识别领涨主线。',calc:'各指数日K收盘价计算：当日 = 末值/前值-1；5日 = 末值/5日前-1；20日同理。',read:'当日成立且5日/20日同步领先的指数才是真主线（如创业板持续领涨=成长风格主导）；仅当日强的多为脉冲。'},
  'factor':{title:'风格因子表现',def:'大小盘与成长/防御风格当日与5日表现，量化机构风格暴露视角。',calc:'沪深300(大盘)/中证500(中盘)/中证1000(小盘)/创业板指+科创50(成长) 当日与5日涨幅对比；小盘-大盘差值判断大小盘风格。',read:'小盘占优=题材游资主导（打板胜率高）；大盘占优=机构权重主导（趋势票更稳）。风格判断决定了用哪套打法，避免逆风格操作。'},
  'tpo':{title:'大盘指数 · TPO四度空间',def:'对大盘指数做 Market Profile（四度空间）分析：时间、价格、价值、成交量四个维度，把日内价格活动按 30 分钟时段（A~H 字母）累加成水平分布直方图。',calc:'取指数日内1分钟K线，按 8 个 30 分钟时段（上午4个+下午4个）分配字母，统计每个价格档位被多少时段触及（TPO 数）。POC=TPO最多的价位（控制点）；价值区(VA)=含 70% TPO 的价格区间，上沿VAH、下沿VAL。',read:'现价在VAH之上=买方控盘强势、回踩VAH可低吸；跌破VAL=卖方主导、反弹VAL减仓；处于VA内部=区间震荡、高抛低吸。POC 是日内最密集成交区，价格围绕 POC 上下运行反映日内平衡与失衡。'},
  'davsenti':{title:'大V动态 · 情绪风向标',def:'监测知名股市大V的最新发言（微博：52198/wu2198、唐史主任司马迁、狂龙十八段；雪球：质变之路；微信公众号：爱在冰川、景峰看盘），通过关键词匹配分析其看多/看空/中性情绪倾向。',calc:'微博走 weibo.com PC版API（需Cookie），雪球走 xueqiu.com 时间线API（匿名token自动获取），微信公众号经搜狗微信搜索中转（受搜狗索引延迟与反爬限制，数据可能滞后数天至数月，且高频访问会触发验证码）。抓取每位大V最新帖子并先做市场相关性过滤（百余个市场关键词，无任何命中的生活帖/社会新闻帖直接剔除），再对保留帖子使用看涨关键词（加仓/抄底/涨停/主升等33个）和看跌关键词（减仓/清仓/暴跌/破位等32个）匹配，考虑否定前缀（不/别/勿等）翻转情绪，计算每条帖子情绪分和总体情绪。',read:'多数大V偏多=市场情绪乐观但需警惕一致性过高；多数偏空=市场悲观但可能接近底部；多空分歧=市场方向不明需谨慎。大V情绪是市场温度计而非直接买卖信号，应结合技术面和资金面综合判断。微信公众号数据依赖搜狗索引时效，参考时注意发布日期。'}
};
function showMetricInfo(key){
  const info=METRIC_INFO[key];if(!info)return;
  let html='<div style="font-size:20px;font-weight:800;margin-bottom:8px;">'+info.title+'</div>';
  html+='<div style="font-size:13px;color:var(--ink);line-height:1.7;margin-bottom:4px;">'+info.def+'</div>';
  html+='<div class="mi-row"><span class="mi-key">计算口径</span><span>'+info.calc+'</span></div>';
  html+='<div class="mi-row"><span class="mi-key">如何解读</span><span>'+info.read+'</span></div>';
  document.getElementById('infoContent').innerHTML=html;
  openOverlay('infoOverlay');
}

// ===== 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();if(typeof _syncToBackend==='function')_syncToBackend();}
function isWatched(code){return watchList.includes(code);}
function updateWatchCount(){document.getElementById('watchCount').textContent=watchList.length;}
updateWatchCount();

function showWatchlist(el){scrollToSec('layer-watch',el);}
// 东财 kline/get 接口要求 ut 参数（缺省返回空），beg 限定自然日范围（lmt 参数已失效，beg=0 会拉全量历史）
function klineUrl(secid,days){
  const d=new Date(Date.now()-(days||200)*864e5);const p=n=>String(n).padStart(2,'0');
  return '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='+d.getFullYear()+p(d.getMonth()+1)+p(d.getDate())+'&end=20500101&ut=fa5fd1943c7b386f172d6893dbfba10b';
}
// 日K稳健获取：push2his 主源（限流时 ERR_EMPTY_RESPONSE，重试1次）→ 腾讯前复权日K降级（CORS 全开）
// 返回统一格式 [{date,open,close,high,low,volume,ma5,ma10,ma20}]，失败返回 null
function withMA(kline){
  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 prev=i>0?kline[i-1].close:kline[i].open;
    kline[i].chg=kline[i].close-prev;
    kline[i].pct=prev>0?(kline[i].close-prev)/prev*100:0;
  }
  return kline;
}
async function fetchKlineSafe(secid){
  for(let attempt=0;attempt<2;attempt++){
    try{
      const r=await jsonpRequest(klineUrl(secid),8000);
      if(r&&r.data&&r.data.klines&&r.data.klines.length){
        return withMA(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])};}));
      }
      if(r&&r.data&&r.data.klines)throw new Error('empty');
    }catch(e){if(attempt===0)await new Promise(res=>setTimeout(res,700));}
  }
  try{
    const parts=secid.split('.');const code=parts[1];
    // secid 市场前缀优先（1=沪 0=深），避免指数 000001 被误判为深市个股
    const mkt=parts[0]==='1'?'sh':'sz';
    const resp=await fetch('https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param='+mkt+code+',day,,,120,qfq');
    const j=await resp.json();
    const node=j&&j.data&&j.data[mkt+code];
    const arr=node?(node.qfqday||node.day):null;
    if(arr&&arr.length){
      return withMA(arr.map(function(p){return{date: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 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;
    const kline=await fetchKlineSafe(secid);
    if(kline){
      const dom=document.getElementById('wlKline_'+s.code);
      if(dom){const old=echarts.getInstanceByDom(dom);if(old)old.dispose();renderKlineChart(kline,'wlKline_'+s.code);}
    }else{
      const dom=document.getElementById('wlKline_'+s.code);
      if(dom)dom.innerHTML='<div style="text-align:center;padding:40px;color:var(--muted);font-size:13px;">K线数据加载失败（行情源限流），请稍后重新进入本页</div>';
    }
  });
  // 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（stock/get 同样要求 ut；push2delay 主源 + push2 备用 + 搜索接口兜底）
    try{
      const qUrl=(h)=>h+'/api/qt/stock/get?secid='+secid+'&fltt=2&fields=f43,f58,f170&ut=fa5fd1943c7b386f172d6893dbfba10b';
      let q=await jsonpRequest(qUrl('https://push2delay.eastmoney.com'),6000).catch(()=>null);
      if(!q||!q.data)q=await jsonpRequest(qUrl('https://push2.eastmoney.com'),6000).catch(()=>null);
      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+')');}
      else{
        // 兜底：搜索接口反查名称
        const s=await searchAllStocks(code);
        if(s&&s.length){const nm=s[0].name||code;const ne=document.getElementById('wlName_'+code);if(ne)ne.textContent=nm;const hd=document.getElementById('wlHeader_'+code);if(hd)hd.setAttribute('onclick','showDetailEx(\''+code+'\',\''+nm+'\','+mkt+')');}
      }
    }catch(e){}
    // Fetch K-line
    const kline=await fetchKlineSafe(secid);
    const dom=document.getElementById('wlKline_'+code);
    if(kline){if(dom)renderKlineChart(kline,'wlKline_'+code);}
    else if(dom)dom.innerHTML='<div style="text-align:center;padding:40px;color:var(--muted);font-size:13px;">K线数据加载失败（行情源限流），请稍后重新进入本页</div>';
  });
  // 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')||code.startsWith('5')||code.startsWith('9'))?'1':'0';
  const secid=prefix+'.'+code;
  try{
    const r=await jsonpRequest('https://push2delay.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&ut=fa5fd1943c7b386f172d6893dbfba10b',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,etfType){
  if(!bars||bars.length<10)return null;
  const sd=scanData.find(x=>x.code===code)||{};
  let atrPct=sd.bt_atr_pct||0;
  // ==== 类型感知基准：ETF 各类品种波动天然不同，用相对倍数替代一刀切绝对阈值 ====
  // ampBase=振幅及格线(%)；spreadBase=最低有效价差比例。个股默认 2% / 0.4% 与原逻辑一致
  const _et=(etfType||'');
  const isBond=_et.indexOf('债券')>=0,isGold=_et.indexOf('黄金')>=0,isComm=_et.indexOf('商品')>=0,isCross=_et.indexOf('跨境')>=0;
  const ampBase=isBond?0.20:(isGold||isComm)?0.60:isCross?1.00:2.00;
  const spreadBase=isBond?0.0012:(isGold||isComm)?0.0020:isCross?0.0025:0.0040;
  // 低价ETF（如0.5元的跨境）保留3位小数，否则价格精度不够
  function fmtP(v){return v<10?v.toFixed(3):v.toFixed(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;
  if(!(atrPct>0)){
    // 无回测ATR时（如ETF），从分时高低点估算日内真实波幅
    var trSum=0,trCnt=0;
    for(var bi=1;bi<bars.length;bi++){
      var bh=bars[bi].high,bl=bars[bi].low,bpc=bars[bi-1].close;
      trSum+=Math.max(bh-bl,Math.abs(bh-bpc),Math.abs(bl-bpc));trCnt++;
    }
    atrPct=trCnt>0?(trSum/trCnt)/(price||1)*100:2;
    if(!(atrPct>0))atrPct=2;
    atrPct=Math.min(atrPct,10);
  }
  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);
  // 四档价位全局排序为 支撑2 ≤ 支撑1 ≤ 阻力1 ≤ 阻力2。
  // 跳空或 ATR 偏小时 preClose±ATR 带会整体落到 VWAP 带之外，旧的两两交换会让支撑价高于阻力价（低吸>高抛，按指导操作必亏）
  var _lv=[s1,s2,r1,r2].sort(function(a,b){return a-b;});
  s2=_lv[0];s1=_lv[1];r1=_lv[2];r2=_lv[3];
  // 做T最低有效价差：覆盖手续费且随波动放大（spreadBase 按类型：债券万一免五下0.12%即可留利润）
  var minSpread=Math.max(price*spreadBase,atrAmt*0.25);
  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 振幅空间（ra=相对类型基准的振幅倍数：个股2.0/跨境1.0/黄金商品0.6/债券0.2）
  var ra=ampBase>0?amplitude/ampBase:0;
  if(ra>=2.5&&amplitude<=ampBase*4){fac('振幅空间',true,25);}
  else if(ra>=1.5&&ra<2.5){fac('振幅空间',true,20);}
  else if(ra>=1&&ra<1.5){fac('振幅空间',true,12);}
  else if(amplitude>ampBase*4){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<ampBase){
    signal='不合做T';badge='wait';direction='观望';
    action='今日振幅仅'+amplitude.toFixed(2)+'%（'+_et+'及格线 '+ampBase.toFixed(2)+'%），波动空间不足，不适合日内回转。建议观望，等待放量扩大振幅后再做T。';
    entry=s1;target=r1;stop=s2;
  }else if(trendUp&&posInRange<=0.55){
    signal='顺向做T';badge='buy';direction='顺向';
    entry=Math.min(Math.max(s1,vwap*0.997),price);
    target=Math.min(r1,Math.max(preClose*1.01,entry+minSpread));
    stop=Math.max(s2,entry-atrAmt*0.8);
    action='分时多头排列、价格贴近支撑，适合<b>顺向做T（先买后卖）</b>：在 '+fmtP(entry)+' 附近分批低吸，反弹至 '+fmtP(target)+' 附近卖出等量底仓摊低成本。';
    batches='两档低吸：首档现价下方 '+fmtP(price*0.998)+'，回踩 '+fmtP(price*0.995)+' 补第二档。';
    timePlan='<b>开盘</b>观察VWAP方向；<b>盘中</b>回踩不破MA10可低吸；<b>尾盘</b>若未反弹则当日止损，不隔夜加码。';
  }else if(trendDn&&posInRange>=0.45){
    signal='逆向做T';badge='sell';direction='逆向';
    entry=Math.max(Math.min(r1,vwap*1.003),price);
    target=Math.max(s1,Math.min(preClose*0.99,entry-minSpread));
    stop=Math.min(r2,entry+atrAmt*0.8);
    action='分时空头排列、价格贴近阻力，适合<b>逆向做T（先卖后买）</b>：在 '+fmtP(entry)+' 附近高抛底仓，回落至 '+fmtP(target)+' 附近买回赚取差价。';
    batches='两档高抛：首档现价上方 '+fmtP(price*1.002)+'，冲高 '+fmtP(price*1.005)+' 抛第二档。';
    timePlan='<b>开盘</b>冲高不追；<b>盘中</b>反弹至VWAP上方可高抛；<b>尾盘</b>回落至支撑再考虑回补。';
  }else if(amplitude>=ampBase*1.5){
    signal='网格做T';badge='buy';direction='网格';
    var gs=Math.max(price*spreadBase*0.8,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(3)+','+gridUpper.toFixed(3)+'] 区间按约 '+(gs<0.05?gs.toFixed(4):gs.toFixed(2))+' 元/格（'+(gs/price*100).toFixed(2)+'%）高抛低吸，共约 '+gridCount+' 格。';
    batches='网格每格仓位≈底仓的'+(100/gridCount).toFixed(0)+'%，单格止损 '+Math.max(gs,price*spreadBase*1.5).toFixed(gs<0.05?4:2)+' 元。';
    timePlan='<b>开盘</b>确定高低点；<b>盘中</b>区间上沿卖出、下沿买回；<b>尾盘</b>回收全部网格仓位。';
  }else{
    signal='观望';badge='wait';direction='观望';
    action='振幅'+amplitude.toFixed(2)+'%、方向与量能均不足以支撑日内回转，建议观望。支撑 '+fmtP(s1)+'，阻力 '+fmtP(r1)+'。';
    entry=s1;target=r1;stop=s2;
  }
  // ==== 价差终检：低吸必须低于高抛且价差≥minSpread，否则先试阻力2兜底、仍不足则降级观望 ====
  if((badge==='buy'||badge==='sell')&&entry&&target&&stop){
    var _sp=badge==='buy'?target-entry:entry-target;
    if(_sp<minSpread){
      if(badge==='buy'&&r2-entry>=minSpread){target=r2;}
      else if(badge==='sell'&&r2-target>=minSpread){entry=r2;stop=entry+atrAmt*0.8;}
    }
    _sp=badge==='buy'?target-entry:entry-target;
    if(_sp<minSpread){
      signal='价差不足';badge='wait';direction='观望';
      action='日内可用价差不足：支撑 '+fmtP(s1)+' ~ 阻力 '+fmtP(r1)+' 仅 '+((r1-s1)/price*100).toFixed(2)+'%，扣除手续费后无做T利润，建议观望。';
      entry=s1;target=r1;stop=s2;batches='';timePlan='';
    }
  }
  // ==== 幻方多因子 → 日内回转胜率概率估计 ====
  var winProb=50+(score-50)*0.55;
  if(badge==='buy'||badge==='sell')winProb+=4;
  if(ra>=1.5&&ra<=3)winProb+=4;
  else if(ra<1)winProb-=10;
  else if(amplitude>ampBase*4)winProb-=4;
  if(direction==='观望'||badge==='wait')winProb=Math.min(winProb,45);
  winProb=Math.max(28,Math.min(82,Math.round(winProb)));
  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,winProb:winProb,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 wp=t0.winProb||0;
  var wpCol=wp>=60?'var(--up)':(wp>=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><span style="font-size:11px;font-weight:800;color:'+wpCol+';">胜率 '+wp+'%</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 =====
// ===== 全市场宽度图（A股全市场涨跌家数，非Top50池） =====
function renderBreadthChart(){
  if(!modVisible('breadth'))return;
  const ch=initChartSafe('chartBreadth','breadth');if(!ch)return;
  const mb=window._marketBreadth;
  if(!mb||!mb.total){
    ch.clear();
    ch.setOption({title:{text:'全市场宽度数据加载中...',left:'center',top:'middle',textStyle:{color:tc.muted,fontSize:13}}});
    return;
  }
  const up=mb.up||0,down=mb.down||0,flat=mb.flat||0;
  const bias=up>down*1.5?'多头主导':(down>up*1.5?'空头主导':'多空均衡');
  const biasCol=up>down*1.5?tc.up:(down>up*1.5?tc.down:tc.gold);
  const lu=mb.limit_up||0,ld=mb.limit_down||0,sus=mb.suspended||0;
  ch.setOption({
    backgroundColor:'transparent',
    tooltip:tipXL({trigger:'item',formatter:function(p){
      return p.marker+p.name+'：'+p.value+' 只（'+p.percent.toFixed(1)+'%）<br/>占全市场 '+mb.total+' 只';
    }}),
    legend:{bottom:0,textStyle:{color:tc.muted,fontSize:11},itemWidth:12,itemHeight:8,icon:'roundRect',
      formatter:function(name){
        const v={'上涨':up,'下跌':down,'平盘':flat}[name]||0;
        return name+' '+v+'（'+(v/mb.total*100).toFixed(1)+'%）';
      }},
    graphic:[{type:'text',left:'center',top:'38%',style:{text:'涨跌比\n'+(down>0?(up/down).toFixed(2):(up>0?'∞':'--'))+':1',textAlign:'center',fill:tc.ink,fontSize:16,fontWeight:700,lineHeight:24}}],
    series:[{
      type:'pie',radius:['50%','72%'],center:['50%','44%'],avoidLabelOverlap:true,
      label:{show:false},labelLine:{show:false},
      data:[
        {value:up,name:'上涨',itemStyle:{color:tc.up}},
        {value:down,name:'下跌',itemStyle:{color:tc.down}},
        {value:flat,name:'平盘',itemStyle:{color:tc.muted}}
      ]
    }],
    title:{text:bias,subtext:'涨停 '+lu+' · 跌停 '+ld+' · 停牌 '+sus+' · 总计 '+mb.total+' 只',
      left:'right',top:2,textStyle:{color:biasCol,fontSize:13,fontWeight:700},subtextStyle:{color:tc.muted,fontSize:10}}
  },true);
}
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);
  // 上证指标排已删除（改由固定指数卡片条+分时图承载），此处仅在其仍存在时更新
  if(el('mtIdx')){
    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);
	if(el('mtSent')){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('');
}
// ===== 全市场信号分布：分页拉取全部A股实时行情（f3涨跌幅/f8换手率/f10量比），按实时因子近似评分 =====
window._fmSignals=null;
async function fetchAllAShareSignals(){
  if(window._fmSignals)return window._fmSignals;
  const hosts=['https://push2delay.eastmoney.com/api/qt/clist/get','https://push2.eastmoney.com/api/qt/clist/get'];
  const FS='m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23';
  let total=0,first=null;
  for(const host of hosts){
    first=await jsonpRequest(host+'?pn=1&pz=100&po=1&np=1&fltt=2&invt=2&fid=f12&fs='+FS+'&fields=f12,f3,f8,f10',9000).catch(()=>null);
    if(first&&first.data&&first.data.diff){total=first.data.total||first.data.diff.length;break;}
  }
  if(!first||!first.data||!first.data.diff)return null;
  const stocks=first.data.diff.filter(s=>s&&s.f12).map(s=>({code:s.f12,pct:(typeof s.f3==='number')?s.f3:0,turn:(typeof s.f8==='number')?s.f8:0,vr:(typeof s.f10==='number')?s.f10:1}));
  const pages=Math.ceil(total/100);
  for(let p=2;p<=pages;p+=8){
    const batch=[];
    for(let k=p;k<Math.min(p+8,pages+1);k++)batch.push(k);
    const rs=await Promise.all(batch.map(pg=>jsonpRequest(hosts[0]+'?pn='+pg+'&pz=100&po=1&np=1&fltt=2&invt=2&fid=f12&fs='+FS+'&fields=f12,f3,f8,f10',9000).catch(()=>null)));
    rs.forEach(r=>{if(r&&r.data&&r.data.diff)r.data.diff.forEach(s=>{if(s&&s.f12)stocks.push({code:s.f12,pct:(typeof s.f3==='number')?s.f3:0,turn:(typeof s.f8==='number')?s.f8:0,vr:(typeof s.f10==='number')?s.f10:1});});});
  }
  if(stocks.length<1000)return null;
  // 实时因子近似评分：50 基准 + 涨跌幅×6 + 换手率加分(上限) + 量能确认/背离
  const dist={'强烈关注':0,'重点关注':0,'适度关注':0,'观望等待':0,'注意风险':0,'建议规避':0};
  stocks.forEach(s=>{
    let sc=50+s.pct*6+Math.min(s.turn,15)*1.2;
    if(s.vr>=1.5)sc+=4;else if(s.vr<0.7)sc-=3;
    sc=Math.max(0,Math.min(100,sc));
    if(sc>=80)dist['强烈关注']++;
    else if(sc>=65)dist['重点关注']++;
    else if(sc>=55)dist['适度关注']++;
    else if(sc>=45)dist['观望等待']++;
    else if(sc>=35)dist['注意风险']++;
    else dist['建议规避']++;
  });
  window._fmSignals={dist:dist,total:stocks.length};
  return window._fmSignals;
}
function renderL1Charts(){
  if(!modVisible('gauge')&&!modVisible('sigdist')&&!modVisible('indtop'))return;
  const g=initChartSafe('chartGauge','gauge');if(!g)return;
  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:'多空偏向'}]}]});
  renderBreadthChart();
  const a=initChartSafe('chartAction','action');if(!a)return;
  function renderSignalDist(){
    const fm=window._fmSignals;
    const acts=fm?Object.entries(fm.dist):Object.entries(actionDist);
    const srcNote=fm?('全市场 '+fm.total+' 只 · 实时因子评分'):'Top50 扫描池（全市场数据加载中…）';
    a.setOption({backgroundColor:'transparent',title:{text:srcNote,left:'center',bottom:0,textStyle:{color:tc.muted,fontSize:10,fontWeight:400}},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%','46%'],label:{formatter:'{b}:{c}',color:tc.ink,fontSize:10}})]},true);
  }
  renderSignalDist();
  if(!window._fmSignals)fetchAllAShareSignals().then(function(fm){if(fm)renderSignalDist();});
  const ind=initChartSafe('chartInd','ind');if(!ind)return;
  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 =====
var _heatmapChart=null;
function _interpRGB(c1,c2,t){
  var r=Math.round(c1[0]+(c2[0]-c1[0])*t);
  var g=Math.round(c1[1]+(c2[1]-c1[1])*t);
  var b=Math.round(c1[2]+(c2[2]-c1[2])*t);
  return 'rgb('+r+','+g+','+b+')';
}
function _pctColor(pct){
  var a=Math.min(Math.abs(pct)/5,1);
  var NEUTRAL=[42,51,68];
  var RED=[229,57,53];
  var GREEN=[67,160,71];
  if(pct>=0)return _interpRGB(NEUTRAL,RED,a);
  return _interpRGB(NEUTRAL,GREEN,a);
}
function _pctTextColor(pct){
  var a=Math.min(Math.abs(pct)/5,1);
  return a>0.3?'#fff':'rgba(255,255,255,0.5)';
}
function renderHeatmap(){
  var el=document.getElementById('heatmapGrid');if(!el)return;
  var dimEl=document.getElementById('hmDim');
  var cntEl=document.getElementById('hmCount');
  var dim=dimEl?dimEl.value:'mktCapYi';
  var cnt=cntEl?parseInt(cntEl.value):50;
  var src=window._fmBoards;
  if(!src||!src.length){
    src=(window._fullMarketSectors||[]).map(function(s){return {code:s.code,name:s.name,pct:s.pct_change||0,amtYi:parseFloat(String(s.amount).replace('亿',''))||0,mktCapYi:s.mkt_cap_yi||0,lead_stock:s.lead_stock||'',lead_code:s.lead_code||''};});
  }
  if(!src||!src.length){
    el.innerHTML='<div style="color:var(--muted);text-align:center;padding:40px;">全市场板块数据加载中...</div>';
    return;
  }
  var list=src.filter(function(s){return Math.abs(s.pct||0)>=0.01;});
  if(dim==='pct'){
    list.sort(function(a,b){return Math.abs(b.pct)-Math.abs(a.pct);});
  }else{
    list.sort(function(a,b){return (b[dim]||0)-(a[dim]||0);});
  }
  if(cnt>0)list=list.slice(0,cnt);
  if(_heatmapChart){try{_heatmapChart.dispose();}catch(e){}_heatmapChart=null;}
  if(typeof echarts==='undefined')return;
  var maxVal=Math.max.apply(null,list.map(function(s){return s[dim]||0;}));
  _heatmapChart=echarts.init(el);
  _heatmapChart.setOption({
    tooltip:{
      backgroundColor:'rgba(19,24,41,0.96)',borderColor:'#2a3050',borderWidth:1,padding:[10,14],
      textStyle:{color:'#e8eaf0',fontSize:12},
      extraCssText:'border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.35);line-height:1.7;',
      formatter:function(p){
        var d=p.data;
        var h='<b style="font-size:14px;">'+d.name+'</b><br/>';
        h+='<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:'+(d.pct>=0?'#e53935':'#43a047')+';margin-right:6px;"></span>';
        h+='涨跌幅: <b>'+(d.pct>=0?'+':'')+d.pct.toFixed(2)+'%</b><br/>';
        h+='市值: '+d.mktCapYi.toFixed(0)+'亿 · 成交: '+d.amtYi.toFixed(0)+'亿';
        if(d.lead_stock&&d.lead_stock!=='--')h+='<br/>领涨: '+d.lead_stock;
        return h;
      }
    },
    series:[{
      type:'treemap',
      data:list.map(function(s){
        var val=s[dim]||0;
        var ratio=val/maxVal;
        var nameFS=ratio>0.15?13:(ratio>0.06?11:(ratio>0.02?10:9));
        var pctFS=ratio>0.15?15:(ratio>0.06?13:(ratio>0.02?11:10));
        var showName=ratio>0.015;
        var showPct=ratio>0.015;
        var c=_pctColor(s.pct||0);
        var tc=_pctTextColor(s.pct||0);
        return {
          name:s.name,
          value:Math.max(val,0.001),
          pct:s.pct||0,
          mktCapYi:s.mktCapYi||0,
          amtYi:s.amtYi||0,
          lead_stock:s.lead_stock||'',
          code:s.code||'',
          itemStyle:{color:c,borderColor:'#0a0e1a',borderWidth:2,gapWidth:2},
          label:{
            show:showName,
            formatter:function(){
              if(!showPct)return '{b|'+s.name+'}';
              return '{b|'+s.name+'}\n{c|'+(s.pct>=0?'+':'')+s.pct.toFixed(2)+'%}';
            },
            rich:{
              b:{fontSize:nameFS,fontWeight:600,color:tc,lineHeight:Math.round(nameFS*1.4),overflow:'truncate'},
              c:{fontSize:pctFS,fontWeight:800,fontFamily:'SF Mono,monospace',color:tc,lineHeight:Math.round(pctFS*1.4)}
            },
            position:'inside',align:'center',verticalAlign:'middle',padding:0
          },
          upperLabel:{show:false},
        };
      }),
      leafDepth:1,roam:false,nodeClick:false,
      width:'100%',height:'100%',top:0,left:0,right:0,bottom:0,
      breadcrumb:{show:false},
      levels:[{itemStyle:{borderColor:'#0a0e1a',borderWidth:2,gapWidth:2},upperLabel:{show:false}}],
    }],
  });
  _heatmapChart.on('click',function(p){if(p.data&&p.data.code)showBoardDetail(p.data.code);});
  window.addEventListener('resize',function(){if(_heatmapChart)_heatmapChart.resize();});
  var leg=document.getElementById('heatmapLegend');
  if(leg){
    var stops=[];
    for(var i=0;i<=20;i++){
      var pct=-5+0.5*i;
      stops.push('<div style="flex:1;height:100%;background:'+_pctColor(pct)+';"></div>');
    }
    leg.innerHTML=stops.join('')+'<div style="position:absolute;display:flex;justify-content:space-between;width:calc(100% - 0px);padding:0 4px;font-size:10px;color:var(--muted);pointer-events:none;margin-top:-24px;"><span>-5%</span><span>0</span><span>+5%</span></div>';
    leg.style.position='relative';
  }
}
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(){
  // 板块买卖信号分布/热门概念分布/行业分布统计已按需求移除，此函数保留为空壳以兼容主题切换等调用点
}

// ===== 板块轮动推演（A股全市场实时） + 产业链上下游推演 =====
const SECTOR_STAGE_MAP=__SECTOR_STAGE_JSON__;
const SECTOR_SUB_MAP=__SECTOR_SUB_JSON__;
const CHAIN_EDGE_INFO=__CHAIN_EDGE_INFO_JSON__;
const FM_STAGES=['上游','中游','下游','金融','交运'];
// 东方财富板块名 → 产业链标准名（两套命名体系归一，否则全市场数据无法挂到产业链边上）
const EM_ALIAS={
  '煤炭行业':'煤炭开采','焦炭加工':'焦炭Ⅱ','钢铁行业':'普钢','特钢':'特钢Ⅱ',
  '电力行业':'电力','燃气':'燃气Ⅱ','中药':'中药Ⅱ','白酒':'白酒Ⅱ',
  '房地产':'房地产开发','家电行业':'白色家电',
  '商业百货':'专业连锁Ⅱ','超市连锁':'专业连锁Ⅱ','专业连锁':'专业连锁Ⅱ',
  '证券':'证券Ⅱ','券商':'证券Ⅱ','航运港口':'港口航运','港口':'港口航运',
  '航天航空':'航空装备Ⅱ','航空装备':'航空装备Ⅱ','农药兽药':'农化制品','化肥行业':'农化制品'
};
function canonName(n){return EM_ALIAS[n]||n;}
function tierColor(t){return ({'领跑主线':tc.up,'接力补涨':tc.orange,'蓄势观察':tc.muted,'动能衰减':tc.blue,'退潮回避':tc.down})[t]||tc.muted;}
window._fmBoards=null;
// JSONP 分页拉取东方财富行业板块（m:90+t:2，涨跌两个方向各取前200，覆盖约400/496个），降级用生成时快照
async function fetchFullMarketBoards(){
  if(window._fmBoards)return window._fmBoards;
  const hosts=['https://push2delay.eastmoney.com/api/qt/clist/get','https://push2.eastmoney.com/api/qt/clist/get'];
  const seen={};let all=[];
  const pull=async function(host,po){
    for(let p=1;p<=2;p++){
      const r=await jsonpRequest(host+'?pn='+p+'&pz=100&po='+po+'&np=1&fltt=2&invt=2&fid=f3&fs=m:90+t:2&fields=f12,f14,f3,f20,f21,f128,f140',9000).catch(()=>null);
      if(r&&r.data&&r.data.diff)r.data.diff.forEach(b=>{if(b&&b.f12&&!seen[b.f12]){seen[b.f12]=1;all.push(b);}});
    }
  };
  for(const host of hosts){
    try{
      await pull(host,1);
      await pull(host,0);
      if(all.length>=50)break;
    }catch(e){}
  }
  if(all.length){
    window._fmBoards=all.map(b=>({code:b.f12,name:b.f14,pct:(typeof b.f3==='number'?b.f3:0),amtYi:(typeof b.f20==='number'?b.f20/1e8:0),mktCapYi:(typeof b.f21==='number'?b.f21/1e8:0),lead_stock:b.f128||'',lead_code:b.f140||''})).sort((a,b)=>b.pct-a.pct);
  }else{
    window._fmBoards=(window._fullMarketSectors||[]).map(s=>({code:s.code,name:s.name,pct:s.pct_change||0,amtYi:parseFloat(String(s.amount).replace('亿',''))||0,mktCapYi:s.mkt_cap_yi||0,lead_stock:s.lead_stock||'',lead_code:s.lead_code||''})).sort((a,b)=>b.pct-a.pct);
  }
  return window._fmBoards;
}
// 全市场轮动条目：先按标准名去重（东财多个板块可映射同名，如 航运港口+港口→港口航运；ECharts 按名合并节点，重名会列内缺位）
function fmRotationItems(){
  const src=window._fmBoards;
  const maxAmt=Math.max(...src.map(b=>b.amtYi),1);
  const byCanon={};
  src.forEach(b=>{
    const c=canonName(b.name);
    if(!byCanon[c])byCanon[c]={code:b.code,pct:b.pct,amtYi:b.amtYi,lead_stock:b.lead_stock,lead_code:b.lead_code};
    else{byCanon[c].amtYi+=b.amtYi;if(Math.abs(b.pct)>Math.abs(byCanon[c].pct))byCanon[c].pct=b.pct;if(!byCanon[c].lead_stock&&b.lead_stock){byCanon[c].lead_stock=b.lead_stock;byCanon[c].lead_code=b.lead_code;}}
  });
  return Object.keys(byCanon).map(c=>{
    const b=byCanon[c];
    const score=Math.max(0,Math.min(100,Math.round(50+b.pct*5+(b.amtYi/maxAmt)*10)));
    const tier=score>=68?'领跑主线':score>=56?'接力补涨':score>=44?'蓄势观察':score>=34?'动能衰减':'退潮回避';
    return {name:c,emName:c,code:b.code,pct:b.pct,amtYi:b.amtYi,lead_stock:b.lead_stock,lead_code:b.lead_code,stage:SECTOR_STAGE_MAP[c]||'其他',sub:SECTOR_SUB_MAP[c]||'',score:score,tier:tier};
  }).sort((a,b)=>b.pct-a.pct);
}
function renderRotationInsight(){
  const dom=document.getElementById('rotationInsight');if(!dom)return;
  const items=fmRotationItems();
  const up=items.filter(i=>i.pct>0).length,down=items.filter(i=>i.pct<0).length;
  const lead=items.slice(0,4),follow=items.filter(i=>i.tier==='接力补涨').slice(0,4),fade=items.slice(-4).reverse();
  const fmt=a=>a.map(i=>i.name+' '+(i.pct>=0?'+':'')+i.pct.toFixed(2)+'%').join('、')||'--';
  const dir=up>=down*1.5?'全市场普涨格局，主线扩散，跟随领涨板块顺势配置':(up>=down?'结构分化，资金高低切换，锁定领涨主线、回避退潮方向':'轮动降温，下跌板块居多，控制仓位等待回暖信号');
  dom.innerHTML='<div class="ri-block"><div class="ri-label">轮动主线（全市场领涨）</div><div class="ri-val" style="color:var(--up);">'+fmt(lead)+'</div></div>'
    +'<div class="ri-block"><div class="ri-label">接力补涨（第二梯队）</div><div class="ri-val" style="color:var(--orange);">'+fmt(follow)+'</div></div>'
    +'<div class="ri-block"><div class="ri-label">退潮回避（全市场领跌）</div><div class="ri-val" style="color:var(--down);">'+fmt(fade)+'</div></div>'
    +'<div class="ri-dir">推演方向：上涨'+up+' / 下跌'+down+'个板块 —— '+dir+'（数据源：东方财富全市场行业板块，页面加载时实时拉取）</div>';
}
function renderRotationLadder(){
  const dom=document.getElementById('rotationLadder');if(!dom)return;
  const items=fmRotationItems();
  const tiers=['领跑主线','接力补涨','蓄势观察','动能衰减','退潮回避'];
  dom.innerHTML=tiers.map(function(t){
    let list=items.filter(function(r){return r.tier===t;});
    const shown=list.slice(0,15);
    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">'+shown.map(function(r){return '<span class="rot-chip" style="border-color:'+hexA(c,.35)+';cursor:pointer;" onclick="'+(r.code?'showBoardDetail(\''+r.code+'\')':'showSectorDetail(\''+r.name+'\')')+'">'+r.name+'<b style="color:'+c+';margin-left:4px;">'+(r.pct>=0?'+':'')+r.pct.toFixed(1)+'%</b></span>';}).join('')+(list.length>15?'<span style="color:var(--muted);font-size:11px;align-self:center;">…等'+list.length+'个</span>':'')+'</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 items=fmRotationItems();
  const data=items.map(function(r){return {name:r.name,value:[r.pct,r.amtYi],pct:r.pct,amt:r.amtYi,score:r.score,tier:r.tier,lead:r.lead_stock,leadCode:r.lead_code,code:r.code};});
  ch.setOption({
    backgroundColor:'transparent',
    tooltip:tipXL({formatter:function(p){const d=p.data;return '<b>'+d.name+'</b>（'+d.tier+'·'+d.score+'分）<br>动量：'+(d.pct>=0?'+':'')+d.pct.toFixed(2)+'%<br>成交额：'+d.amt.toFixed(1)+'亿'+(d.lead?'<br>领涨股：'+d.lead:'');}}),
    grid:{left:10,right:30,top:16,bottom:44,containLabel:true},
    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 8+Math.min(Math.sqrt(Math.max(v[1],0))*1.6,26);},
      itemStyle:{color:function(p){return tierColor(p.data.tier);},opacity:.82,shadowBlur:8,shadowColor:'rgba(0,0,0,.28)'},
      label:{show:true,position:'top',formatter:function(p){return p.dataIndex<12?p.data.name:'';},color:tc.ink,fontSize:10,fontWeight:600},
      labelLayout:{hideOverlap:true},
      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}]}
    }]
  });
  ch.off('click');ch.on('click',function(p){if(p.data&&p.data.name){if(p.data.code)showBoardDetail(p.data.code);else showSectorDetail(p.data.name);}});
}
// 全市场板块详情（Top50池外的板块用实时数据弹窗）
function showBoardDetail(code){
  const items=fmRotationItems();const r=items.find(x=>x.code===code);if(!r)return;
  if(sectorData&&sectorData.find(s=>s.name===r.name)){showSectorDetail(r.name);return;}
  let html='<h2 style="margin-bottom:8px;">'+r.name+'（全市场板块）</h2>';
  html+='<div class="stat-grid" style="margin-bottom:14px;">';
  html+='<div class="stat-item"><div class="stat-val '+(r.pct>=0?'up':'down')+'">'+(r.pct>=0?'+':'')+r.pct.toFixed(2)+'%</div><div class="stat-lbl">板块涨幅</div></div>';
  html+='<div class="stat-item"><div class="stat-val gold">'+r.score+'</div><div class="stat-lbl">轮动分</div></div>';
  html+='<div class="stat-item"><div class="stat-val blue">'+r.amtYi.toFixed(1)+'亿</div><div class="stat-lbl">成交额</div></div>';
  html+='<div class="stat-item"><div class="stat-val" style="color:'+tierColor(r.tier)+';">'+r.tier+'</div><div class="stat-lbl">轮动梯队</div></div>';
  html+='</div>';
  if(r.lead_stock)html+='<div style="font-size:13px;margin-bottom:10px;">领涨股：<b style="color:var(--gold);cursor:pointer;" onclick="closeOverlay(\'sectorOverlay\');showDetailAny(\''+(r.lead_code||'')+'\',\''+String(r.lead_stock).replace(/'/g,'')+'\')">'+r.lead_stock+'</b>（点击查看详情）</div>';
  html+='<div style="font-size:12px;color:var(--muted);">产业链环节：'+(r.stage||'其他')+(r.sub?'（'+r.sub+'）':'')+'。该板块不在当前 Top50 扫描池内，个股级信号暂无；可在全市场行情软件查看成分股。</div>';
  document.getElementById('sectorContent').innerHTML=html;
  openOverlay('sectorOverlay');
}
// 产业链推演说明面板：环节强弱 + 传导结论 + 活跃传导边
function edgeState(srcPct,tgtPct){
  if(srcPct>=0.8&&tgtPct>=0.8)return {k:'resonance',label:'共振上涨',color:tc.up,width:2.6};
  if(srcPct>=0.8&&tgtPct<srcPct-0.8)return {k:'squeeze',label:'成本挤压',color:tc.down,width:2.2};
  if(srcPct>=0.8)return {k:'pending',label:'待传导',color:tc.gold,width:1.8};
  if(srcPct<=-0.8&&tgtPct>srcPct+0.8)return {k:'relief',label:'成本红利',color:tc.blue,width:1.8};
  return {k:'muted',label:'',color:hexA(tc.muted,.35),width:1};
}
function renderChainInsight(){
  const dom=document.getElementById('chainInsight');if(!dom)return;
  const items=fmRotationItems().filter(r=>FM_STAGES.indexOf(r.stage)>=0);
  if(!items.length){dom.innerHTML='';return;}
  const stageAgg=FM_STAGES.map(st=>{
    const list=items.filter(r=>r.stage===st).sort((a,b)=>b.pct-a.pct);
    const avg=list.length?list.reduce((a,b)=>a+b.pct,0)/list.length:0;
    return {stage:st,avg:avg,n:list.length,top:list.slice(0,2)};
  });
  const sorted=[...stageAgg].sort((a,b)=>b.avg-a.avg);
  const strongest=sorted[0],weakest=sorted[sorted.length-1];
  let verdict,verdictColor;
  if(strongest.stage==='上游'&&['中游','下游'].indexOf(weakest.stage)>=0){verdict='成本推动型行情：上游资源领涨（'+strongest.avg.toFixed(2)+'%）而'+weakest.stage+'偏弱（'+weakest.avg.toFixed(2)+'%），涨价沿产业链向下传导受阻。推演：优先持有上游资源，观察下游能否提价顺价——顺价成功则行情扩散至中下游，无法顺价则中游毛利承压（回避）。';verdictColor='var(--gold)';}
  else if(strongest.stage==='下游'){verdict='需求拉动型行情：终端消费/地产率先走强（'+strongest.avg.toFixed(2)+'%），订单将沿下游→中游→上游回补库存。推演：优先下游龙头，中游制造1-2个季度内有望补涨，上游跟随量价齐升。';verdictColor='var(--up)';}
  else if(strongest.stage==='中游'){verdict='制造景气型行情：中游制造独立走强（'+strongest.avg.toFixed(2)+'%），多对应设备更新/出海/订单逻辑。推演：持有中游龙头，上游资源需量能验证后跟进，下游观察提价落地。';verdictColor='var(--up)';}
  else if(strongest.stage==='金融'){verdict='金融先行信号：券商/多元金融领涨（'+strongest.avg.toFixed(2)+'%），通常对应政策预期与风险偏好回升。推演：金融搭台后关注地产链融资改善与成长板块接力。';verdictColor='var(--blue)';}
  else{verdict='交运物流走强（'+strongest.avg.toFixed(2)+'%）：出口/大宗运量驱动，观察集运运价与吞吐量验证。推演：量价齐升则扩散至港口航运全链，运价回落则以波段对待。';verdictColor='var(--blue)';}
  // 活跃传导边（取信号最强的3条）
  const byName={};items.forEach(r=>byName[r.name]=r);
  const active=[];
  CHAIN_EDGE_INFO.forEach(e=>{
    const tn=canonName(e.target);
    if(tn===e.source)return;
    const s=byName[e.source],t=byName[tn];
    if(!s||!t)return;
    const st=edgeState(s.pct,t.pct);
    if(st.k!=='muted')active.push({e:{source:e.source,target:tn,note:e.note},s:s,t:t,st:st});
  });
  active.sort((a,b)=>Math.abs(b.s.pct-b.t.pct)-Math.abs(a.s.pct-a.t.pct));
  const topActive=active.slice(0,3);
  let h='<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;margin-bottom:10px;">';
  stageAgg.forEach(sa=>{
    const c=sa.avg>=0.5?'var(--up)':sa.avg<=-0.5?'var(--down)':'var(--muted)';
    h+='<div style="background:var(--bg3);border-radius:8px;padding:8px 10px;"><div style="font-size:10px;color:var(--muted);">'+sa.stage+' · '+sa.n+'个板块</div><b style="color:'+c+';font-size:15px;">'+(sa.avg>=0?'+':'')+sa.avg.toFixed(2)+'%</b><div style="font-size:10px;color:var(--muted);margin-top:2px;">'+sa.top.map(t=>t.name+' '+(t.pct>=0?'+':'')+t.pct.toFixed(1)+'%').join('<br>')+'</div></div>';
  });
  h+='</div>';
  h+='<div style="background:var(--bg3);border-left:3px solid '+verdictColor+';border-radius:8px;padding:10px 12px;font-size:12px;line-height:1.8;color:var(--ink);"><b style="color:'+verdictColor+';">产业链推演：</b>'+verdict+'</div>';
  if(topActive.length){
    h+='<div style="margin-top:8px;font-size:11px;line-height:1.9;">';
    h+='<b style="color:var(--gold);">今日活跃传导（悬停图上箭头查看完整逻辑）：</b><br>';
    topActive.forEach(a=>{
      h+='<span style="color:'+a.st.color+';">● </span>'+a.e.source+'（'+(a.s.pct>=0?'+':'')+a.s.pct.toFixed(2)+'%）→ '+a.e.target+'（'+(a.t.pct>=0?'+':'')+a.t.pct.toFixed(2)+'%）：'+a.st.label+'。'+a.e.note+'<br>';
    });
    h+='</div>';
  }
  dom.innerHTML=h;
}
// 产业链泳道渲染：五列环节泳道 + 板块卡片（自适应宽度，无需缩放拖拽；传导关系以卡片下方彩色圆点呈现，悬停看完整逻辑）
function renderChainGraph(){
  const dom=document.getElementById('chainLanes');if(!dom)return;
  const items=fmRotationItems();
  const byStage={};
  items.forEach(function(r){if(FM_STAGES.indexOf(r.stage)>=0)(byStage[r.stage]=byStage[r.stage]||[]).push(r);});
  Object.keys(byStage).forEach(function(k){byStage[k].sort(function(a,b){return b.pct-a.pct;});});
  const byName={};items.forEach(function(r){if(FM_STAGES.indexOf(r.stage)>=0)byName[r.name]=r;});
  const edges=[];
  CHAIN_EDGE_INFO.forEach(function(e){
    const tn=canonName(e.target);
    if(tn===e.source)return;
    const s=byName[e.source],t=byName[tn];
    if(!s||!t)return;
    edges.push({s:e.source,t:tn,st:edgeState(s.pct,t.pct),note:e.note});
  });
  const rel={};
  edges.forEach(function(e){
    if(e.st.k==='muted')return;
    (rel[e.s]=rel[e.s]||[]).push(e);
    (rel[e.t]=rel[e.t]||[]).push(e);
  });
  const STAGE_DESC={'上游':'资源与原材料','中游':'加工与制造','下游':'终端消费与需求','金融':'资金与融资服务','交运':'物流与运输枢纽'};
  let h='<div class="chain-lanes">';
  FM_STAGES.forEach(function(st){
    const list=byStage[st]||[];
    const avg=list.length?list.reduce(function(a,b){return a+b.pct;},0)/list.length:0;
    const c=avg>=0.5?'var(--up)':avg<=-0.5?'var(--down)':'var(--muted)';
    h+='<div class="chain-lane"><div class="chain-lane-head" style="border-bottom:2px solid '+c+';"><div class="chain-lane-title">'+st+'</div><div class="chain-lane-sub">'+STAGE_DESC[st]+' · '+list.length+'个板块</div><div class="chain-lane-avg" style="color:'+c+';">'+(avg>=0?'+':'')+avg.toFixed(2)+'%</div></div><div class="chain-lane-body">';
    if(!list.length)h+='<div style="font-size:11px;color:var(--muted);padding:6px 2px;">当前无板块映射</div>';
    list.forEach(function(r){
      const tcol=tierColor(r.tier);
      const rels=rel[r.name]||[];
      const dots=rels.map(function(e){
        const arrow=e.s===r.name?(e.s+' → '+e.t):(e.s+' → '+e.t);
        return '<span class="chain-dot" style="background:'+e.st.color+';" title="'+arrow+'（'+e.st.label+'）\n'+e.note.replace(/"/g,'')+'"></span>';
      }).join('');
      const tip=(r.stage+(r.sub?' · '+r.sub:'')+' ｜ '+r.tier+' '+r.score+'分 ｜ 涨幅'+r.pct.toFixed(2)+'% ｜ 成交'+r.amtYi.toFixed(0)+'亿'+(r.lead_stock?' ｜ 领涨:'+r.lead_stock:'')+(rels.length?'\n\n相关传导：\n'+rels.map(function(e){return e.s+' → '+e.t+'（'+e.st.label+'）';}).join('\n'):'')).replace(/"/g,'');
      h+='<div class="chain-node" style="border-left:3px solid '+tcol+';" onclick="'+(r.code?'showBoardDetail(\''+r.code+'\')':'showSectorDetail(\''+r.name+'\')')+'" title="'+tip+'">'
        +'<div class="chain-node-name">'+r.name+' <b style="color:'+(r.pct>=0?'var(--up)':'var(--down)')+';">'+(r.pct>=0?'+':'')+r.pct.toFixed(2)+'%</b></div>'
        +'<div class="chain-node-meta">'+r.tier+' · '+r.score+'分'+(r.amtYi>0?' · '+r.amtYi.toFixed(0)+'亿':'')+(r.lead_stock?' · 领涨 '+r.lead_stock:'')+'</div>'
        +(dots?'<div class="chain-dots">'+dots+'</div>':'')
        +'</div>';
    });
    h+='</div></div>';
  });
  h+='</div>';
  dom.innerHTML=h;
}

// ===== 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=(function(){const base=cycleState?Math.round(s.position_size*(cf.grade==='A'?1:(cf.grade==='B'?0.6:0.3))):s.position_size;if(!cycleState)return base;const cs=cycleState.stage;const mult=cs==='gaochao'?1.0:(cs==='fajiao'?1.0:(cs==='huinuan'?0.7:(cs==='fenqi'?0.5:(cs==='bing'?0.3:0.2))));return Math.max(5,Math.round(base*mult));})();
    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('');
    const cycBadge=cycleState?'<span style="font-size:10px;padding:2px 8px;border-radius:8px;background:'+cycleState.st.color+'22;color:'+cycleState.st.color+';border:1px solid '+cycleState.st.color+'55;">'+cycleState.st.name+'</span>':'';
    const cycTip=cycleState&&cycleState.stage==='huinuan'?'<div style="margin-top:6px;font-size:10px;color:'+cycleState.st.color+';background:'+cycleState.st.color+'15;padding:4px 8px;border-radius:4px;">回暖试错期：轻仓低吸，快进快出，严格止损</div>':'';
    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>'+cycBadge+(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" title="建议入场价格">入场价</div><div class="val pu">'+s.entry_price.toFixed(2)+'</div></div><div><div class="lbl" title="ATR动态止损价">止损价</div><div class="val pd">'+s.stop_loss.toFixed(2)+'</div></div><div><div class="lbl" title="ATR动态目标价">目标价</div><div class="val" style="color:var(--gold);">'+s.target_price.toFixed(2)+'</div></div><div><div class="lbl" title="建议仓位%">仓位</div><div class="val">'+posAdj+'%</div></div><div><div class="lbl" title="回测胜率%">胜率</div><div class="val">'+s.win_rate.toFixed(1)+'%</div></div><div><div class="lbl" title="量化过滤后胜率%">量化胜率</div><div class="val" style="color:var(--gold);">'+(s.wr_filtered||0).toFixed(1)+'%</div></div><div><div class="lbl" title="盈亏比(收益/风险)">R/R比</div><div class="val" style="color:var(--gold);font-weight:700;">'+rr+'</div></div><div><div class="lbl" title="建议持仓周期">周期</div><div class="val" style="font-size:11px;">'+s.holding_period+'</div></div></div><div class="confluence-factors">'+factorTags+'</div>'+cycTip+'<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(''):(function(){if(!cycleState){return '<div style="text-align:center;padding:30px 16px;color:var(--muted);font-size:13px;line-height:1.8;">'+graded.length+'个买入信号暂未完成共振评估（情绪数据加载中）</div>';}var cs=cycleState;var stageTip={'huinuan':'回暖期以轻仓试错为主，关注率先启动的主线龙头首板/2板低吸机会','bing':'冰点期空仓观望为主，等待涨停回升至40家+、出现3连板再介入','tuichao':'退潮期清仓防守，等待冰点信号出现后再谈新周期','fenqi':'分歧期只做核心龙头的弱转强低吸，非核心回避','gaochao':'高潮期持股为主勿追板，警惕高位炸板见顶','fajiao':'发酵期聚焦核心主线顺势加仓'}[cs.stage]||'';return '<div style="text-align:center;padding:30px 16px;color:var(--muted);font-size:13px;line-height:1.8;">当前情绪周期 <b style="color:'+cs.st.color+';">'+cs.st.name+'</b>('+cs.score+'分)，'+graded.length+'个买入信号中暂无A/B级共振信号<br>'+stageTip+'<br><span style="font-size:11px;opacity:.7;">可取消勾选仅看A/B级共振查看全部'+graded.length+'只C级信号</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" title="建议入场价格">入场价</div><div class="val">'+s.entry_price.toFixed(2)+'</div></div><div><div class="lbl" title="ATR动态止损价">止损价</div><div class="val pd">'+s.stop_loss.toFixed(2)+'</div></div><div><div class="lbl" title="ATR动态目标价">目标价</div><div class="val">'+s.target_price.toFixed(2)+'</div></div><div><div class="lbl" title="建议仓位%">仓位</div><div class="val">'+s.position_size+'%</div></div><div><div class="lbl" title="回测胜率%">胜率</div><div class="val">'+s.win_rate.toFixed(1)+'%</div></div><div><div class="lbl" title="量化综合评分">量化分</div><div class="val">'+(s.quant_score||0).toFixed(0)+'</div></div><div><div class="lbl" title="盈亏比PF">盈亏比</div><div class="val">'+s.profit_factor.toFixed(2)+'</div></div><div><div class="lbl" title="建议持仓周期">持仓周期</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(){
  // 量化因子雷达分析已按需求移除，此函数保留为空壳以兼容调用点
}
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);" title="建议入场价格">入场</span><br><b style="color:var(--up);">'+s.entry_price.toFixed(2)+'</b></div><div><span style="color:var(--muted);" title="ATR动态止损">止损</span><br><b style="color:var(--down);">'+s.stop_loss.toFixed(2)+'</b></div><div><span style="color:var(--muted);" title="ATR动态目标">目标</span><br><b style="color:var(--gold);">'+s.target_price.toFixed(2)+'</b></div><div><span style="color:var(--muted);" title="建议仓位%">仓位</span><br><b>'+s.position_size+'%</b></div><div><span style="color:var(--muted);" title="入场到止损的跌幅%">风险</span><br><b style="color:var(--down);">-'+risk+'%</b></div><div><span style="color:var(--muted);" title="入场到目标的涨幅%">收益</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(){
  // 仓位与风险敞口/信号质量分布/板块集中度已按需求移除，此函数保留为空壳以兼容调用点
}

// ===== 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=initChartSafe('chartIndPerf','indPerf');if(!ip)return;
  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(){
  // 容器在隐藏层（layer0 display:none）时 ECharts 以 0 尺寸初始化会渲染成空白，
  // 这里同时做两件事：容器不可见时跳过初始化（等 ResizeObserver 触发），数据行做 NaN 过滤
  function containerReady(id){const d=document.getElementById(id);return !!(d&&d.offsetWidth>10&&d.offsetHeight>10);}
  function emptyTip(ch,msg){ch.clear&&ch.clear();ch.setOption({title:{text:msg,left:'center',top:'middle',textStyle:{color:tc.muted,fontSize:13}}});}
  try{
    const sc=initChartSafe('chartScatter','scatter');if(!sc)return;
    if(!containerReady('chartScatter'))return;
    const rows=scanData.filter(s=>Number.isFinite(+s.signal_count)&&Number.isFinite(+s.sentiment_score));
    if(!rows.length){emptyTip(sc,'暂无信号数据');return;}
    sc.setOption({backgroundColor:'transparent',tooltip:tipXL({formatter:p=>((p.data&&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=>Math.max((+p[0]||0)*3,6),data:rows.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}};})}]},true);
  }catch(e){console.log('renderL3Charts scatter failed:',e);}
  try{
    const qs=initChartSafe('chartQScatter','qs');if(!qs)return;
    if(!containerReady('chartQScatter'))return;
    const bd=scanData.filter(s=>s.bt_has_data&&Number.isFinite(+s.bt_quant_score)&&Number.isFinite(+s.bt_win_rate)&&Number.isFinite(+s.bt_pf));
    if(!bd.length){emptyTip(qs,'暂无回测数据');return;}
    qs.setOption({backgroundColor:'transparent',tooltip:tipXL({formatter:p=>((p.data&&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]||0)*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}}))}]},true);
  }catch(e){console.log('renderL3Charts qscatter failed:',e);}
}
// layer0 从隐藏变可见（或窗口尺寸变化）时重渲散点图，覆盖初始化时 0 尺寸渲染成空白的场景
(function(){
  var _rt=null;
  try{
    var l0=document.getElementById('layer0');
    if(l0&&window.ResizeObserver){
      var ro=new ResizeObserver(function(entries){
        var w=entries[0].contentRect.width;
        clearTimeout(_rt);
        if(w>100){_rt=setTimeout(renderL3Charts,150);}
      });
      ro.observe(l0);
    }
  }catch(e){}
})();

// ===== 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);
}
// 通用个股详情入口：Top50扫描池内走完整推演，池外自动走全市场实时详情
function showDetailAny(code,name){
  if(!code)return;
  if(scanData.find(x=>x.code===code)){showDetail(code);return;}
  showDetailEx(code,name||code,code.startsWith('6')?1:0);
}
// ===== 副图指标配置系统 =====
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)}}
  ]};}
};
// ===== 价格行为分析引擎（PA_Agent 方法论：市场结构 → 周期位置 → 两阶段决策） =====
// 阶段1 市场诊断：swing点 → HH/HL/LH/LL结构 → BOS/CHoCH → 趋势/周期位置/形态
// 阶段2 交易决策：限价回踩 / 突破追入 / 观望 / 减持，附完整交易计划
function paFindSwings(kline){
  const raw=[];
  for(let i=2;i<kline.length-2;i++){
    const k=kline[i];
    if(k.high>kline[i-1].high&&k.high>kline[i-2].high&&k.high>kline[i+1].high&&k.high>kline[i+2].high)raw.push({idx:i,type:'H',price:k.high,date:k.date});
    if(k.low<kline[i-1].low&&k.low<kline[i-2].low&&k.low<kline[i+1].low&&k.low<kline[i+2].low)raw.push({idx:i,type:'L',price:k.low,date:k.date});
  }
  raw.sort((a,b)=>a.idx-b.idx);
  // 相邻同类型合并保留极值
  const out=[];
  raw.forEach(s=>{
    const last=out[out.length-1];
    if(last&&last.type===s.type){if((s.type==='H'&&s.price>last.price)||(s.type==='L'&&s.price<last.price))out[out.length-1]=s;}
    else out.push(s);
  });
  return out;
}
function analyzePriceAction(kline){
  if(!kline||kline.length<40)return null;
  const swings=paFindSwings(kline);
  if(swings.length<4)return null;
  const last=kline[kline.length-1];
  // --- 结构序列：HH/HL/LH/LL + BOS/CHoCH 事件 ---
  const labels=[];const events=[];
  let prevH=null,prevL=null,trend='震荡';
  const highs=swings.filter(s=>s.type==='H'),lows=swings.filter(s=>s.type==='L');
  const highByIdx={},lowByIdx={};highs.forEach(h=>highByIdx[h.idx]=h);lows.forEach(l=>lowByIdx[l.idx]=l);
  swings.forEach(s=>{
    if(s.type==='H'){
      if(prevH){
        if(s.price>prevH.price){labels.push({...s,lab:'HH'});
          if(trend!=='上升')events.push({idx:s.idx,date:s.date,type:'BOS↑',desc:'上破前高 '+prevH.price.toFixed(2)+'，确认/延续上升结构'});
          trend='上升';
        }else{labels.push({...s,lab:'LH'});if(trend==='上升')events.push({idx:s.idx,date:s.date,type:'CHoCH↓',desc:'高点降低 '+s.price.toFixed(2)+'<'+prevH.price.toFixed(2)+'，上升结构转弱'});}
      }else labels.push({...s,lab:'H'});
      prevH=s;
    }else{
      if(prevL){
        if(s.price<prevL.price){labels.push({...s,lab:'LL'});
          if(trend!=='下降')events.push({idx:s.idx,date:s.date,type:'BOS↓',desc:'跌破前低 '+prevL.price.toFixed(2)+'，确认/延续下降结构'});
          trend='下降';
        }else{labels.push({...s,lab:'HL'});if(trend==='下降')events.push({idx:s.idx,date:s.date,type:'CHoCH↑',desc:'低点抬高 '+s.price.toFixed(2)+'>'+prevL.price.toFixed(2)+'，下降结构转强'});}
      }else labels.push({...s,lab:'L'});
      prevL=s;
    }
  });
  // --- S/R：近120根内的 swing 聚类 ---
  const win=kline.slice(-120);
  const winStart=kline.length-120;
  const sh=highs.filter(h=>h.idx>=winStart).map(h=>h.price).sort((a,b)=>b-a);
  const sl_=lows.filter(l=>l.idx>=winStart).map(l=>l.price).sort((a,b)=>a-b);
  const resistance=sh.length?sh.reduce((a,b)=>a+b)/sh.length:null; // 近端摆动高点均值
  const support=sl_.length?sl_.reduce((a,b)=>a+b)/sl_.length:null;
  const lastHigh=sh[0]||null,lastLow=sl_[0]||null;
  // --- 近5根K线形态 ---
  const pats=[];
  const recent=kline.slice(-5);
  for(let i=1;i<recent.length;i++){
    const p=recent[i-1],c=recent[i];const body=Math.abs(c.close-c.open),pBody=Math.abs(p.close-p.open);
    const rng=c.high-c.low,lower=Math.min(c.open,c.close)-c.low,upper=c.high-Math.max(c.open,c.close);
    if(c.close>c.open&&p.close<p.open&&c.close>=p.open&&c.open<=p.close&&body>pBody)pats.push({date:c.date,name:'看涨吞没',dir:1});
    if(c.close<c.open&&p.close>p.open&&c.close<=p.open&&c.open>=p.close&&body>pBody)pats.push({date:c.date,name:'看跌吞没',dir:-1});
    if(rng>0&&lower>rng*0.6&&body<rng*0.35)pats.push({date:c.date,name:'看涨PinBar(锤子)',dir:1});
    if(rng>0&&upper>rng*0.6&&body<rng*0.35)pats.push({date:c.date,name:'看跌PinBar(射击星)',dir:-1});
    if(c.high<=p.high&&c.low>=p.low)pats.push({date:c.date,name:'内包线(IB)',dir:0});
  }
  // --- 周期位置（积累→拉升→分配→下跌） ---
  const ma20=kline.slice(-20).reduce((a,b)=>a+b.close,0)/Math.min(20,kline.length);
  const ma60=kline.length>=60?kline.slice(-60).reduce((a,b)=>a+b.close,0)/60:ma20;
  const posVs20=(last.close-ma20)/ma20*100;
  let cycleStage,cycleDesc;
  if(trend==='上升'&&posVs20>8){cycleStage='分配区';cycleDesc='价格大幅偏离MA20('+posVs20.toFixed(1)+'%)，结构仍上行但追高风险大，防高位派发';}
  else if(trend==='上升'){cycleStage='拉升区';cycleDesc='结构上行且未过热(MA20偏离'+posVs20.toFixed(1)+'%)，顺势持有/回踩加仓';}
  else if(trend==='下降'&&posVs20<-8){cycleStage='下跌区';cycleDesc='结构下行且深度偏离MA20('+posVs20.toFixed(1)+'%)，超跌可能有反弹但趋势未反转';}
  else if(trend==='下降'){cycleStage='下跌区';cycleDesc='结构下行，反弹视为减仓机会，勿抄底';}
  else{cycleStage=last.close>ma20?'积累区':'积累区';cycleDesc='结构震荡'+(last.close>ma20?'(MA20上方)':'(MA20下方)')+'，等待方向选择，突破前观望';}
  // --- 阶段2 决策 ---
  let decision;
  const atrLocal=calcATR(kline.slice(-30),14)||last.close*0.02;
  const lastPat=pats.length?pats[pats.length-1]:null;
  const nearSupport=support&&Math.abs(last.close-support)/support<0.025;
  const nearResist=resistance&&Math.abs(last.close-resistance)/resistance<0.025;
  if(trend==='上升'&&nearSupport){
    decision={action:'限价回踩买入',plan:'上升趋势中回踩支撑 '+support.toFixed(2)+' 附近限价接多',entry:support,stop:Math.min(support-2*atrLocal,support*0.97),target:lastHigh?Math.max(lastHigh,last.close+2*atrLocal):last.close+2*atrLocal,rr:2,reasons:['结构：HL/HH上行序列完好','回踩支撑='+support.toFixed(2)+'获承接','止损设支撑下方2×ATR='+atrLocal.toFixed(2)]};
  }else if(trend==='上升'&&lastHigh&&last.close<lastHigh&&lastPat&&lastPat.dir===1){
    decision={action:'突破买入',plan:'上破近端高点 '+lastHigh.toFixed(2)+' 后回踩不破再追入',entry:lastHigh,stop:lastHigh-2*atrLocal,target:lastHigh+3*atrLocal,rr:1.5,reasons:['结构上行，突破前高将触发BOS延续','近期形态：'+lastPat.name+' 提供动能确认','突破单风险略高，仓位减半']};
  }else if(trend==='下降'){
    decision={action:'观望/反弹减持',plan:'下降结构中不做多；持仓者利用反弹至 '+(resistance?resistance.toFixed(2):'近端高点')+' 附近减仓',entry:null,stop:null,target:null,rr:null,reasons:['结构：LH/LL下行序列未破坏','反弹到阻力区是减仓点而非买点','等待CHoCH↑（低点抬高）后再评估反转']};
  }else{
    decision={action:'区间观望',plan:'震荡区间 ['+(support?support.toFixed(2):'--')+', '+(resistance?resistance.toFixed(2):'--')+'] 内观望，突破后顺势跟进',entry:null,stop:null,target:null,rr:null,reasons:['结构无方向，BOS未触发','区间内交易胜率低、成本磨损大','设定突破警报：上破 '+(resistance?resistance.toFixed(2):'--')+' / 下破 '+(support?support.toFixed(2):'--')]};
  }
  return {swings,labels,events,support,resistance,lastHigh,lastLow,patterns:pats.slice(-4),cycle:{stage:cycleStage,desc:cycleDesc,posVs20,posVs60:(last.close-ma60)/ma60*100},trend,decision,ma20,ma60};
}

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'}}
  );}
  // ===== PA 价格行为图层（默认开启，可在K线配置栏关闭）=====
  if(cfg.pa!==false){
    const pa=analyzePriceAction(kline);
    if(pa){
      legendData.push('PA结构');
      // swing zigzag 结构线（取近端swing避免过密）
      const zSw=pa.swings.slice(-16);
      series.push({name:'PA结构',type:'line',data:zSw.map(s=>[s.date,s.price]),xAxisIndex:0,yAxisIndex:0,showSymbol:false,smooth:false,z:5,
        lineStyle:{width:1.2,color:hexA(tc.gold,.75),type:'dotted'},emphasis:{disabled:true},silent:true});
      // HH/HL/LH/LL 标签（仅近端，避免重叠）
      pa.labels.slice(-10).forEach(l=>{
        const col=l.lab.indexOf('H')===0||l.lab==='HL'?tc.up:tc.down;
        series.push({name:'PA结构',type:'scatter',data:[[l.date,l.price]],xAxisIndex:0,yAxisIndex:0,symbol:'triangle',symbolSize:1,z:6,silent:true,
          label:{show:true,formatter:l.lab,position:l.type==='H'?'top':'bottom',color:col,fontSize:9,fontWeight:700},
          itemStyle:{color:hexA(col,.35)}});
      });
      // S/R 水平虚线 + 入场计划线（挂在K线系列 markLine）
      const mlData=[];
      if(pa.support)mlData.push({yAxis:+pa.support.toFixed(2),lineStyle:{color:tc.up,type:'dashed',width:1,opacity:.75},label:{formatter:'支撑 '+pa.support.toFixed(2),color:tc.up,fontSize:9,position:'insideStartTop'}});
      if(pa.resistance)mlData.push({yAxis:+pa.resistance.toFixed(2),lineStyle:{color:tc.down,type:'dashed',width:1,opacity:.75},label:{formatter:'阻力 '+pa.resistance.toFixed(2),color:tc.down,fontSize:9,position:'insideStartTop'}});
      if(pa.decision&&pa.decision.entry){mlData.push({yAxis:+pa.decision.entry.toFixed(2),lineStyle:{color:tc.gold,type:'solid',width:1.2,opacity:.9},label:{formatter:'PA入场 '+pa.decision.entry.toFixed(2),color:tc.gold,fontSize:9,position:'insideStartBottom'}});}
      if(mlData.length){
        const ks=series.find(s=>s.type==='candlestick');
        if(ks)ks.markLine={symbol:'none',silent:true,animation:false,data:mlData};
      }
      // 最近一次 BOS/CHoCH 事件标注
      const ev=pa.events[pa.events.length-1];
      if(ev){
        const ks2=series.find(s=>s.type==='candlestick');
        if(ks2)ks2.markPoint={symbol:'pin',symbolSize:0,silent:true,data:[{coord:[ev.date,kline[ev.idx]?kline[ev.idx].close:null],label:{formatter:ev.type,fontSize:9,fontWeight:700,color:ev.type.indexOf('↑')>0?tc.up:tc.down,position:'inside'},value:ev.type}]};
      }
    }
  }
  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];
  // 主图右上角：最新收盘与涨跌幅
  if(kline.length){
    const last=kline[kline.length-1];
    const up=(last.chg||0)>=0;
    titles.push({text:'最新 '+last.close.toFixed(2)+'  '+(up?'+':'')+(last.chg||0).toFixed(2)+' ('+(up?'+':'')+(last.pct||0).toFixed(2)+'%)',right:'2%',top:0,textStyle:{color:up?tc.up:tc.down,fontSize:11,fontWeight:700,fontFamily:"'SF Mono',monospace"}});
  }
  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;',
    formatter:function(ps){
      if(!ps||!ps.length)return'';
      const idx=ps[0].dataIndex;const k=kline[idx];
      if(!k)return ps[0].axisValue;
      const up=(k.chg||0)>=0;const cc=up?tc.up:tc.down;
      let h='<b>'+k.date+'</b>';
      h+='<br>开：'+k.open.toFixed(2)+'　高：'+k.high.toFixed(2)+'　低：'+k.low.toFixed(2);
      h+='<br>收：<b style="color:'+cc+';">'+k.close.toFixed(2)+'</b>　涨跌：<b style="color:'+cc+';">'+(up?'+':'')+(k.chg||0).toFixed(2)+'</b>　涨幅：<b style="color:'+cc+';">'+(up?'+':'')+(k.pct||0).toFixed(2)+'%</b>';
      let sub='';
      ps.forEach(function(p){
        if(p.seriesType==='candlestick'||p.value==null||p.value==='-')return;
        if(p.seriesName.indexOf('MA')===0||p.seriesName==='BOLL'){h+='<br>'+p.marker+p.seriesName+'：'+Number(p.value).toFixed(2);return;}
        const v=Number(p.value);
        sub+='<br>'+p.marker+p.seriesName+'：'+(Math.abs(v)>=10000?(v/1e4).toFixed(1)+'万':v.toFixed(2));
      });
      if(sub)h+='<br><span style="opacity:.65;">── 副图 ──</span>'+sub;
      return h;
    }},
    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>';
  h+='<label style="margin-left:6px;display:flex;align-items:center;gap:4px;cursor:pointer;"><input type="checkbox" data-kf-pa '+(cfg.pa!==false?'checked':'')+' style="accent-color:var(--gold);cursor:pointer;"> PA价格行为(结构/支撑阻力)</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);};
  const pachk=bar.querySelector('[data-kf-pa]');if(pachk)pachk.onchange=function(){window._klineCfg.pa=pachk.checked;saveKlineCfg();rebuildKline(dom);};
}
function rebuildKline(dom){const ctx=dom._klineCtx;if(ctx&&ctx.ch)ctx.ch.setOption(buildKlineOption(ctx.kline,window._klineCfg),true);}

// ====================== 模拟交易 & 模拟组合引擎 ======================
const PAPER_INIT_CASH=1000000;
function loadPaper(){
  try{const s=localStorage.getItem('paperAccount');if(s){const a=JSON.parse(s);if(a&&typeof a.cash==='number'){a.orders=a.orders||[];a.trades=a.trades||[];a.positions=a.positions||{};return a;}}}catch(e){}
  return {cash:PAPER_INIT_CASH,initCash:PAPER_INIT_CASH,positions:{},orders:[],trades:[]};
}
function savePaper(){try{localStorage.setItem('paperAccount',JSON.stringify(paperAcct));}catch(e){}if(typeof _syncToBackend==='function')_syncToBackend();}
let paperAcct=loadPaper();
function paperToday(){const d=new Date();return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');}
function paperFeeBuy(amount){return Math.max(5,amount*0.00025);}
function paperFeeSell(amount){return Math.max(5,amount*0.00025)+amount*0.0005;}
// 下单：side 'B'买/'S'卖；type 'MKT'市价/'LMT'限价；price 市价单传0
function paperOrder(side,type,code,name,price,qty,sl,tp,src){
  qty=Math.floor(qty/100)*100;
  if(!qty||qty<100){alert('数量须为100股整数倍');return false;}
  if(type==='LMT'&&!(price>0)){alert('请输入有效限价');return false;}
  if(type==='MKT'){const ok=paperFillDirect(side,code,name,price,qty,sl,tp,src,'市价');return ok;}
  // 限价挂单：预检查
  if(side==='B'){const need=price*qty+paperFeeBuy(price*qty);if(need>paperAcct.cash){alert('现金不足：需'+need.toFixed(0)+'元，可用'+paperAcct.cash.toFixed(0)+'元');return false;}}
  if(side==='S'){const pos=paperAcct.positions[code];if(!pos||pos.qty<qty){alert('持仓不足（T+1且可用'+((pos&&pos.qty)||0)+'股）');return false;}
    const pendSell=paperAcct.orders.filter(o=>o.status==='PENDING'&&o.side==='S'&&o.code===code).reduce((a,b)=>a+b.qty,0);
    if(pos.qty-pendSell<qty){alert('扣除挂单卖出后可用不足');return false;}}
  paperAcct.orders.push({id:'O'+Date.now()+Math.floor(Math.random()*1000),code,name,side,type,price,qty,sl:sl||null,tp:tp||null,src:src||'手动',status:'PENDING',ts:Date.now(),date:paperToday()});
  savePaper();renderPaperAll();return true;
}
// 市价/手动成交
function paperFillDirect(side,code,name,price,qty,sl,tp,src,how){
  if(!(price>0)){alert('无有效成交价');return false;}
  if(side==='B'){
    const amt=price*qty,fee=paperFeeBuy(amt);
    if(amt+fee>paperAcct.cash){alert('现金不足：需'+(amt+fee).toFixed(0)+'元');return false;}
    paperAcct.cash-=amt+fee;
    const pos=paperAcct.positions[code];
    if(pos){pos.avg=(pos.avg*pos.qty+amt)/(pos.qty+qty);pos.qty+=qty;if(sl)pos.sl=sl;if(tp)pos.tp=tp;}
    else paperAcct.positions[code]={code,name,qty,avg:price,entryDate:paperToday(),entryTs:Date.now(),sl:sl||null,tp:tp||null};
  }else{
    const pos=paperAcct.positions[code];
    if(!pos||pos.qty<qty){alert('持仓不足');return false;}
    if(pos.entryDate===paperToday()){alert('T+1限制：今日买入不可卖出');return false;}
    const amt=price*qty,fee=paperFeeSell(amt);
    paperAcct.cash+=amt-fee;
    const realized=(price-pos.avg)*qty-fee;
    pos.qty-=qty;if(pos.qty<=0)delete paperAcct.positions[code];
    paperAcct.trades.push({id:'T'+Date.now()+Math.floor(Math.random()*1000),code,name,side,price,qty,realized,ts:Date.now(),date:paperToday(),src:src||'手动',how});
  }
  savePaper();renderPaperAll();return true;
}
// 限价单自动撮合 + 止盈止损
function paperMatch(quotes){
  let changed=false;
  paperAcct.orders.filter(o=>o.status==='PENDING').forEach(o=>{
    const q=quotes[o.code];if(!q||!(q.price>0))return;
    if(o.side==='B'&&q.price<=o.price){o.status='DONE';paperFillSilent('B',o.code,o.name,o.price,o.qty,o.sl,o.tp,o.src,o.name+'限价买单自动成交@'+o.price.toFixed(2));changed=true;}
    if(o.side==='S'&&q.price>=o.price){o.status='DONE';paperFillSilent('S',o.code,o.name,o.price,o.qty,null,null,o.src,o.name+'限价卖单自动成交@'+o.price.toFixed(2));changed=true;}
  });
  // 持仓止盈止损
  Object.values(paperAcct.positions).forEach(pos=>{
    const q=quotes[pos.code];if(!q||!(q.price>0))return;
    if(pos.entryDate===paperToday())return; // T+1
    if(pos.tp&&q.price>=pos.tp){paperFillSilent('S',pos.code,pos.name,pos.tp,pos.qty,null,null,pos.src||'信号推荐','止盈自动平仓@'+pos.tp.toFixed(2));changed=true;}
    else if(pos.sl&&q.price<=pos.sl){paperFillSilent('S',pos.code,pos.name,pos.sl,pos.qty,null,null,(pos.src||'信号推荐')+'·止损','止损自动平仓@'+pos.sl.toFixed(2));changed=true;}
  });
  if(changed)savePaper();
  return changed;
}
function paperFillSilent(side,code,name,price,qty,sl,tp,src,how){
  if(side==='B'){
    const amt=price*qty,fee=paperFeeBuy(amt);
    paperAcct.cash-=amt+fee;
    const pos=paperAcct.positions[code];
    if(pos){pos.avg=(pos.avg*pos.qty+amt)/(pos.qty+qty);pos.qty+=qty;if(sl)pos.sl=sl;if(tp)pos.tp=tp;}
    else paperAcct.positions[code]={code,name,qty,avg:price,entryDate:paperToday(),entryTs:Date.now(),sl:sl||null,tp:tp||null,src:src};
  }else{
    const pos=paperAcct.positions[code];if(!pos||pos.qty<qty)return;
    const amt=price*qty,fee=paperFeeSell(amt);
    paperAcct.cash+=amt-fee;
    const realized=(price-pos.avg)*qty-fee;
    pos.qty-=qty;if(pos.qty<=0)delete paperAcct.positions[code];
    paperAcct.trades.push({id:'T'+Date.now()+Math.floor(Math.random()*1000),code,name,side,price,qty,realized,ts:Date.now(),date:paperToday(),src:src||'手动',how});
  }
}
function paperCancel(oid){
  const o=paperAcct.orders.find(x=>x.id===oid);
  if(!o||o.status!=='PENDING')return;
  o.status='CANCELLED';savePaper();renderPaperAll();
}
// 详情页"按信号模拟买入"：默认资金20%按入场价挂限价单，带SL/TP
function paperQuickBuy(code,name,entry,sl,tp,src){
  if(!(entry>0))entry=null;
  const budget=Math.min(paperAcct.cash*0.2,200000);
  const refPrice=entry||paperAcct._lastQuotes&&paperAcct._lastQuotes[code]&&paperAcct._lastQuotes[code].price||0;
  if(!(refPrice>0)){alert('暂无参考价');return;}
  let qty=Math.floor(budget/refPrice/100)*100;
  if(qty<100){alert('资金不足以买入100股');return;}
  const type=entry?'LMT':'MKT';
  const ok=paperOrder('B',type,code,name,entry||refPrice,qty,sl,tp,src||'信号推荐');
  if(ok)alert('已'+(type==='LMT'?'挂限价买单（等待自动撮合）':'市价买入')+'：'+name+' '+qty+'股'+(type==='LMT'?' @'+entry.toFixed(2):'')+(sl?'\n止损 '+sl.toFixed(2):'')+(tp?'\n止盈 '+tp.toFixed(2):'')+'\n可在「模拟交易」层查看');
}
// T+0 ETF 专用下单：跳过 T+1 限制，市价成交（当日买入当日可卖）
function paperT0Fill(side,code,name,price,qty,src){
  if(!(price>0)){alert('无有效成交价');return false;}
  if(side==='B'){
    const amt=price*qty,fee=paperFeeBuy(amt);
    if(amt+fee>paperAcct.cash){alert('现金不足：需'+(amt+fee).toFixed(0)+'元，可用'+paperAcct.cash.toFixed(0)+'元');return false;}
    paperAcct.cash-=amt+fee;
    const pos=paperAcct.positions[code];
    if(pos){pos.avg=(pos.avg*pos.qty+amt)/(pos.qty+qty);pos.qty+=qty;}
    else paperAcct.positions[code]={code,name,qty,avg:price,entryDate:paperToday(),entryTs:Date.now(),sl:null,tp:null,src:src||'T+0'};
  }else{
    const pos=paperAcct.positions[code];
    if(!pos||pos.qty<qty){alert('持仓不足（当前'+(pos?pos.qty:0)+'份）');return false;}
    const amt=price*qty,fee=paperFeeSell(amt);
    paperAcct.cash+=amt-fee;
    const realized=(price-pos.avg)*qty-fee;
    pos.qty-=qty;if(pos.qty<=0)delete paperAcct.positions[code];
    paperAcct.trades.push({id:'T'+Date.now()+Math.floor(Math.random()*1000),code,name,side:'S',price,qty,realized,ts:Date.now(),date:paperToday(),src:src||'T+0',how:'T+0市价卖出@'+price.toFixed(3)});
  }
  savePaper();renderPaperAll();return true;
}
function paperT0QuickBuy(code,name,price){
  if(!(price>0)){alert('无有效现价');return;}
  const qty=Math.max(100,Math.floor(50000/price/100)*100);
  if(paperT0Fill('B',code,name,price,qty,'T+0'))alert('已T+0市价买入 '+name+' '+qty+'份 @'+price.toFixed(3)+'\n可在「模拟交易」层查看');
}
function paperT0QuickSell(code,name,price){
  const pos=paperAcct.positions[code];
  if(!pos){alert('无 '+name+' 持仓，无法卖出');return;}
  if(!(price>0)){alert('无有效现价');return;}
  if(paperT0Fill('S',code,name,price,pos.qty,'T+0'))alert('已T+0市价卖出 '+name+' '+pos.qty+'份 @'+price.toFixed(3));
}
// 个股/ETF 通用快捷下单：弹出小面板选买卖方向，市价成交
function paperQuickPanel(code,name,price,isT0,defaultSide){
  if(!(price>0))price=0;
  const pos=paperAcct.positions[code];
  const holdQty=pos?pos.qty:0;
  const ref=price>0?price.toFixed(3):'未知';
  const defDir=(defaultSide==='S')?'卖':'买';
  const qty=prompt('【模拟下单】'+name+'('+code+')\n现价 '+ref+(isT0?'（T+0 当日可回转）':'（T+1）')+'\n当前持仓 '+holdQty+' 份\n\n输入：方向+数量，如「买1000」或「卖500」\n（买=市价买入，卖=市价卖出，数量为100整数倍）：',defDir+'1000');
  if(qty===null)return;
  const m=String(qty).trim().match(/^\s*(买|卖|b|s)\s*(\d+)\s*$/i);
  if(!m){alert('格式错误，请如「买1000」或「卖500」');return;}
  const side=(/^(买|b)/i.test(m[1]))?'B':'S';
  const n=parseInt(m[2],10);
  if(!(n>=100)||n%100!==0){alert('数量须为100的整数倍且≥100');return;}
  if(!(price>0)){
    // 无传入价时拉实时价
    jsonpRequest('https://push2delay.eastmoney.com/api/qt/stock/get?secid='+((/^(6|9)/.test(code)?'1.':'0.')+code)+'&fltt=2&fields=f43,f58&ut=fa5fd1943c7b386f172d6893dbfba10b',6000).then(function(r){
      if(r&&r.data&&typeof r.data.f43==='number'){doFill(r.data.f43,r.data.f58||name);}
      else alert('实时价获取失败，请重试');
    }).catch(function(){alert('实时价获取失败，请重试');});
    return;
  }
  doFill(price,name);
  function doFill(p,nm){
    if(isT0){paperT0Fill(side,code,nm,p,n,'快捷下单');}
    else{
      if(side==='B')paperFillDirect('B',code,nm,p,n,null,null,'快捷下单','市价买入@'+p.toFixed(2));
      else paperFillDirect('S',code,nm,p,n,null,null,'快捷下单','市价卖出@'+p.toFixed(2));
    }
  }
}

// 批量实时价（持仓+挂单+组合全部股票 + 上证基准）
async function paperQuotes(codes){
  const uniq=[...new Set(codes.filter(Boolean))];
  const out={};
  if(uniq.length){
    const secids=uniq.map(c=>(/^(6|9)/.test(c)?'1.':'0.')+c).join(',');
    const url='https://push2delay.eastmoney.com/api/qt/ulist.np/get?fltt=2&fields=f2,f3,f12,f14&secids='+secids+'&ut=fa5fd1943c7b386f172d6893dbfba10b';
    try{
      const r=await jsonpRequest(url,9000).catch(()=>null);
      if(r&&r.data&&r.data.diff){r.data.diff.forEach(d=>{if(d&&d.f12)out[d.f12]={price:(typeof d.f2==='number')?d.f2:0,pct:(typeof d.f3==='number')?d.f3:0,name:d.f14};});}
    }catch(e){}
  }
  // 上证指数基准（组合对比口径：当日涨跌幅）
  try{
    const b=await jsonpRequest('https://push2delay.eastmoney.com/api/qt/stock/get?secid=1.000001&fltt=2&fields=f43,f170&ut=fa5fd1943c7b386f172d6893dbfba10b',6000).catch(()=>null);
    if(b&&b.data&&typeof b.data.f43==='number')out.__SH__={price:b.data.f43,pct:b.data.f170||0,name:'上证指数'};
  }catch(e){}
  if(Object.keys(out).length>1)return out;
  // 备用：腾讯
  try{
    if(uniq.length){const r2=await fetchTencentQuote(uniq[0]).catch(()=>null);if(r2&&r2.price)out[uniq[0]]=r2;}
  }catch(e){}
  return out;
}
// ------- 渲染 -------
function paperStatsFrom(trades){
  const sells=trades.filter(t=>t.side==='S');
  const wins=sells.filter(t=>t.realized>0),losses=sells.filter(t=>t.realized<=0);
  const totalRealized=sells.reduce((a,b)=>a+b.realized,0);
  const avgWin=wins.length?wins.reduce((a,b)=>a+b.realized,0)/wins.length:0;
  const avgLoss=losses.length?Math.abs(losses.reduce((a,b)=>a+b.realized,0)/losses.length):0;
  return {n:sells.length,winN:wins.length,winRate:sells.length?wins.length/sells.length*100:0,totalRealized,avgWin,avgLoss,pl:avgLoss>0?avgWin/avgLoss:0};
}
function fmtWan(v){return Math.abs(v)>=1e8?(v/1e8).toFixed(2)+'亿':(Math.abs(v)>=1e4?(v/1e4).toFixed(2)+'万':v.toFixed(0));}
async function renderPaperAll(){
  const acctBox=document.getElementById('paperAccountBox');
  if(!acctBox)return; // 层未挂载
  const posCodes=Object.keys(paperAcct.positions);
  const ordCodes=paperAcct.orders.filter(o=>o.status==='PENDING').map(o=>o.code);
  const pfCodes=[];paperPortfolios.forEach(p=>p.stocks.forEach(s=>{if(!pfCodes.includes(s.code))pfCodes.push(s.code);}));
  const quotes=await paperQuotes([...posCodes,...ordCodes,...pfCodes]);
  paperAcct._lastQuotes=quotes;
  paperMatch(quotes);
  // 市值
  let mv=0;Object.values(paperAcct.positions).forEach(p=>{const q=quotes[p.code];mv+=(q&&q.price?q.price:p.avg)*p.qty;});
  const equity=paperAcct.cash+mv;
  const ret=(equity-paperAcct.initCash)/paperAcct.initCash*100;
  // 账户概览
  let h='<div class="dt-grid">';
  h+='<div class="dt-cell"><div class="v">'+fmtWan(paperAcct.cash)+'</div><div class="l">可用资金(元)</div></div>';
  h+='<div class="dt-cell"><div class="v">'+fmtWan(mv)+'</div><div class="l">持仓市值(元)</div></div>';
  h+='<div class="dt-cell"><div class="v" style="color:var(--gold);">'+fmtWan(equity)+'</div><div class="l">总资产(元)</div></div>';
  h+='<div class="dt-cell"><div class="v '+(ret>=0?'pu':'pd')+'">'+(ret>=0?'+':'')+ret.toFixed(2)+'%</div><div class="l">总收益率</div></div>';
  const st=paperStatsFrom(paperAcct.trades);
  h+='<div class="dt-cell"><div class="v '+(st.winRate>=50?'pu':'pd')+'">'+st.winRate.toFixed(0)+'%</div><div class="l">胜率('+st.winN+'/'+st.n+')</div></div>';
  h+='<div class="dt-cell"><div class="v '+(st.pl>=1?'pu':'pd')+'">'+st.pl.toFixed(2)+'</div><div class="l">盈亏比</div></div>';
  h+='<div class="dt-cell"><div class="v '+(st.totalRealized>=0?'pu':'pd')+'">'+(st.totalRealized>=0?'+':'')+st.totalRealized.toFixed(0)+'</div><div class="l">已实现盈亏(元)</div></div>';
  h+='<div class="dt-cell"><div class="v"><button onclick="paperReset()" style="background:var(--bg3);color:var(--muted);border:1px solid var(--rule);border-radius:6px;padding:3px 10px;font-size:11px;cursor:pointer;">重置账户</button></div><div class="l">初始资金 '+fmtWan(paperAcct.initCash)+'</div></div>';
  h+='</div>';
  acctBox.innerHTML=h;
  // 下单面板
  renderPaperOrderBox();
  // 持仓
  const posBox=document.getElementById('paperPositionBox');
  if(posBox){
    const ps=Object.values(paperAcct.positions);
    if(!ps.length)posBox.innerHTML='<div style="color:var(--muted);font-size:12px;padding:20px;text-align:center;">暂无持仓 · 可在个股详情页点「按PA计划模拟买入」或用上方下单面板</div>';
    else{
      let t='<table style="width:100%;font-size:12px;"><thead><tr><th>股票</th><th>数量</th><th>成本</th><th>现价</th><th>浮动盈亏</th><th>止损/止盈</th><th>来源</th><th>操作</th></tr></thead><tbody>';
      ps.forEach(p=>{
        const q=quotes[p.code]||{};const cur=q.price||p.avg;
        const pnl=(cur-p.avg)*p.qty;const pp=p.avg>0?(cur-p.avg)/p.avg*100:0;
        const cls=pnl>=0?'pu':'pd';
        t+='<tr><td><span style="cursor:pointer;color:var(--blue);" onclick="showDetailAny(\''+p.code+'\',\''+String(p.name).replace(/'/g,'')+'\')">'+p.name+'</span><br><span style="color:var(--muted);font-size:10px;">'+p.code+' · '+p.entryDate+'</span></td>';
        t+='<td>'+p.qty+'</td><td>'+p.avg.toFixed(2)+'</td><td class="'+cls+'">'+cur.toFixed(2)+'</td>';
        t+='<td class="'+cls+'"><b>'+(pnl>=0?'+':'')+pnl.toFixed(0)+'</b><br><span style="font-size:10px;">'+(pp>=0?'+':'')+pp.toFixed(2)+'%</span></td>';
        t+='<td>'+(p.sl?'<span class="pd">'+p.sl.toFixed(2)+'</span>':'--')+' / '+(p.tp?'<span class="pu">'+p.tp.toFixed(2)+'</span>':'--')+'<br><button onclick="paperEditSLTP(\''+p.code+'\')" style="font-size:10px;color:var(--blue);background:none;border:none;cursor:pointer;padding:0;">修改</button></td>';
        t+='<td><span class="badge watch" style="font-size:10px;">'+(p.src||'手动')+'</span></td>';
        t+='<td><button onclick="paperSellForm(\''+p.code+'\')" style="background:var(--down);color:#fff;border:none;border-radius:5px;padding:2px 8px;font-size:11px;cursor:pointer;">卖出</button></td></tr>';
      });
      posBox.innerHTML=t+'</tbody></table>';
    }
  }
  // 挂单
  const pendBox=document.getElementById('paperPendingBox');
  if(pendBox){
    const pend=paperAcct.orders.filter(o=>o.status==='PENDING');
    if(!pend.length)pendBox.innerHTML='<div style="color:var(--muted);font-size:12px;padding:20px;text-align:center;">暂无挂单</div>';
    else{
      let t='<table style="width:100%;font-size:12px;"><thead><tr><th>方向</th><th>股票</th><th>价/量</th><th>现价</th><th>SL/TP</th><th>状态</th><th>操作</th></tr></thead><tbody>';
      pend.forEach(o=>{
        const q=quotes[o.code]||{};const cur=q.price||0;
        t+='<tr><td><span class="badge '+(o.side==='B'?'buy':'sell')+'">'+(o.side==='B'?'买入':'卖出')+'</span></td>';
        t+='<td>'+o.name+'<br><span style="color:var(--muted);font-size:10px;">'+o.code+'</span></td>';
        t+='<td class="'+(o.side==='B'?'pu':'pd')+'">'+o.price.toFixed(2)+'<br>'+o.qty+'股</td>';
        t+='<td>'+(cur?cur.toFixed(2):'--')+'<br><span style="font-size:10px;color:var(--muted);">'+(o.side==='B'?(cur&&cur<=o.price?'✅可成交':'等待回调'):(cur&&cur>=o.price?'✅可成交':'等待反弹'))+'</span></td>';
        t+='<td>'+(o.sl?o.sl.toFixed(2):'--')+' / '+(o.tp?o.tp.toFixed(2):'--')+'</td>';
        t+='<td><span class="badge watch">挂单中</span></td>';
        t+='<td><button onclick="paperCancel(\''+o.id+'\')" style="background:var(--bg3);color:var(--ink);border:1px solid var(--rule);border-radius:5px;padding:2px 8px;font-size:11px;cursor:pointer;">撤单</button> <button onclick="paperFillManual(\''+o.id+'\')" style="background:var(--gold);color:#000;border:none;border-radius:5px;padding:2px 8px;font-size:11px;cursor:pointer;">手动成交</button></td></tr>';
      });
      pendBox.innerHTML=t+'</tbody></table>';
    }
  }
  // 统计（按来源分组）
  const statBox=document.getElementById('paperStatsBox');
  if(statBox){
    const sells=paperAcct.trades.filter(t=>t.side==='S');
    if(!sells.length)statBox.innerHTML='<div style="color:var(--muted);font-size:12px;padding:16px;text-align:center;">暂无已实现卖出 · 卖出或止盈止损触发后自动统计各信号来源效果</div>';
    else{
      const groups={};
      sells.forEach(t=>{const g=(t.src||'手动').split('·')[0];if(!groups[g])groups[g]=[];groups[g].push(t);});
      let t='<table style="width:100%;font-size:12px;"><thead><tr><th>信号来源</th><th>笔数</th><th>胜率</th><th>平均盈利</th><th>平均亏损</th><th>盈亏比</th><th>合计盈亏</th></tr></thead><tbody>';
      Object.keys(groups).forEach(g=>{
        const s=paperStatsFrom2(groups[g]);
        t+='<tr><td><span class="badge watch">'+g+'</span></td><td>'+s.n+'</td><td class="'+(s.winRate>=50?'pu':'pd')+'">'+s.winRate.toFixed(0)+'%</td><td class="pu">+'+s.avgWin.toFixed(0)+'</td><td class="pd">'+s.avgLoss.toFixed(0)+'</td><td>'+s.pl.toFixed(2)+'</td><td class="'+(s.totalRealized>=0?'pu':'pd')+'"><b>'+(s.totalRealized>=0?'+':'')+s.totalRealized.toFixed(0)+'</b></td></tr>';
      });
      statBox.innerHTML=t+'</tbody></table>';
    }
  }
  // 成交记录
  const trBox=document.getElementById('paperTradeBox');
  if(trBox){
    const done=paperAcct.trades.slice().reverse().slice(0,60);
    if(!done.length)trBox.innerHTML='<div style="color:var(--muted);font-size:12px;padding:16px;text-align:center;">暂无成交记录</div>';
    else{
      let t='<table style="width:100%;font-size:12px;"><thead><tr><th>时间</th><th>方向</th><th>股票</th><th>价格</th><th>数量</th><th>盈亏</th><th>来源</th><th>方式</th></tr></thead><tbody>';
      done.forEach(x=>{
        const dt=new Date(x.ts);const tm=String(dt.getMonth()+1).padStart(2,'0')+'-'+String(dt.getDate()).padStart(2,'0')+' '+String(dt.getHours()).padStart(2,'0')+':'+String(dt.getMinutes()).padStart(2,'0');
        t+='<tr><td style="color:var(--muted);font-size:11px;">'+tm+'</td><td><span class="badge '+(x.side==='B'?'buy':'sell')+'">'+(x.side==='B'?'买':'卖')+'</span></td><td>'+x.name+'</td><td>'+x.price.toFixed(2)+'</td><td>'+x.qty+'</td>';
        t+=x.side==='S'?'<td class="'+(x.realized>=0?'pu':'pd')+'"><b>'+(x.realized>=0?'+':'')+x.realized.toFixed(0)+'</b></td>':'<td>--</td>';
        t+='<td><span class="badge watch" style="font-size:10px;">'+(x.src||'')+'</span></td><td style="color:var(--muted);font-size:10px;">'+(x.how||'')+'</td></tr>';
      });
      trBox.innerHTML=t+'</tbody></table>';
    }
  }
  renderPortfolios(quotes);
  renderPaperAnalysis();
}
function paperStatsFrom2(sells){return paperStatsFrom(sells);}
function paperFillManual(oid){
  const o=paperAcct.orders.find(x=>x.id===oid);
  if(!o||o.status!=='PENDING')return;
  o.status='DONE';
  paperFillSilent(o.side,o.code,o.name,o.price,o.qty,o.sl,o.tp,o.src,'手动成交@'+o.price.toFixed(2));
  savePaper();renderPaperAll();
}
function paperSellForm(code){
  const pos=paperAcct.positions[code];if(!pos)return;
  const pr=prompt('卖出 '+pos.name+'('+code+')\n持仓 '+pos.qty+' 股，成本 '+pos.avg.toFixed(2)+'\n输入卖出价格（留空=按现价市价卖出）：','');
  if(pr===null)return;
  const price=pr===''?0:parseFloat(pr);
  if(pr!==''&&!(price>0)){alert('价格无效');return;}
  if(price>0)paperOrder('S','LMT',code,pos.name,price,pos.qty,null,null,'手动限价');
  else{
    const q=paperAcct._lastQuotes&&paperAcct._lastQuotes[code];
    const cur=q&&q.price||pos.avg;
    paperFillDirect('S',code,pos.name,cur,pos.qty,null,null,'手动市价','市价卖出@'+cur.toFixed(2));
  }
}
function paperEditSLTP(code){
  const pos=paperAcct.positions[code];if(!pos)return;
  const s=prompt('设置 '+pos.name+' 止损价（当前 '+(pos.sl?pos.sl.toFixed(2):'未设')+'，留空保持）：',pos.sl?pos.sl.toFixed(2):'');
  if(s===null)return;
  if(s!==''){const v=parseFloat(s);if(v>0)pos.sl=v;}
  const t=prompt('设置止盈价（当前 '+(pos.tp?pos.tp.toFixed(2):'未设')+'，留空保持）：',pos.tp?pos.tp.toFixed(2):'');
  if(t===null)return;
  if(t!==''){const v=parseFloat(t);if(v>0)pos.tp=v;}
  savePaper();renderPaperAll();
}
function paperReset(){
  if(!confirm('重置模拟账户为初始 '+PAPER_INIT_CASH+' 元？持仓/挂单/成交记录将清空。'))return;
  paperAcct={cash:PAPER_INIT_CASH,initCash:PAPER_INIT_CASH,positions:{},orders:[],trades:[]};
  savePaper();renderPaperAll();
}
function renderPaperOrderBox(){
  const box=document.getElementById('paperOrderBox');if(!box)return;
  if(box.children.length)return; // 已渲染且用户可能正在填写，不覆盖
  box.innerHTML='<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;align-items:end;">'
    +'<div><label style="font-size:11px;color:var(--muted);">股票代码</label><input id="poCode" placeholder="如 600519" style="width:100%;background:var(--bg3);border:1px solid var(--rule);border-radius:6px;padding:6px 8px;color:var(--ink);font-size:13px;"></div>'
    +'<div><label style="font-size:11px;color:var(--muted);">名称</label><input id="poName" placeholder="自动获取" style="width:100%;background:var(--bg3);border:1px solid var(--rule);border-radius:6px;padding:6px 8px;color:var(--ink);font-size:13px;"></div>'
    +'<div><label style="font-size:11px;color:var(--muted);">方向</label><select id="poSide" style="width:100%;background:var(--bg3);border:1px solid var(--rule);border-radius:6px;padding:6px;color:var(--ink);font-size:13px;"><option value="B">买入</option><option value="S">卖出</option></select></div>'
    +'<div><label style="font-size:11px;color:var(--muted);">类型</label><select id="poType" style="width:100%;background:var(--bg3);border:1px solid var(--rule);border-radius:6px;padding:6px;color:var(--ink);font-size:13px;"><option value="LMT">限价</option><option value="MKT">市价</option></select></div>'
    +'<div><label style="font-size:11px;color:var(--muted);">价格</label><input id="poPrice" type="number" step="0.01" placeholder="市价可留空" style="width:100%;background:var(--bg3);border:1px solid var(--rule);border-radius:6px;padding:6px 8px;color:var(--ink);font-size:13px;"></div>'
    +'<div><label style="font-size:11px;color:var(--muted);">数量(股)</label><input id="poQty" type="number" step="100" placeholder="100整数倍" style="width:100%;background:var(--bg3);border:1px solid var(--rule);border-radius:6px;padding:6px 8px;color:var(--ink);font-size:13px;"></div>'
    +'<div><label style="font-size:11px;color:var(--muted);">止损(选填)</label><input id="poSL" type="number" step="0.01" style="width:100%;background:var(--bg3);border:1px solid var(--rule);border-radius:6px;padding:6px 8px;color:var(--ink);font-size:13px;"></div>'
    +'<div><label style="font-size:11px;color:var(--muted);">止盈(选填)</label><input id="poTP" type="number" step="0.01" style="width:100%;background:var(--bg3);border:1px solid var(--rule);border-radius:6px;padding:6px 8px;color:var(--ink);font-size:13px;"></div>'
    +'<div style="display:flex;gap:6px;"><button onclick="paperFetchQuote()" style="flex:1;background:var(--bg3);color:var(--ink);border:1px solid var(--rule);border-radius:8px;padding:8px 4px;font-size:12px;cursor:pointer;">查现价</button><button onclick="paperOrderFormSubmit()" style="flex:1.4;background:var(--gold);color:#000;font-weight:700;border:none;border-radius:8px;padding:8px 16px;font-size:13px;cursor:pointer;">下单</button></div></div>'
    +'<div style="font-size:11px;color:var(--muted);margin-top:8px;">提示：限价买单在现价≤挂价时自动成交；限价卖单在现价≥挂价时自动成交；持仓触发止损/止盈价自动平仓（T+1限制生效）。「查现价」可自动拉取股票实时价格填入价格栏。来源标记「手动」。</div>';
}
// 下单面板「查现价」：拉取股票实时价格+名称，填入价格/名称栏
async function paperFetchQuote(){
  const code=(document.getElementById('poCode').value||'').trim();
  if(!/^\d{6}$/.test(code)){alert('请输入6位股票代码');return;}
  const secid=((/^(6|9)/.test(code)?'1.':'0.')+code);
  const r=await jsonpRequest('https://push2delay.eastmoney.com/api/qt/stock/get?secid='+secid+'&fltt=2&fields=f43,f58,f170&ut=fa5fd1943c7b386f172d6893dbfba10b',6000).catch(()=>null);
  if(r&&r.data&&typeof r.data.f43==='number'){
    document.getElementById('poPrice').value=r.data.f43.toFixed(2);
    if(r.data.f58)document.getElementById('poName').value=r.data.f58;
    alert('实时价 '+(r.data.f58||code)+'：'+r.data.f43.toFixed(2)+(typeof r.data.f170==='number'?('　'+(r.data.f170>=0?'+':'')+r.data.f170.toFixed(2)+'%'):''));
  }else{
    alert('实时价获取失败，请稍后重试');
  }
}
async function paperOrderFormSubmit(){
  const code=(document.getElementById('poCode').value||'').trim();
  let name=(document.getElementById('poName').value||'').trim();
  const side=document.getElementById('poSide').value;
  const type=document.getElementById('poType').value;
  const price=parseFloat(document.getElementById('poPrice').value)||0;
  const qty=parseInt(document.getElementById('poQty').value,10)||0;
  const sl=parseFloat(document.getElementById('poSL').value)||null;
  const tp=parseFloat(document.getElementById('poTP').value)||null;
  if(!/^\d{6}$/.test(code)){alert('请输入6位股票代码');return;}
  if(!name){
    const s=await searchAllStocks(code).catch(()=>[]);
    if(s&&s.length)name=s[0].name||code;
    if(!name)name=code;
  }
  paperOrder(side,type,code,name,price,qty,sl,tp,'手动');
}
// ====================== 模拟组合引擎 ======================
function loadPF(){
  try{const s=localStorage.getItem('paperPortfolios');if(s){const a=JSON.parse(s);if(Array.isArray(a))return a;}}catch(e){}
  return [{id:'P'+Date.now(),name:'默认组合',note:'示例：可添加成分股并配置权重',createdAt:Date.now(),stocks:[]}];
}
function savePF(){try{localStorage.setItem('paperPortfolios',JSON.stringify(paperPortfolios));}catch(e){}if(typeof _syncToBackend==='function')_syncToBackend();}
let paperPortfolios=loadPF();
function pfAdd(){const nm=prompt('新组合名称：','');if(!nm)return;paperPortfolios.push({id:'P'+Date.now(),name:nm,note:'',createdAt:Date.now(),stocks:[]});savePF();renderPaperAll();}
function pfDel(pid){const p=paperPortfolios.find(x=>x.id===pid);if(!p)return;if(!confirm('删除组合「'+p.name+'」？'))return;paperPortfolios=paperPortfolios.filter(x=>x.id!==pid);if(!paperPortfolios.length)paperPortfolios=[{id:'P'+Date.now(),name:'默认组合',note:'',createdAt:Date.now(),stocks:[]}];savePF();renderPaperAll();}
function pfRename(pid){const p=paperPortfolios.find(x=>x.id===pid);if(!p)return;const nm=prompt('修改组合名称：',p.name);if(!nm)return;p.name=nm;savePF();renderPaperAll();}
function pfNote(pid){const p=paperPortfolios.find(x=>x.id===pid);if(!p)return;const nt=prompt('修改备注：',p.note||'');if(nt===null)return;p.note=nt;savePF();renderPaperAll();}
async function pfStockAdd(pid){
  const p=paperPortfolios.find(x=>x.id===pid);if(!p)return;
  const code=prompt('输入6位股票代码：','');if(!code)return;
  if(!/^\d{6}$/.test(code.trim())){alert('代码格式错误');return;}
  let name=code;
  try{const s=await searchAllStocks(code.trim()).catch(()=>[]);if(s&&s.length)name=s[0].name||code;}catch(e){}
  const w=parseFloat(prompt('权重%（如 20 = 20%）：','10'))||0;
  if(w<=0){alert('权重须大于0');return;}
  let entry=0;
  const q=paperAcct._lastQuotes&&paperAcct._lastQuotes[code.trim()];
  if(q&&q.price)entry=q.price;
  const eStr=prompt('建仓成本价（留空=现价'+(entry?entry.toFixed(2):'未知')+'）：',entry?entry.toFixed(2):'');
  if(eStr!==null&&eStr!==''){const v=parseFloat(eStr);if(v>0)entry=v;}
  if(p.stocks.some(s=>s.code===code.trim())){alert('该股已在组合中');return;}
  p.stocks.push({code:code.trim(),name,weight:w,entryPrice:entry||0,entryDate:paperToday(),addedTs:Date.now()});
  savePF();renderPaperAll();
}
function pfStockDel(pid,code){const p=paperPortfolios.find(x=>x.id===pid);if(!p)return;if(!confirm('移除成分股 '+code+'？'))return;p.stocks=p.stocks.filter(s=>s.code!==code);savePF();renderPaperAll();}
async function pfStockEdit(pid,code){
  const p=paperPortfolios.find(x=>x.id===pid);if(!p)return;
  const s=p.stocks.find(x=>x.code===code);if(!s)return;
  const w=prompt('修改权重%（当前 '+s.weight+'）：',String(s.weight));
  if(w===null)return;const wv=parseFloat(w);if(wv>0)s.weight=wv;
  const e=prompt('修改建仓成本（当前 '+s.entryPrice.toFixed(2)+'，留空保持）：',s.entryPrice.toFixed(2));
  if(e!==null&&e!==''){const ev=parseFloat(e);if(ev>0)s.entryPrice=ev;}
  savePF();renderPaperAll();
}
async function renderPortfolios(quotes){
  const box=document.getElementById('paperPortfolioBox');if(!box)return;
  // 基准：上证指数（用实时涨跌幅作为当日对比口径）
  const bench=quotes['__SH__']||null;
  let h='';
  // 对比总览条
  if(paperPortfolios.length){
    h+='<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:8px;margin-bottom:14px;">';
    paperPortfolios.forEach(p=>{
      let wRet=0,wSum=0;
      p.stocks.forEach(s=>{wSum+=s.weight;const q=quotes[s.code];const cur=q&&q.price||s.entryPrice;const r=s.entryPrice>0?(cur-s.entryPrice)/s.entryPrice*100:0;wRet+=s.weight*r;});
      const ret=wSum>0?wRet/wSum:0;
      const cls=ret>=0?'pu':'pd';
      h+='<div class="dt-cell" style="cursor:pointer;" onclick="document.getElementById(\'pfSec_'+p.id+'\').scrollIntoView({behavior:\'smooth\'})"><div class="v '+cls+'">'+(ret>=0?'+':'')+ret.toFixed(2)+'%</div><div class="l">'+p.name+' · '+p.stocks.length+'只</div></div>';
    });
    if(bench)h+='<div class="dt-cell"><div class="v '+(bench.pct>=0?'pu':'pd')+'">'+(bench.pct>=0?'+':'')+bench.pct.toFixed(2)+'%</div><div class="l">上证指数(当日基准)</div></div>';
    h+='</div>';
  }
  paperPortfolios.forEach(p=>{
    const wSum=p.stocks.reduce((a,b)=>a+b.weight,0);
    let wRet=0;
    p.stocks.forEach(s=>{const q=quotes[s.code];const cur=q&&q.price||s.entryPrice;const r=s.entryPrice>0?(cur-s.entryPrice)/s.entryPrice*100:0;wRet+=s.weight*r;});
    const ret=wSum>0?wRet/wSum:0;const cls=ret>=0?'pu':'pd';
    h+='<div id="pfSec_'+p.id+'" style="border:1px solid var(--rule);border-radius:12px;padding:14px;margin-bottom:12px;">';
    h+='<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px;"><b style="font-size:15px;">'+p.name+'</b><span class="badge '+cls+'">'+(ret>=0?'+':'')+ret.toFixed(2)+'%</span><span style="font-size:11px;color:var(--muted);">'+p.stocks.length+'只 · 权重合计'+wSum+'%'+(wSum!==100&&p.stocks.length?'（未归一）':'')+'</span>'+(p.note?'<span style="font-size:11px;color:var(--muted);">'+p.note+'</span>':'');
    h+='<span style="margin-left:auto;display:flex;gap:6px;"><button onclick="pfStockAdd(\''+p.id+'\')" style="background:var(--up);color:#fff;border:none;border-radius:6px;padding:3px 10px;font-size:11px;cursor:pointer;">+成分股</button><button onclick="pfRename(\''+p.id+'\')" style="background:var(--bg3);color:var(--ink);border:1px solid var(--rule);border-radius:6px;padding:3px 10px;font-size:11px;cursor:pointer;">改名</button><button onclick="pfNote(\''+p.id+'\')" style="background:var(--bg3);color:var(--ink);border:1px solid var(--rule);border-radius:6px;padding:3px 10px;font-size:11px;cursor:pointer;">备注</button><button onclick="pfDel(\''+p.id+'\')" style="background:var(--bg3);color:var(--down);border:1px solid var(--rule);border-radius:6px;padding:3px 10px;font-size:11px;cursor:pointer;">删除</button></span></div>';
    if(!p.stocks.length)h+='<div style="color:var(--muted);font-size:12px;padding:12px;text-align:center;">暂无成分股，点击「+成分股」添加</div>';
    else{
      h+='<table style="width:100%;font-size:12px;"><thead><tr><th>股票</th><th>权重</th><th>成本</th><th>现价</th><th>区间涨跌</th><th>加权贡献</th><th>当日</th><th>操作</th></tr></thead><tbody>';
      p.stocks.forEach(s=>{
        const q=quotes[s.code]||{};const cur=q.price||0;
        const r=s.entryPrice>0&&cur>0?(cur-s.entryPrice)/s.entryPrice*100:0;
        const contrib=s.weight*r/100;
        const rcls=r>=0?'pu':'pd';
        h+='<tr><td><span style="cursor:pointer;color:var(--blue);" onclick="showDetailAny(\''+s.code+'\',\''+String(s.name).replace(/'/g,'')+'\')">'+s.name+'</span> <span style="color:var(--muted);font-size:10px;">'+s.code+'</span></td>';
        h+='<td>'+s.weight+'%</td><td>'+s.entryPrice.toFixed(2)+'</td><td>'+(cur?cur.toFixed(2):'--')+'</td>';
        h+='<td class="'+rcls+'">'+(r>=0?'+':'')+r.toFixed(2)+'%</td><td class="'+rcls+'">'+(contrib>=0?'+':'')+contrib.toFixed(2)+'%</td>';
        h+='<td class="'+((q.pct||0)>=0?'pu':'pd')+'">'+(q.pct!==undefined?(q.pct>=0?'+':'')+q.pct.toFixed(2)+'%':'--')+'</td>';
        h+='<td><button onclick="pfStockEdit(\''+p.id+'\',\''+s.code+'\')" style="font-size:10px;color:var(--blue);background:none;border:none;cursor:pointer;padding:0 4px;">改</button><button onclick="pfStockDel(\''+p.id+'\',\''+s.code+'\')" style="font-size:10px;color:var(--down);background:none;border:none;cursor:pointer;padding:0 4px;">删</button></td></tr>';
      });
      h+='</tbody></table>';
    }
    h+='</div>';
  });
  h+='<button onclick="pfAdd()" style="background:var(--gold);color:#000;font-weight:700;border:none;border-radius:8px;padding:7px 18px;font-size:13px;cursor:pointer;">+ 新建组合</button>';
  box.innerHTML=h;
}
// ===== 交易分析：权益曲线 + 回撤 + 盈亏分布 =====
function renderPaperAnalysis(){
  const box=document.getElementById('paperAnalysisBox');if(!box)return;
  const sells=paperAcct.trades.filter(function(t){return t.side==='S';});
  if(!sells.length){
    box.innerHTML='<div style="color:var(--muted);font-size:12px;padding:16px;text-align:center;">暂无已实现交易 · 卖出或止盈止损触发后自动生成权益曲线与回撤分析</div>';
    return;
  }
  const sorted=sells.slice().sort(function(a,b){return a.ts-b.ts;});
  let floatPnL=0;
  Object.values(paperAcct.positions).forEach(function(p){const q=(paperAcct._lastQuotes&&paperAcct._lastQuotes[p.code])||{};const cur=q.price||p.avg;floatPnL+=(cur-p.avg)*p.qty;});
  const st=paperStatsFrom(sells);
  const realized=sorted.map(function(s){return s.realized;});
  // 最大回撤（基于累计权益）
  let peak=paperAcct.initCash,run=paperAcct.initCash,maxDD=0,maxDDPct=0;
  sorted.forEach(function(s){run+=s.realized;if(run>peak)peak=run;const dd=peak-run,ddp=peak>0?dd/peak*100:0;if(dd>maxDD){maxDD=dd;maxDDPct=ddp;}});
  const maxWin=Math.max.apply(null,realized),maxLoss=Math.min.apply(null,realized);
  const finalEquity=paperAcct.initCash+st.totalRealized+floatPnL;
  const totalRet=(finalEquity-paperAcct.initCash)/paperAcct.initCash*100;
  let h='<div class="dt-grid" style="margin-bottom:10px;">';
  h+='<div class="dt-cell"><div class="v '+(totalRet>=0?'pu':'pd')+'">'+(totalRet>=0?'+':'')+totalRet.toFixed(2)+'%</div><div class="l">累计收益</div></div>';
  h+='<div class="dt-cell"><div class="v">'+st.n+'</div><div class="l">已平仓笔数</div></div>';
  h+='<div class="dt-cell"><div class="v '+(st.winRate>=50?'pu':'pd')+'">'+st.winRate.toFixed(0)+'%</div><div class="l">胜率</div></div>';
  h+='<div class="dt-cell"><div class="v">'+st.pl.toFixed(2)+'</div><div class="l">盈亏比</div></div>';
  h+='<div class="dt-cell"><div class="v '+(st.totalRealized>=0?'pu':'pd')+'">'+(st.totalRealized>=0?'+':'')+st.totalRealized.toFixed(0)+'</div><div class="l">已实现盈亏(元)</div></div>';
  h+='<div class="dt-cell"><div class="v pd">-'+(maxDDPct||0).toFixed(2)+'%</div><div class="l">最大回撤</div></div>';
  h+='<div class="dt-cell"><div class="v pu">+'+maxWin.toFixed(0)+'</div><div class="l">最大单笔盈利</div></div>';
  h+='<div class="dt-cell"><div class="v pd">'+maxLoss.toFixed(0)+'</div><div class="l">最大单笔亏损</div></div>';
  h+='</div>';
  h+='<div style="display:grid;grid-template-columns:1.6fr 1fr;gap:10px;">';
  h+='<div id="paperEquityChart" style="height:220px;"></div><div id="paperPnlChart" style="height:220px;"></div></div>';
  box.innerHTML=h;
  // 权益曲线
  const pts=[{t:'起始',v:paperAcct.initCash}];
  let cum=paperAcct.initCash;
  sorted.forEach(function(s){cum+=s.realized;pts.push({t:s.code,v:cum});});
  const eqDom=document.getElementById('paperEquityChart');
  if(eqDom){
    const old=echarts.getInstanceByDom(eqDom);if(old)old.dispose();
    const ch=echarts.init(eqDom);
    ch.setOption({
      backgroundColor:'transparent',animation:false,
      grid:{left:56,right:16,top:16,bottom:26},
      tooltip:{trigger:'axis',backgroundColor:hexA(tc.bg2,.96),borderColor:tc.rule,textStyle:{color:tc.ink,fontSize:11}},
      xAxis:{type:'category',data:pts.map(function(p){return p.t;}),axisLabel:{color:tc.muted,fontSize:8,interval:Math.max(0,Math.floor(pts.length/8))},axisLine:{lineStyle:{color:tc.rule}}},
      yAxis:{type:'value',axisLabel:{color:tc.muted,fontSize:9,formatter:function(v){return fmtWan(v);}},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},
      series:[{name:'权益',type:'line',data:pts.map(function(p){return +p.v.toFixed(0);}),smooth:true,showSymbol:false,lineStyle:{width:2,color:tc.blue},areaStyle:{color:hexA(tc.blue,.12)}}]
    });
  }
  // 盈亏分布
  const pnlDom=document.getElementById('paperPnlChart');
  if(pnlDom){
    const old=echarts.getInstanceByDom(pnlDom);if(old)old.dispose();
    const ch=echarts.init(pnlDom);
    ch.setOption({
      backgroundColor:'transparent',animation:false,
      grid:{left:56,right:16,top:16,bottom:26},
      tooltip:{trigger:'axis',backgroundColor:hexA(tc.bg2,.96),borderColor:tc.rule,textStyle:{color:tc.ink,fontSize:11}},
      xAxis:{type:'category',data:realized.map(function(_,i){return (i+1)+'';}),axisLabel:{color:tc.muted,fontSize:8},axisLine:{lineStyle:{color:tc.rule}}},
      yAxis:{type:'value',axisLabel:{color:tc.muted,fontSize:9,formatter:function(v){return v.toFixed(0);}},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},
      series:[{name:'单笔盈亏',type:'bar',data:realized.map(function(r){return {value:r,itemStyle:{color:r>=0?tc.up:tc.down}};}),barWidth:'60%'}]
    });
  }
}
// 模拟层定时刷新（仅激活时）
let _paperTimer=null;
function startPaperLoop(){
  if(_paperTimer)clearInterval(_paperTimer);
  renderPaperAll();
  _paperTimer=setInterval(function(){if(document.getElementById('layer-paper').classList.contains('active'))renderPaperAll();},60000);
}


// ===== 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{
    kline=await fetchKlineSafe(secid)||[];
  }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 style="display:flex;gap:8px;margin-top:10px;"><button onclick="paperQuickPanel(\''+code+'\',\''+String(name).replace(/'/g,'')+'\','+price.toFixed(2)+',false,\'B\')" style="flex:1;background:var(--up);color:#fff;border:none;border-radius:8px;padding:7px 0;font-size:13px;font-weight:700;cursor:pointer;">模拟买入</button><button onclick="paperQuickPanel(\''+code+'\',\''+String(name).replace(/'/g,'')+'\','+price.toFixed(2)+',false,\'S\')" style="flex:1;background:var(--down);color:#fff;border:none;border-radius:8px;padding:7px 0;font-size:13px;font-weight:700;cursor:pointer;">模拟卖出</button></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){
    const pa=analyzePriceAction(kline);
    if(pa){
      html+='<div class="dt-section" style="border-left:3px solid var(--purple);"><h4><span style="width:3px;height:14px;background:var(--purple);border-radius:2px;display:inline-block;"></span>价格行为诊断 PA（两阶段：市场诊断 → 交易决策）<span style="font-size:10px;color:var(--muted);margin-left:6px;">方法论：PA_Agent · 市场结构/周期位置/形态</span></h4>';
      // 阶段1：市场诊断
      html+='<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;margin-bottom:10px;">';
      const tCol=pa.trend==='上升'?'pu':(pa.trend==='下降'?'pd':'');
      html+='<div class="dt-cell"><div class="v '+tCol+'">'+pa.trend+'</div><div class="l">市场结构</div></div>';
      html+='<div class="dt-cell"><div class="v">'+pa.cycle.stage+'</div><div class="l">周期位置</div></div>';
      html+='<div class="dt-cell"><div class="v '+(pa.cycle.posVs20>=0?'pu':'pd')+'">'+(pa.cycle.posVs20>=0?'+':'')+pa.cycle.posVs20.toFixed(1)+'%</div><div class="l">偏离MA20</div></div>';
      if(pa.support)html+='<div class="dt-cell"><div class="v pu">'+pa.support.toFixed(2)+'</div><div class="l">结构支撑</div></div>';
      if(pa.resistance)html+='<div class="dt-cell"><div class="v pd">'+pa.resistance.toFixed(2)+'</div><div class="l">结构阻力</div></div>';
      const lastEv=pa.events[pa.events.length-1];
      if(lastEv)html+='<div class="dt-cell"><div class="v" style="color:var(--gold);">'+lastEv.type+'</div><div class="l">最近结构事件</div></div>';
      html+='</div>';
      html+='<div style="font-size:12px;color:var(--muted);line-height:1.7;padding:8px 10px;background:var(--bg2);border-radius:8px;margin-bottom:8px;">'+pa.cycle.desc+(lastEv?'<br>结构事件：'+lastEv.desc+'（'+lastEv.date+'）':'')+'</div>';
      // 形态
      if(pa.patterns.length)html+='<div style="font-size:12px;margin-bottom:10px;">近端K线形态：'+pa.patterns.map(p=>'<span class="badge '+(p.dir>0?'buy':(p.dir<0?'sell':'watch'))+'" style="margin-right:6px;">'+p.name+'</span>').join('')+'<span style="color:var(--muted);font-size:11px;">（'+pa.patterns.map(p=>p.date).join('、')+'）</span></div>';
      // 阶段2：交易决策
      const d=pa.decision;
      const dCls=d.action.indexOf('买入')>=0?'buy':(d.action.indexOf('减')>=0||d.action.indexOf('卖')>=0?'sell':'watch');
      html+='<div style="border:1px solid var(--rule);border-radius:10px;padding:12px 14px;background:var(--bg2);">';
      html+='<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;"><span style="font-size:11px;color:var(--muted);">阶段2 决策</span><span class="badge '+dCls+'">'+d.action+'</span>'+(d.rr?'<span style="font-size:11px;color:var(--muted);">盈亏比 '+d.rr+':1</span>':'')+'</div>';
      html+='<div style="font-size:13px;line-height:1.7;margin-bottom:8px;">'+d.plan+'</div>';
      if(d.entry)html+='<div class="dt-grid" style="margin-bottom:8px;"><div class="dt-cell"><div class="v pu">'+d.entry.toFixed(2)+'</div><div class="l">PA入场价</div></div><div class="dt-cell"><div class="v pd">'+d.stop.toFixed(2)+'</div><div class="l">PA止损</div></div><div class="dt-cell"><div class="v" style="color:var(--gold);">'+d.target.toFixed(2)+'</div><div class="l">PA目标</div></div></div>';
      html+='<ul style="font-size:12px;color:var(--muted);line-height:1.8;padding-left:18px;margin:0;">'+d.reasons.map(r=>'<li>'+r+'</li>').join('')+'</ul>';
      if(d.entry){
        html+='<div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;">';
        html+='<button onclick="paperQuickBuy(\''+code+'\',\''+String(name).replace(/'/g,'')+'\','+(d.entry?d.entry.toFixed(2):'null')+','+(d.stop?d.stop.toFixed(2):'null')+','+(d.target?d.target.toFixed(2):'null')+',\'PA决策\')" style="background:var(--up);color:#fff;padding:7px 16px;font-size:12px;font-weight:700;border:none;border-radius:8px;cursor:pointer;">📋 按PA计划模拟买入</button>';
        html+='<span style="font-size:11px;color:var(--muted);align-self:center;">记录到模拟交易账户，用于观测信号推荐效果</span></div>';
      }
      html+='</div></div>';
    }
    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/PA结构 + 副图指标）</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 '<div class="ticker-item" style="cursor:pointer;" onclick="showIndexDetail(\''+code+'\')" title="点击查看指数详情">'
      +'<span class="tk-name">'+name+'</span>'
      +'<span class="tk-price" style="color:var(--'+pc+');">'+(price||0).toFixed(2)+'</span>'
      +'<span class="tk-pct" style="color:var(--'+pc+');">'+(pct>=0?'+':'')+(pct||0).toFixed(2)+'%</span></div>';
  }).join('');
  document.getElementById('tickerTrack').innerHTML=items;
}
// 点击悬浮底栏指数项 → 详情弹层（实时点位+均线+20日涨跌+日K图）
async function showIndexDetail(code){
  var _ev=window.event||event;var _it=(_ev&&_ev.target&&_ev.target.closest)?_ev.target.closest('.ticker-item'):null;
  document.querySelectorAll('.ticker-item.active').forEach(function(x){x.classList.remove('active');});
  if(_it)_it.classList.add('active');
  const name=IDX_MAP[code]||code;
  const secid=IDX_SECIDS.find(s=>s.split('.')[1]===code)||('1.'+code);
  const rt=realtimeIndices[code]||{};
  // 实时源未就绪时兜底用 marketTrend 数据（与悬浮条显示一致，避免弹层显示0.00）
  if(!rt.price&&marketTrend&&marketTrend.indices){
    const fb=marketTrend.indices.find(i=>i.name===name);
    if(fb){rt.price=fb.index_close||0;rt.pct=fb.index_change||0;}
  }
  const mt=(marketTrend&&marketTrend.indices)?marketTrend.indices.find(i=>i.name===name):null;
  const pc=(rt.pct||0)>=0?'up':'down';
  document.getElementById('detailContent').innerHTML='<div style="padding:18px 20px 20px;">'
    +'<div style="display:flex;align-items:baseline;gap:14px;margin-bottom:10px;flex-wrap:wrap;">'
    +'<span style="font-size:20px;font-weight:700;">'+name+'</span>'
    +'<span style="font-size:22px;font-weight:700;color:var(--'+pc+');font-family:var(--font-mono);">'+(rt.price||0).toFixed(2)+'</span>'
    +'<span style="color:var(--'+pc+');font-weight:600;font-size:14px;">'+((rt.pct||0)>=0?'+':'')+(rt.pct||0).toFixed(2)+'%</span></div>'
    +(mt?'<div style="font-size:12px;color:var(--muted);margin-bottom:12px;line-height:1.8;">MA5 '+(mt.ma5||0).toFixed(1)+' · MA10 '+(mt.ma10||0).toFixed(1)+' · MA20 '+(mt.ma20||0).toFixed(1)+' · MA60 '+(mt.ma60||0).toFixed(1)+' · 20日 '+(((mt.ret_20d||0)>=0)?'+':'')+(mt.ret_20d||0).toFixed(2)+'% · 趋势 <b>'+(mt.trend||'--')+'</b></div>':'')
    +'<div id="idxDetailChart" style="width:100%;height:420px;"></div></div>';
  openOverlay('detailOverlay');
  const kl=await fetchKlineSafe(secid);
  if(kl&&kl.length){renderKlineChart(kl,'idxDetailChart');}
  else{const d=document.getElementById('idxDetailChart');if(d)d.innerHTML='<div style="text-align:center;padding:60px 0;color:var(--muted);font-size:13px;">K线数据加载失败（行情源限流），请稍后重试</div>';}
}

// ===== 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();
}

// ===== Real-time Index Data =====
let realtimeIndices={};
let indexKlineLoaded=false;
const IDX_SECIDS=['1.000001','0.399001','0.399006','1.000688','1.000680','0.899050'];
const IDX_MAP={'000001':'上证指数','399001':'深证成指','399006':'创业板指','000688':'科创50','000680':'科创综指','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 kl=await fetchKlineSafe(secid);
      if(kl&&kl.length){
        const closes=kl.map(k=>k.close);
        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');
}

// ===== TPO 四度空间（Time Price Opportunity / Market Profile） =====
// 四度：时间(时段字母) × 价格(档位) × 价值区(70%TPO) × 成交量(活跃度)
// 输出：POC(控制点) / VAH / VAL / 平衡/失衡方向 → 给出日内操作建议
let _idxTpoCur='000001'; // 当前TPO分析的指数代码
const IDX_TPO_LETTERS=['A','B','C','D','E','F','G','H'];
function idxTpoLetter(t){
  const hm=String(t).slice(11,16);
  const h=parseInt(hm.slice(0,2),10),m=parseInt(hm.slice(3),10);
  const mins=h*60+m;
  if(mins>=570&&mins<600)return 'A';   // 09:30-10:00
  if(mins>=600&&mins<630)return 'B';   // 10:00-10:30
  if(mins>=630&&mins<660)return 'C';   // 10:30-11:00
  if(mins>=660&&mins<690)return 'D';   // 11:00-11:30
  if(mins>=780&&mins<810)return 'E';   // 13:00-13:30
  if(mins>=810&&mins<840)return 'F';   // 13:30-14:00
  if(mins>=840&&mins<870)return 'G';   // 14:00-14:30
  if(mins>=870&&mins<=900)return 'H';  // 14:30-15:00
  return null;
}
async function fetchIndexIntraday(secid){
  try{
    const r=await jsonpRequest('https://push2delay.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&ut=fa5fd1943c7b386f172d6893dbfba10b',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 computeTPO(bars){
  if(!bars||bars.length<10)return null;
  let high=-Infinity,low=Infinity;
  bars.forEach(function(b){if(b.high>high)high=b.high;if(b.low<low)low=b.low;});
  if(!isFinite(high)||!isFinite(low)||high<=low)return null;
  // 价格档步长：把日内区间切约60档，保留2位小数（指数点位级）
  const tick=Math.max(0.01,Math.round((high-low)/60*100)/100);
  const n=Math.floor((high-low)/tick)+1;
  const priceOf=function(i){return +(low+i*tick).toFixed(3);};
  const idxOf=function(p){return Math.max(0,Math.min(n,Math.floor((p-low)/tick+0.5)));};
  // 每个时段字母触及的价格档集合
  const letterIdxs={};
  bars.forEach(function(b){
    const L=idxTpoLetter(b.time);if(!L)return;
    if(!letterIdxs[L])letterIdxs[L]=new Set();
    const i0=idxOf(b.low),i1=idxOf(b.high);
    for(let i=i0;i<=i1;i++)letterIdxs[L].add(i);
  });
  const letters=Object.keys(letterIdxs).sort();
  if(!letters.length)return null;
  // 汇总每个价格档的TPO数（有多少时段触及）
  const tpoByIdx={};
  letters.forEach(function(L){letterIdxs[L].forEach(function(i){tpoByIdx[i]=(tpoByIdx[i]||0)+1;});});
  const rows=[];
  for(let i=0;i<=n;i++){
    const c=tpoByIdx[i]||0;
    if(c>0)rows.push({price:priceOf(i),tpo:c});
  }
  if(!rows.length)return null;
  const totalTPO=rows.reduce(function(a,b){return a+b.tpo;},0);
  let poc=rows[0];rows.forEach(function(r){if(r.tpo>poc.tpo)poc=r;});
  // 价值区：按TPO数从高到低累加至70%
  const sorted=rows.slice().sort(function(a,b){return b.tpo-a.tpo;});
  let acc=0;const vaIdx=new Set();
  for(const r of sorted){acc+=r.tpo;vaIdx.add(r.price);if(acc>=totalTPO*0.7)break;}
  const vaRows=rows.filter(function(r){return vaIdx.has(r.price);});
  const vah=vaRows[vaRows.length-1].price;
  const val=vaRows[0].price;
  rows.forEach(function(r){r.va=vaIdx.has(r.price);r.isPoc=(r.price===poc.price);});
  // === 四度空间市场结构分析 ===
  // 1. 每个时段字母的高低点（判断强势单边市的核心：字母高低点逐级上移/下移）
  var letterHL={};
  bars.forEach(function(b){
    var L=idxTpoLetter(b.time);if(!L)return;
    if(!letterHL[L])letterHL[L]={high:-Infinity,low:Infinity,n:0};
    if(b.high>letterHL[L].high)letterHL[L].high=b.high;
    if(b.low<letterHL[L].low)letterHL[L].low=b.low;
    letterHL[L].n++;
  });
  var sortedLetters=Object.keys(letterHL).sort();
  // 检查单调性：每个字母高点>上一字母高点 且 低点>上一字母低点=向上单边；反之=向下
  var monoUp=true,monoDown=true;
  for(var i=1;i<sortedLetters.length;i++){
    var pl=letterHL[sortedLetters[i-1]],cl=letterHL[sortedLetters[i]];
    if(cl.high<=pl.high||cl.low<=pl.low)monoUp=false;
    if(cl.high>=pl.high||cl.low>=pl.low)monoDown=false;
  }
  // 2. 平均每行TPO数（<5=窄钟形=强势特征）
  var avgTpoPerRow=totalTPO/rows.length;
  // 3. 中枢线(POC)上下TPO分布：上方TPO多=倾向卖出，下方TPO多=倾向买入
  var tpoAbovePoc=0,tpoBelowPoc=0;
  rows.forEach(function(r){if(r.price>poc.price)tpoAbovePoc+=r.tpo;else if(r.price<poc.price)tpoBelowPoc+=r.tpo;});
  // 4. 钟形曲线检测：按价格排序找高TPO簇和低TPO谷
  var priceSorted=rows.slice().sort(function(a,b){return a.price-b.price;});
  var avgTpo=totalTPO/rows.length;
  var bellCurves=[],inCurve=false,curveStart=0,valleyMin=Infinity;
  for(var i=0;i<priceSorted.length;i++){
    var r=priceSorted[i];
    if(r.tpo>=avgTpo*0.7){
      if(!inCurve){inCurve=true;curveStart=i;valleyMin=Infinity;}
    }else{
      if(inCurve){
        if(valleyMin===Infinity||r.tpo<valleyMin)valleyMin=r.tpo;
        if(i===priceSorted.length-1||priceSorted[i+1].tpo>=avgTpo*0.7){
          bellCurves.push({start:curveStart,end:i-1,valleyTpo:valleyMin});
          inCurve=false;
        }
      }
    }
  }
  if(inCurve)bellCurves.push({start:curveStart,end:priceSorted.length-1,valleyTpo:valleyMin});
  var bellCount=bellCurves.length;
  var valleyTpos=bellCurves.length>1?bellCurves.slice(1).map(function(b){return b.valleyTpo;}):[];
  var maxValleyTpo=valleyTpos.length?Math.max.apply(null,valleyTpos):0;
  // 5. 市场类型判定
  var rangePct=poc.price>0?(high-low)/poc.price*100:0;
  // === IBR: 期初平衡区间（前2个时段的高低点）===
  var ibrHigh=-Infinity,ibrLow=Infinity;
  var firstLetters=sortedLetters.slice(0,2);
  firstLetters.forEach(function(L){var hl=letterHL[L];if(hl){if(hl.high>ibrHigh)ibrHigh=hl.high;if(hl.low<ibrLow)ibrLow=hl.low;}});
  if(!isFinite(ibrHigh))ibrHigh=high;
  if(!isFinite(ibrLow))ibrLow=low;
  // === 量能概况：每个价格档的成交量分布 ===
  var volProfile={},volMax=0;
  bars.forEach(function(b){
    var L=idxTpoLetter(b.time);if(!L)return;
    var i0=idxOf(b.low),i1=idxOf(b.high);
    var perBar=b.volume||(b.amount?b.amount/priceOf(Math.floor((i0+i1)/2)):0)||0;
    perBar=Math.max(1,perBar);
    for(var i=i0;i<=i1;i++){var p=priceOf(i);volProfile[p]=(volProfile[p]||0)+perBar;if(volProfile[p]>volMax)volMax=volProfile[p];}
  });
  // === 每个时段的详细数据（列值：高低/成交量）===
  var letterData={};
  bars.forEach(function(b){
    var L=idxTpoLetter(b.time);if(!L)return;
    if(!letterData[L])letterData[L]={high:-Infinity,low:Infinity,volume:0,time:b.time,n:0};
    if(b.high>letterData[L].high)letterData[L].high=b.high;
    if(b.low<letterData[L].low)letterData[L].low=b.low;
    letterData[L].volume+=(b.volume||0);
    letterData[L].n++;
  });
  // === 方块之上/之下TPO数 ===
  var tpoAbove=0,tpoBelow=0;
  rows.forEach(function(r){if(r.price>poc.price)tpoAbove+=r.tpo;else if(r.price<poc.price)tpoBelow+=r.tpo;});
  var marketType=detectMarketType({
    monoUp:monoUp,monoDown:monoDown,avgTpoPerRow:avgTpoPerRow,bellCount:bellCount,
    maxValleyTpo:maxValleyTpo,rangePct:rangePct,tpoAbovePoc:tpoAbovePoc,tpoBelowPoc:tpoBelowPoc,
    hasLetters:sortedLetters.length>0,letterCount:sortedLetters.length
  });
  return {rows:rows,letters:letters,poc:poc,vah:vah,val:val,totalTPO:totalTPO,high:high,low:low,tick:tick,
    letterHL:letterHL,sortedLetters:sortedLetters,monoUp:monoUp,monoDown:monoDown,
    avgTpoPerRow:avgTpoPerRow,tpoAbovePoc:tpoAbovePoc,tpoBelowPoc:tpoBelowPoc,
    bellCount:bellCount,maxValleyTpo:maxValleyTpo,rangePct:rangePct,marketType:marketType,
    ibrHigh:ibrHigh,ibrLow:ibrLow,letterData:letterData,volProfile:volProfile,volMax:volMax,
    tpoAbove:tpoAbove,tpoBelow:tpoBelow,bars:bars};
}
// 四度空间市场类型判定（5种市况+中枢线方向）
function detectMarketType(m){
  var type,name,desc,strategy,cls,pocBias;
  // 中枢线分析：POC上下TPO对比
  var pocTotal=(m.tpoAbovePoc||0)+(m.tpoBelowPoc||0);
  var pocRatio=pocTotal>0?(m.tpoAbovePoc||0)/pocTotal:0.5;
  if(pocRatio>0.58){pocBias='中枢线上方TPO偏多，市场倾向卖出';}
  else if(pocRatio<0.42){pocBias='中枢线下方TPO偏多，市场倾向买入';}
  else{pocBias='中枢线上下TPO均衡，多空平衡';}
  // 1. 强势单边市：每行TPO<5 且 字母高低点单调递增/递减
  if(m.hasLetters&&m.avgTpoPerRow<5&&(m.monoUp||m.monoDown)){
    type='trend_strong';name='强势单边市';cls=m.monoUp?'pu':'pd';
    desc=m.monoUp?'钟形曲线长而窄（每行TPO<5），字母高低点逐级上移，长线买家控制市势。':'钟形曲线长而窄（每行TPO<5），字母高低点逐级下移，长线卖家控制市势。';
    strategy=m.monoUp?'顺势做多，切勿逆势操作；回踩VAH不破为多头承接位，止损设在POC下方。':'顺势做空或减仓，切勿逆势抄底；反弹至VAL承压为减仓位。';
  }
  // 2. 普通单边市：2-3个钟形曲线，连接处TPO仅1-3个
  else if(m.bellCount>=2&&m.maxValleyTpo<=3){
    type='trend_normal';name='普通单边市';cls='';
    desc='存在'+m.bellCount+'个钟形曲线，连接处TPO仅'+m.maxValleyTpo+'个，趋势分段推进。';
    strategy='在钟形曲线内部做短线买卖，在曲线连接处追涨杀跌（连接处突破后顺势跟进）。';
  }
  // 3. 牛皮市：区间幅度小 且 每行TPO少
  else if(m.rangePct<1.5&&m.avgTpoPerRow<4){
    type='choppy';name='牛皮市';cls='';
    desc='钟形曲线高低幅度小（'+m.rangePct.toFixed(2)+'%），同一字母TPO较少，市场缺乏方向。';
    strategy='速战速决，微利即平仓离场；或袖手旁观，捕捉突破时机再入市。'+pocBias+'。';
  }
  // 4. 上落市（默认）：单钟形曲线，顶底部TPO简单
  else{
    type='range';name='上落市';cls='';
    desc='单一钟形曲线，顶部和底部TPO数目较简单，多空表面均衡。';
    strategy='价位高于价值区(VAH)时卖出，低于价值区(VAL)时买入。'+pocBias+'。';
  }
  return {type:type,name:name,desc:desc,strategy:strategy,cls:cls,pocBias:pocBias,pocRatio:pocRatio};
}
// 中长周期 TPO：用日K构建 composite profile（每根日K=一个时段，跨多日累计价格分布）
function computeDailyTPO(kline,days){
  if(!kline||kline.length<10)return null;
  const win=kline.slice(-days);
  let high=-Infinity,low=Infinity;
  win.forEach(function(k){if(k.high>high)high=k.high;if(k.low<low)low=k.low;});
  if(!isFinite(high)||!isFinite(low)||high<=low)return null;
  const tick=Math.max(0.01,Math.round((high-low)/80*100)/100);
  const n=Math.floor((high-low)/tick)+1;
  const idxOf=function(p){return Math.max(0,Math.min(n,Math.floor((p-low)/tick+0.5)));};
  const tpoByIdx={};
  win.forEach(function(k){
    const i0=idxOf(k.low),i1=idxOf(k.high);
    for(let i=i0;i<=i1;i++)tpoByIdx[i]=(tpoByIdx[i]||0)+1;
  });
  const rows=[];
  for(let i=0;i<=n;i++){const c=tpoByIdx[i]||0;if(c>0)rows.push({price:+(low+i*tick).toFixed(3),tpo:c});}
  if(!rows.length)return null;
  const totalTPO=rows.reduce(function(a,b){return a+b.tpo;},0);
  let poc=rows[0];rows.forEach(function(r){if(r.tpo>poc.tpo)poc=r;});
  const sorted=rows.slice().sort(function(a,b){return b.tpo-a.tpo;});
  let acc=0;const vaIdx=new Set();
  for(const r of sorted){acc+=r.tpo;vaIdx.add(r.price);if(acc>=totalTPO*0.7)break;}
  const vaRows=rows.filter(function(r){return vaIdx.has(r.price);});
  const vah=vaRows[vaRows.length-1].price;
  const val=vaRows[0].price;
  rows.forEach(function(r){r.va=vaIdx.has(r.price);r.isPoc=(r.price===poc.price);});
  // === 日K级市场结构分析 ===
  var avgTpoPerRow=totalTPO/rows.length;
  var tpoAbovePoc=0,tpoBelowPoc=0;
  rows.forEach(function(r){if(r.price>poc.price)tpoAbovePoc+=r.tpo;else if(r.price<poc.price)tpoBelowPoc+=r.tpo;});
  // 钟形曲线检测
  var priceSorted=rows.slice().sort(function(a,b){return a.price-b.price;});
  var avgTpo=totalTPO/rows.length;
  var bellCurves=[],inCurve=false,curveStart=0,valleyMin=Infinity;
  for(var i=0;i<priceSorted.length;i++){
    var r=priceSorted[i];
    if(r.tpo>=avgTpo*0.7){
      if(!inCurve){inCurve=true;curveStart=i;valleyMin=Infinity;}
    }else{
      if(inCurve){
        if(valleyMin===Infinity||r.tpo<valleyMin)valleyMin=r.tpo;
        if(i===priceSorted.length-1||priceSorted[i+1].tpo>=avgTpo*0.7){bellCurves.push({start:curveStart,end:i-1,valleyTpo:valleyMin});inCurve=false;}
      }
    }
  }
  if(inCurve)bellCurves.push({start:curveStart,end:priceSorted.length-1,valleyTpo:valleyMin});
  var bellCount=bellCurves.length;
  var valleyTpos=bellCurves.length>1?bellCurves.slice(1).map(function(b){return b.valleyTpo;}):[];
  var maxValleyTpo=valleyTpos.length?Math.max.apply(null,valleyTpos):0;
  var rangePct=poc.price>0?(high-low)/poc.price*100:0;
  var marketType=detectMarketType({monoUp:false,monoDown:false,avgTpoPerRow:avgTpoPerRow,bellCount:bellCount,
    maxValleyTpo:maxValleyTpo,rangePct:rangePct,tpoAbovePoc:tpoAbovePoc,tpoBelowPoc:tpoBelowPoc,hasLetters:false,letterCount:0});
  return {poc:poc,vah:vah,val:val,rows:rows,totalTPO:totalTPO,high:high,low:low,sessions:win.length,
    avgTpoPerRow:avgTpoPerRow,tpoAbovePoc:tpoAbovePoc,tpoBelowPoc:tpoBelowPoc,
    bellCount:bellCount,maxValleyTpo:maxValleyTpo,rangePct:rangePct,marketType:marketType};
}
// 中长周期综合建议：结合月度/季度价值区判断中期趋势定位
function analyzePeriodTPO(curPrice,month,quarter,ret20){
  let bias,pos,advice,cls;
  const qVah=quarter.vah,qVal=quarter.val,qPoc=quarter.poc.price;
  const mVah=month.vah,mVal=month.val;
  if(curPrice>qVah&&ret20>0){
    bias='中期多头';pos='季度价值区上方';cls='pu';
    advice='现价站上季度价值区（'+qVal.toFixed(0)+'~'+qVah.toFixed(0)+'）上沿且20日为正，中期趋势向上。回踩季度VAH '+qVah.toFixed(0)+' 一带不破可视为中长线加仓/持有区；若放量跌破季度POC '+qPoc.toFixed(0)+' 则趋势转弱需警惕。';
  }else if(curPrice<qVal&&ret20<0){
    bias='中期空头';pos='季度价值区下方';cls='pd';
    advice='现价跌破季度价值区下沿且20日为负，中期趋势向下。反弹至季度VAL '+qVal.toFixed(0)+' 附近承压可视为减仓/回避区；不轻易抄底，等待重新站回季度价值区再评估中期反转。';
  }else{
    bias='中期平衡';pos='季度价值区内';cls='';
    advice='现价处于季度价值区内部，中期多空拉锯、方向待选择。月度价值区 ['+mVal.toFixed(0)+'~'+mVah.toFixed(0)+'] 为近期博弈核心，站上季度VAH偏多、跌破季度VAL偏空，突破前维持区间操作思路。';
  }
  const vsPoc=(curPrice-qPoc)/qPoc*100;
  return {bias:bias,pos:pos,advice:advice,cls:cls,vsPoc:vsPoc,qPoc:qPoc,qVal:qVal,qVah:qVah,mVal:mVal,mVah:mVah};
}
function analyzeTPOAdvice(tpo,price,pct,name){
  const vah=tpo.vah,val=tpo.val,poc=tpo.poc.price;
  let pos,bias,advice,cls;
  var mt=tpo.marketType;
  // 按市场类型给出针对性策略
  if(mt&&mt.type==='trend_strong'){
    pos=mt.cls==='pu'?'强势单边向上':'强势单边向下';
    bias=mt.cls==='pu'?'长线买家主导 · 顺势做多':'长线卖家主导 · 顺势做空';
    cls=mt.cls;
    advice=mt.strategy;
  }else if(mt&&mt.type==='trend_normal'){
    pos='分段趋势';bias='普通单边 · 钟形间过渡';cls='';
    advice=mt.strategy;
  }else if(mt&&mt.type==='choppy'){
    pos='窄幅牛皮';bias='缺乏方向 · 速战速决';cls='';
    advice=mt.strategy;
  }else{
    // 上落市：按价格vs价值区位置给出建议
    if(price>vah){
      pos='价值区上方';bias='多头主导';cls='pu';
      advice='价格运行于日内价值区（VA）上沿之上，买方控盘、短期强势。回踩 '+vah.toFixed(2)+'（VAH）不破可视为多头承接位，顺势持有或逢回踩低吸；若放量滞涨则需警惕冲高回落。';
    }else if(price<val){
      pos='价值区下方';bias='空头主导';cls='pd';
      advice='价格跌破日内价值区下沿，卖方主导、短期弱势。反弹至 '+val.toFixed(2)+'（VAL）附近承压可视为减仓位；不轻易抄底，待重新站回价值区再评估反转。';
    }else{
      pos='价值区内';bias='多空均衡';cls='';
      advice='价格处于日内价值区内部，多空胶着、区间震荡。上沿 '+vah.toFixed(2)+'、下沿 '+val.toFixed(2)+' 为日内主要博弈边界，靠近边界高抛低吸，突破边界后再顺势跟随。';
    }
    if(mt&&mt.pocBias)advice+=' 中枢线分析：'+mt.pocBias+'。';
  }
  const vsPoc=(price-poc)/poc*100;
  const pocNote='现价位于 POC（'+poc.toFixed(2)+'，日内最密集成交区）'+(vsPoc>=0?'上方 +':'下方 ')+vsPoc.toFixed(2)+'%';
  return {pos:pos,bias:bias,advice:advice,cls:cls,poc:poc,vah:vah,val:val,vsPoc:vsPoc,pocNote:pocNote,marketType:mt};
}
function renderTPOChart(tpo,price,code,domId){
  const dom=document.getElementById(domId||'idxTpoChart');if(!dom||!tpo)return;
  const old=echarts.getInstanceByDom(dom);if(old)old.dispose();
  const ch=echarts.init(dom);
  const rows=tpo.rows;
  const cats=rows.map(function(r){return r.price.toFixed(2);});
  const tpoData=rows.map(function(r){
    const c=r.isPoc?tc.gold:(r.va?tc.blue:tc.bg4);
    return{value:r.tpo,itemStyle:{color:c,borderColor:r.isPoc?tc.gold:(r.va?tc.blue:tc.bg3),borderWidth:r.isPoc?1:0.5}};
  });
  var volData=[];
  if(tpo.volProfile&&tpo.volMax>0){
    volData=rows.map(function(r){var v=tpo.volProfile[r.price]||0;return{value:Math.round(v/tpo.volMax*100),itemStyle:{color:hexA(tc.purple,0.35)}};});
  }
  const ibrLines=[];
  if(tpo.ibrHigh&&tpo.ibrLow){
    ibrLines.push({yAxis:tpo.ibrHigh.toFixed(2),lineStyle:{color:tc.orange,type:'dashed',width:1},label:{formatter:'IBR高 '+tpo.ibrHigh.toFixed(2),color:tc.orange,position:'insideStartTop',fontSize:9}});
    ibrLines.push({yAxis:tpo.ibrLow.toFixed(2),lineStyle:{color:tc.orange,type:'dashed',width:1},label:{formatter:'IBR低 '+tpo.ibrLow.toFixed(2),color:tc.orange,position:'insideEndBottom',fontSize:9}});
  }
  const labelInt=Math.max(1,Math.floor(rows.length/14));
  var series=[{
    type:'bar',data:tpoData,barWidth:'55%',
    name:'TPO计数',
    label:{show:true,position:'right',color:tc.muted,fontSize:8,formatter:function(p){return p.value>1?p.value:'';}},
    markLine:{symbol:'none',silent:true,label:{fontSize:9,fontWeight:700},data:[
      {yAxis:tpo.poc.price.toFixed(2),lineStyle:{color:tc.gold,type:'solid',width:1.4},label:{formatter:'POC '+tpo.poc.price.toFixed(2),color:tc.gold,position:'insideStartTop'}},
      {yAxis:tpo.vah.toFixed(2),lineStyle:{color:tc.up,type:'dashed',width:1},label:{formatter:'VAH '+tpo.vah.toFixed(2),color:tc.up,position:'insideEndTop'}},
      {yAxis:tpo.val.toFixed(2),lineStyle:{color:tc.down,type:'dashed',width:1},label:{formatter:'VAL '+tpo.val.toFixed(2),color:tc.down,position:'insideEndBottom'}},
      {yAxis:price.toFixed(2),lineStyle:{color:tc.ink,type:'solid',width:1.6},label:{formatter:'现价 '+price.toFixed(2),color:tc.ink,position:'insideEndBottom'}}
    ].concat(ibrLines)}
  }];
  if(volData.length){
    series.push({type:'bar',data:volData,barWidth:'30%',barGap:'-100%',name:'量能概况',itemStyle:{borderRadius:[0,2,2,0]},yAxisIndex:0});
  }
  ch.setOption({
    backgroundColor:'transparent',animation:false,
    legend:{show:true,data:['TPO计数','量能概况'],textStyle:{color:tc.muted,fontSize:9},top:2,right:10,icon:'roundRect',itemWidth:10,itemHeight:6},
    grid:{left:70,right:40,top:24,bottom:24},
    tooltip:{trigger:'axis',axisPointer:{type:'shadow'},backgroundColor:hexA(tc.bg2,.96),borderColor:tc.rule,textStyle:{color:tc.ink,fontSize:11},formatter:function(ps){
      if(!ps||!ps.length)return'';
      var r=rows[ps[0].dataIndex];if(!r)return'';
      var lines=[r.price.toFixed(2)];
      lines.push('TPO：'+r.tpo+(r.va?'（价值区内）':'（价值区外）')+(r.isPoc?' · POC':''));
      if(tpo.volProfile&&tpo.volProfile[r.price])lines.push('成交量：'+(tpo.volProfile[r.price]/tpo.volMax*100).toFixed(0)+'%');
      if(tpo.ibrHigh&&r.price>=tpo.ibrLow&&r.price<=tpo.ibrHigh)lines.push('（IBR期初平衡区间内）');
      return lines.join('<br>');
    }},
    xAxis:{type:'value',axisLabel:{color:tc.muted,fontSize:9},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}},name:'TPO数',nameTextStyle:{color:tc.muted,fontSize:9}},
    yAxis:{type:'category',data:cats,axisLabel:{color:tc.muted,fontSize:9,interval:labelInt},axisLine:{lineStyle:{color:tc.rule}},axisTick:{show:false}},
    series:series
  });
}
async function renderIdxTpo(){
  if(!modVisible('idxtpo'))return;
  // 顶部指数切换 tabs
  const tabsBox=document.getElementById('idxTpoTabs');
  if(!tabsBox)return;
  const tabs=IDX_SECIDS.map(function(secid){
    const code=secid.split('.')[1];const name=IDX_MAP[code];
    const rt=realtimeIndices[code]||{};
    const pc=(rt.pct||0)>=0?'var(--up)':'var(--down)';
    const act=code===_idxTpoCur;
    return '<button data-idx="'+code+'" style="cursor:pointer;border-radius:6px;border:1px solid '+(act?'var(--gold)':'var(--rule)')+';background:'+(act?'var(--bg4)':'var(--bg3)')+';color:var(--ink);padding:4px 10px;font-size:12px;margin:2px;">'+name+' <b style="color:'+pc+';font-size:11px;">'+(rt.pct!==undefined?((rt.pct>=0?'+':'')+rt.pct.toFixed(2)+'%'):'')+'</b></button>';
  }).join('');
  tabsBox.innerHTML=tabs;
  tabsBox.querySelectorAll('[data-idx]').forEach(function(btn){
    btn.onclick=function(){_idxTpoCur=btn.getAttribute('data-idx');renderIdxTpo();};
  });
  const code=_idxTpoCur;
  const name=IDX_MAP[code]||code;
  const secid=IDX_SECIDS.find(function(s){return s.split('.')[1]===code;})||('1.'+code);
  const rt=realtimeIndices[code]||{};
  const price=rt.price||0;
  const advBox=document.getElementById('idxTpoAdvice');
  const chartDom=document.getElementById('idxTpoChart');
  if(chartDom)chartDom.innerHTML='<div style="text-align:center;color:var(--muted);font-size:12px;padding:40px;">'+name+' TPO四度空间计算中...</div>';
  if(advBox)advBox.innerHTML='<div style="text-align:center;color:var(--muted);font-size:12px;padding:8px;">分析建议生成中...</div>';
  const bars=await fetchIndexIntraday(secid);
  const tpo=bars?computeTPO(bars):null;
  if(!tpo){
    if(chartDom)chartDom.innerHTML='<div style="text-align:center;color:var(--muted);font-size:12px;padding:40px;">TPO数据暂不可用（非交易时段或行情源限流）</div>';
    if(advBox)advBox.innerHTML='';
    return;
  }
  const curPrice=price>0?price:tpo.rows[tpo.rows.length-1].price;
  renderTPOChart(tpo,curPrice,code);
  if(advBox){
    const a=analyzeTPOAdvice(tpo,curPrice,rt.pct||0,name);
    const upLetters=tpo.letters.length;
    var mtBadge=a.marketType?'<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;margin-left:6px;background:'+(a.marketType.cls==='pu'?'var(--up)':(a.marketType.cls==='pd'?'var(--down)':'var(--bg4)'))+';color:'+(a.marketType.cls==='pu'||a.marketType.cls==='pd'?'#fff':'var(--ink)')+';">'+a.marketType.name+'</span>':'';
    var mtDesc=a.marketType?'<div style="color:var(--muted);font-size:11px;margin-top:4px;">'+a.marketType.desc+'</div>':'';
    var pocBiasTxt=a.marketType&&a.marketType.pocBias?'<div style="font-size:11px;color:var(--blue);margin-top:3px;">中枢线：'+a.marketType.pocBias+'</div>':'';
    var structStats='钟形曲线 '+tpo.bellCount+' 个 · 每行TPO均 '+tpo.avgTpoPerRow.toFixed(1)+' · 区间幅度 '+tpo.rangePct.toFixed(2)+'%';
    advBox.innerHTML='<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:stretch;font-size:12px;">'
      +'<div style="flex:1;min-width:220px;background:var(--bg3);border-radius:8px;padding:10px 12px;"><div style="color:var(--muted);font-size:10px;margin-bottom:4px;">日内结构定位'+mtBadge+'</div><b class="'+a.cls+'" style="font-size:15px;">'+a.bias+'</b><span style="color:var(--muted);margin-left:6px;">'+a.pos+'</span>'+mtDesc+'<div style="color:var(--muted);font-size:11px;margin-top:4px;">'+a.pocNote+' · 价值区 ['+a.val.toFixed(2)+' ~ '+a.vah.toFixed(2)+'] · 时段 '+upLetters+'/8</div><div style="color:var(--muted);font-size:10px;margin-top:2px;">'+structStats+'</div>'+pocBiasTxt+'</div>'
      +'<div style="flex:2;min-width:260px;background:var(--bg2);border-left:3px solid var(--gold);border-radius:8px;padding:10px 12px;"><div style="color:var(--gold);font-size:10px;margin-bottom:4px;">操作建议</div><div style="line-height:1.7;">'+a.advice+'</div></div>'
      +'</div>';
  }
  // 中长周期 TPO 分析（日线 Profile：月度20日 + 季度60日）
  const periodBox=document.getElementById('idxTpoPeriod');
  if(periodBox){
    periodBox.innerHTML='<div style="text-align:center;color:var(--muted);font-size:11px;padding:6px;">中长周期价值区计算中...</div>';
    fetchKlineSafe(secid).then(function(kl){
      if(!kl||kl.length<20){periodBox.innerHTML='';return;}
      const month=computeDailyTPO(kl,20);
      const quarter=computeDailyTPO(kl,Math.min(60,kl.length));
      if(!month||!quarter){periodBox.innerHTML='';return;}
      const ret20=kl.length>=21?((kl[kl.length-1].close-kl[kl.length-21].close)/kl[kl.length-21].close*100):0;
      const pa=analyzePeriodTPO(curPrice,month,quarter,ret20);
      function pCell(tp,label,days){
        const up=(tp.vah-curPrice)/curPrice*100,down=(tp.val-curPrice)/curPrice*100;
        return '<div style="flex:1;min-width:200px;background:var(--bg3);border-radius:8px;padding:9px 12px;">'
          +'<div style="color:var(--muted);font-size:10px;margin-bottom:5px;">'+label+'（近'+days+'个交易日 · '+tp.sessions+'根日K）</div>'
          +'<div style="display:flex;gap:14px;font-size:11px;line-height:1.8;flex-wrap:wrap;">'
          +'<span>POC <b style="color:var(--gold);">'+tp.poc.price.toFixed(2)+'</b></span>'
          +'<span>VAH <b style="color:var(--up);">'+tp.vah.toFixed(2)+'</b> <span style="color:var(--muted);">'+(up>=0?'+':'')+up.toFixed(1)+'%</span></span>'
          +'<span>VAL <b style="color:var(--down);">'+tp.val.toFixed(2)+'</b> <span style="color:var(--muted);">'+(down>=0?'+':'')+down.toFixed(1)+'%</span></span>'
          +'</div></div>';
      }
      periodBox.innerHTML='<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:stretch;font-size:12px;margin-top:4px;">'
        +pCell(month,'月度价值区',20)
        +pCell(quarter,'季度价值区',60)
        +'</div>'
        +'<div style="margin-top:8px;background:var(--bg2);border-left:3px solid var(--blue);border-radius:8px;padding:9px 12px;">'
        +'<div style="color:var(--blue);font-size:10px;margin-bottom:4px;">中长线定位 · 20日 '+(ret20>=0?'+':'')+ret20.toFixed(2)+'% · 距季度POC '+(pa.vsPoc>=0?'+':'')+pa.vsPoc.toFixed(2)+'%</div>'
        +'<b class="'+pa.cls+'" style="font-size:14px;">'+pa.bias+'</b><span style="color:var(--muted);margin-left:6px;">'+pa.pos+'</span>'
        +'<div style="line-height:1.7;margin-top:4px;">'+pa.advice+'</div></div>';
    }).catch(function(){periodBox.innerHTML='';});
  }
}

// ====================== 四度空间独立菜单（Market Profile 深度分析） ======================
function mpMethodHtml(){
  return '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:10px;">'
    +'<div style="background:var(--bg3);border-radius:8px;padding:10px 12px;"><b style="color:var(--gold);">什么是四度空间</b><div style="color:var(--muted);font-size:12px;margin-top:4px;line-height:1.7;">四度空间（Market Profile）由 Peter Steidlmayer 提出，把市场行为拆解为 <b>时间、价格、价值、成交量</b> 四个维度，用水平分布图替代传统K线，揭示"市场在哪些价位花了多少时间"。</div></div>'
    +'<div style="background:var(--bg3);border-radius:8px;padding:10px 12px;"><b style="color:var(--gold);">TPO 时间价格机会</b><div style="color:var(--muted);font-size:12px;margin-top:4px;line-height:1.7;">每个 30 分钟时段分配一个字母（A~H），价格被越多时段触及（TPO越多），说明该价位是市场认可的公允价格带。</div></div>'
    +'<div style="background:var(--bg3);border-radius:8px;padding:10px 12px;"><b style="color:var(--gold);">POC 控制点 / 中枢线</b><div style="color:var(--muted);font-size:12px;margin-top:4px;line-height:1.7;">TPO 最多的价位，多空力量最均衡、成交最密集的"磁吸点"，即<b>中枢线</b>。对比中枢线上下TPO数目可发现表面均衡市势中市场倾向买入还是卖出：上方TPO多=倾向卖出，下方TPO多=倾向买入。</div></div>'
    +'<div style="background:var(--bg3);border-radius:8px;padding:10px 12px;"><b style="color:var(--gold);">价值区 VA</b><div style="color:var(--muted);font-size:12px;margin-top:4px;line-height:1.7;">覆盖约 70% TPO 的价格区间，上沿 VAH、下沿 VAL。价值区是"公允价值带"，价格在区内=平衡盘整，突破区外=趋势重新定价。</div></div>'
    +'</div>'
    +'<div style="margin-top:10px;"><b style="color:var(--gold);font-size:13px;">五种市况形态识别</b><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:8px;margin-top:6px;">'
    +'<div style="background:var(--bg3);border-radius:8px;padding:8px 10px;border-left:3px solid var(--up);"><b style="color:var(--up);font-size:12px;">1. 强势单边市</b><div style="color:var(--muted);font-size:11px;margin-top:3px;line-height:1.7;">钟形曲线长而窄（每行TPO&lt;5），字母高低点逐级上/下移。长线买家/卖家控制市势。<b>顺势而为，切勿逆市操作。</b></div></div>'
    +'<div style="background:var(--bg3);border-radius:8px;padding:8px 10px;border-left:3px solid var(--blue);"><b style="color:var(--blue);font-size:12px;">2. 普通单边市</b><div style="color:var(--muted);font-size:11px;margin-top:3px;line-height:1.7;">2-3个钟形曲线，连接处仅1-3个TPO。<b>钟形内短线买卖，连接处追涨杀跌。</b></div></div>'
    +'<div style="background:var(--bg3);border-radius:8px;padding:8px 10px;border-left:3px solid var(--gold);"><b style="color:var(--gold);font-size:12px;">3. 上落市</b><div style="color:var(--muted);font-size:11px;margin-top:3px;line-height:1.7;">单一钟形曲线，顶底部TPO简单。<b>高于价值区卖出，低于价值区买入。</b></div></div>'
    +'<div style="background:var(--bg3);border-radius:8px;padding:8px 10px;border-left:3px solid var(--muted);"><b style="font-size:12px;">4. 牛皮市</b><div style="color:var(--muted);font-size:11px;margin-top:3px;line-height:1.7;">钟形曲线幅度小，同一字母TPO少。<b>速战速决微利平仓，或观望捕捉突破。</b></div></div>'
    +'<div style="background:var(--bg3);border-radius:8px;padding:8px 10px;border-left:3px solid var(--down);"><b style="color:var(--down);font-size:12px;">5. 中枢线分析</b><div style="color:var(--muted);font-size:11px;margin-top:3px;line-height:1.7;">在上落市和牛皮市中，对比中枢线(POC)上下TPO数目，发现市场倾向买入还是卖出。</div></div>'
    +'</div></div>'
    +'<div style="margin-top:10px;background:var(--bg2);border-left:3px solid var(--blue);border-radius:8px;padding:10px 12px;font-size:12px;color:var(--muted);line-height:1.9;"><b style="color:var(--blue);">交易可执行性核心：</b>价值区边界（VAH/VAL）是最重要的支撑阻力——价格向上突破 VAH 且回踩不破，VAH 由阻力转为支撑（做多）；向下突破 VAL 且反弹不破，VAL 由支撑转为阻力（做空/减仓）；价格在价值区内则按区间高抛低吸，止损设在价值区外侧。POC 是趋势的生命线，跌破/站上 POC 往往意味着日内方向反转。</div>';
}
// 交易可执行计划生成：根据现价 vs 价值区位置，给出方向/入场/止损/目标/仓位/触发条件
function buildMpPlan(tpo,curPrice){
  if(!tpo)return null;
  const poc=tpo.poc.price,vah=tpo.vah,val=tpo.val;
  const vaH=Math.max(vah-val,curPrice*0.002);
  var mt=tpo.marketType;
  let side,entry,stop,target,position,trigger,logic,cls,bias,pos;
  // 按市场类型调整交易计划
  if(mt&&mt.type==='trend_strong'){
    // 强势单边市：顺势操作，不逆势
    if(mt.cls==='pu'){
      side='多头';cls='pu';pos='强势单边向上';bias='长线买家主导 · 顺势做多';
      entry=vah;stop=poc;target=vah+vaH*1.2;position='40%~50%';
      trigger='回踩 VAH '+vah.toFixed(2)+' 企稳不破顺势做多，切勿逆势';
      logic='强势单边市，字母高低点逐级上移，长线买家控制市势。顺势做多，VAH由阻力转支撑。';
    }else{
      side='空头';cls='pd';pos='强势单边向下';bias='长线卖家主导 · 顺势做空';
      entry=val;stop=poc;target=val-vaH*1.2;position='30%~40%（减仓为主）';
      trigger='反弹至 VAL '+val.toFixed(2)+' 承压顺势减仓，切勿逆势抄底';
      logic='强势单边市，字母高低点逐级下移，长线卖家控制市势。顺势做空/减仓，VAL由支撑转阻力。';
    }
  }else if(mt&&mt.type==='trend_normal'){
    // 普通单边市：钟形内短线，连接处追涨杀跌
    if(curPrice>vah){
      side='多头';cls='pu';pos='钟形上方';bias='钟形间过渡 · 连接处突破';
      entry=vah;stop=poc;target=vah+vaH*0.6;position='25%~30%';
      trigger='在钟形内低吸，连接处突破后顺势追涨';
      logic='普通单边市，'+mt.desc+'。钟形曲线内部做短线，连接处突破顺势跟进。';
    }else if(curPrice<val){
      side='空头';cls='pd';pos='钟形下方';bias='钟形间过渡 · 连接处突破';
      entry=val;stop=poc;target=val-vaH*0.6;position='20%~25%';
      trigger='在钟形内高抛，连接处跌破后顺势杀跌';
      logic='普通单边市，'+mt.desc+'。钟形曲线内部做短线，连接处突破顺势跟进。';
    }else{
      side='区间';cls='';pos='钟形内部';bias='钟形内短线';
      entry=val;stop=val-vaH*0.15;target=vah;position='20%（钟形内高抛低吸）';
      trigger='VAL '+val.toFixed(2)+' 低吸，VAH '+vah.toFixed(2)+' 高抛，连接处突破顺势跟进';
      logic='普通单边市，'+mt.desc+'。在钟形内做短线买卖。';
    }
  }else if(mt&&mt.type==='choppy'){
    // 牛皮市：速战速决，微利平仓
    side='短线';cls='';pos='窄幅牛皮';bias='缺乏方向 · 速战速决';
    entry=curPrice;stop=curPrice-vaH*0.3;target=curPrice+vaH*0.3;position='15%（微利快进快出）';
    trigger='微利即平仓离场，或观望等待突破时机再入市';
    logic='牛皮市，'+mt.desc+(mt.pocBias?'。'+mt.pocBias:'')+'。';
  }else{
    // 上落市（默认）：高于价值区卖出，低于买入
    if(curPrice>vah){
      side='多头';cls='pu';pos='价值区上方';bias='失衡向上 · 买方主导';
      entry=vah;stop=poc;target=vah+vaH*0.8;position='30%~40%';
      trigger='回踩 VAH '+vah.toFixed(2)+' 企稳不破再介入，避免直接追高';
      logic='价值区上沿被突破代表市场重新定价向上，VAH 由阻力转为支撑；回踩确认后是较优的多头入场点。'+(mt&&mt.pocBias?' '+mt.pocBias+'。':'');
    }else if(curPrice<val){
      side='空头';cls='pd';pos='价值区下方';bias='失衡向下 · 卖方主导';
      entry=val;stop=poc;target=val-vaH*0.8;position='20%~30%（以反弹减仓为主）';
      trigger='反弹至 VAL '+val.toFixed(2)+' 承压不破再介入，不追空';
      logic='价值区下沿被跌破代表市场重新定价向下，VAL 由支撑转为阻力；反弹确认后是较优的空头/减仓点。'+(mt&&mt.pocBias?' '+mt.pocBias+'。':'');
    }else{
      side='区间';cls='';pos='价值区内';bias='平衡 · 多空拉锯';
      entry=val;stop=val-vaH*0.2;target=vah;position='20%（高抛低吸）';
      trigger='下沿 VAL '+val.toFixed(2)+' 附近低吸，上沿 VAH '+vah.toFixed(2)+' 附近高抛';
      logic='价格处于价值区内，市场处于平衡状态；在价值区边界反向操作，突破边界则止损并顺势。'+(mt&&mt.pocBias?' '+mt.pocBias+'。':'');
    }
  }
  const rr=side==='空头'?Math.abs(entry-target)/Math.abs(stop-entry):Math.abs(target-entry)/Math.abs(entry-stop);
  return {side:side,cls:cls,pos:pos,bias:bias,entry:entry,stop:stop,target:target,position:position,trigger:trigger,logic:logic,rr:isFinite(rr)?rr.toFixed(2):'--',poc:poc,vah:vah,val:val,marketType:mt};
}
function mpPeriodCard(label,sub,tpo,curPrice,withChart){
  if(!tpo)return '<div style="background:var(--bg2);border:1px solid var(--rule);border-radius:12px;padding:14px;margin-bottom:12px;"><b style="font-size:15px;">'+label+'</b><div style="color:var(--muted);font-size:12px;margin-top:6px;">数据不足（非交易时段或行情源限流）</div></div>';
  const p=buildMpPlan(tpo,curPrice);
  let h='<div style="background:var(--bg2);border:1px solid var(--rule);border-radius:12px;padding:14px;margin-bottom:12px;">';
  var mtBadge=p.marketType?'<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;background:'+(p.marketType.cls==='pu'?'var(--up)':(p.marketType.cls==='pd'?'var(--down)':'var(--bg4)'))+';color:'+(p.marketType.cls==='pu'||p.marketType.cls==='pd'?'#fff':'var(--ink)')+';">'+p.marketType.name+'</span>':'';
  h+='<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px;"><b style="font-size:15px;">'+label+'</b>'+mtBadge+'<span class="badge '+(p.cls==='pu'?'buy':(p.cls==='pd'?'sell':'watch'))+'">'+p.side+'</span><span style="font-size:11px;color:var(--muted);">'+sub+'</span></div>';
  h+='<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;">'
    +'<div class="dt-cell" style="flex:1;min-width:80px;"><div class="v" style="color:var(--gold);">'+p.poc.toFixed(2)+'</div><div class="l">POC/中枢线</div></div>'
    +'<div class="dt-cell" style="flex:1;min-width:80px;"><div class="v pu">'+p.vah.toFixed(2)+'</div><div class="l">VAH上沿</div></div>'
    +'<div class="dt-cell" style="flex:1;min-width:80px;"><div class="v pd">'+p.val.toFixed(2)+'</div><div class="l">VAL下沿</div></div>'
    +'<div class="dt-cell" style="flex:1;min-width:80px;"><div class="v">'+curPrice.toFixed(2)+'</div><div class="l">现价</div></div>'
    +'<div class="dt-cell" style="flex:1;min-width:90px;"><div class="v">'+tpo.rows.length+'</div><div class="l">价格档位</div></div>'
    +'</div>';
  // IBR + 方块上下 + 量能
  var ibrHtml='';
  if(tpo.ibrHigh&&tpo.ibrLow){
    var ibrRange=((tpo.ibrHigh-tpo.ibrLow)/tpo.ibrLow*100).toFixed(2);
    var ibrVsCur=curPrice>tpo.ibrHigh?'突破IBR上沿':(curPrice<tpo.ibrLow?'跌破IBR下沿':'IBR区间内');
    ibrHtml='<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;">'
      +'<div class="dt-cell" style="flex:1;min-width:80px;"><div class="v" style="color:var(--orange);">'+tpo.ibrHigh.toFixed(2)+'</div><div class="l">IBR上沿</div></div>'
      +'<div class="dt-cell" style="flex:1;min-width:80px;"><div class="v" style="color:var(--orange);">'+tpo.ibrLow.toFixed(2)+'</div><div class="l">IBR下沿</div></div>'
      +'<div class="dt-cell" style="flex:1;min-width:80px;"><div class="v">'+ibrRange+'%</div><div class="l">IBR幅度</div></div>'
      +'<div class="dt-cell" style="flex:1;min-width:100px;"><div class="v" style="color:'+(curPrice>tpo.ibrHigh?'var(--up)':(curPrice<tpo.ibrLow?'var(--down)':'var(--muted)'))+';">'+ibrVsCur+'</div><div class="l">现价vsIBR</div></div>'
      +'<div class="dt-cell" style="flex:1;min-width:80px;"><div class="v pu">'+(tpo.tpoAbove||0)+'</div><div class="l">POC上方TPO</div></div>'
      +'<div class="dt-cell" style="flex:1;min-width:80px;"><div class="v pd">'+(tpo.tpoBelow||0)+'</div><div class="l">POC下方TPO</div></div>'
      +'<div class="dt-cell" style="flex:1;min-width:80px;"><div class="v">'+(tpo.totalTPO||0)+'</div><div class="l">总TPO数</div></div>'
      +'</div>';
  }else{
    ibrHtml='<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;">'
      +'<div class="dt-cell" style="flex:1;min-width:80px;"><div class="v pu">'+(tpo.tpoAbove||0)+'</div><div class="l">POC上方TPO</div></div>'
      +'<div class="dt-cell" style="flex:1;min-width:80px;"><div class="v pd">'+(tpo.tpoBelow||0)+'</div><div class="l">POC下方TPO</div></div>'
      +'<div class="dt-cell" style="flex:1;min-width:80px;"><div class="v">'+(tpo.totalTPO||0)+'</div><div class="l">总TPO数</div></div>'
      +'</div>';
  }
  h+=ibrHtml;
  var mtDesc=p.marketType?'<div style="font-size:11px;color:var(--muted);margin-top:2px;">'+p.marketType.desc+'</div>':'';
  var pocBiasTxt=p.marketType&&p.marketType.pocBias?'<div style="font-size:11px;color:var(--blue);margin-top:2px;">中枢线：'+p.marketType.pocBias+'</div>':'';
  h+='<div style="font-size:12px;color:var(--muted);line-height:1.8;margin-bottom:10px;"><b class="'+p.cls+'">'+p.bias+'</b> · '+p.pos+' · '+p.logic+mtDesc+pocBiasTxt+'</div>';
  if(withChart){
    h+='<div id="mpIntraChart" style="width:100%;height:260px;margin-bottom:10px;"></div>';
  }
  h+='<div style="background:var(--bg3);border-radius:8px;padding:10px 12px;">'
    +'<div style="font-size:11px;color:var(--gold);margin-bottom:6px;font-weight:600;">可执行交易计划</div>'
    +'<div class="dt-grid">'
    +'<div class="dt-cell"><div class="v pu">'+p.entry.toFixed(2)+'</div><div class="l">入场价</div></div>'
    +'<div class="dt-cell"><div class="v pd">'+p.stop.toFixed(2)+'</div><div class="l">止损价</div></div>'
    +'<div class="dt-cell"><div class="v" style="color:var(--gold);">'+p.target.toFixed(2)+'</div><div class="l">目标价</div></div>'
    +'<div class="dt-cell"><div class="v">'+p.position+'</div><div class="l">仓位建议</div></div>'
    +'<div class="dt-cell"><div class="v">'+p.rr+':1</div><div class="l">盈亏比</div></div>'
    +'</div>'
    +'<div style="font-size:11px;color:var(--muted);margin-top:6px;">触发条件：'+p.trigger+'</div>'
    +'</div>';
  // TPO时段列值明细
  if(tpo.letterData&&Object.keys(tpo.letterData).length>0){
    var ld=tpo.letterData;
    var lkeys=Object.keys(ld).sort();
    h+='<div style="margin-top:10px;background:var(--bg3);border-radius:8px;padding:10px 12px;">';
    h+='<div style="font-size:11px;color:var(--blue);margin-bottom:6px;font-weight:600;">TPO时段列值（每时段高低点+成交量）</div>';
    h+='<div style="overflow-x:auto;"><table style="width:100%;font-size:10px;border-collapse:collapse;"><thead><tr style="color:var(--muted);"><th style="padding:3px 6px;text-align:left;border-bottom:1px solid var(--rule);">字母</th><th style="padding:3px 6px;text-align:right;border-bottom:1px solid var(--rule);">时间</th><th style="padding:3px 6px;text-align:right;border-bottom:1px solid var(--rule);">最高</th><th style="padding:3px 6px;text-align:right;border-bottom:1px solid var(--rule);">最低</th><th style="padding:3px 6px;text-align:right;border-bottom:1px solid var(--rule);">幅度</th><th style="padding:3px 6px;text-align:right;border-bottom:1px solid var(--rule);">TPO数</th></tr></thead><tbody>';
    lkeys.forEach(function(k){
      var d=ld[k];var range=d.high-d.low;
      var rangePct=d.low>0?(range/d.low*100).toFixed(2):'0';
      h+='<tr style="color:var(--ink);"><td style="padding:3px 6px;font-weight:700;color:var(--gold);">'+k+'</td><td style="padding:3px 6px;text-align:right;color:var(--muted);">'+(d.time||'')+'</td><td style="padding:3px 6px;text-align:right;color:var(--up);">'+d.high.toFixed(2)+'</td><td style="padding:3px 6px;text-align:right;color:var(--down);">'+d.low.toFixed(2)+'</td><td style="padding:3px 6px;text-align:right;">'+rangePct+'%</td><td style="padding:3px 6px;text-align:right;font-weight:600;">'+d.n+'</td></tr>';
    });
    h+='</tbody></table></div></div>';
  }
  h+='</div>';
  return h;
}
function mpCompositeCard(short,mid,long,curPrice,ret20){
  function d(p){return p?(p.side==='多头'?1:(p.side==='空头'?-1:0)):0;}
  const sum=d(short)+d(mid)+d(long);
  let verdict,verdictCls,note;
  if(sum>=2){verdict='多头共振 · 高置信';verdictCls='pu';note='短中长期均偏多，趋势一致性高，可顺势持有/加仓，止损统一守中长期价值区下沿，回撤即机会。';}
  else if(sum<=-2){verdict='空头共振 · 高置信';verdictCls='pd';note='短中长期均偏空，趋势一致性高，规避做多，反弹至价值区上沿减仓，等待重新站回价值区再评估。';}
  else if(sum===1){verdict='偏多 · 周期分歧';verdictCls='';note='多数周期偏多但存在分歧（如短期已超买或中期未确认），仓位减半，等分歧周期转向一致后再加仓。';}
  else if(sum===-1){verdict='偏空 · 周期分歧';verdictCls='';note='多数周期偏空但存在分歧，严控仓位，反弹减仓为主，等待方向统一。';}
  else{verdict='多空平衡 · 过渡期';verdictCls='';note='短中长期方向不一致，市场处于过渡/换挡期，以区间思路应对，突破中长期价值区边界再定方向。';}
  const keyVah=long?long.vah:(mid?mid.vah:0);
  const keyVal=long?long.val:(mid?mid.val:0);
  return '<div style="background:var(--bg2);border:1px solid var(--gold);border-radius:12px;padding:14px;margin-bottom:12px;">'
    +'<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;"><b style="font-size:15px;">综合交易决策（三级共振）</b><span class="badge '+(verdictCls==='pu'?'buy':(verdictCls==='pd'?'sell':'watch'))+'">'+verdict+'</span></div>'
    +'<div style="font-size:13px;color:var(--muted);line-height:1.8;margin-top:8px;">'+note+'</div>'
    +'<div style="margin-top:8px;font-size:12px;color:var(--muted);line-height:1.7;">关键价位：中长期价值区 <b style="color:var(--down);">'+keyVal.toFixed(2)+'</b> ~ <b style="color:var(--up);">'+keyVah.toFixed(2)+'</b> · 20日 '+(ret20>=0?'+':'')+ret20.toFixed(2)+'% · 现价 '+curPrice.toFixed(2)+'</div>'
    +'</div>';
}
// ===== 四度空间对象切换（指数/板块/个股）+ 可执行建议扫描 =====
let _mpObjType='idx';
let _mpBoardCode='';
let _mpStockCode='';
function mpBoards(){
  if(window._fmBoards&&window._fmBoards.length)return window._fmBoards.map(function(b){return {code:b.code,name:b.name,pct:b.pct||0};});
  return (window._fullMarketSectors||[]).map(function(s){return {code:s.code,name:s.name,pct:s.pct_change||0};});
}
function mpSecidName(){
  if(_mpObjType==='idx'){
    const code=_idxTpoCur;
    return {secid:IDX_SECIDS.find(function(s){return s.split('.')[1]===code;})||('1.'+code),name:IDX_MAP[code]||code,rtPrice:(realtimeIndices[code]||{}).price||0};
  }
  if(_mpObjType==='board'){
    const b=mpBoards().find(function(x){return x.code===_mpBoardCode;});
    return {secid:'90.'+_mpBoardCode,name:(b&&b.name)||_mpBoardCode,rtPrice:0};
  }
  const s=scanData.find(function(x){return x.code===_mpStockCode;});
  return {secid:((/^(6|9)/.test(_mpStockCode)?'1.':'0.')+_mpStockCode),name:(s&&s.name)||_mpStockCode,rtPrice:0};
}
function mpObjButton(tag,attr,act,name,sub){
  return '<button data-'+tag+'="'+attr+'" style="cursor:pointer;border-radius:6px;border:1px solid '+(act?'var(--gold)':'var(--rule)')+';background:'+(act?'var(--bg4)':'var(--bg3)')+';color:var(--ink);padding:4px 10px;font-size:12px;margin:2px;">'+name+(sub?' '+sub:'')+'</button>';
}
async function renderTpoLayer(){
  const mbox=document.getElementById('mpMethodBox');
  if(mbox&&!mbox.dataset.loaded){mbox.dataset.loaded='1';mbox.innerHTML=mpMethodHtml();}
  const otBox=document.getElementById('mpObjTypeTabs');
  if(otBox){
    const types=[{k:'idx',n:'指数'},{k:'board',n:'板块'},{k:'stock',n:'个股'}];
    otBox.innerHTML=types.map(function(t){return mpObjButton('ot',t.k,_mpObjType===t.k,t.n,'');}).join('');
    otBox.querySelectorAll('[data-ot]').forEach(function(b){b.onclick=function(){_mpObjType=b.getAttribute('data-ot');
      if(_mpObjType==='board'&&!_mpBoardCode){const f=mpBoards()[0];_mpBoardCode=f?f.code:'';}
      if(_mpObjType==='stock'&&!_mpStockCode){const f=scanData.slice().sort(function(a,b){return (b.bt_quant_score||0)-(a.bt_quant_score||0);})[0];_mpStockCode=f?f.code:'';}
      renderTpoLayer();};});
  }
  const tabsBox=document.getElementById('mpTabs');
  if(!tabsBox)return;
  let tabsHtml='';
  if(_mpObjType==='idx'){
    tabsHtml=IDX_SECIDS.map(function(secid){const code=secid.split('.')[1];const name=IDX_MAP[code];const rt=realtimeIndices[code]||{};const pc=(rt.pct||0)>=0?'var(--up)':'var(--down)';return mpObjButton('idx',code,code===_idxTpoCur,name,'<b style="color:'+pc+';font-size:11px;">'+(rt.pct!==undefined?((rt.pct>=0?'+':'')+rt.pct.toFixed(2)+'%'):'')+'</b>');}).join('');
  }else if(_mpObjType==='board'){
    const boards=mpBoards().slice(0,15);
    if(!boards.length)tabsHtml='<span style="color:var(--muted);font-size:12px;">板块数据加载中，请稍后刷新</span>';
    else tabsHtml=boards.map(function(b){const pc=(b.pct||0)>=0?'var(--up)':'var(--down)';return mpObjButton('b',b.code,b.code===_mpBoardCode,b.name,'<b style="color:'+pc+';font-size:11px;">'+((b.pct||0)>=0?'+':'')+(b.pct||0).toFixed(2)+'%</b>');}).join('');
  }else{
    const stocks=scanData.slice().sort(function(a,b){return (b.bt_quant_score||0)-(a.bt_quant_score||0);}).slice(0,15);
    if(!stocks.length)tabsHtml='<span style="color:var(--muted);font-size:12px;">个股数据加载中</span>';
    else tabsHtml=stocks.map(function(s){const pc=(s.pct_change||0)>=0?'var(--up)':'var(--down)';return mpObjButton('s',s.code,s.code===_mpStockCode,s.name,'<b style="color:'+pc+';font-size:11px;">'+((s.pct_change||0)>=0?'+':'')+(s.pct_change||0).toFixed(2)+'%</b>');}).join('');
    tabsHtml+='<div style="position:relative;margin-top:4px;"><input id="mpSearchInput" type="text" placeholder="🔍 搜索个股（代码/拼音/中文）" oninput="mpOnSearchInput()" onkeydown="if(event.key===\'Enter\')mpDoSearch()" style="width:100%;padding:6px 12px;font-size:12px;border-radius:8px;border:1px solid var(--rule);background:var(--bg3);color:var(--ink);outline:none;box-sizing:border-box;"><div id="mpSearchSuggest" style="display:none;position:absolute;top:100%;left:0;right:0;z-index:200;max-height:280px;overflow-y:auto;background:var(--bg2);border:1px solid var(--rule);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.3);"></div></div>';
  }
  tabsBox.innerHTML=tabsHtml;
  tabsBox.querySelectorAll('[data-idx]').forEach(function(b){b.onclick=function(){_idxTpoCur=b.getAttribute('data-idx');renderTpoLayer();};});
  tabsBox.querySelectorAll('[data-b]').forEach(function(b){b.onclick=function(){_mpBoardCode=b.getAttribute('data-b');renderTpoLayer();};});
  tabsBox.querySelectorAll('[data-s]').forEach(function(b){b.onclick=function(){_mpStockCode=b.getAttribute('data-s');renderTpoLayer();};});
  const sn=mpSecidName();
  await mpRenderAnalysis(sn.secid,sn.name,sn.rtPrice);
  renderMpSuggest();
}
async function mpRenderAnalysis(secid,name,rtPrice){
  const box=document.getElementById('mpAnalysisBox');
  if(!box)return;
  box.innerHTML='<div style="text-align:center;color:var(--muted);font-size:12px;padding:30px;">'+name+' 短中长期四度空间计算中...</div>';
  const bars=await fetchIndexIntraday(secid).catch(function(){return null;});
  const kl=await fetchKlineSafe(secid).catch(function(){return null;});
  var rt=rtPrice||0;
  if(!rt||rt<=0){
    try{var rq=await jsonpRequest('https://push2delay.eastmoney.com/api/qt/stock/get?secid='+secid+'&fltt=2&fields=f43,f58&ut=fa5fd1943c7b386f172d6893dbfba10b',5000);
    if(rq&&rq.data&&rq.data.f43)rt=rq.data.f43;}catch(e){}
  }
  const intra=bars?computeTPO(bars):null;
  const month=kl?computeDailyTPO(kl,20):null;
  const quarter=kl?computeDailyTPO(kl,Math.min(60,kl.length)):null;
  const curPrice=rt||(intra?intra.rows[intra.rows.length-1].price:(kl?kl[kl.length-1].close:0));
  const ret20=kl&&kl.length>=21?((kl[kl.length-1].close-kl[kl.length-21].close)/kl[kl.length-21].close*100):0;
  const shortP=intra?buildMpPlan(intra,curPrice):null;
  const midP=month?buildMpPlan(month,curPrice):null;
  const longP=quarter?buildMpPlan(quarter,curPrice):null;
  let h='<div style="font-size:11px;color:var(--muted);margin-bottom:6px;">分析对象：<b style="color:var(--gold);">'+name+'</b></div>';
  h+=mpPeriodCard('短期 · 日内（交易周期：当日）','8个30分钟时段 · T+0日内回转',intra,curPrice,true);
  h+=mpPeriodCard('中期 · 月度（交易周期：数日~数周）','近20个交易日日线Profile',month,curPrice,false);
  h+=mpPeriodCard('长期 · 季度（交易周期：数周~数月）','近60个交易日日线Profile',quarter,curPrice,false);
  h+=mpCompositeCard(shortP,midP,longP,curPrice,ret20);
  box.innerHTML=h;
  if(intra){renderTPOChart(intra,curPrice,secid,'mpIntraChart');}
}
function mpPickBoard(code){_mpObjType='board';_mpBoardCode=code;renderTpoLayer();}
function mpPickStock(code){_mpObjType='stock';_mpStockCode=code;renderTpoLayer();}
// 四度空间搜索历史
var _mpSearchHistory=[];
try{_mpSearchHistory=JSON.parse(localStorage.getItem('mp-search-history')||'[]');}catch(e){}
function _mpSaveHistory(code,name){
  _mpSearchHistory=_mpSearchHistory.filter(function(h){return h.code!==code;});
  _mpSearchHistory.unshift({code:code,name:name,time:Date.now()});
  if(_mpSearchHistory.length>8)_mpSearchHistory=_mpSearchHistory.slice(0,8);
  try{localStorage.setItem('mp-search-history',JSON.stringify(_mpSearchHistory));}catch(e){}
}
function _mpRenderHistory(){
  if(!_mpSearchHistory.length)return '';
  var h='<div style="padding:6px 12px;font-size:10px;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="_mpClearHistory()">清除</span></div>';
  h+=_mpSearchHistory.map(function(item){
    return '<div style="padding:7px 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="_mpPickSearch(\''+item.code+'\',\''+item.name.replace(/'/g,'')+'\')"><div><span style="font-weight:600;color:var(--ink);font-size:12px;">'+item.name+'</span> <span style="color:var(--muted);font-size:11px;">'+item.code+'</span></div></div>';
  }).join('');
  return h;
}
function _mpClearHistory(){_mpSearchHistory=[];try{localStorage.removeItem('mp-search-history');}catch(e){}mpShowSuggestions();}
function _mpPickSearch(code,name){_mpObjType='stock';_mpStockCode=code;_mpSaveHistory(code,name);renderTpoLayer();}
var _mpSearchTmr=null;
function mpOnSearchInput(){
  var inp=document.getElementById('mpSearchInput');
  if(!inp)return;
  var q=inp.value.trim();
  if(_mpSearchTmr)clearTimeout(_mpSearchTmr);
  if(!q){mpShowSuggestions();return;}
  _mpSearchTmr=setTimeout(function(){mpFetchSuggestions(q);},300);
}
function mpShowSuggestions(){
  var box=document.getElementById('mpSearchSuggest');
  if(!box)return;
  var hist=_mpRenderHistory();
  if(hist){box.innerHTML=hist;box.style.display='block';}else{box.style.display='none';}
}
function mpFetchSuggestions(q){
  var box=document.getElementById('mpSearchSuggest');
  if(!box)return;
  var local=scanData.filter(function(s){
    var code=(s.code||'').toLowerCase();var name=(s.name||'').toLowerCase();
    return code.indexOf(q.toLowerCase())>=0||name.indexOf(q.toLowerCase())>=0;
  }).slice(0,8);
  var localHtml=local.map(function(s){
    var pc=(s.pct_change||0)>=0?'var(--up)':'var(--down)';
    return '<div style="padding:7px 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="_mpPickSearch(\''+s.code+'\',\''+s.name.replace(/'/g,'')+'\')"><div><span style="font-weight:600;color:var(--ink);font-size:12px;">'+s.name+'</span> <span style="color:var(--muted);font-size:11px;">'+s.code+'</span></div><span style="color:'+pc+';font-size:11px;">'+(s.pct_change>=0?'+':'')+s.pct_change.toFixed(2)+'%</span></div>';
  }).join('');
  if(local.length>=5){
    box.innerHTML=localHtml;box.style.display='block';return;
  }
  searchAllStocks(q).then(function(apiResults){
    if(!apiResults||!apiResults.length){
      box.innerHTML=localHtml||'<div style="padding:12px;color:var(--muted);font-size:12px;text-align:center;">未找到匹配个股</div>';box.style.display='block';return;
    }
    var apiHtml=apiResults.slice(0,10-local.length).map(function(r){
      return '<div style="padding:7px 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="_mpPickSearch(\''+r.code+'\',\''+r.name.replace(/'/g,'')+'\')"><div><span style="font-weight:600;color:var(--ink);font-size:12px;">'+r.name+'</span> <span style="color:var(--muted);font-size:11px;">'+r.code+'</span></div></div>';
    }).join('');
    box.innerHTML=localHtml+apiHtml;box.style.display='block';
  }).catch(function(){box.innerHTML=localHtml||'<div style="padding:12px;color:var(--muted);font-size:12px;text-align:center;">搜索失败</div>';box.style.display='block';});
}
function mpDoSearch(){
  var inp=document.getElementById('mpSearchInput');
  if(!inp)return;var q=inp.value.trim();if(!q)return;
  var local=scanData.find(function(s){var code=(s.code||'').toLowerCase();var name=(s.name||'').toLowerCase();return code.indexOf(q.toLowerCase())>=0||name.indexOf(q.toLowerCase())>=0;});
  if(local){_mpObjType='stock';_mpStockCode=local.code;_mpSaveHistory(local.code,local.name);renderTpoLayer();return;}
  searchAllStocks(q).then(function(apiResults){
    if(apiResults&&apiResults.length){
      var r=apiResults[0];_mpObjType='stock';_mpStockCode=r.code;_mpSaveHistory(r.code,r.name);renderTpoLayer();
    }else{alert('未找到匹配个股：'+q);}
  });
}
// 可执行交易建议：扫描板块+个股，筛选"现价站上日内价值区上沿(VAH)"＝失衡向上
async function renderMpSuggest(){
  const box=document.getElementById('mpSuggestBox');
  if(!box)return;
  box.innerHTML='<div style="text-align:center;color:var(--muted);font-size:12px;padding:20px;">四度空间机会扫描中（板块 + 个股）...</div>';
  const boardRes=[];
  for(const b of mpBoards().slice(0,6)){
    const bars=await fetchIndexIntraday('90.'+b.code).catch(function(){return null;});
    const tpo=bars?computeTPO(bars):null;
    if(tpo){const last=tpo.rows[tpo.rows.length-1].price;if(last>tpo.vah)boardRes.push({name:b.name,code:b.code,pct:b.pct,last:last,vah:tpo.vah,poc:tpo.poc.price});}
  }
  const stockRes=[];
  const stocks=scanData.slice().sort(function(a,b){return (b.bt_quant_score||0)-(a.bt_quant_score||0);}).slice(0,6);
  for(const s of stocks){
    const bars=await fetchIndexIntraday((/^(6|9)/.test(s.code)?'1.':'0.')+s.code).catch(function(){return null;});
    const tpo=bars?computeTPO(bars):null;
    if(tpo){const last=tpo.rows[tpo.rows.length-1].price;if(last>tpo.vah)stockRes.push({name:s.name,code:s.code,pct:s.pct_change||0,last:last,vah:tpo.vah,poc:tpo.poc.price,signal:s.trade_signal||''});}
  }
  let h='<div style="font-size:12px;color:var(--muted);margin-bottom:8px;line-height:1.7;">筛选逻辑：现价站上日内价值区上沿（VAH）＝失衡向上、买方主导，具备顺势做多的可执行性；点击条目可直接切换分析对象查看完整交易计划。</div>';
  h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">';
  h+='<div><b style="font-size:13px;color:var(--up);">可执行板块（'+boardRes.length+'）</b>';
  if(!boardRes.length)h+='<div style="color:var(--muted);font-size:12px;padding:8px;">涨幅榜前6暂无站上VAH的板块（或数据加载中）</div>';
  else h+=boardRes.map(function(b){return '<div style="background:var(--bg3);border-radius:8px;padding:8px 10px;margin-top:6px;cursor:pointer;" onclick="mpPickBoard(\''+b.code+'\')"><b>'+b.name+'</b> <span style="color:var(--up);">'+(b.pct>=0?'+':'')+b.pct.toFixed(2)+'%</span><div style="font-size:10px;color:var(--muted);">现价 '+b.last.toFixed(2)+' &gt; VAH '+b.vah.toFixed(2)+' · POC '+b.poc.toFixed(2)+'</div></div>';}).join('');
  h+='</div>';
  h+='<div><b style="font-size:13px;color:var(--up);">可执行个股（'+stockRes.length+'）</b>';
  if(!stockRes.length)h+='<div style="color:var(--muted);font-size:12px;padding:8px;">量化评分前6暂无站上VAH的个股（或数据加载中）</div>';
  else h+=stockRes.map(function(s){return '<div style="background:var(--bg3);border-radius:8px;padding:8px 10px;margin-top:6px;cursor:pointer;" onclick="mpPickStock(\''+s.code+'\')"><b>'+s.name+'</b> <span style="color:var(--up);">'+(s.pct>=0?'+':'')+s.pct.toFixed(2)+'%</span>'+(s.signal?' <span class="badge buy" style="font-size:9px;">'+s.signal+'</span>':'')+'<div style="font-size:10px;color:var(--muted);">现价 '+s.last.toFixed(2)+' &gt; VAH '+s.vah.toFixed(2)+' · POC '+s.poc.toFixed(2)+'</div></div>';}).join('');
  h+='</div></div>';
  box.innerHTML=h;
}

// ===== 大V动态情绪监测 =====
function renderDavSenti(){
  if(!modVisible('davsenti'))return;
  var box=document.getElementById('davSentiBox');
  if(!box)return;
  var d=window._davSentiData||{};
  if(!d.ok){
    var err=d.error||'数据未生成';
    box.innerHTML='<div style="text-align:center;padding:16px 8px;">'
      +'<div style="color:var(--muted);font-size:13px;margin-bottom:10px;">大V情绪数据暂不可用<br><span style="font-size:10px;">('+err+')</span></div>'
      +'<div style="color:var(--muted);font-size:11px;line-height:1.8;text-align:left;max-width:420px;margin:0 auto;background:var(--bg3);border-radius:8px;padding:12px;">'
      +'<b style="color:var(--gold);">启用方法：</b><br>'
      +'1. 浏览器登录 m.weibo.cn<br>'
      +'2. F12 → Network → 刷新 → 找请求 → 复制 Cookie 值<br>'
      +'3. 粘贴保存到 data/weibo_cookie.txt<br>'
      +'4. 运行 python3 fetch_weibo_dav.py<br>'
      +'5. 重新生成看板 python3 generate_report_v6.py'
      +'</div></div>';
    return;
  }
  var davs=d.davs||[];
  var total=d.total_posts||0;
  var bull=d.total_bull||0,bear=d.total_bear||0,neu=d.total_neutral||0;
  var sentColor=d.overall_sentiment==='bullish'?'var(--up)':(d.overall_sentiment==='bearish'?'var(--down)':'var(--muted)');
  var sentLabel=d.overall_sentiment==='bullish'?'整体偏多 · 看涨占优':(d.overall_sentiment==='bearish'?'整体偏空 · 看跌占优':'整体中性 · 多空均衡');
  var scorePct=Math.round(((d.overall_score||0)+1)/2*100);
  var pct=function(n){return total>0?Math.round(n/total*100):0;};
  var esc=function(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');};

  var h='';
  // 顶部汇总横幅
  h+='<div style="display:grid;grid-template-columns:180px 1fr 300px;gap:14px;align-items:center;'
    +'background:linear-gradient(135deg,rgba(168,85,247,.08),rgba(234,179,8,.06));'
    +'border-radius:10px;padding:12px 16px;margin-bottom:10px;border:1px solid var(--bd2);">';
  // 左:总体情绪大字
  h+='<div style="text-align:center;">'
    +'<div style="font-size:11px;color:var(--muted);margin-bottom:4px;">大V总体情绪</div>'
    +'<div style="font-size:26px;font-weight:800;color:'+sentColor+';">'+sentLabel+'</div>'
    +'<div style="font-size:11px;color:var(--muted);margin-top:2px;">综合分 '+(d.overall_score>=0?'+':'')+(d.overall_score||0).toFixed(2)+' / ±1</div>'
    +'</div>';
  // 中:情绪计+统计条
  h+='<div>';
  h+='<div style="position:relative;height:10px;border-radius:5px;background:linear-gradient(90deg,var(--down),var(--bg4),var(--up));margin-bottom:8px;">';
  h+='<div style="position:absolute;left:'+scorePct+'%;top:-3px;width:4px;height:16px;background:#fff;border-radius:2px;box-shadow:0 0 6px rgba(0,0,0,.5);transform:translateX(-50%);"></div></div>';
  h+='<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;text-align:center;">'
    +'<div style="background:var(--bg2);border-radius:6px;padding:6px;">'
    +'<div style="font-size:20px;font-weight:700;color:var(--up);">'+bull+'</div>'
    +'<div style="font-size:10px;color:var(--muted);">看多 ('+pct(bull)+'%)</div></div>'
    +'<div style="background:var(--bg2);border-radius:6px;padding:6px;">'
    +'<div style="font-size:20px;font-weight:700;color:var(--muted);">'+neu+'</div>'
    +'<div style="font-size:10px;color:var(--muted);">中性 ('+pct(neu)+'%)</div></div>'
    +'<div style="background:var(--bg2);border-radius:6px;padding:6px;">'
    +'<div style="font-size:20px;font-weight:700;color:var(--down);">'+bear+'</div>'
    +'<div style="font-size:10px;color:var(--muted);">看空 ('+pct(bear)+'%)</div></div>'
    +'</div>';
  h+='</div>';
  // 右:来源统计
  var wbCnt=0,xqCnt=0,wxCnt=0;
  davs.forEach(function(v){if(v.source==='weibo')wbCnt+=v.post_count||0;else if(v.source==='xueqiu')xqCnt+=v.post_count||0;else if(v.source==='wechat')wxCnt+=v.post_count||0;});
  h+='<div style="text-align:right;">'
    +'<div style="font-size:11px;color:var(--muted);margin-bottom:4px;">数据来源 · 共 '+total+' 条市场相关帖</div>'
    +'<div style="display:flex;justify-content:flex-end;gap:6px;flex-wrap:wrap;">'
    +'<span style="display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;background:rgba(239,68,68,.12);color:#ef4444;">微博 '+wbCnt+'</span>'
    +'<span style="display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;background:rgba(59,130,246,.15);color:#3b82f6;">雪球 '+xqCnt+'</span>'
    +'<span style="display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;background:rgba(34,197,94,.15);color:#22c55e;">微信 '+wxCnt+'</span>'
    +'</div>'
    +(d.total_filtered>0?'<div style="font-size:10px;color:var(--muted);margin-top:6px;">已过滤无关内容 '+d.total_filtered+' 条</div>':'')
    +'</div>';
  h+='</div>'; // 汇总横幅结束

  // 大V卡片网格（响应式:窄屏2列,宽屏3列）
  h+='<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:10px;">';
  davs.forEach(function(dav,di){
    var dColor=dav.overall==='bullish'?'var(--up)':(dav.overall==='bearish'?'var(--down)':'var(--muted)');
    var dLabel=dav.overall==='bullish'?'偏多':(dav.overall==='bearish'?'偏空':'中性');
    var dBadge='<span style="display:inline-block;padding:1px 7px;border-radius:10px;font-size:10px;font-weight:700;background:'+(dav.overall==='bullish'?'rgba(34,197,94,.15)':(dav.overall==='bearish'?'rgba(239,68,68,.15)':'var(--bg4)'))+';color:'+(dav.overall==='bullish'?'var(--up)':(dav.overall==='bearish'?'var(--down)':'var(--muted)'))+';">'+dLabel+'</span>';
    var dName=(dav.screen_name&&dav.screen_name!==dav.name)?dav.name+' · '+dav.screen_name:dav.name;
    var srcTag=dav.source==='xueqiu'?'<span style="display:inline-block;padding:0 6px;border-radius:10px;font-size:9px;background:rgba(59,130,246,.18);color:#3b82f6;margin-left:4px;vertical-align:middle;">雪球</span>':(dav.source==='wechat'?'<span style="display:inline-block;padding:0 6px;border-radius:10px;font-size:9px;background:rgba(34,197,94,.15);color:#22c55e;margin-left:4px;vertical-align:middle;">微信</span>':'<span style="display:inline-block;padding:0 6px;border-radius:10px;font-size:9px;background:rgba(239,68,68,.15);color:#ef4444;margin-left:4px;vertical-align:middle;">微博</span>');
    var fInfo=dav.filtered_count>0?' <span style="font-size:9px;color:var(--muted);">(滤'+dav.filtered_count+'条)</span>':'';
    var n=dav.post_count||1;
    var bullP=Math.round((dav.bull_count||0)/n*100);
    var bearP=Math.round((dav.bear_count||0)/n*100);
    var neuP=100-bullP-bearP;

    h+='<div style="background:var(--bg3);border-radius:10px;padding:10px 12px;border:1px solid var(--bd2);display:flex;flex-direction:column;">';
    // 卡片头部
    h+='<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid var(--bd2);">';
    h+='<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">'
      +'<b style="font-size:13px;color:var(--purple);">'+dName+'</b>'+srcTag+' '+dBadge+'</div>';
    h+='<div style="font-size:10px;color:var(--muted);">'+dav.post_count+'条'+fInfo+'</div></div>';
    // 情绪分布条
    h+='<div style="display:flex;height:6px;border-radius:3px;overflow:hidden;margin-bottom:6px;">'
      +'<div style="width:'+bullP+'%;background:var(--up);"></div>'
      +'<div style="width:'+neuP+'%;background:var(--muted);opacity:.5;"></div>'
      +'<div style="width:'+bearP+'%;background:var(--down);"></div>'
      +'</div>';
    h+='<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--muted);margin-bottom:8px;">'
      +'<span style="color:var(--up);">看多 '+dav.bull_count+'</span>'
      +'<span>中性 '+dav.neutral_count+'</span>'
      +'<span style="color:var(--down);">看空 '+dav.bear_count+'</span>'
      +'</div>';
    // 帖子列表
    if(dav.posts&&dav.posts.length){
      dav.posts.forEach(function(post,pi){
        var pColor=post.sentiment==='bullish'?'var(--up)':(post.sentiment==='bearish'?'var(--down)':'var(--muted)');
        var pDot='<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:'+pColor+';margin-right:5px;vertical-align:middle;flex-shrink:0;"></span>';
        var kwHtml='';
        if(post.bull_kw&&post.bull_kw.length)kwHtml+='<span style="color:var(--up);font-size:9px;margin-right:6px;">看多:'+post.bull_kw.join(',')+'</span>';
        if(post.bear_kw&&post.bear_kw.length)kwHtml+='<span style="color:var(--down);font-size:9px;">看空:'+post.bear_kw.join(',')+'</span>';
        var fullText=esc(post.text);
        var pid='davPost_'+di+'_'+pi;
        var isLong=fullText.length>200;
        h+='<div style="font-size:11px;line-height:1.65;color:var(--ink);margin-bottom:5px;padding:6px 8px;background:var(--bg2);border-radius:6px;display:flex;gap:0;">'
          +pDot+'<div style="flex:1;min-width:0;">'
          +'<span id="'+pid+'" style="white-space:pre-wrap;word-break:break-all;display:inline;">'+(isLong?fullText.slice(0,200):fullText)+'</span>'
          +(isLong?'<span onclick="toggleDavPost(\''+pid+'\',this,\''+fullText.replace(/'/g,"\\'")+'\')" style="color:var(--gold);font-size:10px;cursor:pointer;margin-left:4px;user-select:none;">展开</span>':'')
          +'<div style="font-size:9px;color:var(--muted);margin-top:3px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:4px;">'
          +'<span>'+post.time+'</span>'
          +'<span>'+kwHtml+'</span>'
          +'</div></div></div>';
      });
    }else{
      h+='<div style="font-size:11px;color:var(--muted);padding:8px;text-align:center;background:var(--bg2);border-radius:6px;">暂无'+(dav.source==='wechat'?'文章':'帖子')+'数据'+(dav.source==='wechat'?'（搜狗索引延迟或限流）':'')+'</div>';
    }
    h+='</div>'; // 卡片结束
  });
  h+='</div>'; // 网格结束

  h+='<div style="font-size:10px;color:var(--muted);text-align:right;margin-top:8px;">数据更新: '+d.fetch_time+'</div>';
  box.innerHTML=h;
}
function toggleDavPost(pid,btn,fullText){
  var el=document.getElementById(pid);
  if(!el)return;
  var full=fullText||el.getAttribute('data-full')||el.textContent;
  var cut=parseInt(el.getAttribute('data-cut')||'200',10);
  if(btn.getAttribute('data-open')==='1'){
    // 收起
    el.textContent=full.substring(0,cut)+'...';
    btn.setAttribute('data-open','0');
    btn.textContent='展开';
  }else{
    // 展开
    el.textContent=full;
    btn.setAttribute('data-open','1');
    btn.textContent='收起';
  }
}

// ===== 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==='layer0'){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 fetchPoolAll(kind,sort,pz,dateStr,base,ut){
  var all=[],total=0;
  var r=await jsonpRequest(base+kind+'?'+ut+'&Pageindex=0&pagesize='+pz+'&sort='+sort+'&date='+dateStr,9000).catch(function(){return null;});
  if(r&&r.data&&r.data.pool){all=r.data.pool;total=r.data.tc||all.length;}
  var pages=total>pz?Math.ceil(total/pz):1;
  if(pages>1){
    var jobs=[];
    for(var p=1;p<pages;p++)jobs.push(p);
    for(var i=0;i<jobs.length;i+=8){
      var batch=jobs.slice(i,i+8);
      var rs=await Promise.all(batch.map(function(p){return jsonpRequest(base+kind+'?'+ut+'&Pageindex='+p+'&pagesize='+pz+'&sort='+sort+'&date='+dateStr,9000).catch(function(){return null;});}));
      rs.forEach(function(rr){if(rr&&rr.data&&rr.data.pool)all=all.concat(rr.data.pool);});
    }
  }
  return all;
}
async function fetchEmotionPools(){
  const base='https://push2ex.eastmoney.com/';
  const ut='ut=7eea3edcaed734bea9cbfc24409ed989&dpt=wz.ztzt';
  var _d=new Date();
  const dateStr=_d.getFullYear()+('0'+(_d.getMonth()+1)).slice(-2)+('0'+_d.getDate()).slice(-2);
  try{
    // 全市场分页抓取：按 data.tc 总数翻页，避免 pagesize 截断（覆盖沪深京全A股）
    const pools=await Promise.all([
      fetchPoolAll('getTopicZTPool','fbt%3Aasc',500,dateStr,base,ut),
      fetchPoolAll('getTopicDTPool','fund%3Aasc',200,dateStr,base,ut),
      fetchPoolAll('getTopicZBPool','fbt%3Aasc',300,dateStr,base,ut)
    ]);
    emoPools.zt=pools[0]||[];emoPools.dt=pools[1]||[];emoPools.zb=pools[2]||[];
    emoPools.ok=emoPools.zt.length>0||emoPools.dt.length>0;
    computeEmotionCycle();
    renderEmotionCycle();renderLadder();renderPlaybook();renderSignalCenter();renderUpDownDist();
  }catch(e){console.log('Emotion pools JSONP failed, trying Python fallback:',e.message);
    // Fallback 1: use Python-injected data
    const epd=window._emoPoolsData||{};
    if(epd.ok&&epd.zt&&epd.zt.length>0){
      emoPools.zt=epd.zt;emoPools.dt=epd.dt||[];emoPools.zb=epd.zb||[];
      emoPools.ok=true;
      console.log('Using Python fallback emoPools: ZT='+emoPools.zt.length+' DT='+emoPools.dt.length+' ZB='+emoPools.zb.length);
      computeEmotionCycle();
      renderEmotionCycle();renderLadder();renderPlaybook();renderSignalCenter();renderUpDownDist();
      return;
    }
    // Fallback 2: 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();renderUpDownDist();}
}
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(!modVisible('emotion'))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(!modVisible('ladder'))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','fenqi'].includes(stage):true,stage?(['fajiao','gaochao'].includes(stage)?12:(stage==='huinuan'?10:(stage==='fenqi'?4: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机会 =====
// 前端JSONP获取全市场涨跌统计
async function fetchMarketBreadth(){
  try{
    const base='https://push2delay.eastmoney.com/api/qt/clist/get';
    const fs='m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81%2Bs:2048';
    const PZ=200;
    const fields='f3,f292';
    const first=await jsonpRequest(base+'?pn=1&pz='+PZ+'&po=1&np=1&fltt=2&invt=2&fs='+fs+'&fields='+fields,8000);
    const rawTotal=(first&&first.data&&first.data.total)||0;
    if(!rawTotal)return;
    const all=[...(first.data.diff||[])];
    const pages=Math.min(Math.ceil(rawTotal/PZ),60);
    const jobs=[];
    for(let p=2;p<=pages;p++)jobs.push(p);
    for(let i=0;i<jobs.length;i+=10){
      const batch=jobs.slice(i,i+10);
      const rs=await Promise.all(batch.map(p=>jsonpRequest(base+'?pn='+p+'&pz='+PZ+'&po=1&np=1&fltt=2&invt=2&fs='+fs+'&fields='+fields,8000).catch(()=>null)));
      rs.forEach(r=>{if(r&&r.data&&r.data.diff)all.push(...r.data.diff);});
    }
    let up=0,down=0,flat=0,sus=0,delisted=0;
    const hist=[0,0,0,0,0,0,0,0,0,0,0,0,0];
    function bk(v){if(v===0)return 6;if(v<0){if(v<=-7)return 0;if(v<=-5)return 1;if(v<=-3)return 2;if(v<=-2)return 3;if(v<=-1)return 4;return 5;}if(v>=7)return 12;if(v>=5)return 11;if(v>=3)return 10;if(v>=2)return 9;if(v>=1)return 8;return 7;}
    all.forEach(s=>{
      if(s.f292===7||s.f292==='7'){delisted++;return;}
      const v=s.f3;
      let p=null;
      if(typeof v==='number'){p=v;}
      else if(v==='-'||v==null||v===undefined){sus++;return;}
      else{const pp=parseFloat(v);if(!isNaN(pp)){p=pp;}else{sus++;return;}}
      if(p>0)up++;else if(p<0)down++;else flat++;
      hist[bk(p)]++;
    });
    const total=rawTotal-delisted;
    const lu=cycleState?cycleState.ztC:0,ld=cycleState?cycleState.dtC:0;
    const histNames=['≤-7%','-7~-5%','-5~-3%','-3~-2%','-2~-1%','-1~0%','平盘','0~1%','1~2%','2~3%','3~5%','5~7%','≥7%'];
    window._marketBreadth={total:total,up:up,down:down,flat:flat,suspended:sus,limit_up:lu,limit_down:ld,hist:histNames.map((n,i)=>({name:n,value:hist[i]}))};
    renderUpDownDist();renderBreadthChart();
  }catch(e){console.log('Market breadth JSONP failed:',e.message);}
}
// 前端JSONP获取T+0 ETF
async function fetchT0Etfs(){
  try{
    const base='https://push2delay.eastmoney.com/api/qt/clist/get';
    // fs=全市场场内基金（沪m:1+t:9 + 深m:0+t:10），按成交额f6降序取首页100只即可覆盖全部活跃T+0品种。
    // 旧板块MK0024~MK0405实际只含黄金ETF+LOF，债券/跨境ETF根本不在池内，导致机会列表长期为空
    const r=await jsonpRequest(base+'?pn=1&pz=100&po=1&np=1&fltt=2&invt=2&fid=f6&fs=m:1+t:9,m:0+t:10&fields=f12,f14,f2,f3,f6,f8',8000);
    if(r&&r.data&&r.data.diff){
      const list=[];
      r.data.diff.forEach(e=>{
        const code=e.f12||'',name=e.f14||'',amount=e.f6||0;
        const nu=name.toUpperCase();
        // 货币ETF（添益/日利/理财等）净值化运作、日内振幅≈0，永远不会有做T机会，直接排除避免霸占候选名额
        if(['货币','添益','日利','理财','保证金','现金'].some(k=>name.indexOf(k)>=0))return;
        let type='',strategy='';
        if(name.indexOf('黄金')>=0||nu.indexOf('GOLD')>=0||code.startsWith('518')){type='黄金ETF';strategy='趋势跟踪：跟随国际金价波动，RSI<30低吸、RSI>70高抛；地缘事件驱动时可加大仓位；止损3%';}
        else if(['债','短融','国开','国债','政金'].some(k=>name.indexOf(k)>=0)||code.startsWith('511')){type='债券ETF';strategy='日内波段：利用债券ETF低波动特性，在盘中低点买入、高点卖出，日收益0.1-0.3%；适合大资金低风险套利';}
        else if(['原油','豆粕','有色','能源','商品','白银','化工'].some(k=>name.indexOf(k)>=0)){type='商品ETF';strategy='事件驱动：关注大宗商品价格和库存变化，突破前日高点买入、跌破前日低点止损；趋势行情可留仓过夜';}
        else if(['跨境','纳指','恒生','日经','标普','德国','法国','港股','中概','海外','东南亚','沙特','印度','美国'].some(k=>name.indexOf(k)>=0)||nu.indexOf('QDII')>=0||code.startsWith('513')||code.startsWith('1596')){type='跨境ETF';strategy='隔日溢价套利：关注夜间海外市场走势，次日开盘顺势操作；若溢价>2%可做空溢价回落；波动大设2%止损';}
        if(type&&amount>1e7){
          list.push({code,name,price:e.f2||0,pct_change:Math.round((e.f3||0)*100)/100,
            amount:Math.round(amount/1e8*10)/10+'亿',amountRaw:amount,turnover:Math.round((e.f8||0)*10)/10,type,strategy});
        }
      });
      // 类型内按成交额降序；再按类型配额挑选（跨境10+黄金3+商品3+债券4=20只），
      // 保证高波动类型占主体的同时各类都有代表，避免债券霸榜或债券绝迹两个极端
      const rankT={'跨境ETF':0,'黄金ETF':1,'商品ETF':1,'债券ETF':2};
      list.sort((a,b)=>(rankT[a.type]-rankT[b.type])||(b.amountRaw-a.amountRaw));
      const quota={'跨境ETF':10,'黄金ETF':3,'商品ETF':3,'债券ETF':4};
      const picked={};const finalList=[];
      list.forEach(x=>{picked[x.type]=picked[x.type]||0;if(picked[x.type]<(quota[x.type]||0)){picked[x.type]++;finalList.push(x);}});
      window._t0EtfList=finalList;
      renderT0List();
    }
  }catch(e){console.log('T+0 ETF JSONP failed:',e.message);}
}
function renderUpDownDist(){
  const el=document.getElementById('upDownDist');if(!el)return;
  if(!modVisible('updown'))return;
  // 先 dispose 再清 DOM：若先 innerHTML='' 移除 canvas，dispose 内部 removeChild 可能因 parentNode 为 null 抛错，
  // 中断后续 echarts.init，导致 60s 情绪池刷新链把图表刷成空白
  const old=echarts.getInstanceByDom(el);if(old){try{old.dispose();}catch(e){}}
  el.innerHTML='';
  const mb=window._marketBreadth||{};
  if(!mb.total){el.innerHTML='<div style="color:var(--muted);text-align:center;padding:40px;">全市场涨跌数据加载中...</div>';return;}
  const ch=echarts.init(el);chartInstances.upDownDist=ch;
  const up=mb.up||0,down=mb.down||0,flat=mb.flat||0,sus=mb.suspended||0;
  // 涨停/跌停家数优先取情绪池（实时准确），仅用于统计摘要，不参与直方图分档
  let lu=mb.limit_up||0,ld=mb.limit_down||0;
  if(cycleState&&cycleState.ztC>0)lu=cycleState.ztC;
  if(cycleState&&cycleState.dtC>0)ld=cycleState.dtC;
  const total=mb.total||1;
  // 涨跌幅区间直方图（左跌→右涨，13 档）
  const defHist=[{name:'≤-7%',value:0},{name:'-7~-5%',value:0},{name:'-5~-3%',value:0},{name:'-3~-2%',value:0},{name:'-2~-1%',value:0},{name:'-1~0%',value:0},{name:'平盘',value:0},{name:'0~1%',value:0},{name:'1~2%',value:0},{name:'2~3%',value:0},{name:'3~5%',value:0},{name:'5~7%',value:0},{name:'≥7%',value:0}];
  const hist=(mb.hist&&mb.hist.length)?mb.hist:defHist;
  const catData=hist.map(x=>x.name);
  const valData=hist.map((x,i)=>{
    const c=i<6?tc.down:(i===6?tc.muted:tc.up);
    return {value:x.value,itemStyle:{color:c,borderRadius:[2,2,0,0]}};
  });
  ch.setOption({backgroundColor:'transparent',
    tooltip:tipXL({trigger:'axis',axisPointer:{type:'shadow',shadowStyle:{color:hexA(tc.bg4,.35)}},formatter:function(p){return p[0].name+': <b>'+p[0].value+'</b>只 ('+(p[0].value/total*100).toFixed(1)+'%)';}}),
    grid:{left:36,right:8,top:26,bottom:42},
    xAxis:{type:'category',data:catData,axisLabel:{color:tc.muted,fontSize:9,interval:0,rotate:38},axisLine:{lineStyle:{color:tc.rule}},axisTick:{show:false}},
    yAxis:{type:'value',axisLabel:{color:tc.muted,fontSize:9},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},
    series:[{type:'bar',barWidth:'58%',data:valData,
      label:{show:false},
    }],
  });
  // 统计摘要
  const summary=document.createElement('div');
  summary.style.cssText='text-align:center;font-size:11px;color:var(--muted);margin-top:6px;line-height:1.9;';
  const upPct=(up/total*100).toFixed(1),downPct=(down/total*100).toFixed(1);
  summary.innerHTML='总计 <b style="color:var(--ink);">'+total+'</b> 只 | <b style="color:var(--up);">上涨 '+up+'</b>('+upPct+'%) <b style="color:var(--down);">下跌 '+down+'</b>('+downPct+'%) | 平盘 '+flat+' | <b style="color:var(--up);">涨停 '+lu+'</b> <b style="color:var(--down);">跌停 '+ld+'</b> | 停牌 '+sus+' | '+(up>down*1.5?'<b style="color:var(--up);">多头主导</b>':(down>up*1.5?'<b style="color:var(--down);">空头主导</b>':'<b style="color:var(--gold);">多空均衡</b>'));
  el.appendChild(summary);
}
// ===== 重磅事件日历 =====
function renderEconCal(){
  var el=document.getElementById('econCalBox');if(!el)return;
  if(!modVisible('econcal'))return;
  var d=window._econCalData||{};
  if(!d.ok||!d.events||d.events.length===0){el.innerHTML='<div style="text-align:center;padding:30px;color:var(--muted);">事件数据加载中...</div>';return;}
  var events=d.events;
  var s=d.summary||{};
  var now=new Date();
  var todayStr=now.getFullYear()+'-'+String(now.getMonth()+1).padStart(2,'0')+'-'+String(now.getDate()).padStart(2,'0');
  var catColors={'重磅':'var(--gold)','宏观':'var(--blue)','外汇':'var(--purple)','商品':'var(--orange)','债券':'var(--green)','A股':'var(--up)','港股':'var(--red)','美股':'var(--blue)'};
  var impactColors={bullish:{bg:'rgba(34,197,94,0.12)',border:'var(--down)',text:'var(--down)',label:'偏利好'},bearish:{bg:'rgba(239,68,68,0.12)',border:'var(--up)',text:'var(--up)',label:'偏利空'},neutral:{bg:'rgba(255,200,61,0.10)',border:'var(--gold)',text:'var(--gold)',label:'中性/待定'}};
  var h='<div style="display:flex;gap:12px;margin-bottom:12px;flex-wrap:wrap;">';
  h+='<div style="padding:8px 14px;border-radius:8px;background:var(--bg3);"><span style="color:var(--muted);font-size:11px;">事件总数</span><br><b style="font-size:18px;color:var(--ink);">'+(s.total||0)+'</b></div>';
  h+='<div style="padding:8px 14px;border-radius:8px;background:rgba(255,200,61,0.10);"><span style="color:var(--muted);font-size:11px;">高重要事件</span><br><b style="font-size:18px;color:var(--gold);">'+(s.high_impact||0)+'</b></div>';
  h+='<div style="padding:8px 14px;border-radius:8px;background:rgba(34,197,94,0.10);"><span style="color:var(--muted);font-size:11px;">偏利好</span><br><b style="font-size:18px;color:var(--down);">'+(s.bullish||0)+'</b></div>';
  h+='<div style="padding:8px 14px;border-radius:8px;background:rgba(239,68,68,0.10);"><span style="color:var(--muted);font-size:11px;">偏利空</span><br><b style="font-size:18px;color:var(--up);">'+(s.bearish||0)+'</b></div>';
  h+='<div style="padding:8px 14px;border-radius:8px;background:var(--bg3);flex:1;min-width:200px;"><span style="color:var(--muted);font-size:11px;">下一个关键节点</span><br><b style="font-size:13px;color:var(--gold);">'+(s.next_major||'')+'</b></div>';
  h+='</div>';
  var currentDate='';
  events.forEach(function(e){
    if(e.date!==currentDate){
      currentDate=e.date;
      var isToday=e.date===todayStr;
      var dt=new Date(e.date+'T00:00:00');
      var weekday=['日','一','二','三','四','五','六'][dt.getDay()];
      h+='<div style="margin-top:'+(currentDate===events[0].date?'0':'10')+'px;margin-bottom:6px;padding:6px 10px;border-radius:6px;background:'+(isToday?'rgba(255,200,61,0.12)':'var(--bg2)')+';border-left:3px solid '+(isToday?'var(--gold)':'var(--bd2)')+';">';
      h+='<span style="font-weight:700;font-size:13px;color:'+(isToday?'var(--gold)':'var(--ink)')+';">'+e.date.replace(/-/g,'/')+' 周'+weekday+'</span>';
      if(isToday)h+='<span style="margin-left:8px;font-size:10px;padding:1px 6px;border-radius:4px;background:var(--gold);color:#000;font-weight:700;">今天</span>';
      h+='</div>';
    }
    var ic=impactColors[e.impact]||impactColors.neutral;
    var cc=catColors[e.cat]||'var(--muted)';
    var impStar=e.importance==='high'?'★★★':(e.importance==='medium'?'★★':'★');
    var flag=e.country==='美国'?'🇺🇸':(e.country==='中国'?'🇨🇳':(e.country==='日本'?'🇯🇵':'🌐'));
    h+='<div style="display:flex;gap:10px;padding:8px 10px;margin-bottom:4px;border-radius:6px;background:'+ic.bg+';border:1px solid '+ic.border+'33;">';
    h+='<div style="width:50px;flex-shrink:0;text-align:center;"><div style="font-size:14px;font-weight:700;color:'+ic.text+';">'+e.time+'</div><div style="font-size:10px;color:var(--muted);margin-top:1px;">'+impStar+'</div></div>';
    h+='<div style="flex:1;">';
    h+='<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">';
    h+='<span style="font-size:10px;padding:1px 6px;border-radius:4px;background:'+cc+'22;color:'+cc+';font-weight:600;">'+e.cat+'</span>';
    h+='<span style="font-weight:700;font-size:13px;color:var(--ink);">'+flag+' '+e.title+'</span>';
    h+='<span style="font-size:10px;padding:1px 8px;border-radius:4px;background:'+ic.border+'22;color:'+ic.text+';font-weight:600;">'+ic.label+'</span>';
    h+='</div>';
    h+='<div style="font-size:11px;color:var(--muted);margin-top:3px;">'+e.desc+'</div>';
    if(e.forecast&&e.forecast!=='待定'){h+='<div style="font-size:11px;margin-top:2px;"><span style="color:var(--muted);">预期：</span><b style="color:var(--gold);">'+e.forecast+'</b> <span style="color:var(--muted);margin-left:8px;">前值：</span><b style="color:var(--muted);">'+e.prior+'</b></div>';}
    h+='<div style="font-size:11px;color:'+ic.text+';margin-top:3px;line-height:1.5;">💡 '+e.impact_text+'</div>';
    if(e.markets&&e.markets.length){h+='<div style="font-size:10px;color:var(--muted);margin-top:3px;">影响市场：'+e.markets.map(function(m){return '<span style="padding:1px 5px;border-radius:3px;background:var(--bg3);margin-right:3px;">'+m+'</span>';}).join('')+'</div>';}
    h+='</div></div>';
  });
  h+='<div style="margin-top:12px;padding:8px 10px;border-radius:6px;background:var(--bg2);font-size:10px;color:var(--muted);line-height:1.6;">';
  h+='<b style="color:var(--ink);">研判说明：</b>利好/利空判断基于事件结果与市场预期的偏差。降息/数据偏弱=利好债市+成长股；通胀超预期/鹰派=利空。绿色=偏利好、红色=偏利空、黄色=中性/待定。数据更新时间：'+(d.fetch_time||'')+'</div>';
  el.innerHTML=h;
}
// ===== 市场异动监测 =====
function renderMarketPulse(){
  var el=document.getElementById('marketPulseBox');if(!el)return;
  if(!modVisible('marketpulse'))return;
  if(!scanData||scanData.length===0){el.innerHTML='<div style="text-align:center;padding:30px;color:var(--muted);">数据加载中...</div>';return;}
  var sectors=(sectorData||[]).slice().sort(function(a,b){return b.avg_sentiment-a.avg_sentiment;});
  var topSec=sectors.slice(0,5),botSec=sectors.slice(-5).reverse();
  var volShock=scanData.filter(function(s){return(s.bt_vol_ratio||0)>=2.5;}).sort(function(a,b){return(b.bt_vol_ratio||0)-(a.bt_vol_ratio||0);}).slice(0,8);
  var amtTop=scanData.slice().sort(function(a,b){return(b.turnover||0)-(a.turnover||0);}).slice(0,8);
  var pctTop=scanData.slice().sort(function(a,b){return(b.pct_change||0)-(a.pct_change||0);}).slice(0,5);
  var pctBot=scanData.slice().sort(function(a,b){return(a.pct_change||0)-(b.pct_change||0);}).slice(0,5);
  var h='<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">';
  h+='<div><div style="font-size:12px;font-weight:700;color:var(--up);margin-bottom:8px;">板块急涨 Top5</div>';
  topSec.forEach(function(s){
    var stocks=(s.stocks||[]).slice().sort(function(a,b){return(b.pct||0)-(a.pct||0);}).slice(0,2);
    h+='<div style="margin-bottom:8px;padding:6px 8px;border-radius:6px;background:rgba(239,68,68,0.08);border-left:2px solid var(--up);">';
    h+='<div style="display:flex;justify-content:space-between;align-items:center;"><span style="font-weight:600;font-size:13px;cursor:pointer;" onclick="scrollToSec(\'layer2\',document.querySelectorAll(\'.nav-link\')[2])">'+s.name+'</span><span style="color:var(--up);font-weight:700;font-size:13px;">+'+(s.avg_sentiment||0).toFixed(2)+'%</span></div>';
    if(stocks.length){h+='<div style="font-size:11px;color:var(--muted);margin-top:3px;">'+stocks.map(function(x){return '<span style="cursor:pointer;color:var(--ink);" onclick="showDetail(\''+x.code+'\')">'+x.name+' '+(x.pct||0).toFixed(1)+'%</span>';}).join(' · ')+'</div>';}
    h+='</div>';
  });
  h+='</div>';
  h+='<div><div style="font-size:12px;font-weight:700;color:var(--down);margin-bottom:8px;">板块急跌 Top5</div>';
  botSec.forEach(function(s){
    var stocks=(s.stocks||[]).slice().sort(function(a,b){return(a.pct||0)-(b.pct||0);}).slice(0,2);
    h+='<div style="margin-bottom:8px;padding:6px 8px;border-radius:6px;background:rgba(34,197,94,0.08);border-left:2px solid var(--down);">';
    h+='<div style="display:flex;justify-content:space-between;align-items:center;"><span style="font-weight:600;font-size:13px;cursor:pointer;" onclick="scrollToSec(\'layer2\',document.querySelectorAll(\'.nav-link\')[2])">'+s.name+'</span><span style="color:var(--down);font-weight:700;font-size:13px;">'+(s.avg_sentiment||0).toFixed(2)+'%</span></div>';
    if(stocks.length){h+='<div style="font-size:11px;color:var(--muted);margin-top:3px;">'+stocks.map(function(x){return '<span style="cursor:pointer;color:var(--ink);" onclick="showDetail(\''+x.code+'\')">'+x.name+' '+(x.pct||0).toFixed(1)+'%</span>';}).join(' · ')+'</div>';}
    h+='</div>';
  });
  h+='</div>';
  h+='<div><div style="font-size:12px;font-weight:700;color:var(--gold);margin-bottom:8px;">量比异动（资金突击）</div>';
  volShock.forEach(function(s){
    var vr=s.bt_vol_ratio||0;
    var pc=s.pct_change||0;
    var tag=vr>=4?'<span style="color:var(--up);font-weight:700;">巨量</span>':(vr>=3?'<span style="color:var(--gold);">放量</span>':'<span style="color:var(--blue);">温和</span>');
    h+='<div style="margin-bottom:6px;padding:6px 8px;border-radius:6px;background:var(--bg3);display:flex;justify-content:space-between;align-items:center;cursor:pointer;" onclick="showDetail(\''+s.code+'\')">';
    h+='<div><span style="font-weight:600;font-size:13px;">'+s.name+'</span> <span style="font-size:10px;color:var(--muted);">'+s.code+'</span> '+tag+'</div>';
    h+='<div style="text-align:right;"><span style="color:'+(pc>=0?'var(--up)':'var(--down)')+';font-weight:600;">'+(pc>=0?'+':'')+pc.toFixed(2)+'%</span> <span style="font-size:10px;color:var(--muted);margin-left:4px;">量比'+vr.toFixed(1)+'</span></div>';
    h+='</div>';
  });
  h+='</div>';
  h+='</div>';
  h+='<div style="margin-top:12px;padding-top:10px;border-top:1px solid var(--bd2);display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
  h+='<div><div style="font-size:12px;font-weight:700;color:var(--up);margin-bottom:6px;">成交额 Top5（资金聚集）</div>';
  amtTop.forEach(function(s){h+='<div style="font-size:11px;margin-bottom:4px;cursor:pointer;" onclick="showDetail(\''+s.code+'\')"><span style="font-weight:600;">'+s.name+'</span> <span style="color:var(--muted);">'+s.code+'</span> <span style="color:var(--gold);font-weight:600;">'+((s.turnover||0)/1e8).toFixed(1)+'亿</span> <span style="color:'+((s.pct_change||0)>=0?'var(--up)':'var(--down)')+';">'+((s.pct_change||0)>=0?'+':'')+(s.pct_change||0).toFixed(2)+'%</span></div>';});
  h+='</div>';
  h+='<div><div style="font-size:12px;font-weight:700;margin-bottom:6px;"><span style="color:var(--up);">涨幅Top5</span> / <span style="color:var(--down);">跌幅Top5</span></div>';
  pctTop.forEach(function(s,i){h+='<div style="font-size:11px;margin-bottom:3px;cursor:pointer;" onclick="showDetail(\''+s.code+'\')"><span style="color:var(--up);">▲</span> <span style="font-weight:600;">'+s.name+'</span> <span style="color:var(--up);font-weight:600;">+'+(s.pct_change||0).toFixed(2)+'%</span></div>';});
  pctBot.forEach(function(s,i){h+='<div style="font-size:11px;margin-bottom:3px;cursor:pointer;" onclick="showDetail(\''+s.code+'\')"><span style="color:var(--down);">▼</span> <span style="font-weight:600;">'+s.name+'</span> <span style="color:var(--down);font-weight:600;">'+(s.pct_change||0).toFixed(2)+'%</span></div>';});
  h+='</div>';
  h+='</div>';
  el.innerHTML=h;
}
// ===== 蓄势待发 =====
function renderMomentum(){
  var el=document.getElementById('momentumBox');if(!el)return;
  if(!modVisible('momentum'))return;
  if(!scanData||scanData.length===0){el.innerHTML='<div style="text-align:center;padding:30px;color:var(--muted);">数据加载中...</div>';return;}
  var cands=scanData.filter(function(s){
    var sq=s.signal_quality||0;
    var qs=s.bt_quant_score||0;
    var pc=s.pct_change||0;
    var vr=s.bt_vol_ratio||0;
    var adx=s.bt_adx||0;
    return sq>=60&&qs>=50&&pc>=-1&&pc<=3&&vr>=0.8&&vr<=2.0&&adx>=20;
  });
  if(cands.length===0){el.innerHTML='<div style="text-align:center;padding:30px;color:var(--muted);font-size:13px;">暂无符合条件的蓄势个股<br><span style="font-size:11px;">条件：信号质量≥60 + 量化评分≥50 + 涨跌幅[-1%,3%] + 量比0.8~2.0 + ADX≥20</span></div>';return;}
  cands.forEach(function(s){
    var sq=s.signal_quality||0;
    var qs=s.bt_quant_score||0;
    var vr=s.bt_vol_ratio||0;
    var adx=s.bt_adx||0;
    var score=sq*0.4+qs*0.3+Math.max(0,2.0-vr)*20*0.2+(adx/50)*100*0.1;
    s._momScore=Math.round(score);
  });
  cands.sort(function(a,b){return b._momScore-a._momScore;});
  var h='';
  cands.slice(0,10).forEach(function(s){
    var cycBadge=cycleState?'<span style="font-size:9px;padding:1px 6px;border-radius:6px;background:'+cycleState.st.color+'22;color:'+cycleState.st.color+';">'+cycleState.st.name+'</span>':'';
    var indicators=(s.signals||[]).map(function(sig){return sig.indicator;}).filter(function(v,i,arr){return arr.indexOf(v)===i;});
    h+='<div style="margin-bottom:8px;padding:8px 10px;border-radius:8px;background:var(--bg3);border-left:3px solid var(--purple);cursor:pointer;" onclick="showDetail(\''+s.code+'\')">';
    h+='<div style="display:flex;justify-content:space-between;align-items:center;">';
    h+='<div><span style="font-weight:700;font-size:14px;">'+s.name+'</span> <span style="font-size:11px;color:var(--muted);">'+s.code+'</span> '+cycBadge+'</div>';
    h+='<div><span style="font-size:12px;font-weight:700;color:var(--purple);">蓄势 '+s._momScore+'分</span></div>';
    h+='</div>';
    h+='<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-top:6px;font-size:11px;">';
    h+='<div><span style="color:var(--muted);">现价</span><br><b style="color:'+(s.pct_change>=0?'var(--up)':'var(--down)')+';">'+(s.latest_price||0).toFixed(2)+'</b></div>';
    h+='<div><span style="color:var(--muted);">涨跌</span><br><b style="color:'+(s.pct_change>=0?'var(--up)':'var(--down)')+';">'+(s.pct_change>=0?'+':'')+s.pct_change.toFixed(2)+'%</b></div>';
    h+='<div><span style="color:var(--muted);">量比</span><br><b style="color:'+(s.bt_vol_ratio>=1.5?'var(--gold)':'var(--muted)')+';">'+(s.bt_vol_ratio||0).toFixed(2)+'</b></div>';
    h+='<div><span style="color:var(--muted);">成交</span><br><b>'+((s.turnover||0)/1e8).toFixed(1)+'亿</b></div>';
    h+='</div>';
    h+='<div style="margin-top:5px;font-size:10px;color:var(--muted);">信号质量 '+(s.signal_quality||0).toFixed(0)+'分 · 量化评分 '+(s.bt_quant_score||0).toFixed(0)+'分 · ADX '+(s.bt_adx||0).toFixed(0)+' · '+(indicators.length?'触发: '+indicators.slice(0,3).join('、'):'无活跃信号')+'</div>';
    h+='</div>';
  });
  el.innerHTML=h;
}
// ===== 个股做T参考 =====
var _stockT0Cache={};
async function renderStockT0(){
  var el=document.getElementById('stockT0Box');if(!el)return;
  if(!modVisible('stockt0'))return;
  if(!scanData||scanData.length===0){el.innerHTML='<div style="text-align:center;padding:30px;color:var(--muted);">数据加载中...</div>';return;}
  var cands=scanData.filter(function(s){
    var amt=s.turnover||0;
    var vr=s.bt_vol_ratio||0;
    var pc=Math.abs(s.pct_change||0);
    return amt>=3e8&&vr>=1.0&&pc>=1.0;
  });
  if(cands.length===0){el.innerHTML='<div style="text-align:center;padding:30px;color:var(--muted);font-size:13px;">暂无适合做T的个股<br><span style="font-size:11px;">条件：成交额≥3亿 + 量比≥1.0 + 涨跌幅绝对值≥1%</span></div>';return;}
  cands.sort(function(a,b){return(b.bt_vol_ratio||0)-(a.bt_vol_ratio||0);});
  var top=cands.slice(0,8);
  var now=new Date();
  var isTradeTime=now.getHours()>=9&&now.getHours()<15;
  el.innerHTML='<div style="text-align:center;padding:20px;color:var(--muted);font-size:13px;">正在获取分时数据...</div>';
  var results=[];
  for(var i=0;i<top.length;i++){
    var s=top[i];
    try{
      var data=await jsonpRequest('https://push2delay.eastmoney.com/api/qt/stock/get?secid='+(s.code.startsWith('6')?'1.':'0.')+s.code+'&fields=f43,f44,f45,f46,f47,f48,f50,f57,f58,f60,f170,f171',8000);
      if(data&&data.data){
        var d=data.data;
        var price=(d.f43||0)/100;
        var high=(d.f44||0)/100;
        var low=(d.f45||0)/100;
        var open=(d.f46||0)/100;
        var vol=d.f47||0;
        var amount=d.f48||0;
        var amplitude=((high-low)/Math.max(0.01,low)*100);
        var vwap=vol>0?amount/vol/100:price;
        var support,resistance,t0Type,advice;
        if(amplitude<1.5){t0Type='观望';support=low;resistance=high;advice='振幅'+amplitude.toFixed(2)+'%不足，不适合做T';}
        else if(price>=vwap){
          support=vwap;resistance=high;t0Type='顺向';
          advice='价格在VWAP<b style="color:var(--up);">上方</b>（偏强），适合<b>顺向做T（先买后卖）</b>：回踩VWAP '+support.toFixed(2)+' 附近低吸，冲高 '+resistance.toFixed(2)+' 附近卖出';
        }else{
          support=low;resistance=vwap;t0Type='逆向';
          advice='价格在VWAP<b style="color:var(--down);">下方</b>（偏弱），适合<b>逆向做T（先卖后买）</b>：反弹至VWAP '+resistance.toFixed(2)+' 附近高抛，回落 '+support.toFixed(2)+' 附近买回';
        }
        results.push({s:s,price:price,high:high,low:low,open:open,amplitude:amplitude,vwap:vwap,support:support,resistance:resistance,t0Type:t0Type,advice:advice,amount:amount});
      }
    }catch(e){}
  }
  if(results.length===0){el.innerHTML='<div style="text-align:center;padding:30px;color:var(--muted);">分时数据获取失败，请稍后重试</div>';return;}
  results.sort(function(a,b){return b.amplitude-a.amplitude;});
  var h='';
  results.forEach(function(r){
    var s=r.s;
    var typeColor=r.t0Type==='顺向'?'var(--up)':(r.t0Type==='逆向'?'var(--down)':'var(--muted)');
    var typeBadge=r.t0Type==='顺向'?'买入':(r.t0Type==='逆向'?'卖出':'观望');
    h+='<div style="margin-bottom:8px;padding:8px 10px;border-radius:8px;background:var(--bg3);border-left:3px solid '+typeColor+';cursor:pointer;" onclick="showDetail(\''+s.code+'\')">';
    h+='<div style="display:flex;justify-content:space-between;align-items:center;">';
    h+='<div><span style="font-weight:700;font-size:14px;">'+s.name+'</span> <span style="font-size:11px;color:var(--muted);">'+s.code+'</span></div>';
    h+='<div><span style="font-size:11px;padding:2px 8px;border-radius:6px;background:'+typeColor+'22;color:'+typeColor+';">'+r.t0Type+'做T</span></div>';
    h+='</div>';
    h+='<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:4px;margin-top:6px;font-size:11px;">';
    h+='<div><span style="color:var(--muted);">现价</span><br><b style="color:'+(s.pct_change>=0?'var(--up)':'var(--down)')+';">'+r.price.toFixed(2)+'</b></div>';
    h+='<div><span style="color:var(--muted);">支撑</span><br><b style="color:var(--down);">'+r.support.toFixed(2)+'</b></div>';
    h+='<div><span style="color:var(--muted);">压力</span><br><b style="color:var(--up);">'+r.resistance.toFixed(2)+'</b></div>';
    h+='<div><span style="color:var(--muted);">VWAP</span><br><b style="color:var(--gold);">'+r.vwap.toFixed(2)+'</b></div>';
    h+='<div><span style="color:var(--muted);">振幅</span><br><b>'+r.amplitude.toFixed(2)+'%</b></div>';
    h+='</div>';
    h+='<div style="margin-top:5px;font-size:10px;color:var(--muted);line-height:1.5;">'+r.advice+' · 成交'+(r.amount/1e8).toFixed(1)+'亿 · 量比'+(s.bt_vol_ratio||0).toFixed(1)+'</div>';
    h+='</div>';
  });
  el.innerHTML=h;
}
function renderTodayHot(){
  const el=document.getElementById('todayHot');if(!el)return;
  if(!modVisible('todayhot'))return;
  const hotMap=window._hotSectorStocks||{};
  // 扫描池 代码→胜率 索引，用于给题材机会个股补充量化胜率
  const wrMap={};
  scanData.forEach(s=>{wrMap[s.code]={win_rate:s.bt_win_rate||0,sq:s.signal_quality||50};});
  // 扫描池行业→个股映射（作为机会个股兜底）
  const indMap={};
  scanData.forEach(s=>{const ind=s.industry||'';if(!ind||ind==='未知')return;(indMap[ind]=indMap[ind]||[]).push(s);});
  function matchStocks(sectorName){
    let hits=[];
    for(const ind in indMap){
      if(ind===sectorName||ind.indexOf(sectorName)>=0||sectorName.indexOf(ind)>=0){
        hits=hits.concat(indMap[ind]);
      }
    }
    return hits;
  }
  function renderOpp(st){
    const wr=st.win_rate||0;
    const pct=parseFloat(st.pct_change)||0;
    return '<span style="display:inline-block;margin:1px 3px;padding:1px 7px;background:var(--bg3);border-radius:3px;cursor:pointer;" onclick="showDetailAny(\''+st.code+'\',\''+String(st.name||st.code).replace(/'/g,'')+'\')" title="'+(wr>0?('回测胜率'+wr.toFixed(1)+'% · 信号质量'+st.sq.toFixed(0)):(pct>=0?'今日上涨':'今日下跌'))+'">'+st.name+' <b style="color:'+(pct>=0?'var(--up)':'var(--down)')+';">'+(pct>=0?'+':'')+pct.toFixed(1)+'%</b>'+(wr>0?' <b style="color:var(--gold);">'+wr.toFixed(0)+'%</b>':'')+'</span>';
  }
  let h='';
  const sectors=window._fullMarketSectors||[];
  // 1. 最强题材方向 → 机会个股（题材成分股TOP，而非全市场涨幅榜）
  const topSec=sectors.filter(s=>s.pct_change>0).sort((a,b)=>b.pct_change-a.pct_change).slice(0,6);
  if(topSec.length){
    h+='<div style="margin-bottom:4px;"><b style="color:var(--up);">🔥 最强题材方向</b> <span style="font-size:10px;color:var(--muted);">（题材 → 机会个股）</span></div>';
    topSec.forEach(s=>{
      const pct=parseFloat(s.pct_change)||0;
      h+='<div style="padding:5px 0;border-bottom:1px dashed var(--rule);">';
      h+='<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;"><span style="display:inline-block;padding:2px 8px;border-radius:6px;background:var(--up-bg);color:var(--up);font-size:12px;font-weight:600;">'+s.name+'</span><span style="font-size:12px;color:var(--up);font-weight:700;">+'+pct.toFixed(2)+'%</span></div>';
      h+='<div style="font-size:11px;line-height:1.7;">';
      const hs=(hotMap[s.name]||[]).map(m=>{
        const w=wrMap[m.code]||{};
        return {code:m.code,name:m.name,pct_change:m.pct_change,win_rate:w.win_rate||0,sq:w.sq||50};
      });
      const opps=hs.length?hs:matchStocks(s.name).slice(0,5).map(m=>({code:m.code,name:m.name,pct_change:m.pct_change||0,win_rate:m.bt_win_rate||0,sq:m.signal_quality||50}));
      if(opps.length){
        h+='<span style="color:var(--muted);">机会个股：</span>';
        h+=opps.slice(0,5).map(renderOpp).join('');
      }else if(s.lead_stock&&s.lead_stock!=='--'){
        h+='<span style="color:var(--muted);">领涨：</span>';
        if(s.lead_code){
          h+='<span style="display:inline-block;padding:1px 7px;background:var(--bg3);border-radius:3px;cursor:pointer;" onclick="showDetailAny(\''+s.lead_code+'\',\''+String(s.lead_stock).replace(/'/g,'')+'\')">'+s.lead_stock+'</span>';
        }else{
          h+='<span style="display:inline-block;padding:1px 7px;background:var(--bg3);border-radius:3px;">'+s.lead_stock+'</span>';
        }
      }else{
        h+='<span style="color:var(--muted);">暂无匹配个股</span>';
      }
      h+='</div></div>';
    });
  }else{
    h+='<div style="color:var(--muted);text-align:center;padding:20px;">当前无上涨题材（或全市场板块数据加载中）</div>';
  }
  // 2. 最弱板块
  const weakSec=sectors.filter(s=>s.pct_change<0).sort((a,b)=>a.pct_change-b.pct_change).slice(0,3);
  if(weakSec.length){h+='<div style="margin-top:10px;margin-bottom:4px;"><b style="color:var(--down);">❄️ 最弱板块：</b>';weakSec.forEach(s=>{h+='<span style="display:inline-block;padding:3px 8px;margin:2px;border-radius:6px;background:var(--down-bg);color:var(--down);font-size:12px;">'+s.name+' '+(parseFloat(s.pct_change)||0).toFixed(2)+'%</span>';});h+='</div>';}
  // 3. 量化信号精选（按回测胜率，非涨幅榜）
  const buys=buySignals.slice().sort((a,b)=>b.win_rate-a.win_rate);
  if(buys.length){h+='<div style="margin-top:10px;"><b style="color:var(--blue);">🎯 量化信号精选：</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;cursor:pointer;" onclick="showDetail(\''+s.code+'\')">'+s.name+' 胜率'+s.win_rate.toFixed(0)+'%</span>';});h+='</div>';}
  el.innerHTML=h||'<div style="color:var(--muted);text-align:center;padding:20px;">暂无数据</div>';
}
function renderT0FallbackTable(etfs){
  return '<table style="width:100%;font-size:11px;"><thead><tr><th title="ETF代码">代码</th><th title="ETF名称">名称</th><th title="实时涨跌幅">涨跌</th><th title="成交额">成交</th><th title="T+0类型">类型</th></tr></thead><tbody>'+
    etfs.map(e=>{
      const cls=e.pct_change>=0?'pu':'pd';
      return '<tr><td>'+e.code+'</td><td style="font-weight:600;cursor:pointer;color:var(--blue);" onclick="showEtfDetail(\''+e.code+'\')">'+e.name+'</td><td class="'+cls+'">'+(e.pct_change>=0?'+':'')+e.pct_change.toFixed(2)+'%</td><td style="color:var(--muted);">'+e.amount+'</td><td style="font-size:10px;color:var(--gold);">'+e.type+'</td></tr>';
    }).join('')+'</tbody></table>';
}
async function renderT0List(){
  const el=document.getElementById('t0List');if(!el)return;
  if(!modVisible('t0etf'))return;
  const etfs=window._t0EtfList||[];
  if(!etfs.length){el.innerHTML='<div style="color:var(--muted);text-align:center;padding:30px;">暂无T+0 ETF数据<br><span style="font-size:10px;">T+0 ETF包括跨境/债券/黄金/商品ETF（货币ETF已剔除）</span></div>';return;}
  el.innerHTML='<div style="color:var(--muted);text-align:center;padding:18px;font-size:12px;">幻方多因子模型分析 T+0 交易机会中...</div>';
  const top=etfs.slice(0,20);
  const res=[];
  // 分批并发拉取分时（每批5只），比逐只串行快4倍，降低行情源限流风险
  for(let bStart=0;bStart<top.length;bStart+=5){
    const batch=top.slice(bStart,bStart+5);
    const rs=await Promise.all(batch.map(e=>fetchIntradayData(e.code).then(bars=>{
      if(bars&&bars.length>10){
        const t0=analyzeT0(e.code,bars,e.price,e.type);
        if(t0&&t0.entry&&t0.target&&t0.stop&&t0.badge!=='wait')return{e:e,t0:t0};
      }
      return null;
    }).catch(()=>null)));
    rs.forEach(r=>{if(r)res.push(r);});
  }
  if(!res.length){
    // 无达标机会时给出最接近的参考：按当日涨跌幅排序展示候选池概况，而非干等
    const refs=etfs.slice(0,6).map(e=>'<span style="display:inline-block;margin:2px;padding:2px 8px;background:var(--bg3);border-radius:4px;font-size:11px;cursor:pointer;" onclick="showEtfDetail(\''+e.code+'\')">'+e.name+' <b style="color:'+(e.pct_change>=0?'var(--up)':'var(--down)')+';">'+(e.pct_change>=0?'+':'')+e.pct_change.toFixed(2)+'%</b></span>').join('');
    el.innerHTML='<div style="color:var(--muted);text-align:center;padding:20px;font-size:12px;line-height:1.9;">当前无适合做T的机会<br><span style="font-size:11px;">（全部候选振幅/方向/量能未达类型及格线：跨境1.0% · 黄金/商品0.6% · 债券0.2%）</span><br><div style="margin-top:8px;text-align:left;"><span style="font-size:10px;color:var(--muted);">候选池概览（点击查看详情）：</span><div style="margin-top:4px;">'+refs+'</div></div></div>';
    return;
  }
  res.sort((a,b)=>b.t0.winProb-a.t0.winProb);
  const shown=res.slice(0,10);
  let h='<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:2px;"><span style="font-size:10px;color:var(--muted);">已分析 '+top.length+' 只 · 达标 '+res.length+' 只 · 展示前 '+shown.length+' 只</span><span style="font-size:10px;color:var(--muted);">按胜率排序</span></div>';
  h+='<div style="display:flex;flex-direction:column;gap:8px;">';
  shown.forEach(({e,t0})=>{
    const pc=t0.winProb>=60?'var(--up)':(t0.winProb>=52?'var(--gold)':'var(--muted)');
    const lb=t0.badge==='sell';
    const inLb=lb?'高抛价':'低吸价', outLb=lb?'低吸价':'高抛价';
    const topF=(t0.factors||[]).filter(f=>f.on).slice(0,3).map(f=>f.name).join('·');
    const reason=(t0.action||'').replace(/<[^>]+>/g,'');
    h+='<div style="background:var(--bg3);border-radius:8px;padding:8px 10px;border-left:3px solid '+pc+';cursor:pointer;" onclick="showEtfDetail(\''+e.code+'\')">';
    h+='<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;"><b style="font-size:13px;">'+e.name+'</b><span style="font-size:10px;color:var(--muted);">'+e.type+'</span></div>';
    h+='<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px;flex-wrap:wrap;">';
    h+='<span class="t0-badge '+t0.badge+'">'+t0.signal+'</span>';
    h+='<span style="font-size:12px;font-weight:800;color:'+pc+';">胜率 '+t0.winProb+'%</span>';
    h+='<span style="font-size:11px;color:var(--muted);">'+t0.grade+'级 · '+t0.score+'分</span></div>';
    h+='<div style="display:flex;gap:10px;font-size:11px;flex-wrap:wrap;line-height:1.6;">';
    h+='<span><b style="color:var(--blue);">'+inLb+'</b> '+t0.entry.toFixed(3)+'</span>';
    h+='<span><b style="color:var(--up);">'+outLb+'</b> '+t0.target.toFixed(3)+'</span>';
    h+='<span><b style="color:var(--down);">止损</b> '+t0.stop.toFixed(3)+'</span>';
    h+='<span><b>空间</b> '+(t0.revExp>=0?'+':'')+t0.revExp.toFixed(2)+'%</span></div>';
    h+='<div style="display:flex;gap:8px;font-size:10px;color:var(--muted);margin-top:4px;flex-wrap:wrap;">';
    h+='<span>振幅 '+t0.amplitude.toFixed(2)+'%</span><span>量比 '+t0.volRatio.toFixed(2)+'</span><span>ATR '+t0.atrPct.toFixed(1)+'%</span><span>动量 '+(t0.momentumPct>=0?'+':'')+t0.momentumPct.toFixed(2)+'%</span>'+(topF?'<span style="color:var(--gold);">'+topF+'</span>':'');
    h+='</div>';
    h+='<div style="font-size:10.5px;color:var(--muted);margin-top:6px;line-height:1.65;background:var(--bg2);border-left:2px solid var(--gold);padding:5px 8px;border-radius:4px;"><b style="color:var(--gold);">操作依据</b> '+reason+'</div>';
    h+='<div style="display:flex;gap:6px;margin-top:6px;" onclick="event.stopPropagation();">'
      +'<button onclick="paperT0QuickBuy(\''+e.code+'\',\''+String(e.name).replace(/'/g,'')+'\','+t0.price+')" style="flex:1;background:var(--up);color:#fff;border:none;border-radius:5px;padding:4px 0;font-size:11px;font-weight:700;cursor:pointer;">买入</button>'
      +'<button onclick="paperT0QuickSell(\''+e.code+'\',\''+String(e.name).replace(/'/g,'')+'\','+t0.price+')" style="flex:1;background:var(--down);color:#fff;border:none;border-radius:5px;padding:4px 0;font-size:11px;font-weight:700;cursor:pointer;">卖出</button></div>';
    h+='</div>';
  });
  h+='</div>';
  el.innerHTML=h;
}
async function showEtfDetail(code){
  const e=(window._t0EtfList||[]).find(x=>x.code===code);
  if(!e)return;
  let html='<div style="padding:12px 4px;"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;"><div><span style="font-size:22px;font-weight:800;">'+e.name+'</span> <span style="color:var(--muted);">'+e.code+'</span></div><span class="badge '+(e.pct_change>=0?'buy':'sell')+'">'+(e.pct_change>=0?'+':'')+e.pct_change.toFixed(2)+'%</span></div>';
  html+='<div class="dt-grid">';
  html+='<div class="dt-cell"><div class="v '+(e.pct_change>=0?'pu':'pd')+'">'+(e.pct_change>=0?'+':'')+e.pct_change.toFixed(2)+'%</div><div class="l">涨跌幅</div></div>';
  html+='<div class="dt-cell"><div class="v">'+e.amount+'</div><div class="l">成交额</div></div>';
  html+='<div class="dt-cell"><div class="v gold">'+e.type+'</div><div class="l">ETF类型</div></div>';
  html+='<div class="dt-cell"><div class="v">'+(e.turnover||'--')+'%</div><div class="l">换手率</div></div>';
  html+='</div>';
  html+='<div class="watch-advice" style="margin-top:12px;"><b style="color:var(--blue);">交易策略：</b>'+e.strategy+'</div>';
  html+='<div style="display:flex;gap:8px;margin-top:12px;"><button onclick="paperQuickPanel(\''+e.code+'\',\''+String(e.name).replace(/'/g,'')+'\','+(e.price||0)+',true,\'B\')" style="flex:1;background:var(--up);color:#fff;border:none;border-radius:8px;padding:8px 0;font-size:13px;font-weight:700;cursor:pointer;">T+0 模拟买入</button><button onclick="paperQuickPanel(\''+e.code+'\',\''+String(e.name).replace(/'/g,'')+'\','+(e.price||0)+',true,\'S\')" style="flex:1;background:var(--down);color:#fff;border:none;border-radius:8px;padding:8px 0;font-size:13px;font-weight:700;cursor:pointer;">T+0 模拟卖出</button></div>';
  html+='<div id="etfT0Box" style="margin-top:12px;"><div style="text-align:center;color:var(--muted);font-size:12px;padding:12px;">T+0日内回转分析加载中...</div></div>';
  html+='<div style="margin-top:10px;font-size:11px;color:var(--muted);line-height:1.6;">该ETF支持当日回转交易（T+0），当日买入当日可卖出。适合日内波段操作和跨市场套利。</div></div>';
  document.getElementById('detailContent').innerHTML=html;openOverlay('detailOverlay');
  try{
    const bars=await fetchIntradayData(code);
    if(bars&&bars.length>10){
      const t0=analyzeT0(code,bars,e.price,e.type);
      if(t0){
        const box=document.getElementById('etfT0Box');
        if(box){box.innerHTML=renderT0Box(code,t0,'etfDetailChart_');renderT0Chart(code,t0,'etfDetailChart_');}
      }
    }
  }catch(err){}
}
// ===== 需求4: 全市场板块轮动 =====
function renderFullMarketRotation(){
  const boards=window._fmBoards;
  if(boards&&boards.length)window._fullMarketSectors=boards.map(b=>({code:b.code,name:b.name,pct_change:b.pct,amount:(b.amtYi>0?b.amtYi.toFixed(1)+'亿':'--'),lead_stock:b.lead_stock||'--',lead_code:b.lead_code||''}));
  renderFullMarketRotationTable();
}
async function refreshFullMarketRotation(){
  await fetchFullMarketBoards();
  renderRotationInsight();renderRotationQuadrant();renderRotationLadder();renderChainInsight();renderChainGraph();renderFullMarketRotation();
  renderHeatmap();
}
function renderFullMarketRotationTable(){
  const el=document.getElementById('fullMarketRotation');if(!el)return;
  const sumEl=document.getElementById('fullMarketRotationSummary');
  if(!window._fullMarketSectors){el.innerHTML='<div style="color:var(--muted);text-align:center;padding:20px;">全市场板块数据加载中...</div>';return;}
  const data=window._fullMarketSectors;
  // 全市场轮动摘要
  if(sumEl){
    const up=data.filter(s=>(s.pct_change||0)>0);
    const down=data.filter(s=>(s.pct_change||0)<0);
    const flat=data.filter(s=>(s.pct_change||0)===0);
    const sum=data.reduce((a,s)=>a+(s.pct_change||0),0);
    const avg=data.length?(sum/data.length):0;
    const ratio=down.length>0?(up.length/down.length):(up.length>0?99:1);
    let heat,heatColor,heatNote;
    if(ratio>=1.5){heat='轮动活跃 · 多方主导';heatColor='var(--up)';heatNote='上涨板块显著多于下跌板块，主线清晰，可跟随领涨主线顺势配置。';}
    else if(ratio>=1){heat='结构分化 · 均衡震荡';heatColor='var(--gold)';heatNote='涨跌板块大致相当，资金高低切换，重点锁定领涨强势板块，回避退潮方向。';}
    else{heat='轮动降温 · 空方占优';heatColor='var(--down)';heatNote='下跌板块居多，赚钱效应弱，控制仓位，等待冰点后的回暖信号。';}
    const tops=data.slice(0,3).map((s,i)=>(i+1)+'.'+s.name+' +'+(s.pct_change||0).toFixed(2)+'%').join('　');
    const bots=data.slice(-3).reverse().map((s,i)=>(i+1)+'.'+s.name+' '+(s.pct_change||0).toFixed(2)+'%').join('　');
    sumEl.innerHTML='<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:stretch;font-size:12px;">'+
      '<div style="flex:1;min-width:180px;background:var(--bg3);border-radius:8px;padding:9px 12px;"><div style="color:var(--muted);font-size:10px;margin-bottom:3px;">全市场板块热度</div><b style="color:'+heatColor+';font-size:14px;">'+heat+'</b><div style="color:var(--muted);font-size:11px;line-height:1.6;margin-top:3px;">'+heatNote+'</div></div>'+
      '<div style="flex:1;min-width:180px;background:var(--bg3);border-radius:8px;padding:9px 12px;"><div style="color:var(--muted);font-size:10px;margin-bottom:3px;">板块涨跌分布</div><span style="color:var(--up);font-weight:700;">上涨 '+up.length+'</span> <span style="color:var(--muted);">/</span> <span style="color:var(--down);font-weight:700;">下跌 '+down.length+'</span> <span style="color:var(--muted);">/ 平盘 '+flat.length+'</span><div style="color:var(--muted);font-size:11px;margin-top:3px;">平均涨跌 <b style="color:'+(avg>=0?'var(--up)':'var(--down)')+';">'+(avg>=0?'+':'')+avg.toFixed(2)+'%</b></div></div>'+
      '<div style="flex:2;min-width:220px;background:var(--bg3);border-radius:8px;padding:9px 12px;"><div style="color:var(--muted);font-size:10px;margin-bottom:3px;">领涨主线</div><span style="color:var(--up);line-height:1.6;">'+tops+'</span><div style="color:var(--muted);font-size:10px;margin:4px 0 3px;">领跌方向</div><span style="color:var(--down);line-height:1.6;">'+bots+'</span></div>'+
      '</div>';
  }
  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.slice(0,20).map(s=>{
      const pct=s.pct_change||0;
      const cls=pct>=0?'pu':'pd';
      const str=pct>=2?'🔥强势':pct>=0?'📈偏强':pct>=-2?'📉偏弱':'❄️弱势';
      const lead=s.lead_code?( '<span style="cursor:pointer;color:var(--blue);" onclick="showDetailAny(\''+s.lead_code+'\',\''+String(s.lead_stock||s.lead_code).replace(/'/g,'')+'\')" title="点击查看 '+s.lead_stock+' 详情">'+(s.lead_stock||s.lead_code)+'</span>'):(s.lead_stock||'--');
      return '<tr><td style="font-weight:600;">'+s.name+'</td><td class="'+cls+'">'+(pct>=0?'+':'')+pct.toFixed(2)+'%</td><td style="font-size:11px;">'+lead+'</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('');
}
// ===== 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>';
}

// ============================================================
// ===== 模块化看板系统（首页模块自定义：开关 + 排序 + 持久化） =====
// ============================================================
const MODULE_DEFS=[
  {id:'idxtpo',cat:'市场状态',name:'大盘指数·TPO四度空间',desc:'指数Market Profile价值区/POC分析，日内结构定位与操作建议',def:true},
  {id:'gauge',cat:'市场状态',name:'多空偏向仪表盘',desc:'全市场多空力量综合读数，开盘第一眼定基调：进攻还是防守',def:true},
  {id:'breadth',cat:'市场状态',name:'市场宽度分析',desc:'A股全市场涨跌家数与涨跌比，宽度确认指数行情的真实性',def:true},
  {id:'rsgrid',cat:'市场状态',name:'指数相对强度',desc:'五大指数当日/5日/20日强度对比，快速锁定领涨主线',def:true},
  {id:'emotion',cat:'情绪与主线',name:'涨停情绪周期',desc:'六阶段情绪温度计，决定总仓位与打法激进程度',def:true},
  {id:'ladder',cat:'情绪与主线',name:'涨停梯队·连板天梯',desc:'连板高度与梯队结构，锁定主线龙头与空间板',def:true},
  {id:'todayhot',cat:'情绪与主线',name:'今天炒什么',desc:'全市场热门方向与机会个股（可点击查看详情）',def:true},
  {id:'marketpulse',cat:'市场异动',name:'市场异动监测',desc:'板块急涨急跌+量比异动+资金异动+领涨领跌全景扫描',def:true},
  {id:'momentum',cat:'交易机会',name:'蓄势待发',desc:'高评分+缩量整理+临近突破位，锁定即将启动个股',def:true},
  {id:'stockt0',cat:'交易机会',name:'个股做T参考',desc:'Top50个股日内支撑压力位+量价配合+T0买卖点',def:true},
  {id:'econcal',cat:'市场异动',name:'重磅事件日历',desc:'未来30天重磅经济事件+利好利空研判+市场影响分析',def:true},
  {id:'updown',cat:'市场结构',name:'全市场涨跌幅分布',desc:'13档涨跌幅直方图，观察市场分化与极端程度',def:true},
  {id:'t0etf',cat:'交易机会',name:'场内T+0 ETF机会',desc:'日内回转标的、具体买卖点与操作依据',def:true},
  {id:'turnover',cat:'资金面',name:'两市成交额监控',desc:'沪深分时累计成交额，与昨日同期对比判断放量/缩量',def:true},
  {id:'moneyflow',cat:'资金面',name:'主力资金流向',desc:'板块主力净流入排行，验证主线是真金白银还是纯情绪',def:true},
  {id:'factorstyle',cat:'量化因子',name:'风格因子表现',desc:'大小盘/成长防御轮动监控，量化机构风格暴露视角',def:true},
  {id:'volmonitor',cat:'风险控制',name:'波动率监控',desc:'日内已实现波动率年化，风控开关与仓位上限参考',def:true},
  {id:'idxfly',cat:'风险控制',name:'全球市场·隔夜与亚太',desc:'美股/港股/日经/离岸人民币，隔夜风险传导第一道防线',def:true},
  {id:'sigdist',cat:'统计反馈',name:'信号分布',desc:'Top50池操作建议分布，当日信号结构',def:true},
  {id:'indtop',cat:'统计反馈',name:'指标触发频次Top10',desc:'当日指标触发统计，观察市场在交易什么逻辑',def:true},
  {id:'btwin',cat:'统计反馈',name:'回测胜率统计',desc:'策略历史胜率与量化过滤提升，执行前的信心校验',def:true},
  {id:'davsenti',cat:'情绪与主线',name:'大V动态·情绪风向标',desc:'监测微博(52198/唐史主任司马迁/狂龙十八段)+雪球(质变之路)+微信(爱在冰川/景峰看盘)最新发言情绪，看多/看空关键词分析',def:true},
];
const LAYOUT_KEY='ashare_mod_layout_v2';
function defLayoutOrder(){return MODULE_DEFS.map(m=>m.id);}
function getLayoutCfg(){
  let order=defLayoutOrder().slice(),off=MODULE_DEFS.filter(m=>!m.def).map(m=>m.id);
  try{
    const raw=localStorage.getItem(LAYOUT_KEY);
    if(raw){const c=JSON.parse(raw);if(Array.isArray(c.order))order=c.order;if(Array.isArray(c.off))off=c.off;}
  }catch(e){}
  defLayoutOrder().forEach(id=>{if(order.indexOf(id)<0)order.push(id);});
  return{order:order,off:off};
}
function saveLayoutCfg(cfg){try{localStorage.setItem(LAYOUT_KEY,JSON.stringify(cfg));}catch(e){}}
function modVisible(id){return getLayoutCfg().off.indexOf(id)<0;}
const LAZY_RENDER={
  idxtpo:function(){renderIdxTpo();},
  gauge:function(){renderL1Charts();},
  breadth:function(){renderBreadthChart();},
  sigdist:function(){renderL1Charts();},
  indtop:function(){renderL1Charts();},
  updown:function(){renderUpDownDist();},
  todayhot:function(){renderTodayHot();},
  t0etf:function(){renderT0List();},
  emotion:function(){renderEmotionCycle();},
  ladder:function(){renderLadder();},
  btwin:function(){renderMarketTrend();},
  davsenti:function(){renderDavSenti();},
  turnover:function(){fetchTurnover();},
  moneyflow:function(){fetchMoneyFlow();},
  volmonitor:function(){fetchVolMonitor();},
  idxfly:function(){fetchGlobalMarkets();},
  rsgrid:function(){fetchRSGrid();},
  factorstyle:function(){fetchFactorStyle();},
};
function applyLayout(){
  const cfg=getLayoutCfg();
  const grid=document.getElementById('modGrid');if(!grid)return;
  const wraps={};
  grid.querySelectorAll('.module-wrap').forEach(w=>{wraps[w.dataset.mod]=w;});
  cfg.order.forEach(id=>{if(wraps[id])grid.appendChild(wraps[id]);});
  Object.keys(wraps).forEach(id=>{if(cfg.order.indexOf(id)<0)grid.appendChild(wraps[id]);});
  Object.keys(wraps).forEach(id=>{
    const w=wraps[id];
    if(cfg.off.indexOf(id)>=0){w.setAttribute('hidden','');}
    else{
      w.removeAttribute('hidden');
      if(!w.dataset.loaded){w.dataset.loaded='1';const fn=LAZY_RENDER[id];if(fn){try{fn();}catch(e){console.log('lazy render '+id+' failed',e);}}}
    }
  });
  setTimeout(function(){Object.values(chartInstances).forEach(c=>{if(c&&c.resize)try{c.resize();}catch(e){}});},150);
  enableModDrag();
}
function openLayoutPanel(){renderLayoutPanel();openOverlay('layoutOverlay');}
function renderLayoutPanel(){
  const cfg=getLayoutCfg();
  const body=document.getElementById('layoutPanelBody');
  let html='';
  cfg.order.forEach(function(id,idx){
    const def=MODULE_DEFS.find(m=>m.id===id);if(!def)return;
    const on=cfg.off.indexOf(id)<0;
    html+='<div class="lp-row'+(on?'':' lp-off')+'" data-mod="'+id+'">'
      +'<input type="checkbox" '+(on?'checked':'')+' onchange="toggleModule(\''+id+'\',this.checked)">'
      +'<span class="lp-name">'+def.name+'</span>'
      +'<span style="font-size:10px;background:var(--bg3);border-radius:4px;padding:1px 6px;color:var(--gold);white-space:nowrap;">'+def.cat+'</span>'
      +'<span class="lp-desc">'+def.desc+'</span>'
      +'<button class="lp-btn" onclick="moveModule(\''+id+'\',-1)" title="上移">↑</button>'
      +'<button class="lp-btn" onclick="moveModule(\''+id+'\',1)" title="下移">↓</button>'
      +'</div>';
  });
  body.innerHTML=html;
  enablePanelDrag();
}
function toggleModule(id,on){
  const cfg=getLayoutCfg();
  if(on){cfg.off=cfg.off.filter(x=>x!==id);}
  else if(cfg.off.indexOf(id)<0){cfg.off.push(id);}
  saveLayoutCfg(cfg);
  applyLayout();
}
function moveModule(id,dir){
  const cfg=getLayoutCfg();
  const i=cfg.order.indexOf(id),j=i+dir;
  if(i<0||j<0||j>=cfg.order.length)return;
  const t=cfg.order[i];cfg.order[i]=cfg.order[j];cfg.order[j]=t;
  saveLayoutCfg(cfg);
  applyLayout();
  renderLayoutPanel();
}
function resetLayout(){
  try{localStorage.removeItem(LAYOUT_KEY);}catch(e){}
  applyLayout();
  renderLayoutPanel();
}
// ============================================================
// ===== 模块拖拽排序（页面网格 + 配置面板列表双入口） =====
// ============================================================
let dragModId=null;
function clearDropMarks(){
  document.querySelectorAll('.drop-mark-before,.drop-mark-after,.drop-mark-top,.drop-mark-bottom').forEach(function(x){x.classList.remove('drop-mark-before','drop-mark-after','drop-mark-top','drop-mark-bottom');});
}
function dropReorder(fromId,toId,insertBefore){
  if(!fromId||fromId===toId)return;
  const cfg=getLayoutCfg();
  const fi=cfg.order.indexOf(fromId);
  if(fi<0)return;
  cfg.order.splice(fi,1);
  let ti=cfg.order.indexOf(toId);
  if(ti<0){cfg.order.push(fromId);}
  else{
    if(!insertBefore)ti=ti+1;
    cfg.order.splice(ti,0,fromId);
  }
  saveLayoutCfg(cfg);
  applyLayout();
  const ov=document.getElementById('layoutOverlay');
  if(ov&&ov.classList.contains('active'))renderLayoutPanel();
}
function enableModDrag(){
  document.querySelectorAll('#modGrid .module-wrap').forEach(function(w){
    if(w.dataset.dragBound)return;
    w.dataset.dragBound='1';
    w.setAttribute('draggable','true');
    w.addEventListener('dragstart',function(e){
      dragModId=w.dataset.mod;
      w.classList.add('dragging');
      try{e.dataTransfer.setData('text/plain',dragModId);e.dataTransfer.effectAllowed='move';}catch(err){}
    });
    w.addEventListener('dragend',function(){w.classList.remove('dragging');clearDropMarks();dragModId=null;});
    w.addEventListener('dragover',function(e){
      if(!dragModId)return;
      e.preventDefault();
      try{e.dataTransfer.dropEffect='move';}catch(err){}
      const r=w.getBoundingClientRect();
      const dx=Math.abs(e.clientX-(r.left+r.width/2));
      const dy=Math.abs(e.clientY-(r.top+r.height/2));
      clearDropMarks();
      if(dx>=dy){w.classList.add(e.clientX<r.left+r.width/2?'drop-mark-before':'drop-mark-after');}
      else{w.classList.add(e.clientY<r.top+r.height/2?'drop-mark-top':'drop-mark-bottom');}
    });
    w.addEventListener('dragleave',function(){w.classList.remove('drop-mark-before','drop-mark-after','drop-mark-top','drop-mark-bottom');});
    w.addEventListener('drop',function(e){
      if(!dragModId)return;
      e.preventDefault();
      const before=w.classList.contains('drop-mark-before')||w.classList.contains('drop-mark-top');
      const toId=w.dataset.mod;
      clearDropMarks();
      dropReorder(dragModId,toId,before);
      dragModId=null;
    });
  });
}
function enablePanelDrag(){
  document.querySelectorAll('#layoutPanelBody .lp-row').forEach(function(row){
    if(row.dataset.dragBound)return;
    row.dataset.dragBound='1';
    row.setAttribute('draggable','true');
    row.addEventListener('dragstart',function(e){
      dragModId=row.dataset.mod;
      row.classList.add('dragging');
      try{e.dataTransfer.setData('text/plain',dragModId);e.dataTransfer.effectAllowed='move';}catch(err){}
    });
    row.addEventListener('dragend',function(){row.classList.remove('dragging');clearDropMarks();dragModId=null;});
    row.addEventListener('dragover',function(e){
      if(!dragModId)return;
      e.preventDefault();
      clearDropMarks();
      row.classList.add(e.clientY<row.getBoundingClientRect().top+row.offsetHeight/2?'drop-mark-before':'drop-mark-after');
    });
    row.addEventListener('drop',function(e){
      if(!dragModId)return;
      e.preventDefault();
      const before=row.classList.contains('drop-mark-before');
      clearDropMarks();
      dropReorder(dragModId,row.dataset.mod,before);
      dragModId=null;
    });
  });
}
// ============================================================
// ===== 新增模块数据引擎 =====
// ============================================================
// --- 两市成交额监控 ---
async function fetchTurnover(){
  if(!modVisible('turnover'))return;
  const ch=initChartSafe('turnoverChart','turnover');
  if(!ch)return;
  try{
    // f57=每分钟成交额（元）。注意 f58 是均价不是成交额；push2delay 更稳，push2his 备用
    const mkUrl=(secid,host)=>host+'/api/qt/stock/trends2/get?secid='+secid+'&fields1=f1,f2,f3,f7,f8&fields2=f51,f53,f56,f57&iscr=0&ndays=2&ut=fa5fd1943c7b386f172d6893dbfba10b';
    const hosts=['https://push2delay.eastmoney.com','https://push2his.eastmoney.com'];
    async function tryFetch(secid){
      for(const host of hosts){
        const r=await jsonpRequest(mkUrl(secid,host),9000).catch(()=>null);
        if(r&&r.data&&r.data.trends&&r.data.trends.length)return r;
      }
      return null;
    }
    const rs=await Promise.all([tryFetch('1.000001'),tryFetch('0.399001')]);
    // 解析两日分钟数据 → {date:{time:该分钟成交额}}，再转累计值
    function parseDays(r){
      if(!r||!r.data||!r.data.trends)return null;
      const days={};
      r.data.trends.forEach(function(t){
        const p=t.split(',');const d=p[0].slice(0,10);const tm=p[0].slice(11);
        const amt=parseFloat(p[3]||0);
        if(!isFinite(amt))return;
        if(!days[d])days[d]={};
        days[d][tm]=(days[d][tm]||0)+amt;
      });
      const keys=Object.keys(days).sort();
      if(!keys.length)return null;
      // 转为累计序列
      function cum(o){let acc=0;const out={};Object.keys(o).sort().forEach(function(tm){acc+=o[tm];out[tm]=acc;});return out;}
      return{today:cum(days[keys[keys.length-1]]),prev:keys.length>1?cum(days[keys[keys.length-2]]):null,todayKey:keys[keys.length-1]};
    }
    const a=parseDays(rs[0]),b=parseDays(rs[1]);
    if(!a||!b){ch.clear();ch.setOption({title:{text:'成交额数据暂不可用',left:'center',top:'middle',textStyle:{color:tc.muted,fontSize:13}}});return;}
    const times=[],today=[],prev=[];
    Object.keys(a.today).forEach(function(tm){
      const yv=(a.prev&&a.prev[tm]!==undefined?a.prev[tm]:null);
      const bv=(b.prev&&b.prev[tm]!==undefined?b.prev[tm]:null);
      times.push(tm);
      today.push(((a.today[tm]||0)+(b.today[tm]||0))/1e8);
      prev.push((yv!==null&&bv!==null)?(yv+bv)/1e8:null);
    });
    const cur=today[today.length-1]||0;
    let prevSame=0,prevFull=0;
    const lastTm=times[times.length-1];
    if(a.prev&&b.prev){
      const pTimes=Object.keys(a.prev).filter(function(tm){return b.prev[tm]!==undefined;}).sort();
      if(pTimes.length)prevFull=(a.prev[pTimes[pTimes.length-1]]+b.prev[pTimes[pTimes.length-1]])/1e8;
      if(a.prev[lastTm]!==undefined&&b.prev[lastTm]!==undefined)prevSame=(a.prev[lastTm]+b.prev[lastTm])/1e8;
    }
    const vsSame=prevSame>0?((cur/prevSame-1)*100):0;
    const vsFull=prevFull>0?((cur/prevFull-1)*100):0;
    const maxV=Math.max.apply(null,today.filter(v=>v!=null).concat([1]));
    ch.setOption({
      backgroundColor:'transparent',
      tooltip:tipXL({trigger:'axis',formatter:function(ps){
        if(!ps||!ps.length)return'';
        var h='<b>'+ps[0].axisValue+'</b>';
        ps.forEach(function(p){if(p.value!=null)h+='<br/>'+p.marker+p.seriesName+'：'+p.value.toFixed(0)+' 亿元';});
        return h;
      }}),
      legend:{top:0,right:0,textStyle:{color:tc.muted,fontSize:11},itemWidth:14,itemHeight:8},
      grid:{left:56,right:16,top:26,bottom:22},
      xAxis:{type:'category',data:times,axisLabel:{color:tc.muted,fontSize:9,interval:29},axisLine:{lineStyle:{color:tc.rule}}},
      yAxis:{type:'value',axisLabel:{color:tc.muted,fontSize:10,formatter:v=>(v/1000)+'千亿'},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},
      series:[
        {name:'今日累计',type:'line',data:today,symbol:'none',lineStyle:{color:tc.gold,width:2},areaStyle:{color:hexA(tc.gold,0.15)}},
        {name:'昨日同期',type:'line',data:prev,symbol:'none',lineStyle:{color:tc.muted,width:1.2,type:'dashed'}}
      ]
    },true);
    const fmtV=cur>=10000?(cur/10000).toFixed(2)+'万亿':cur.toFixed(0)+'亿';
    const dCol=vsSame>=0?tc.up:tc.down;
    document.getElementById('turnoverStats').innerHTML='<div style="display:flex;flex-wrap:wrap;gap:14px;font-size:12px;padding:2px 4px;">'
      +'<span>当前累计 <b style="color:var(--gold);font-size:15px;">'+fmtV+'</b></span>'
      +'<span>较昨日同期 <b style="color:'+dCol+';">'+(vsSame>=0?'+':'')+vsSame.toFixed(1)+'%</b></span>'
      +'<span>昨日全天 <b>'+((prevFull/10000).toFixed(2))+'万亿</b></span>'
      +(Math.abs(vsSame)>15?'<span style="color:var(--gold);">'+(vsSame>0?'显著放量 · 情绪升温':'显著缩量 · 谨慎为主')+'</span>':'')
      +'</div>';
  }catch(e){
    ch.clear();ch.setOption({title:{text:'成交额加载失败',left:'center',top:'middle',textStyle:{color:tc.muted,fontSize:13}}});
  }
}
// --- 主力资金流向（板块） ---
async function fetchMoneyFlow(){
  if(!modVisible('moneyflow'))return;
  const ch=initChartSafe('moneyflowChart','moneyflow');
  if(!ch)return;
  try{
    const r=await jsonpRequest('https://push2delay.eastmoney.com/api/qt/clist/get?pn=1&pz=100&po=1&np=1&fltt=2&invt=2&fid=f62&fs=m:90+t:2+f:!50&fields=f12,f14,f3,f62,f184',9000);
    const list=((r&&r.data&&r.data.diff)||[]).map(s=>({name:s.f14,pct:s.f3,flow:(s.f62||0)/1e8})).filter(x=>x.name&&isFinite(x.flow));
    if(!list.length)throw new Error('empty');
    const inflow=list.slice(0,8);
    const outflow=list.slice(-8).reverse();
    const names=outflow.map(x=>x.name).concat(inflow.map(x=>x.name));
    const vals=outflow.map(x=>x.flow).concat(inflow.map(x=>x.flow));
    ch.setOption({
      backgroundColor:'transparent',
      tooltip:tipXL({trigger:'axis',axisPointer:{type:'shadow'},formatter:function(ps){
        if(!ps||!ps.length)return'';
        const p=ps[0];const nm=p.name;
        const it=list.find(x=>x.name===nm);
        return '<b>'+nm+'</b><br/>主力净流入：'+p.value.toFixed(2)+' 亿'+(it?'<br/>今日涨跌：'+(it.pct>=0?'+':'')+it.pct+'%':'');
      }}),
      grid:{left:2,right:40,top:10,bottom:6,containLabel:true},
      xAxis:{type:'value',show:false},
      yAxis:{type:'category',data:names,axisLabel:{color:tc.ink,fontSize:11},axisLine:{show:false},axisTick:{show:false}},
      series:[{type:'bar',data:vals.map(v=>({value:v,itemStyle:{color:v>=0?tc.up:tc.down,borderRadius:v>=0?[0,3,3,0]:[3,0,0,3]}})),barWidth:'62%',label:{show:true,position:'right',fontSize:10,color:tc.muted,formatter:p=>p.value.toFixed(1)}}]
    },true);
  }catch(e){
    ch.clear();ch.setOption({title:{text:'主力资金数据暂不可用',left:'center',top:'middle',textStyle:{color:tc.muted,fontSize:13}}});
  }
}
// --- 波动率监控（上证分时已实现波动率） ---
async function fetchVolMonitor(){
  if(!modVisible('volmonitor'))return;
  const box=document.getElementById('volBox');if(!box)return;
  box.innerHTML='<div style="color:var(--muted);font-size:12px;text-align:center;padding:30px 0;">波动率数据加载中...</div>';
  try{
    const r=await jsonpRequest('https://push2his.eastmoney.com/api/qt/stock/trends2/get?secid=1.000001&fields1=f1,f2,f3,f7,f8&fields2=f51,f53,f56,f58&iscr=0&ndays=5',9000);
    if(!r||!r.data||!r.data.trends)throw new Error('empty');
    const days={};
    r.data.trends.forEach(function(t){const p=t.split(',');const d=p[0].slice(0,10);if(!days[d])days[d]=[];days[d].push(parseFloat(p[1]));});
    const keys=Object.keys(days).sort();
    const vols=keys.map(function(d){
      const cl=days[d];if(cl.length<30)return null;
      let s2=0,n=0;
      for(let i=1;i<cl.length;i++){const ret=Math.log(cl[i]/cl[i-1]);s2+=ret*ret;n++;}
      const std=Math.sqrt(s2/n);
      return{day:d,ann:std*Math.sqrt(240*252)*100};
    }).filter(Boolean);
    if(!vols.length)throw new Error('novol');
    const cur=vols[vols.length-1];
    const prevAvg=vols.slice(0,-1).reduce((s,v)=>s+v.ann,0)/Math.max(1,vols.length-1);
    const chg=(cur.ann/prevAvg-1)*100;
    const state=cur.ann>18?'高波动':(cur.ann>12?'中波动':'低波动');
    const advice=cur.ann>18?'波动放大，降低仓位与杠杆，止损收紧':(chg>10?'波动抬升，注意仓位节奏':'波动平稳，正常执行策略');
    const col=cur.ann>18?tc.down:(cur.ann>12?tc.gold:tc.up);
    let bars=vols.map(function(v,i){
      const w=Math.min(v.ann/25*100,100);
      return '<div style="display:flex;align-items:center;gap:8px;margin:7px 0;">'
        +'<span style="font-size:10px;color:var(--muted);width:38px;">'+v.day.slice(5)+'</span>'
        +'<div style="flex:1;height:10px;background:var(--bg3);border-radius:5px;overflow:hidden;"><div style="width:'+w+'%;height:100%;background:'+(v===cur?col:hexA(tc.blue,0.5))+';border-radius:5px;"></div></div>'
        +'<span style="font-size:11px;width:52px;text-align:right;color:'+(v===cur?col:'var(--muted)')+';font-weight:'+(v===cur?700:400)+';">'+v.ann.toFixed(1)+'%</span></div>';
    }).join('');
    box.innerHTML='<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px;">'
      +'<span style="font-size:12px;color:var(--muted);">今日年化波动率</span>'
      +'<span style="font-size:22px;font-weight:800;color:'+col+';">'+cur.ann.toFixed(1)+'%</span></div>'
      +'<div style="font-size:11px;color:var(--muted);margin-bottom:10px;">较4日均值 '+(chg>=0?'+':'')+chg.toFixed(0)+'% · '+state+'</div>'
      +bars
      +'<div style="font-size:11px;color:var(--gold);margin-top:10px;line-height:1.6;border-top:1px solid var(--rule);padding-top:8px;">'+advice+'</div>';
  }catch(e){
    box.innerHTML='<div style="color:var(--muted);font-size:12px;text-align:center;padding:30px 0;">波动率数据暂不可用</div>';
  }
}
// --- 全球市场隔夜 ---
const GLOBAL_SECIDS={'100.NDX':'纳斯达克','100.SPX':'标普500','100.DJIA':'道琼斯','100.HSI':'恒生指数','100.N225':'日经225','133.USDCNH':'离岸人民币'};
async function fetchGlobalMarkets(){
  if(!modVisible('idxfly'))return;
  const box=document.getElementById('globalMkt');if(!box)return;
  try{
    const secids=Object.keys(GLOBAL_SECIDS).join(',');
    const r=await jsonpRequest('https://push2delay.eastmoney.com/api/qt/ulist.np/get?secids='+secids+'&fields=f2,f3,f4,f12,f13,f14&fltt=2&invt=2',9000);
    const list=(r&&r.data&&r.data.diff)||[];
    if(!list.length)throw new Error('empty');
    box.innerHTML=list.map(function(s){
      const nm=GLOBAL_SECIDS[s.f13+'.'+s.f12]||s.f14||s.f12;
      const px=(typeof s.f2==='number')?s.f2:parseFloat(s.f2);
      const pct=(typeof s.f3==='number')?s.f3:parseFloat(s.f3);
      if(!isFinite(px))return '';
      const c=pct>=0?'var(--up)':'var(--down)';
      const isFx=(s.f12==='USDCNH');
      return '<div class="gm-cell"><div class="nm">'+nm+'</div><div class="px" style="color:'+c+';">'+(isFx?px.toFixed(4):px.toFixed(2))+'</div><div class="ct" style="color:'+c+';">'+(pct>=0?'+':'')+pct.toFixed(2)+'%</div></div>';
    }).join('');
  }catch(e){
    box.innerHTML='<div style="color:var(--muted);font-size:12px;text-align:center;padding:30px 0;">全球市场数据暂不可用</div>';
  }
}
// --- 指数相对强度 ---
function idxKlineUrl(secid,naturalDays){
  const d=new Date(Date.now()-naturalDays*864e5);
  const p=n=>String(n).padStart(2,'0');
  return 'https://push2his.eastmoney.com/api/qt/stock/kline/get?secid='+secid+'&fields1=f1,f2,f3&fields2=f51,f53&klt=101&fqt=1&beg='+d.getFullYear()+p(d.getMonth()+1)+p(d.getDate())+'&end=20500101&ut=fa5fd1943c7b386f172d6893dbfba10b';
}
async function fetchRSGrid(){
  if(!modVisible('rsgrid'))return;
  const ch=initChartSafe('rsChart','rsgrid');
  if(!ch)return;
  try{
    const rs=await Promise.all(IDX_SECIDS.map(s=>jsonpRequest(idxKlineUrl(s,50),9000).catch(()=>null)));
    const names=[],d1=[],d5=[],d20=[];
    rs.forEach(function(r,i){
      const code=IDX_SECIDS[i].split('.')[1];
      names.push(IDX_MAP[code]||code);
      if(r&&r.data&&r.data.klines&&r.data.klines.length>5){
        const cl=r.data.klines.map(l=>parseFloat(l.split(',')[1]));
        const last=cl[cl.length-1];
        d1.push((last/cl[cl.length-2]-1)*100);
        d5.push(cl.length>=6?(last/cl[cl.length-6]-1)*100:null);
        d20.push(cl.length>=21?(last/cl[cl.length-21]-1)*100:null);
      }else{d1.push(null);d5.push(null);d20.push(null);}
    });
    ch.setOption({
      backgroundColor:'transparent',
      tooltip:tipXL({trigger:'axis',axisPointer:{type:'shadow'},formatter:function(ps){
        if(!ps||!ps.length)return'';
        var h='<b>'+ps[0].axisValue+'</b>';
        ps.forEach(function(p){if(p.value!=null)h+='<br/>'+p.marker+p.seriesName+'：'+(p.value>=0?'+':'')+p.value.toFixed(2)+'%';});
        return h;
      }}),
      legend:{top:0,right:0,textStyle:{color:tc.muted,fontSize:11},itemWidth:12,itemHeight:8},
      grid:{left:44,right:10,top:28,bottom:24},
      xAxis:{type:'category',data:names,axisLabel:{color:tc.muted,fontSize:10,interval:0},axisLine:{lineStyle:{color:tc.rule}}},
      yAxis:{type:'value',axisLabel:{color:tc.muted,fontSize:10,formatter:v=>v+'%'},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},
      series:[
        {name:'当日',type:'bar',data:d1,barWidth:'22%',itemStyle:{color:p=>p.value>=0?tc.up:tc.down,borderRadius:[2,2,0,0]}},
        {name:'5日',type:'bar',data:d5,barWidth:'22%',itemStyle:{color:hexA(tc.blue,0.75),borderRadius:[2,2,0,0]}},
        {name:'20日',type:'bar',data:d20,barWidth:'22%',itemStyle:{color:hexA(tc.gold,0.55),borderRadius:[2,2,0,0]}}
      ]
    },true);
  }catch(e){
    ch.clear();ch.setOption({title:{text:'相对强度数据暂不可用',left:'center',top:'middle',textStyle:{color:tc.muted,fontSize:13}}});
  }
}
// --- 风格因子表现 ---
const FACTOR_IDX=[['1.000300','沪深300','大盘'],['1.000905','中证500','中盘'],['1.000852','中证1000','小盘'],['0.399006','创业板指','成长'],['1.000688','科创50','科创']];
async function fetchFactorStyle(){
  if(!modVisible('factorstyle'))return;
  const ch=initChartSafe('factorChart','factorstyle');
  if(!ch)return;
  try{
    const rs=await Promise.all(FACTOR_IDX.map(x=>jsonpRequest(idxKlineUrl(x[0],20),9000).catch(()=>null)));
    const names=[],d1=[],d5=[];
    rs.forEach(function(r,i){
      names.push(FACTOR_IDX[i][1]);
      if(r&&r.data&&r.data.klines&&r.data.klines.length>5){
        const cl=r.data.klines.map(l=>parseFloat(l.split(',')[1]));
        const last=cl[cl.length-1];
        d1.push((last/cl[cl.length-2]-1)*100);
        d5.push(cl.length>=6?(last/cl[cl.length-6]-1)*100:null);
      }else{d1.push(null);d5.push(null);}
    });
    const big=d1[0],small=d1[2],growth=(d1[3]+d1[4])/2;
    let concl='';
    if(big!=null&&small!=null){
      const diff=small-big;
      concl+=(diff>0.15?'小盘占优（风格偏题材游资）':(diff<-0.15?'大盘占优（风格偏机构权重）':'大小盘均衡'));
      if(growth!=null&&isFinite(growth))concl+=growth>big+0.15?' · 成长跑赢':' · 价值防守占优';
    }
    ch.setOption({
      backgroundColor:'transparent',
      tooltip:tipXL({trigger:'axis',axisPointer:{type:'shadow'},formatter:function(ps){
        if(!ps||!ps.length)return'';
        var h='<b>'+ps[0].axisValue+'</b>';
        ps.forEach(function(p){if(p.value!=null)h+='<br/>'+p.marker+p.seriesName+'：'+(p.value>=0?'+':'')+p.value.toFixed(2)+'%';});
        return h;
      }}),
      legend:{top:0,right:0,textStyle:{color:tc.muted,fontSize:11},itemWidth:12,itemHeight:8},
      grid:{left:44,right:10,top:28,bottom:24},
      xAxis:{type:'category',data:names,axisLabel:{color:tc.muted,fontSize:10,interval:0},axisLine:{lineStyle:{color:tc.rule}}},
      yAxis:{type:'value',axisLabel:{color:tc.muted,fontSize:10,formatter:v=>v+'%'},splitLine:{lineStyle:{color:tc.bg3,type:'dashed'}}},
      series:[
        {name:'当日',type:'bar',data:d1,barWidth:'30%',itemStyle:{color:p=>p.value>=0?tc.up:tc.down,borderRadius:[2,2,0,0]}},
        {name:'5日',type:'bar',data:d5,barWidth:'30%',itemStyle:{color:hexA(tc.purple||'#a78bfa',0.6),borderRadius:[2,2,0,0]}}
      ],
      graphic:concl?[{type:'text',right:8,bottom:6,style:{text:'今日风格：'+concl,fill:tc.gold,fontSize:11,fontWeight:600}}]:[]
    },true);
  }catch(e){
    ch.clear();ch.setOption({title:{text:'风格因子数据暂不可用',left:'center',top:'middle',textStyle:{color:tc.muted,fontSize:13}}});
  }
}
// ===== Init =====
function _safeInit(fn){try{fn();}catch(e){console.error('[init]',fn.name,e);}}
_safeInit(renderRegimeBadge);_safeInit(renderRiskDashboard);_safeInit(renderSignalCenter);
_safeInit(renderMarketTrend);_safeInit(renderL1Charts);_safeInit(renderUpDownDist);_safeInit(renderTodayHot);_safeInit(renderT0List);_safeInit(renderHeatmap);_safeInit(renderL2Charts);_safeInit(renderMarketPulse);_safeInit(renderMomentum);_safeInit(renderStockT0);_safeInit(renderEconCal);
fetchFullMarketBoards().then(function(){_safeInit(renderRotationInsight);_safeInit(renderRotationQuadrant);_safeInit(renderRotationLadder);_safeInit(renderChainInsight);_safeInit(renderChainGraph);_safeInit(renderFullMarketRotation);_safeInit(renderHeatmap);_safeInit(renderMarketPulse);});
_safeInit(renderRadarCharts);_safeInit(renderOpPanel);_safeInit(renderSignalMatrix);
_safeInit(renderQuantStrategy);_safeInit(renderLearningCenter);_safeInit(renderWatchlistPage);_safeInit(renderTable);_safeInit(renderTicker);_safeInit(applyLayout);
setInterval(function(){
  if(modVisible('turnover'))fetchTurnover();
  if(modVisible('moneyflow'))fetchMoneyFlow();
  if(modVisible('volmonitor'))fetchVolMonitor();
  if(modVisible('idxfly'))fetchGlobalMarkets();
  if(modVisible('rsgrid'))fetchRSGrid();
  if(modVisible('factorstyle'))fetchFactorStyle();
  if(modVisible('marketpulse'))renderMarketPulse();
  if(modVisible('momentum'))renderMomentum();
},120000);
renderEmotionCycle();renderLadder();renderPlaybook();renderCalibration();renderMarketPulse();renderMomentum();renderStockT0();renderEconCal();
fetchRealtimeQuotes();fetchRealtimeIndices();fetchIndexKlines();fetchEmotionPools();
fetchMarketBreadth();fetchT0Etfs();
setInterval(fetchRealtimeQuotes,30000);setInterval(fetchRealtimeIndices,30000);setInterval(fetchEmotionPools,60000);
setInterval(fetchMarketBreadth,300000);setInterval(fetchT0Etfs,120000);
setInterval(function(){if(modVisible('stockt0'))renderStockT0();},120000);
setInterval(function(){var a=document.querySelector('.page-section.active');if(a&&a.id==='layer-watch'&&watchList.length>0)loadT0ForWatchlist();},120000);

// ===== 用户系统 =====
</script>
<script>
// ===== 用户系统（独立块，主代码异常不影响登录） =====
var _currentUser=null;
var _syncTimer=null;
function _getUsername(){try{return localStorage.getItem('dashboard_username')||'';}catch(e){return '';}}
function _setUsername(n){try{localStorage.setItem('dashboard_username',n);}catch(e){}}
function _showLogin(){
  var ov=document.getElementById('loginOverlay');
  if(ov){ov.style.display='flex';var inp=document.getElementById('loginUsername');if(inp)setTimeout(function(){inp.focus();},100);}
}
function _hideLogin(){var ov=document.getElementById('loginOverlay');if(ov)ov.style.display='none';}
function _showBadge(){
  var b=document.getElementById('userBadge');
  if(b){b.style.display='flex';var n=document.getElementById('userBadgeName');if(n)n.textContent=_currentUser;}
}
function doLogin(){
  var inp=document.getElementById('loginUsername');
  var err=document.getElementById('loginError');
  var name=inp?inp.value.trim():'';
  if(!name){if(err){err.textContent='请输入用户名';err.style.display='block';}return;}
  if(err)err.style.display='none';
  fetch('/api/user/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:name})})
    .then(function(r){return r.json();})
    .then(function(res){
      if(res.ok){
        _currentUser=name;
        _setUsername(name);
        _hideLogin();
        _showBadge();
        var d=res.data||{};
        if(d.watchlist&&d.watchlist.length){
          watchList=d.watchlist;
          try{localStorage.setItem('stock-watchlist',JSON.stringify(watchList));}catch(e){}
          updateWatchCount();renderTable();renderSignalCenter();
        }
        if(d.paperAccount){
          try{localStorage.setItem('paperAccount',JSON.stringify(d.paperAccount));}catch(e){}
          paperAcct=d.paperAccount;
          renderPaperAll();
        }
        if(d.paperPortfolios&&d.paperPortfolios.length){
          try{localStorage.setItem('paperPortfolios',JSON.stringify(d.paperPortfolios));}catch(e){}
          paperPortfolios=d.paperPortfolios;
        }
        if(window._track)window._track('user_login',{username:name});
      }else{
        if(err){err.textContent=res.error||'登录失败';err.style.display='block';}
      }
    })
    .catch(function(){
      _currentUser=name;_setUsername(name);_hideLogin();_showBadge();
    });
}
function _syncToBackend(){
  if(!_currentUser)return;
  if(_syncTimer)clearTimeout(_syncTimer);
  _syncTimer=setTimeout(function(){
    var payload={username:_currentUser,watchlist:watchList,paperAccount:paperAcct,paperPortfolios:paperPortfolios||[]};
    try{
      navigator.sendBeacon('/api/user/sync',JSON.stringify(payload));
    }catch(e){
      fetch('/api/user/sync',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
    }
  },2000);
}
function showUserMenu(){
  if(!_currentUser)return;
  var existing=document.getElementById('userMenuDropdown');
  if(existing){existing.remove();return;}
  var badge=document.getElementById('userBadge');
  if(!badge)return;
  var rect=badge.getBoundingClientRect();
  var menu=document.createElement('div');
  menu.id='userMenuDropdown';
  menu.style.cssText='position:fixed;top:'+(rect.bottom+6)+'px;right:20px;z-index:9999;background:var(--bg2);border:1px solid var(--rule);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.4);overflow:hidden;min-width:220px;font-size:13px;';
  menu.innerHTML=
    '<div style="padding:14px 16px;border-bottom:1px solid var(--rule);background:var(--bg3);">'+
      '<div style="font-size:10px;color:var(--muted);margin-bottom:2px;">当前用户</div>'+
      '<div style="font-size:15px;font-weight:700;color:var(--gold);">'+_currentUser+'</div>'+
    '</div>'+
    '<div style="padding:6px;">'+
      '<button onclick="_userSyncNow()" style="width:100%;padding:10px 14px;border:none;border-radius:8px;background:transparent;color:var(--ink);font-size:13px;cursor:pointer;text-align:left;display:flex;align-items:center;gap:8px;" onmouseenter="this.style.background=\'var(--bg3)\'" onmouseleave="this.style.background=\'transparent\'">'+
        '<span style="font-size:14px;">☁</span> 同步数据到云端'+
      '</button>'+
      '<button onclick="_userSwitch()" style="width:100%;padding:10px 14px;border:none;border-radius:8px;background:transparent;color:var(--ink);font-size:13px;cursor:pointer;text-align:left;display:flex;align-items:center;gap:8px;" onmouseenter="this.style.background=\'var(--bg3)\'" onmouseleave="this.style.background=\'transparent\'">'+
        '<span style="font-size:14px;">⇄</span> 切换账户'+
      '</button>'+
      '<button onclick="_userLogout()" style="width:100%;padding:10px 14px;border:none;border-radius:8px;background:transparent;color:var(--up);font-size:13px;cursor:pointer;text-align:left;display:flex;align-items:center;gap:8px;" onmouseenter="this.style.background=\'var(--bg3)\'" onmouseleave="this.style.background=\'transparent\'">'+
        '<span style="font-size:14px;">⏻</span> 退出登录'+
      '</button>'+
    '</div>';
  document.body.appendChild(menu);
  setTimeout(function(){
    document.addEventListener('click',_closeUserMenuOutside);
  },0);
}
function _closeUserMenuOutside(e){
  var menu=document.getElementById('userMenuDropdown');
  if(menu&&!menu.contains(e.target)&&!document.getElementById('userBadge').contains(e.target)){menu.remove();document.removeEventListener('click',_closeUserMenuOutside);}
}
function _closeUserMenu(){var m=document.getElementById('userMenuDropdown');if(m)m.remove();document.removeEventListener('click',_closeUserMenuOutside);}
function _userSyncNow(){
  _closeUserMenu();
  _syncToBackend();
  var b=document.getElementById('userBadge');
  if(b){var orig=b.style.borderColor;b.style.borderColor='var(--down)';b.style.transition='border-color .3s';setTimeout(function(){b.style.borderColor=orig;},1500);}
  if(window._track)window._track('user_sync',{action:'manual'});
}
function _userSwitch(){
  _closeUserMenu();
  _syncToBackend();
  setTimeout(function(){
    localStorage.removeItem('dashboard_username');
    _currentUser=null;
    _hideBadge();
    var inp=document.getElementById('loginUsername');
    if(inp)inp.value='';
    _showLogin();
  },800);
}
function _userLogout(){
  _closeUserMenu();
  _syncToBackend();
  setTimeout(function(){
    localStorage.removeItem('dashboard_username');
    try{localStorage.removeItem('stock-watchlist');}catch(e){}
    try{localStorage.removeItem('paperAccount');}catch(e){}
    try{localStorage.removeItem('paperPortfolios');}catch(e){}
    _currentUser=null;
    watchList=[];paperAcct=null;paperPortfolios=[];
    updateWatchCount();renderTable();renderSignalCenter();renderPaperAll();
    _hideBadge();
    var inp=document.getElementById('loginUsername');
    if(inp)inp.value='';
    _showLogin();
    if(window._track)window._track('user_logout',{username:_currentUser});
  },800);
}
function _hideBadge(){var b=document.getElementById('userBadge');if(b)b.style.display='none';}
// 初始化
(function(){
  var saved=_getUsername();
  if(saved){
    fetch('/api/user/data?username='+encodeURIComponent(saved))
      .then(function(r){return r.json();})
      .then(function(res){
        if(res.ok){
          _currentUser=saved;
          _showBadge();
          var d=res.data||{};
          if(d.watchlist&&d.watchlist.length){
            watchList=d.watchlist;
            try{localStorage.setItem('stock-watchlist',JSON.stringify(watchList));}catch(e){}
            updateWatchCount();renderTable();renderSignalCenter();
          }
          if(d.paperAccount){
            try{localStorage.setItem('paperAccount',JSON.stringify(d.paperAccount));}catch(e){}
            paperAcct=d.paperAccount;
            renderPaperAll();
          }
          if(d.paperPortfolios&&d.paperPortfolios.length){
            try{localStorage.setItem('paperPortfolios',JSON.stringify(d.paperPortfolios));}catch(e){}
            paperPortfolios=d.paperPortfolios;
          }
        }else{
          _showLogin();
        }
      })
      .catch(function(){_currentUser=saved;_showBadge();});
  }else{
    _showLogin();
  }
})();
</script>
<script>
// ===== 用户行为埋点 SDK v2 =====
(function(){
  var EP='/api/track';
  var sid=(function(){try{var s=sessionStorage.getItem('_trk_sid');if(s)return s;s=Date.now().toString(36)+Math.random().toString(36).slice(2,10);sessionStorage.setItem('_trk_sid',s);return s;}catch(e){return 'anon';}})();
  var uid=(function(){try{var u=localStorage.getItem('_trk_uid');if(!u){u='u_'+Date.now().toString(36)+Math.random().toString(36).slice(2,6);localStorage.setItem('_trk_uid',u);}return u;}catch(e){return 'unknown';}})();
  var pageTs=Date.now(),secTs=pageTs,curSec='layer1',seq=0;
  var q=[],tmr=null,scrollTmr=null,maxScroll=0,idleTmr=null,lastActTs=pageTs;
  var secDwell={},secScrollDepth={},secInteractions={},actionPath=[];
  var VIEWPORT_SECTIONS={layer1:'市场全景','layer2':'板块轮动','layer0':'交易信号','layer4':'量化学习','layer-paper':'模拟交易','layer-watch':'我关注的','layer-tpo':'四度空间'};
  function flush(){if(!q.length)return;var b=q.splice(0,q.length);try{navigator.sendBeacon&&navigator.sendBeacon(EP,JSON.stringify({events:b}));}catch(e){try{var img=new Image();img.src=EP+'?batch='+encodeURIComponent(JSON.stringify(b));}catch(e2){}}}
  function sched(){if(tmr)return;tmr=setTimeout(function(){tmr=null;flush();},1500);}
  function track(type,data){
    var ev={ts:Date.now(),uid:uid,sid:sid,seq:++seq,type:type,page:curSec,url:location.pathname,ua:navigator.userAgent.slice(0,120),ref:document.referrer.slice(0,100),data:data||{}};
    q.push(ev);sched();
  }
  function leaveSection(){
    var dur=Math.round((Date.now()-secTs)/1000);
    if(dur>0){
      secDwell[curSec]=(secDwell[curSec]||0)+dur;
      track('section_leave',{section:curSec,duration_s:dur,scroll_pct:secScrollDepth[curSec]||0,interactions:secInteractions[curSec]||0,max_seq:seq});
    }
  }
  function enterSection(sec){
    leaveSection();
    curSec=sec;secTs=Date.now();maxScroll=0;
    actionPath.push({t:Date.now(),sec:sec,act:'enter'});
    if(actionPath.length>50)actionPath=actionPath.slice(-50);
    track('section_enter',{section:sec,name:VIEWPORT_SECTIONS[sec]||sec,seq:seq});
  }
  function resetIdle(){lastActTs=Date.now();if(idleTmr)clearTimeout(idleTmr);idleTmr=setTimeout(function(){track('idle',{section:curSec,idle_after_s:30});},30000);}
  // 页面加载
  track('page_view',{title:document.title,load_ms:0,viewport:window.innerWidth+'x'+window.innerHeight,dpr:window.devicePixelRatio,lang:navigator.language});
  resetIdle();
  // 滚动深度追踪
  window.addEventListener('scroll',function(){
    var st=window.scrollY||document.documentElement.scrollTop;
    var ph=document.documentElement.scrollHeight-window.innerHeight;
    var pct=ph>0?Math.min(100,Math.round(st/ph*100)):0;
    if(pct>maxScroll)maxScroll=pct;
    if(secScrollDepth[curSec]===undefined||pct>secScrollDepth[curSec])secScrollDepth[curSec]=pct;
    secInteractions[curSec]=(secInteractions[curSec]||0)+1;
    if(scrollTmr)clearTimeout(scrollTmr);
    scrollTmr=setTimeout(function(){track('scroll_depth',{section:curSec,scroll_pct:maxScroll});},3000);
    resetIdle();
  },{passive:true});
  // 鼠标移动 → 活跃信号
  document.addEventListener('mousemove',function(){resetIdle();},{passive:true});
  // 导航 + 点击
  document.addEventListener('click',function(e){
    resetIdle();
    var el=e.target;
    var nav=el.closest&&el.closest('.nav-link');
    if(nav){
      var oc=nav.getAttribute('onclick')||'';
      var m=oc.match(/scrollToSec\('([^']+)'/);
      if(m&&m[1]!==curSec)enterSection(m[1]);
    }
    var btn=el.closest&&el.closest('button,.btn,.star-btn,[onclick],.heatmap-block,.module-wrap,.card');
    if(btn&&btn!==nav){
      var label=btn.textContent?btn.textContent.trim().slice(0,50):'';
      var cls=(btn.className||'').slice(0,80);
      var tag=btn.tagName;
      var id=btn.id||'';
      secInteractions[curSec]=(secInteractions[curSec]||0)+1;
      actionPath.push({t:Date.now(),sec:curSec,act:'click',label:label.slice(0,20)});
      if(actionPath.length>50)actionPath=actionPath.slice(-50);
      track('click',{label:label,tag:tag,cls:cls,id:id,section:curSec,x:e.clientX,y:e.clientY,vp_w:window.innerWidth,vp_h:window.innerHeight});
    }
    var diagInput=document.getElementById('diagInput');
    if(diagInput&&el===diagInput){
      clearTimeout(window._trkSearchT);
      window._trkSearchT=setTimeout(function(){if(diagInput.value)track('search',{query:diagInput.value.slice(0,60),section:curSec});},1500);
    }
  },true);
  // select / filter 变更
  document.addEventListener('change',function(e){
    var el=e.target;
    if(el&&el.tagName==='SELECT'){
      track('filter_change',{id:el.id||'',value:el.value,section:curSec,label:(el.options[el.selectedIndex]||{}).text||''});
      secInteractions[curSec]=(secInteractions[curSec]||0)+1;
      resetIdle();
    }
  },true);
  // 内容展开/收起
  document.addEventListener('click',function(e){
    var el=e.target;
    var exp=el.closest&&el.closest('[onclick*="expand"],[onclick*="toggle"],[onclick*="show"],[onclick*="detail"]');
    if(exp&&exp!==el.closest('.nav-link')){
      track('content_expand',{label:(exp.textContent||'').trim().slice(0,30),onclick:(exp.getAttribute('onclick')||'').slice(0,60),section:curSec});
    }
  },true);
  // 图表交互（ECharts）
  if(typeof echarts!=='undefined'){
    var _origInit=echarts.init;
    echarts.init=function(){
      var chart=_origInit.apply(this,arguments);
      var _origOn=chart.on;
      chart.on=function(t,fn){
        if(typeof fn==='function'){
          var wrapped=function(p){
            try{if(window._track)window._track('chart_interact',{type:t,section:curSec,series:p.seriesName||'',name:p.name||'',value:p.value?JSON.stringify(p.value).slice(0,50):''});}catch(e){}
            return fn.apply(this,arguments);
          };
          return _origOn.call(this,t,wrapped);
        }
        return _origOn.apply(this,arguments);
      };
      return chart;
    };
  }
  // 错误追踪
  window.addEventListener('error',function(e){
    track('error',{msg:(e.message||'').slice(0,100),file:(e.filename||'').slice(0,80),line:e.lineno,col:e.colno,section:curSec});
  });
  // 交易动作
  var _origPaperOrder=window.paperOrder;
  if(typeof _origPaperOrder==='function'){
    window.paperOrder=function(){var r=_origPaperOrder.apply(this,arguments);if(r!==false)track('trade_action',{action:'order',section:curSec,args:JSON.stringify(arguments[0]||{}).slice(0,80)});return r;};
  }
  var _origToggleWatch=window.toggleWatch;
  if(typeof _origToggleWatch==='function'){
    window.toggleWatch=function(){var r=_origToggleWatch.apply(this,arguments);track('watchlist_change',{code:arguments[0],action:watchList.includes(arguments[0])?'add':'remove',total:watchList.length,section:curSec});return r;};
  }
  // 心跳：30秒，含当前模块和累计停留
  setInterval(function(){
    var curDur=Math.round((Date.now()-secTs)/1000);
    track('heartbeat',{section:curSec,cur_dur_s:curDur,total_dwell:JSON.parse(JSON.stringify(secDwell)),interactions:secInteractions,active:Date.now()-lastActTs<5000,seq:seq});
  },30000);
  // 页面关闭
  window.addEventListener('beforeunload',function(){
    leaveSection();
    track('page_exit',{duration_s:Math.round((Date.now()-pageTs)/1000),path:actionPath.slice(-20),total_dwell:JSON.parse(JSON.stringify(secDwell))});
    flush();
  });
  // 暴露
  window._track=track;
  window._getActionPath=function(){return actionPath;};
})();
</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,
    "__SECTOR_STAGE_JSON__": sector_stage_json,
    "__SECTOR_SUB_JSON__": sector_sub_json,
    "__CHAIN_EDGE_INFO_JSON__": chain_edge_info_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,
    "__FULL_MARKET_SECTORS_JSON__": full_market_sectors_json,
    "__HOT_SECTOR_STOCKS_JSON__": hot_sector_stocks_json,
    "__MARKET_BREADTH_JSON__": market_breadth_json,
    "__T0_ETF_LIST_JSON__": t0_etf_list_json,
    "__EMO_POOLS_JSON__": emo_pools_json,
    "__DAV_SENTI_JSON__": weibo_dav_json,
    "__ECON_CAL_JSON__": econ_cal_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")

# 部署到 web 服务目录（Tailscale 公网: /ashare-dashboard.html）
try:
    import shutil
    _web_html = os.path.join(BASE_DIR, "ashare-dashboard.html")
    if os.path.abspath(OUTPUT_HTML) != os.path.abspath(_web_html):
        shutil.copy2(OUTPUT_HTML, _web_html)
        print(f"Deployed to web dir: {_web_html}")
    else:
        print(f"Web dir is output dir, skip copy: {_web_html}")
    # 带时间戳版本副本：浏览器缓存旧版时，用带版本号的新 URL 可强制拉取最新文件
    _ver = time.strftime("%Y%m%d%H%M")
    _ver_html = os.path.join(BASE_DIR, f"ashare-dashboard-v{_ver}.html")
    shutil.copy2(OUTPUT_HTML, _ver_html)
    print(f"Versioned copy (cache-busting URL): {_ver_html}")
except Exception as _dwe:
    print(f"Deploy copy failed: {_dwe}")
