Skill: gold3bear/a-stock-radar (version 2.1.1)

--- name: a-stock-radar description: A股综合监控与分析工具，支持寒鸦框架决策输出。当用户询问「A股今天怎么样」「大盘如何」「哪些板块在动」「今日涨跌」「北向资金」「涨停/跌停」「市场情绪」「现在适合上仓位吗」「分析A股」「A股复盘」「当前主线是什么」「买入/卖出」「加仓/减仓」「仓位建议」时使用。覆盖实时行情监控、宏观快照、寒鸦四维决策框架（宏观定调/微观验证/情绪水位/风控矩阵）、核心企业篮子、短线情绪判断。所有操作建议均附免责声明「不构成投资建议」。📢微信公众号：PM熊叔（打造一人公司的投研团队） --- # A股雷达 (A-Stock Radar) v2.0 > ⚠️ 本工具输出包含寒鸦框架决策建议，但所有建议仅供研究参考，不构成任何投资建议。市场有风险，投资需谨慎。 ## 🆕 v2.0 重大升级：寒鸦决策框架 除原有行情监控外，新增**交易决策矩阵**输出： ### 寒鸦四维框架 | 维度 | 内容 | |------|------| | **宏观定调** | LPR周期、SHIBOR流动性、中美利差、政策发令枪指向 | | **微观验证** | 机构龙虎榜资金、筹码高低切、ETF异动 | | **情绪水位** | 涨停/跌停/炸板率/连板高度 → 冰点/分歧/修复/亢奋 | | **风控矩阵** | 仓位建议、买入板块、卖出板块、量化止损阈值 | ### 情绪数据修复（2026-04-29） ⚠️ **已知Bug**：akshare的`stock_zt_pool_em`等函数默认日期为2024年10月，导致涨停数据永远为0。**已在`sentiment_snapshot.py`中修复**——所有API调用现已传入当日日期（YYYYMMDD格式）。 --- `a-stock-radar` 现在是统一的 A 股入口，内部按四层能力组织： 1. `实时行情监控` 2. `宏观快照与核心企业` 3. `寒鸦决策框架`（含情绪判断） `a-stock-radar` 现在是统一的 A 股入口，内部按三层能力组织： 1. `实时行情监控` 2. `宏观快照与核心企业` 3. `短线情绪判断` 4. `深度量化分析` ## 1. 实时行情监控 适用问题： - A股今天怎么样 - 哪些板块在动 - 北向资金如何 - 涨停跌停情况 - 某只股票现在多少钱 主要脚本： - `scripts/index_spot.py`：主要指数 - `scripts/sector_ranking.py`：板块排行 - `scripts/stock_quote.py`：个股行情 - `scripts/zt_pool.py`：涨停池/跌停池 - `scripts/dashboard.py`：综合看盘入口 - `scripts/macro_snapshot.py`：SHIBOR、LPR、中美10Y利差、北向、新增人民币贷款 - `scripts/core_companies.py`：A股核心企业篮子 主要数据源： - 东方财富：板块/排行 - 新浪财经：指数与个股实时行情 - akshare：涨跌停池、北向等扩展数据 ## 2. 宏观快照与核心企业 适用问题： - A股宏观数据怎么看 - SHIBOR/LPR 现在是什么水平 - 中美10Y利差如何 - 北向资金和融资需求怎么样 - 茅台/宁德时代/比亚迪现在怎么样 - 给我看A股核心企业篮子 主要脚本： - `scripts/macro_snapshot.py` - `scripts/core_companies.py` 覆盖维度： - SHIBOR（银行间流动性） - LPR（贷款市场报价利率） - 中美10Y国债利差 - 北向资金净流入 - 新增人民币贷款 - 贵州茅台、宁德时代、比亚迪、招商银行、工商银行、美的集团、中芯国际、立讯精密 ## 3. 短线情绪判断 适用问题： - 今天市场情绪怎么样 - 现在是冰点还是亢奋 - 适不适合上仓位 - 短线环境强不强 主要脚本： - `scripts/sentiment_snapshot.py` 核心指标： - 涨停家数 - 跌停家数 - 炸板率 - 连板高度 输出内容： - 情绪阶段：`冰点 / 分歧 / 修复 / 亢奋` - 仓位建议：`空仓 / 轻仓 / 半仓 / 重仓` - 短线打法建议 ## 4. 深度量化分析 适用问题： - 分析A股 - 做一份A股复盘 - 当前主线是什么 - 用量化视角看A股 主要脚本： - `scripts/quant_analysis.py` 分析框架： - 宏观流动性 - 政策主线 - ETF/龙虎榜/换手结构 - 情绪与筹码共振 输出内容： - 主线判断 - 择时建议 - 复盘框架 - 风险提示 ## 使用原则 - 只问盘面与行情：优先走 `实时行情监控` - 问仓位与短线环境：优先走 `短线情绪判断` - 问中高层判断与复盘：优先走 `深度量化分析` ## 迁移说明 以下旧 skill 已并入本 skill： - `a-stock-market-sentiment` - `ashare-quant`

path: scripts/browser_fetch.py
content: """
浏览器备用抓取工具
当 API 请求失败时，通过抓取网页 HTML 解析数据。
纯 requests + BeautifulSoup，不依赖 Selenium。
"""
import requests
import time
import io
import contextlib
from typing import Optional, Callable, Any


class FetchError(Exception):
    """抓取失败异常"""
    pass


def fetch_with_retry(
    url: str,
    headers: Optional[dict] = None,
    timeout: int = 10,
    retries: int = 3,
    backoff: float = 1.5,
    encoding: Optional[str] = None,
    parser: Optional[str] = "html.parser",
    session: Optional[requests.Session] = None,
) -> Any:
    """
    带重试的抓取，同时返回 (text, soup)。

    Args:
        url: 目标URL
        headers: 请求头
        timeout: 单次超时（秒）
        retries: 最大重试次数
        backoff: 退避倍数
        encoding: 响应编码（默认自动检测）
        parser: BeautifulSoup 解析器
        session: requests.Session（可复用连接）
    Returns:
        (response_text, BeautifulSoup)
    Raises:
        FetchError: 所有重试失败后
    """
    default_headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
    }
    if headers:
        default_headers.update(headers)

    if session is None:
        session = requests.Session()

    last_error = None
    for attempt in range(retries):
        try:
            r = session.get(url, headers=default_headers, timeout=timeout)
            r.raise_for_status()
            text = r.text
            if encoding:
                text = text.encode(r.encoding or "utf-8").decode(encoding, errors="replace")
            try:
                from bs4 import BeautifulSoup
                soup = BeautifulSoup(text, parser)
            except ImportError:
                soup = None
            return text, soup
        except Exception as e:
            last_error = e
            if attempt < retries - 1:
                wait = backoff ** attempt
                time.sleep(wait)
            continue

    raise FetchError(f"全部{retries}次重试失败: {last_error}")


def parse_eastmoney_table(url: str, retries: int = 2) -> list:
    """
    抓取东方财富网页表格数据。

    Args:
        url: 东方财富页面URL
        retries: 重试次数
    Returns:
        list[dict]，每行一个字典
    """
    try:
        from bs4 import BeautifulSoup
    except ImportError:
        raise FetchError("需要安装 bs4: pip install beautifulsoup4")

    text, soup = fetch_with_retry(url, retries=retries, parser="lxml")
    tables = soup.find_all("table")
    results = []
    for table in tables:
        try:
            import pandas as pd
            dfs = pd.read_html(io.StringIO(str(table)))
            if dfs:
                results.extend(dfs)
        except Exception:
            continue
    return results


# ─── 常用东方财富行情页面 ────────────────────────────────────────────

INDEX_PAGE = "https://quote.eastmoney.com/center/gridlist.html"


def fetch_eastmoney_index() -> dict:
    """
    备用：从东方财富网页抓取A股主要指数。
    当 Sina hq API 不可用时调用。
    Returns: dict {代码: {"name": str, "price": float, "chg": float, "pct": float}}
    """
    from bs4 import BeautifulSoup

    # 东方财富 A股指数页面
    url = "https://push2.eastmoney.com/api/qt/clist/get?pn=1&pz=20&po=1&np=1&fltt=2&invt=2&fid=f3&fs=m:1+t:2&fields=f2,f3,f4,f12,f14"
    headers = {"Referer": "https://quote.eastmoney.com/", "User-Agent": "Mozilla/5.0"}

    try:
        r = requests.get(url, headers=headers, timeout=10)
        r.raise_for_status()
        data = r.json()
        items = data.get("data", {}).get("diff", [])
        result = {}
        name_map = {
            "000001": "上证指数", "399001": "深证成指", "399006": "创业板指",
            "000688": "科创50", "000300": "沪深300", "399905": "中证500",
        }
        for item in items:
            code = str(item.get("f12", ""))
            if code in name_map:
                result[code] = {
                    "name": name_map[code],
                    "price": item.get("f2", 0),
                    "pct": item.get("f3", 0),
                }
        return result
    except Exception:
        pass

    # 兜底：直接解析 Eastmoney 行情页 HTML
    try:
        text, soup = fetch_with_retry(
            "https://quote.eastmoney.com/center/gridlist.html",
            headers={"Referer": "https://www.eastmoney.com/"},
            retries=2,
        )
        # 解析指数名称和价格
        import re
        indices = {}
        # 东方财富页面里含实时数据在 JS 变量中
        script_texts = soup.find_all("script")
        for script in script_texts:
            content = script.string or ""
            # 找 hq_str_xxx 格式的数据
            matches = re.findall(r'hq_str_(sh\d{6}|sz\d{6})="([^"]+)"', content)
            for code, data in matches:
                parts = data.split(",")
                if len(parts) > 4:
                    try:
                        price = float(parts[3])
                        prev = float(parts[2])
                        pct = (price - prev) / prev * 100 if prev else 0
                        indices[code] = {"price": price, "pct": pct}
                    except ValueError:
                        continue
        return indices
    except Exception:
        return {}


if __name__ == "__main__":
    # 测试
    print("测试 browser_fetch...")
    result = fetch_eastmoney_index()
    print(f"获取到 {len(result)} 个指数")
    for v in result.values():
        print(v)


path: scripts/core_companies.py
content: """
A股核心企业篮子
贵州茅台 / 宁德时代 / 比亚迪 / 招商银行 / 工商银行 / 美的集团 / 中芯国际 / 立讯精密
"""
import os
import sys

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
    sys.path.insert(0, SCRIPT_DIR)

from stock_quote import fetch_stock_quotes, format_stock_quote


CORE_COMPANIES = {
    "sh600519": "贵州茅台",
    "sz300750": "宁德时代",
    "sz002594": "比亚迪",
    "sh600036": "招商银行",
    "sh601398": "工商银行",
    "sz000333": "美的集团",
    "sh688981": "中芯国际",
    "sz002475": "立讯精密",
}


if __name__ == "__main__":
    print("\n=== 🏭 A股核心企业 ===")
    quotes = fetch_stock_quotes(list(CORE_COMPANIES.keys()))
    for item in quotes:
        print(format_stock_quote(item))


path: scripts/dashboard.py
content: """
A股雷达仪表盘：一键汇总主要指数 + 宏观快照 + 核心企业 + 板块涨跌
用法: python dashboard.py
"""
import subprocess
import sys
import os

base = os.path.dirname(os.path.abspath(__file__))

print("=" * 50)
print("  🦞 A股雷达仪表盘")
print("=" * 50)

# 1. 主要指数
print("\n>>> 主要指数")
subprocess.run([sys.executable, os.path.join(base, "index_spot.py")])

# 2. 宏观快照
print("\n>>> 宏观快照")
subprocess.run([sys.executable, os.path.join(base, "macro_snapshot.py")])

# 3. 核心企业
print("\n>>> A股核心企业")
subprocess.run([sys.executable, os.path.join(base, "core_companies.py")])

# 4. 行业板块
print("\n>>> 行业涨幅榜TOP10")
result = subprocess.run([sys.executable, os.path.join(base, "sector_ranking.py"), "行业板块"], capture_output=True, text=True)
if "暂时不可用" in result.stdout:
    print("（行业板块数据暂时不可用，请稍后重试）")
else:
    print(result.stdout, end="")

# 5. 短线情绪
print("\n>>> 短线情绪")
result2 = subprocess.run([sys.executable, os.path.join(base, "sentiment_snapshot.py")], capture_output=True, text=True)
print(result2.stdout, end="")


path: scripts/index_spot.py
content: """
A股主要指数实时行情
上证、深成、创业板、科创50、沪深300

【双层架构】
1. 主：新浪财经 hq.sinajs.cn API
2. 备：东方财富网页 + 浏览器抓取（API 失败时自动切换）
"""
import requests
import time

MAJOR_INDICES = {
    "sh000001": "上证指数",
    "sz399001": "深证成指",
    "sz399006": "创业板指",
    "sh000688": "科创50",
    "sh000300": "沪深300",
    "sz399905": "中证500",
}

_NAME_MAP = {v: k for k, v in MAJOR_INDICES.items()}


def _get_via_sina() -> dict:
    """新浪财经 API（主）"""
    codes = ",".join(MAJOR_INDICES.keys())
    url = f"https://hq.sinajs.cn/list={codes}"
    headers = {"Referer": "http://finance.sina.com.cn"}
    r = requests.get(url, headers=headers, timeout=10)
    r.raise_for_status()
    r.encoding = 'gbk'
    results = {}
    for line in r.text.strip().split('\n'):
        if '=' not in line:
            continue
        key, data = line.split('=', 1)
        vals = data.replace('"', '').replace(';', '').split(',')
        if len(vals) < 4:
            continue
        try:
            code = key[-8:].replace('_hq_str', '').replace('"', '')
            prev = float(vals[2])
            price = float(vals[3])
            if prev == 0:
                continue
            pct = (price - prev) / prev * 100
            chg = price - prev
            arrow = "🔴" if pct > 0 else "🟢" if pct < 0 else "⚪"
            name = MAJOR_INDICES.get(code, code)
            results[code] = f"{arrow} {name}: {price:.2f} {chg:+.2f}({pct:+.2f}%)"
        except (ValueError, IndexError):
            continue
    return results


def _get_via_eastmoney() -> dict:
    """东方财富 API（备用1）"""
    url = (
        "https://push2.eastmoney.com/api/qt/clist/get"
        "?pn=1&pz=20&po=1&np=1&fltt=2&invt=2&fid=f3"
        "&fs=m:1+t:2&fields=f2,f3,f4,f12,f14"
    )
    headers = {
        "Referer": "https://quote.eastmoney.com/",
        "User-Agent": "Mozilla/5.0",
    }
    r = requests.get(url, headers=headers, timeout=10)
    r.raise_for_status()
    data = r.json()
    items = data.get("data", {}).get("diff", [])
    results = {}
    for item in items:
        code = "sh" + str(item.get("f12", ""))
        name = MAJOR_INDICES.get(code)
        if not name:
            continue
        price = item.get("f2", 0)
        pct = item.get("f3", 0)
        chg = price * pct / 100
        arrow = "🔴" if pct > 0 else "🟢" if pct < 0 else "⚪"
        results[code] = f"{arrow} {name}: {price:.2f} {chg:+.2f}({pct:+.2f}%)"
    return results


def get_major_indices() -> dict:
    """
    获取A股主要指数，尝试顺序：
    新浪API → 东方财富API → 返回空dict（不抛异常）
    """
    # 尝试新浪（主）
    try:
        result = _get_via_sina()
        if result:
            return result
    except Exception:
        pass

    # 等待1秒再试东方财富（避免同时失败）
    time.sleep(1)

    # 尝试东方财富（备）
    try:
        result = _get_via_eastmoney()
        if result:
            return result
    except Exception:
        pass

    return {}


if __name__ == "__main__":
    print("\n=== A股主要指数 ===")
    indices = get_major_indices()
    for v in indices.values():
        print(v)



path: scripts/macro_snapshot.py
content: """
A股宏观快照
优先展示 SHIBOR、LPR、中美10Y利差、北向资金和新增人民币贷款。
"""
try:
    import akshare as ak
except ImportError:
    ak = None
import contextlib
import io


MACRO_NOTES = {
    "SHIBOR": {"desc": "银行间借贷成本，衡量流动性松紧", "us_equivalent": "≈ SOFR"},
    "LPR": {"desc": "贷款市场报价利率，影响房贷与企业融资", "us_equivalent": "≈ Fed Rate"},
    "CN_US_10Y_SPREAD": {"desc": "中美10Y国债利差，影响汇率与外资预期", "us_equivalent": "≈ 10Y-2Y利差"},
    "NORTHBOUND": {"desc": "北向资金净流入，反映外资配置A股情绪", "us_equivalent": "—"},
    "BANK_FINANCING": {"desc": "新增人民币贷款，反映实体融资需求", "us_equivalent": "—"},
}


def extract_shibor_snapshot(df):
    row = df.iloc[-1]
    return {
        "date": str(row["日期"]),
        "on": float(row["O/N-定价"]),
        "1w": float(row["1W-定价"]),
        "1m": float(row["1M-定价"]),
        "3m": float(row["3M-定价"]),
    }


def extract_lpr_snapshot(df):
    row = df.iloc[-1]
    return {
        "date": str(row["TRADE_DATE"]),
        "1y": float(row["LPR1Y"]),
        "5y": float(row["LPR5Y"]),
    }


def extract_cn_us_spread(df):
    row = df.iloc[-1]
    china_10y = float(row["中国国债收益率10年"])
    us_10y = float(row["美国国债收益率10年"])
    return {
        "date": str(row["日期"]),
        "china_10y": china_10y,
        "us_10y": us_10y,
        "spread": china_10y - us_10y,
        "spread_bp": (china_10y - us_10y) * 100,
    }


def extract_northbound_flow(df):
    latest_date = df["交易日"].max()
    latest = df[(df["交易日"] == latest_date) & (df["资金方向"] == "北向")]
    net_buy = float(latest["成交净买额"].sum())
    return {
        "date": str(latest_date),
        "net_buy": net_buy,
    }


def extract_bank_financing(df):
    row = df.iloc[-1]
    return {
        "date": str(row["日期"]),
        "value": float(row["最新值"]),
        "pct": float(row["涨跌幅"]),
    }


def get_macro_snapshot():
    if ak is None:
        return {}

    snapshot = {}
    try:
        snapshot["SHIBOR"] = extract_shibor_snapshot(_quiet_call(ak.macro_china_shibor_all))
    except Exception:
        pass

    try:
        snapshot["LPR"] = extract_lpr_snapshot(_quiet_call(ak.macro_china_lpr))
    except Exception:
        pass

    try:
        snapshot["CN_US_10Y_SPREAD"] = extract_cn_us_spread(_quiet_call(ak.bond_zh_us_rate))
    except Exception:
        pass

    try:
        snapshot["NORTHBOUND"] = extract_northbound_flow(_quiet_call(ak.stock_hsgt_fund_flow_summary_em))
    except Exception:
        pass

    try:
        snapshot["BANK_FINANCING"] = extract_bank_financing(_quiet_call(ak.macro_china_bank_financing))
    except Exception:
        pass

    return snapshot


def _quiet_call(func):
    sink = io.StringIO()
    with contextlib.redirect_stdout(sink), contextlib.redirect_stderr(sink):
        return func()


def format_macro_snapshot(snapshot):
    lines = []

    shibor = snapshot.get("SHIBOR")
    if shibor:
        lines.append(
            f"🔹 SHIBOR ({shibor['date']}): O/N {shibor['on']:.3f}% | 1W {shibor['1w']:.3f}% | "
            f"1M {shibor['1m']:.3f}% | 3M {shibor['3m']:.3f}%"
        )

    lpr = snapshot.get("LPR")
    if lpr:
        lines.append(f"🔹 LPR ({lpr['date']}): 1Y {lpr['1y']:.2f}% | 5Y {lpr['5y']:.2f}%")

    spread = snapshot.get("CN_US_10Y_SPREAD")
    if spread:
        lines.append(
            f"🔹 中美10Y利差 ({spread['date']}): 中国 {spread['china_10y']:.2f}% | 美国 {spread['us_10y']:.2f}% | "
            f"利差 {spread['spread_bp']:+.0f}bp"
        )

    northbound = snapshot.get("NORTHBOUND")
    if northbound:
        arrow = "🔴" if northbound["net_buy"] > 0 else "🟢" if northbound["net_buy"] < 0 else "⚪"
        lines.append(f"{arrow} 北向资金 ({northbound['date']}): 净买额 {northbound['net_buy']:+.2f} 亿")

    financing = snapshot.get("BANK_FINANCING")
    if financing:
        lines.append(
            f"🔹 新增人民币贷款 ({financing['date']}): {financing['value']:.0f} 亿元 | "
            f"涨跌幅 {financing['pct']:+.2f}%"
        )

    return lines


if __name__ == "__main__":
    print("\n=== 🌏 A股宏观快照 ===")
    snapshot = get_macro_snapshot()
    if not snapshot:
        print("宏观数据暂不可用")
    else:
        for line in format_macro_snapshot(snapshot):
            print(line)
        print("\n说明:")
        print(f"- SHIBOR: {MACRO_NOTES['SHIBOR']['desc']} {MACRO_NOTES['SHIBOR']['us_equivalent']}")
        print(f"- LPR: {MACRO_NOTES['LPR']['desc']} {MACRO_NOTES['LPR']['us_equivalent']}")
        print(
            f"- 中美10Y利差: {MACRO_NOTES['CN_US_10Y_SPREAD']['desc']} "
            f"{MACRO_NOTES['CN_US_10Y_SPREAD']['us_equivalent']}"
        )


path: scripts/quant_analysis.py
content: """
A股宏观量化分析骨架
用于整合流动性、政策、资金结构和情绪信息，输出简要结论。
"""


def _score_signal(value):
    mapping = {
        "看多": 2,
        "偏多": 1,
        "中性": 0,
        "偏空": -1,
        "看空": -2,
    }
    return mapping.get(value, 0)


def summarize_quant_view(liquidity_view, policy_view, structure_view, sentiment_view):
    score = (
        _score_signal(liquidity_view)
        + _score_signal(policy_view)
        + _score_signal(structure_view)
        + _score_signal(sentiment_view)
    )
    if score >= 4:
        stance = "积极进攻"
    elif score >= 1:
        stance = "偏积极"
    elif score <= -4:
        stance = "防守收缩"
    elif score <= -1:
        stance = "谨慎观望"
    else:
        stance = "中性平衡"

    return {
        "liquidity": liquidity_view,
        "policy": policy_view,
        "structure": structure_view,
        "sentiment": sentiment_view,
        "score": score,
        "stance": stance,
        "conclusion": "先定宏观与政策方向，再看结构资金是否共振。",
    }


def build_quant_report(
    liquidity_view,
    policy_view,
    structure_view,
    sentiment_view,
    main_theme,
    risk_note,
):
    report = summarize_quant_view(
        liquidity_view=liquidity_view,
        policy_view=policy_view,
        structure_view=structure_view,
        sentiment_view=sentiment_view,
    )
    report["main_theme"] = main_theme
    report["risk_note"] = risk_note
    return report


def format_quant_report(report):
    return (
        f"综合倾向: {report['stance']} (score={report['score']}) | "
        f"流动性:{report['liquidity']} 政策:{report['policy']} "
        f"结构:{report['structure']} 情绪:{report['sentiment']} | "
        f"主线: {report['main_theme']} | 风险: {report['risk_note']}"
    )


if __name__ == "__main__":
    sample = build_quant_report(
        liquidity_view="偏多",
        policy_view="偏多",
        structure_view="中性",
        sentiment_view="偏多",
        main_theme="稳增长与科技主线轮动",
        risk_note="高位拥挤板块若放量滞涨，警惕高低切失败",
    )
    print("A股量化分析")
    print(format_quant_report(sample))


path: scripts/sector_ranking.py
content: """
东方财富板块涨跌排行
行业板块: m:90+t:2
概念板块: m:90+t:3

【双层架构】
1. 主：东方财富 push2 API
2. 备：东方财富网页 HTML 解析（502/超时/网络错误时切换）
"""
import requests
import sys
import time
import io

def _get_via_api(market_type, top, retries):
    """东方财富 API（主）"""
    fs = "m:90+t:2" if market_type == "行业板块" else "m:90+t:3"
    url = "https://push2.eastmoney.com/api/qt/clist/get"
    params = {
        "pn": 1, "pz": top, "po": 1, "np": 1,
        "fltt": 2, "invt": 2,
        "fid": "f3",
        "fs": fs,
        "fields": "f12,f14,f2,f3,f5,f6,f8,f10,f15,f16,f18"
    }
    headers = {"Referer": "https://quote.eastmoney.com/"}

    for attempt in range(retries):
        try:
            r = requests.get(url, params=params, headers=headers, timeout=10)
            if r.status_code == 502:
                if attempt < retries - 1:
                    time.sleep(2)
                    continue
                break
            r.raise_for_status()
            return r.json()["data"]["diff"]
        except Exception:
            if attempt < retries - 1:
                time.sleep(2)
                continue
            raise
    return None  # API 层失败，返回 None 触发备用


def _get_via_html(market_type, top=30):
    """
    东方财富板块排行 HTML 解析（备用）
    通过 pandas.read_html 直接解析网页表格，无需浏览器
    """
    market_id = "2" if market_type == "行业板块" else "3"
    url = f"https://quote.eastmoney.com/center/boardlist.html#行业板块_{market_id}"

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
        "Referer": "https://quote.eastmoney.com/",
    }
    r = requests.get(url, headers=headers, timeout=15)
    r.encoding = "utf-8"
    r.raise_for_status()

    import pandas as pd

    # 东方财富板块页面含多个表格，找到正确的那个
    tables = pd.read_html(io.StringIO(r.text))
    for df in tables:
        cols = df.columns.tolist()
        # 找含"板块名称"或"涨跌幅"的列
        if any("板块" in str(c) for c in cols) or any("涨跌幅" in str(c) for c in cols):
            # 标准化列名
            df.columns = [str(c) for c in df.columns]
            # 找涨跌幅列
            pct_col = next((c for c in df.columns if "涨跌幅" in c or "涨跌" in c), None)
            name_col = next((c for c in df.columns if "板块" in c or "名称" in c), None)
            if pct_col and name_col:
                result = []
                for _, row in df.iterrows():
                    try:
                        pct = float(str(row[pct_col]).replace("%", "").replace("+", ""))
                        name = str(row[name_col])
                        if name and name != "nan":
                            result.append({"f14": name, "f3": pct, "f6": 0})
                    except ValueError:
                        continue
                return result[:top]
    return []


def get_sector_ranking(market_type="行业板块", top=20, retries=3):
    """
    获取板块涨跌排行。
    优先走 API，失败时自动切换 HTML 解析。
    """
    # 尝试 API（主）
    data = None
    try:
        data = _get_via_api(market_type, top, retries)
    except Exception:
        pass

    if data is not None:
        return data

    # API 失败，切换 HTML 备用（等1秒）
    time.sleep(1)
    try:
        return _get_via_html(market_type, top)
    except Exception:
        return []

def format_sector(s):
    pct = s["f3"]
    arrow = "🔴" if pct > 0 else "🟢" if pct < 0 else "⚪"
    return f"{arrow} {s['f14']:12s}  {'+' if pct>=0 else ''}{pct}%  成交额:{s['f6']/1e8:.1f}亿"

if __name__ == "__main__":
    market = sys.argv[1] if len(sys.argv) > 1 else "行业板块"
    print(f"\n=== {market}涨跌榜 ===")
    data = get_sector_ranking(market, 30)
    if not data:
        print("（板块数据暂时不可用，请稍后重试）")
    else:
        print("\n【涨幅榜】")
        for s in sorted(data, key=lambda x: x["f3"], reverse=True)[:10]:
            print(format_sector(s))
        print("\n【跌幅榜】")
        for s in sorted(data, key=lambda x: x["f3"])[:10]:
            print(format_sector(s))


path: scripts/zt_pool.py
content: """
涨跌停池查询（需要 akshare）
pip install akshare
"""
try:
    import akshare as ak
except ImportError:
    print("需要安装akshare: pip install akshare")
    exit(1)

def get_zt_pool():
    print("\n=== 涨停池 ===")
    try:
        zt = ak.stock_zt_pool_previous_em()
        print(f"涨停家数: {len(zt)}")
        print("涨停股(代码/名称/涨幅):")
        for _, row in zt.head(15).iterrows():
            print(f"  🔴 {row.get('代码','?')} {row.get('名称','?')} {row.get('涨幅','?')}%")
    except Exception as e:
        print(f"涨停池获取失败: {e}")

    print("\n=== 跌停池 ===")
    try:
        dt = ak.stock_zt_pool_dtgc_em()
        print(f"跌停家数: {len(dt)}")
        print("跌停股(代码/名称/跌幅):")
        for _, row in dt.head(15).iterrows():
            print(f"  🟢 {row.get('代码','?')} {row.get('名称','?')} {row.get('跌幅','?')}%")
    except Exception as e:
        print(f"跌停池获取失败: {e}")

if __name__ == "__main__":
    get_zt_pool()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)