SuperAPI AI Directory

global-stock-data

Data & analytics

View repository

Updated 10-16 08:45

About this skill

📦 Project homepage:https://github.com/simonlin1212/global-stock-data — updates, feedback, support the author

Author: Simon Lin · X @linsizhen · Email:simonlin0423@gmail.com

US/HK full-stack data toolkit V2.0 — official sources first

Thirteen-layer data architecture, 30+ endpoints, 11 data sources, all zero-auth, verified working (full regression validation on 2026-07-24).

V2.0 design principle: official sources first. The main data for the newly added layers comes from the public endpoints of US government (SEC EDGAR / Treasury / CFTC), self-regulatory organizations (FINRA), and exchanges (CBOE / Nasdaq).Each data source is labeled with its compliance level and the original text of its terms (see “Data source compliance tiers” below) — “official” does not mean “free to use”; sources differ greatly.

This tool only distributes code; it does not distribute or resell any market data; users obtain data themselves in accordance with each source’s terms.

How to use: Place this file into ~/.claude/skills/global-stock-data/SKILL.md, and Claude Code will automatically recognize it and activate it in US stock/HK stock related conversations.

行情层(实时/延时)
├── 新浪财经     → 美股 gb_XXXX 36字段 / 港股 rt_hkXXXXX 25字段
├── 腾讯财经     → 美股 usXXXX 71字段 / 港股 r_hkXXXXX 78字段
└── 东财 push2   → 美股/港股 secid 实时行情,含中文名/涨跌幅/换手率

K线层(日/周/月/分钟)
├── 新浪          → 美股日K (回溯至1984年)
└── Yahoo chart   → 美股+港股 (v8 API, 零crumb)

技术指标层(纯计算,零额外依赖)
└── MA/EMA + MACD + RSI + KDJ + 布林带    基于K线OHLCV,纯Python计算

基本面层
├── 东财 datacenter → 美股/港股三表(资产负债+利润+现金流) + GMAININDICATOR(关键指标)
├── Yahoo crumb     → 23个模块(财务数据+关键指标+分析师+机构持仓)
└── SEC EDGAR XBRL  → 美股503个GAAP指标 (仅美股)

资金面层
└── 东财 push2his → 日级资金流(主力/大单/中单/小单) 美股+港股

期权层(仅美股)
└── Yahoo crumb → 期权链(calls+puts, 所有到期日) 仅美股(港股期权不在Yahoo覆盖范围)

SEC Filing层(仅美股)
├── EDGAR submissions → 10-K/10-Q/8-K 完整Filing列表
└── EDGAR XBRL        → 结构化财务指标(营收/净利/EPS等)

工具层
├── 东财 search    → 股票搜索(中英文, 含市场代码映射)
├── 东财 push2     → 全市场股票列表(涨跌幅/成交量排名, 美股5925只+港股18000+只)
├── Yahoo search   → 新闻资讯(按股票代码)
└── SEC CIK mapping → ticker↔CIK 映射 (仅美股)

━━━ 以下为 V2.0 新增(官方源优先)━━━

期权层·CBOE 官方(仅美股)⭐
└── CBOE cdn → 全链 + IV + delta/gamma/vega/theta/rho + 0DTE + 异动flow   [C级·需授权]

做空层(仅美股)⭐
└── FINRA Reg SHO → 全市场每日空头成交量(实测12,112只) + 个股时序 + 排行  [B级]

申报事件流(仅美股)⭐
├── EDGAR 每日索引 → Form4内部人/8-K/13F机构持仓/144,当日全量           [S级]
└── EDGAR 全文检索 → 2001至今所有申报正文,按关键词+表单+日期            [S级]

全市场横截面(仅美股)⭐
└── EDGAR frames → 任意XBRL标签一次拿全市场(实测1,842~5,309家)=免费screener [S级]

宏观 / 日历 ⭐
├── Treasury → 美债收益率曲线(1M~30Y, 每日)                             [S级]
├── CFTC     → COT 持仓报告                                             [S级]
└── Nasdaq   → 财报日历(含盘前盘后+EPS预期)                             [C级·未核实]

Endpoint routing quick reference (locate as needed; no need to read the entire document)

I want to… Which layer to go to Primary source Compliance level
Real-time/delayed quotes Layer 1 Sina / Tencent / Eastmoney C
K-line (daily/weekly/monthly) Layer 2 Sina / Yahoo C
Technical indicators MA/MACD/RSI/KDJ/Bollinger Layer 3 Local calculation —
Financial statements three tables / key metrics / analysts / institutional holdings Layer 4 Eastmoney / Yahoo / EDGAR C·S
Daily capital flows Layer 5 Eastmoney C
Option chain + Greeks + IV + 0DTE + unusual flow Layer 6.1 CBOE official ⭐ C
Option chain (no Greeks, fallback) Layer 6.2 Yahoo C
10-K/10-Q/8-K lists, XBRL financials Layer 7 SEC EDGAR S
Search / news / CIK mapping / market-wide lists Layer 8 Eastmoney / Yahoo / SEC C·S
Market-wide daily short sale volume, per-stock short sale ratio Layer 9 FINRA Reg SHO ⭐ B
Same-day filing flow: Form 4 insider / 8-K / 13F Layer 10.1 EDGAR daily index ⭐ S
Filing full-text search (body text from 2001 to present) Layer 10.2 EDGAR FTS ⭐ S
Market-wide fundamentals cross-section (free screener) Layer 11 EDGAR frames ⭐ S
US Treasury yield curve / CFTC COT / earnings calendar Layer 12 Treasury / CFTC / Nasdaq S·C

⭐ = newly added in V2.0, and capabilities that yfinance and most open-source solutions do not have.


Data source compliance tiers (must read before use — original terms text cited for each tier)

The conclusions in the table below come from reading the original terms text of each source one by one on 2026-07-24, not inference. Text in quotation marks is original wording. Sources differ greatly; “official” does not mean “free to use”.

Tier S — free to use (including commercial use and redistribution)

Source Basis (original text)
SEC EDGAR Stated on the official website:"Anyone can access and download this information for free"、"We allow scripted access to sec.gov content"。Hard requirements:Current max request rate: 10 requests/second, and the User-Agent must be declared (format Company Name AdminContact@domain.com), otherwise it triggers "Undeclared Automated Tool" / Access Denied
US Treasury / CFTC U.S. federal government works are not subject to copyright protection (17 U.S.C. §105). ⚠️ This timenot verified item by itemthe terms of both sites' main text, classified at this level per government data conventions

Level B — the data files are proactively published, but the site terms contain restrictions

Source Basis (original text)
FINRA The Reg SHO daily files are regulatory disclosure files proactively published by FINRA for download; however, its Terms of Use also prohibit "use any process to monitor or copy the FINRA Website in bulk, or use any data mining, scraping or harvesting tools (including robots)", and the site states "FINRA Data provides non-commercial use of data"。→ Downloading published data files is a regular use; bulk crawling of site pages is not. Please confirm with FINRA yourself before commercial use.

Level C — use requires prior authorization, or the terms have not been verified

Source Basis (original text)
CBOE Use of Content policy: use of any Cboe Content requires "receive approval in advance from Cboe", and requires "execution of a license agreement"; the policydoes not distinguishcommercial/non-commercial, nor real-time/delayed. → The CBOE options layer of this tool is for personal research only; before commercial use or redistribution, authorization must first be applied for from Cboe.
Nasdaq This time the terms page crawl timed out,not verified. Handled as unverified
Yahoo / Eastmoney / Sina / Tencent Yahoo's official documentation states personal use only; the rest are site frontend interfaces. For personal research only, do not use in commercial products or redistribute

⛔ Excluded sources

Source Reason
HKEX (CCASS Hong Kong stock seat holdings) Its Terms of Use expressly prohibit "any 'robot', 'bot', 'spider', 'scraper' or other automated device... to access, obtain, copy, monitor or republish any portion of the Website", prohibit "text or data mining or web scraping", and apply to "whether or not for gain"(whether or not for gain). → This tool does not provide CCASS scraping code. Those who need Hong Kong stock seat holdings/southbound capital data should use HKEX authorized channels or manually query its webpages

Three hard rules for users

  1. Commercial use: rely only on Level S(SEC EDGAR / Treasury / CFTC). Level B requires your own confirmation, and Level C requires obtaining authorization first.
  2. Redistribution: this toolonly distributes code, not any market data. You also should not redistribute externally any data obtained from Level B/C sources.
  3. Rate limiting: requests for all newly added layers have built-in throttling (see "Unified HTTP Layer").Do not bypass it—SEC's 10 req/s is the official hard limit.

When to Activate

  • The user wants to checkU.S. stocks/Hong Kong stocksquotes (price/change percent/volume)
  • The user wants to pull candlesticks (daily/weekly/monthly/minute)
  • The user wants to viewfinancial statements(balance sheet/income statement/cash flow statement)
  • The user wants to viewkey financial metrics(PE/PB/ROE/profit margin/target price)
  • The user wants to viewanalyst expectations(EPS forecasts/ratings/target price range)
  • User wants to seeInstitutional holdings(Top ten institutions / shareholding percentage)
  • User wants to seeCapital flows(Net inflow of main force / large orders / medium orders / small orders)
  • User wants to checkOptions chain(calls/puts/expiration date/Greeks)
  • User wants to check SEC Filing(10-K/10-Q/8-K/annual report/quarterly report)
  • User wants to doU.S. stock financial report quantitative analysis(Pull multi-year revenue/net profit/EPS trends from XBRL)
  • User wants toSearch stocks(Chinese or English both acceptable)
  • User wants to seeU.S. stock/Hong Kong stock news
  • User wants to seeWhole-market gainers and losers ranking(Stocks with the largest gains/losses of the day)
  • User wants to doWhole-market screening(Iterate through the U.S. stock/Hong Kong stock list for preliminary screening)
  • User wants to seeOverview of key financial indicators(Revenue/net profit/EPS/ROE/ROA/debt-to-asset ratio, Chinese version)
  • User wants to seeTechnical indicators(MACD/RSI/KDJ/Bollinger Bands/moving averages)
  • User wants to judgeGolden cross/death cross/overbought/oversold/reversal signal
  • Keywords: U.S. stocks, Hong Kong stocks, AAPL, Apple, Tencent, 00700, TSLA, Tesla, BABA, Alibaba, quotes, candlestick chart, financial reports, PE, PB, ROE, analyst, target price, options, call, put, SEC, 10-K, annual report, quarterly report, capital flow, main force, institutional holdings, news, gainers ranking, whole market, screening, key indicators, MACD, RSI, KDJ, Bollinger Bands, moving averages, golden cross, death cross, overbought, oversold, technical analysis

Prerequisites

pip install requests
Dependencies Version requirements Purpose
requests any All HTTP APIs are directly connected

Minimal dependencies: Only requires requests; all data sources are directly connected HTTP APIs, with zero third-party data wrappers.


Market code rules

Eastmoney secid prefix (used by push2/push2his)

Prefix Market Example
105 U.S. stock NASDAQ 105.AAPL, 105.TSLA
106 U.S. stock NYSE 106.BABA, 106.JD
107 U.S. stock ETF/other 107.CRSH
116 Hong Kong stocks 116.00700, 116.09988

How to determine 105/106/107? Call stock_search() Get the MktNum field for automatic mapping.

Yahoo Finance ticker format

Market Format Example
U.S. stocks Direct ticker AAPL, TSLA, BABA
Hong Kong stocks Four/five-digit number + .HK 0700.HK, 9988.HK

Eastmoney datacenter SECUCODE format

Market Format Example
U.S. stock NASDAQ TICKER.O AAPL.O, TSLA.O
U.S. stock NYSE TICKER.N BABA.N, JD.N
Hong Kong stocks CODE.HK 00700.HK, 09988.HK

Shared Helper functions

Yahoo Finance crumb manager

Yahoo quoteSummary/options and other v7/v10 APIs require cookie+crumb. The following helper automatically obtains and caches them:

import requests

_yahoo_session = None

def get_yahoo_session() -> requests.Session:
    """获取带 crumb 的 Yahoo Finance session(自动缓存)"""
    global _yahoo_session
    if _yahoo_session and hasattr(_yahoo_session, '_crumb'):
        return _yahoo_session
    
    s = requests.Session()
    s.headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
    
    # Step 1: 获取 cookie
    s.get('https://fc.yahoo.com', timeout=10)
    
    # Step 2: 获取 crumb
    r = s.get('https://query2.finance.yahoo.com/v1/test/getcrumb', timeout=10)
    r.raise_for_status()
    s._crumb = r.text
    
    _yahoo_session = s
    return s

def yahoo_quote_summary(symbol: str, modules: list[str]) -> dict:
    """Yahoo quoteSummary 统一查询"""
    s = get_yahoo_session()
    r = s.get(f'https://query2.finance.yahoo.com/v10/finance/quoteSummary/{symbol}', params={
        'modules': ','.join(modules),
        'crumb': s._crumb,
    }, timeout=15)
    r.raise_for_status()
    results = r.json().get('quoteSummary', {}).get('result', [{}])
    return results[0] if results else {}

Eastmoney data center unified query

UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
DATACENTER_URL = "https://datacenter-web.eastmoney.com/api/data/v1/get"

def eastmoney_datacenter(report_name: str, columns: str = "ALL",
                          filter_str: str = "", page_size: int = 50,
                          sort_columns: str = "", sort_types: str = "-1") -> list[dict]:
    """东财数据中心统一查询"""
    params = {
        "reportName": report_name, "columns": columns,
        "filter": filter_str, "pageNumber": "1", "pageSize": str(page_size),
        "sortColumns": sort_columns, "sortTypes": sort_types,
        "source": "WEB", "client": "WEB",
    }
    r = requests.get(DATACENTER_URL, params=params, headers={"User-Agent": UA}, timeout=15)
    d = r.json()
    if d.get("result") and d["result"].get("data"):
        return d["result"]["data"]
    return []

Official source unified exit (rate limiting + UA declaration) — New in V2.0

All Layer 9–12 added in V2.0 go through this exit. It handles three things:Rate limiting by source、SEC User-Agent declaration、 Friendly error messages。

⚠️ Must change before use: replace SEC_CONTACT with your own real name and email. SEC officially requires declaring a User-Agent; if not declared, it will be judged as Undeclared Automated Tool and service will be refused.

import requests, time, threading

# ⚠️⚠️ 必改:SEC 要求 UA 含真实联系方式,格式 "Company Name AdminContact@domain.com"
SEC_CONTACT = "your-name your-email@example.com"


class DataNotAvailable(RuntimeError):
    """该日/该标的确实没有数据(如非交易日、文件尚未发布)——可安全回退到下一个候选日。

    与配置错误、网络错误区分开:后者必须立刻抛给调用方,
    否则「SEC_CONTACT 没配」会被日期回退循环吞掉,最后伪装成「没找到数据」。
    """


class _RateLimiter:
    """线程安全的最小间隔节流器(用锁,避免并发下被击穿)"""

    def __init__(self, max_per_sec: float):
        self._interval = 1.0 / float(max_per_sec)
        self._last = 0.0
        self._lock = threading.Lock()

    def wait(self) -> None:
        with self._lock:
            gap = self._interval - (time.monotonic() - self._last)
            if gap > 0:
                time.sleep(gap)
            self._last = time.monotonic()


# 各源限速:SEC 官方硬上限 10/s,此处取 8/s 留余量;其余为自律保护值
_LIMITS = {
    "sec.gov": _RateLimiter(8),
    "finra.org": _RateLimiter(4),
    "cboe.com": _RateLimiter(4),
    "nasdaq.com": _RateLimiter(2),
    "_default": _RateLimiter(5),
}


def _limiter_for(url: str) -> _RateLimiter:
    for host, lim in _LIMITS.items():
        if host != "_default" and host in url:
            return lim
    return _LIMITS["_default"]


def _is_object_missing(resp) -> bool:
    """
    正向识别「资源确实不存在」。

    ⚠️ SEC Archives 与 FINRA CDN 都托管在 S3 上,而 S3 在调用方没有
    ListBucket 权限时,对**不存在的对象**返回 `403 AccessDenied`(XML)
    而不是 404 NoSuchKey。实测 2026-07-24 两个源行为一致。

    真正的拒绝长得完全不同(SEC 的 UA 未声明返回 ~4.8KB HTML 页面),
    所以这里按 Content-Type + XML 错误码正向判定,
    而不是用「排除法」——否则限流/封禁会被伪装成「没数据」。
    """
    if resp.status_code == 404:
        return True
    if resp.status_code != 403:
        return False
    ctype = (resp.headers.get("Content-Type") or "").lower()
    head = (resp.text or "")[:500]
    return "xml" in ctype and "<Code>AccessDenied</Code>" in head


def official_get(url: str, params: dict = None, headers: dict = None,
                 timeout: int = 30, as_json: bool = False):
    """
    V2.0 官方源统一出口:自动节流 + UA 处理 + 友好错误。
    as_json=True 返回 dict,否则返回 str。

    异常语义:资源不存在 → DataNotAvailable(调用方可回退到下一个候选日);
             配置/限流/网络 → RuntimeError(必须冒泡)。
    """
    if "sec.gov" in url:
        if "your-email@example.com" in SEC_CONTACT:
            raise RuntimeError(
                "请先把 SEC_CONTACT 改成你的真实姓名与邮箱 —— SEC 要求声明 "
                "User-Agent,否则返回 Undeclared Automated Tool 错误。")
        h = {"User-Agent": SEC_CONTACT, "Accept-Encoding": "gzip, deflate"}
    else:
        h = {"User-Agent": UA}
    h.update(headers or {})

    _limiter_for(url).wait()
    try:
        r = requests.get(url, params=params, headers=h, timeout=timeout)
        r.raise_for_status()
    except requests.HTTPError as e:
        resp = e.response
        code = resp.status_code
        low = (resp.text or "")[:4000].lower()
        # ① 正向识别:资源确实不存在(404,或 S3 风格的 403 AccessDenied)
        if _is_object_missing(resp):
            raise DataNotAvailable(
                f"HTTP {code} {url[:80]} — 资源不存在(该日无数据/尚未发布)") from e
        # ② SEC 的 UA 未声明(返回 HTML 页,含 Undeclared Automated Tool)
        if code == 403 and "undeclared" in low:
            raise RuntimeError(
                f"SEC 拒绝请求:User-Agent 未被识别为已声明。"
                f"当前 SEC_CONTACT={SEC_CONTACT!r},"
                f"格式应为 'Company Name AdminContact@domain.com'") from e
        # ③ 其余一律视为真错误,必须冒泡(限流/封禁/权限/接口变更)
        hint = {403: "被拒绝:限流、封禁或权限问题(已排除「资源不存在」)",
                404: "端点不存在:接口可能已变更",
                429: "请求过快:已内置节流,若仍触发请调低 _LIMITS"}.get(code, "")
        raise RuntimeError(f"HTTP {code} {url[:80]} — {hint}") from e
    except requests.RequestException as e:
        raise RuntimeError(f"请求失败 {url[:80]} — {type(e).__name__}: {e}") from e
    return r.json() if as_json else r.text


# ── 异常约定(V2.0)──
#   DataNotAvailable : 该日/该标的确实没有数据(非交易日、文件未发布、标的无期权…)
#                      → 调用方可安全回退到下一个候选日
#   RuntimeError     : 配置错误(SEC_CONTACT 未改)、限流、网络故障
#                      → 必须冒泡给使用者,不可伪装成「没数据」
#   ValueError       : 参数错误(如把港股代码传给仅支持美股的层)


def assert_us_ticker(ticker: str) -> str:
    """Layer 6.1 / 9 / 10 / 11 仅支持美股;传入港股代码时给出明确提示"""
    t = str(ticker).upper()
    if t.endswith(".HK") or (t.isdigit() and len(t) in (4, 5)):
        raise ValueError(f"'{ticker}' 看起来是港股代码;该层仅支持美股。"
                         f"港股请用 Layer 1-5 的港股端点。")
    if not t.replace(".", "").replace("-", "").isalnum():
        raise ValueError(f"无效的 ticker: '{ticker}'")
    return t

requests automatically decompresses gzip responses, so the above with Accept-Encoding is safe. If you switch to urllib to implement it yourself,you must manually gzip.decompress, otherwise the content returned by SEC will fail to parse.


Layer 1: Market Data Layer

1.1 US Stock Real-Time Quotes — Sina + Tencent

Two independent data sources; either one being available is enough. Sina's fields focus on price and trades, while Tencent's fields are more complete (including 52-week high/low/market cap/PE).

import requests, re

def us_stock_quote_sina(ticker: str) -> dict:
    """
    新浪美股行情 — 36字段
    ticker: 纯字母,如 "AAPL", "TSLA", "BABA"
    """
    url = f"https://hq.sinajs.cn/list=gb_{ticker.lower()}"
    r = requests.get(url, headers={
        "Referer": "https://finance.sina.com.cn/",
        "User-Agent": UA,
    }, timeout=10)
    r.encoding = "gbk"
    text = r.text
    
    m = re.search(r'"(.+)"', text)
    if not m:
        return {}
    
    fields = m.group(1).split(",")
    if len(fields) < 30:
        return {}
    
    return {
        "name": fields[0],           # 中文名
        "price": float(fields[1]),    # 最新价
        "change_pct": float(fields[2]),  # 涨跌幅 %
        "timestamp": fields[3],       # 时间
        "prev_close": float(fields[26]),  # 昨收
        "open": float(fields[5]),     # 开盘
        "high": float(fields[6]),     # 最高
        "low": float(fields[7]),      # 最低
        "volume": float(fields[10]) if fields[10] else 0,  # 成交量
        "high_52w": float(fields[8]) if fields[8] else 0,  # 52周最高
        "low_52w": float(fields[9]) if fields[9] else 0,   # 52周最低
        "market_cap": float(fields[12]) if fields[12] else 0,  # 市值
        "eps": float(fields[13]) if fields[13] else 0,  # EPS
        "pe": float(fields[14]) if fields[14] else 0,   # PE
    }


def us_stock_quote_tencent(ticker: str) -> dict:
    """
    腾讯美股行情 — 71字段
    ticker: 纯字母,如 "AAPL"
    """
    url = f"https://qt.gtimg.cn/q=us{ticker.upper()}"
    r = requests.get(url, timeout=10)
    r.encoding = "gbk"
    text = r.text
    
    m = re.search(r'"(.+)"', text)
    if not m:
        return {}
    
    fields = m.group(1).split("~")
    if len(fields) < 52:   # 需读到 fields[51](PB),美股正常返回 71 个
        return {}
    
    # ⚠️ 下标以实测为准,勿照抄港股那套(两市布局不同,见本节末「腾讯行情字段对照表」)
    return {
        "name": fields[1],           # 中文名
        "name_en": fields[46],       # 英文名,如 "Apple Inc."
        "price": float(fields[3]) if fields[3] else 0,
        "prev_close": float(fields[4]) if fields[4] else 0,
        "open": float(fields[5]) if fields[5] else 0,
        "volume": int(float(fields[6])) if fields[6] else 0,
        "high": float(fields[33]) if fields[33] else 0,
        "low": float(fields[34]) if fields[34] else 0,
        "high_52w": float(fields[48]) if fields[48] else 0,
        "low_52w": float(fields[49]) if fields[49] else 0,
        "change_pct": float(fields[32]) if fields[32] else 0,
        "float_market_cap": float(fields[44]) if fields[44] else 0,  # 流通市值,亿美元
        "market_cap": float(fields[45]) if fields[45] else 0,        # 总市值,亿美元
        "eps": float(fields[47]) if fields[47] else 0,
        "pe": float(fields[39]) if fields[39] else 0,
        "pb": float(fields[51]) if fields[51] else 0,
        "currency": fields[35],      # "USD"
        "timestamp": fields[30],
    }

1.2 Hong Kong Stock Real-Time Quotes — Tencent + Sina

def hk_stock_quote_tencent(code: str) -> dict:
    """
    腾讯港股行情 — 78字段(最全)
    code: 五位数字,如 "00700", "09988"
    """
    url = f"https://qt.gtimg.cn/q=r_hk{code}"
    r = requests.get(url, timeout=10)
    r.encoding = "gbk"
    text = r.text
    
    m = re.search(r'"(.+)"', text)
    if not m:
        return {}
    
    fields = m.group(1).split("~")
    if len(fields) < 76:   # 需读到 fields[75](币种),港股正常返回 78 个
        return {}
    
    # ⚠️ 下标以实测为准,与美股那套不同(见本节末「腾讯行情字段对照表」)
    return {
        "name": fields[1],           # 中文名
        "code": fields[2],           # 五位代码,如 "00700"(旧版误当英文名)
        "name_en": fields[46],       # 英文名,如 "TENCENT"
        "price": float(fields[3]) if fields[3] else 0,
        "prev_close": float(fields[4]) if fields[4] else 0,
        "open": float(fields[5]) if fields[5] else 0,
        "high": float(fields[33]) if fields[33] else 0,
        "low": float(fields[34]) if fields[34] else 0,
        "volume": int(float(fields[6])) if fields[6] else 0,  # 成交量(股)
        "amount": float(fields[37]) if fields[37] else 0,     # 成交额
        "change_pct": float(fields[32]) if fields[32] else 0,
        "pe": float(fields[39]) if fields[39] else 0,
        "pb": float(fields[58]) if fields[58] else 0,
        "high_52w": float(fields[48]) if fields[48] else 0,
        "low_52w": float(fields[49]) if fields[49] else 0,
        "float_market_cap": float(fields[44]) if fields[44] else 0,  # 流通市值,亿港元
        "market_cap": float(fields[45]) if fields[45] else 0,        # 总市值,亿港元
        "currency": fields[75],      # "HKD"
        "timestamp": fields[30],
    }


def hk_stock_quote_sina(code: str) -> dict:
    """
    新浪港股行情 — 25字段
    code: 五位数字,如 "00700"
    """
    url = f"https://hq.sinajs.cn/list=rt_hk{code}"
    r = requests.get(url, headers={
        "Referer": "https://finance.sina.com.cn/",
        "User-Agent": UA,
    }, timeout=10)
    r.encoding = "gbk"
    text = r.text
    
    m = re.search(r'"(.+)"', text)
    if not m:
        return {}
    
    fields = m.group(1).split(",")
    if len(fields) < 15:
        return {}
    
    return {
        "name_en": fields[0],
        "name": fields[1],           # 中文名
        "open": float(fields[2]) if fields[2] else 0,
        "prev_close": float(fields[3]) if fields[3] else 0,
        "high": float(fields[4]) if fields[4] else 0,
        "low": float(fields[5]) if fields[5] else 0,
        "price": float(fields[6]) if fields[6] else 0,
        "change": float(fields[7]) if fields[7] else 0,
        "change_pct": float(fields[8]) if fields[8] else 0,
        "volume": float(fields[12]) if fields[12] else 0,
        "amount": float(fields[11]) if fields[11] else 0,
    }

Tencent quote field mapping table (qt.gtimg.cn · Calibrated by actual testing on 2026-07-26)

⚠️ The field layouts for US stocks and Hong Kong stocks are different, so the same set of indices cannot be shared. US stocks return 71 fields, Hong Kong stocks 78. Each index below has been checked one by one against real responses (usAAPL / hk00700); many mappings circulating online contain errors.

Meaning US Stock Index Hong Kong Stock Index Measured Value (AAPL / 00700)
Chinese Name 1 1 Apple / Tencent Holdings
Code 2 2 AAPL.OQ / 00700
English Name 46 46 Apple Inc. / TENCENT
Current Price 3 3 333.02 / 434.600
Previous Close / Today's Open 4 / 5 4 / 5 —
Volume 6 6 ⚠️ Hong Kong stocks include decimal places (22959603.0), must int(float(x))
Change % 32 32 3.53 / -2.38
Day's High / Low 33 / 34 33 / 34 —
Currency 35 75 USD / HKD
PE 39 39 40.32 / 15.87
Float Market Cap(100 million local currency) 44 44 48881.62 / 39516.08
Total Market Cap(100 million local currency) 45 45 48911.83 / 39516.08
EPS 47 — 8.26 (333.02 ÷ 8.26 = 40.32 ✓ consistent with PE)
52-Week High / Low 48 / 49 48 / 49 334.99·200.72 / 677.7·411.0
PB 51 58 45.93 / 3.14

Market cap unit is "100 million local currency", not number of shares: AAPL total market cap 48911.83 (100 million USD) = current price 333.02 × total shares 14,687,356,000, can be verified by back-calculating with fields[62] 's total shares. Same for Hong Kong stocks, unit is 100 million HKD.

Reproduce-it-yourself command(line number N ↔ array index N−1):

curl -s "https://qt.gtimg.cn/q=usAAPL"  | iconv -f GBK -t UTF-8 | tr '~' '\n' | cat -n
curl -s "https://qt.gtimg.cn/q=hk00700" | iconv -f GBK -t UTF-8 | tr '~' '\n' | cat -n

Hong Kong stocks hk and r_hk the two prefixes return exactly the same field layout (both 78), and are interchangeable. Thanks to @HoRiZonn0 for providing the complete mapping in issue #2; this table was revised after re-testing each entry accordingly.

1.3 Eastmoney push2 Real-Time Quotes — US Stocks + Hong Kong Stocks

Eastmoney push2 interface, uniformly queries US/Hong Kong real-time quotes via secid. Advantages: has Chinese name, turnover rate, change %, and secid can be automatically obtained by stock_search() .

def stock_quote_eastmoney(ticker_or_code: str, secid_prefix: int = 105) -> dict:
    """
    东财 push2 实时行情 — 美股+港股统一接口
    美股: stock_quote_eastmoney("AAPL", 105)  # NASDAQ
          stock_quote_eastmoney("BABA", 106)  # NYSE
    港股: stock_quote_eastmoney("00700", 116)
    返回: 最新价/开高低收/成交量/成交额/换手率/涨跌幅/中文名
    
    secid_prefix 说明: 105=NASDAQ, 106=NYSE, 107=US_ETF, 116=港股
    如不确定前缀,先调 stock_search() 获取 mkt_num
    """
    url = "https://push2.eastmoney.com/api/qt/stock/get"
    params = {
        "secid": f"{secid_prefix}.{ticker_or_code}",
        "fields": "f43,f44,f45,f46,f47,f48,f55,f57,f58,f59,f60,f170",
    }
    r = requests.get(url, params=params, timeout=10)
    d = r.json().get("data")
    if not d:
        return {}
    
    # f59 = 小数位数, 价格字段需除以 10^f59 还原真实值
    dec = d.get("f59", 3)
    divisor = 10 ** dec
    
    def _p(key):
        v = d.get(key)
        if v is None or v == "-":
            return None
        return round(v / divisor, dec)
    
    return {
        "code": d.get("f57"),           # 股票代码
        "name": d.get("f58"),           # 中文名
        "price": _p("f43"),             # 最新价
        "high": _p("f44"),              # 最高
        "low": _p("f45"),               # 最低
        "open": _p("f46"),              # 开盘
        "volume": d.get("f47"),         # 成交量(股)
        "amount": d.get("f48"),         # 成交额
        "turnover_rate": d.get("f55"),  # 换手率(%)
        "prev_close": _p("f60"),        # 昨收
        "change_pct": round(d["f170"] / 100, 2) if d.get("f170") is not None else None,  # 涨跌幅(%)
    }

Layer 2: K-Line Layer

2.1 US Stock K-Line — Sina (primary) + Yahoo (backup)

Two independent data sources. Sina can go back as far as 1984; Yahoo is suitable for scenarios requiring adjusted data.

Note: Dongcai push2his kline/get endpoint tested and does not return US/HK stock data (verified 2026-05-20), only supports A-shares. For US/HK K-lines, use Sina and Yahoo.

def us_stock_kline_sina(ticker: str, num: int = 120) -> list[dict]:
    """
    新浪美股日K — 可回溯到1984年
    ticker: 如 "AAPL"
    返回: [{date, open, high, low, close, volume}, ...]
    """
    url = "https://stock.finance.sina.com.cn/usstock/api/jsonp.php/var/US_MinKService.getDailyK"
    params = {"symbol": ticker.upper(), "num": num}
    r = requests.get(url, params=params, headers={"Referer": "https://finance.sina.com.cn/"}, timeout=15)
    text = r.text
    
    # 解析 JSONP: var=([{...},...])
    import json
    m = re.search(r'\((\[.+\])\)', text)
    if not m:
        return []
    
    items = json.loads(m.group(1))
    result = []
    for item in items:
        result.append({
            "date": item.get("d"),
            "open": float(item.get("o", 0)),
            "high": float(item.get("h", 0)),
            "low": float(item.get("l", 0)),
            "close": float(item.get("c", 0)),
            "volume": int(item.get("v", 0)),
        })
    return result


def stock_kline_yahoo(symbol: str, interval: str = "1d",
                       range_: str = "6mo") -> list[dict]:
    """
    Yahoo Finance chart API — 美股+港股通用,零crumb
    symbol: "AAPL" (美股) 或 "0700.HK" (港股)
    interval: "1d", "1wk", "1mo", "5m", "15m", "1h"
    range_: "1d", "5d", "1mo", "3mo", "6mo", "1y", "5y", "max"
    返回: [{date, open, high, low, close, volume}, ...]
    """
    url = f"https://query2.finance.yahoo.com/v8/finance/chart/{symbol}"
    params = {"interval": interval, "range": range_}
    r = requests.get(url, params=params, headers={
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
    }, timeout=15)
    r.raise_for_status()
    
    d = r.json()
    chart = d.get("chart", {}).get("result", [{}])[0]
    timestamps = chart.get("timestamp", [])
    quote = chart.get("indicators", {}).get("quote", [{}])[0]
    
    from datetime import datetime
    result = []
    for i, ts in enumerate(timestamps):
        result.append({
            "date": datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M") if "m" in interval or "h" in interval else datetime.fromtimestamp(ts).strftime("%Y-%m-%d"),
            "open": round(quote["open"][i], 2) if quote["open"][i] else 0,
            "high": round(quote["high"][i], 2) if quote["high"][i] else 0,
            "low": round(quote["low"][i], 2) if quote["low"][i] else 0,
            "close": round(quote["close"][i], 2) if quote["close"][i] else 0,
            "volume": int(quote["volume"][i]) if quote["volume"][i] else 0,
        })
    return result

2.2 HK Stock K-line — Yahoo (only available source)

HK stock K-line has only one available source, Yahoo (Sina HK stock K-line is no longer functional, Dongcai push2his does not return HK stock K-line data).

# 港股 Yahoo K线: 直接调 stock_kline_yahoo("0700.HK")

Layer 3: Technical Indicator Layer

Pure Python technical indicator calculation based on K-line OHLCV data, zero extra dependencies.

Usage: First call the K-line function to get data, then pass it into the technical indicator function:

klines = us_stock_kline_sina("AAPL", 120)
macd = calc_macd(klines)
rsi = calc_rsi(klines)

3.1 Moving Averages MA / EMA

def _ema(values: list[float], period: int) -> list[float]:
    """EMA 指数移动平均(内部辅助)"""
    result = [values[0]]
    k = 2 / (period + 1)
    for v in values[1:]:
        result.append(v * k + result[-1] * (1 - k))
    return result


def calc_ma(klines: list[dict], periods: list[int] = None) -> list[dict]:
    """
    移动平均线 MA + EMA
    klines: K线数据 [{date, open, high, low, close, volume}, ...]
    periods: 周期列表,默认 [5, 10, 20, 60]
    返回: [{date, close, ma5, ma10, ma20, ma60, ema12, ema26}, ...]
    """
    if periods is None:
        periods = [5, 10, 20, 60]
    closes = [k["close"] for k in klines]
    
    # EMA 12/26(MACD 常用)
    ema12 = _ema(closes, 12)
    ema26 = _ema(closes, 26)
    
    result = []
    for i, k in enumerate(klines):
        row = {"date": k["date"], "close": k["close"]}
        for p in periods:
            if i >= p - 1:
                row[f"ma{p}"] = round(sum(closes[i - p + 1:i + 1]) / p, 4)
            else:
                row[f"ma{p}"] = None
        row["ema12"] = round(ema12[i], 4)
        row["ema26"] = round(ema26[i], 4)
        result.append(row)
    return result

3.2 MACD

def calc_macd(klines: list[dict], fast: int = 12, slow: int = 26,
              signal: int = 9) -> list[dict]:
    """
    MACD (Moving Average Convergence Divergence)
    klines: K线数据
    fast/slow/signal: 快线/慢线/信号线周期(默认 12/26/9)
    返回: [{date, close, dif, dea, macd_hist}, ...]
    
    dif = EMA(fast) - EMA(slow)        金叉/死叉看 dif 穿越 dea
    dea = EMA(signal) of dif           信号线
    macd_hist = (dif - dea) * 2        柱状图(红涨绿跌)
    """
    closes = [k["close"] for k in klines]
    ema_fast = _ema(closes, fast)
    ema_slow = _ema(closes, slow)
    
    dif = [round(f - s, 4) for f, s in zip(ema_fast, ema_slow)]
    dea = _ema(dif, signal)
    
    result = []
    for i, k in enumerate(klines):
        result.append({
            "date": k["date"],
            "close": k["close"],
            "dif": round(dif[i], 4),
            "dea": round(dea[i], 4),
            "macd_hist": round((dif[i] - dea[i]) * 2, 4),
        })
    return result

3.3 RSI

def calc_rsi(klines: list[dict],
             periods: list[int] = None) -> list[dict]:
    """
    RSI (Relative Strength Index)
    klines: K线数据
    periods: 周期列表(默认 [6, 12, 24])
    返回: [{date, close, rsi6, rsi12, rsi24}, ...]
    
    RSI > 70 超买区(可能回调)
    RSI < 30 超卖区(可能反弹)
    """
    if periods is None:
        periods = [6, 12, 24]
    closes = [k["close"] for k in klines]
    
    # 涨跌额序列
    changes = [0.0] + [closes[i] - closes[i - 1] for i in range(1, len(closes))]
    gains = [max(c, 0) for c in changes]
    losses = [max(-c, 0) for c in changes]
    
    result = []
    for i, k in enumerate(klines):
        row = {"date": k["date"], "close": k["close"]}
        for p in periods:
            if i < p:
                row[f"rsi{p}"] = None
                continue
            avg_gain = sum(gains[i - p + 1:i + 1]) / p
            avg_loss = sum(losses[i - p + 1:i + 1]) / p
            if avg_loss == 0:
                row[f"rsi{p}"] = 100.0
            else:
                rs = avg_gain / avg_loss
                row[f"rsi{p}"] = round(100 - 100 / (1 + rs), 2)
        result.append(row)
    return result

3.4 KDJ

def calc_kdj(klines: list[dict], n: int = 9,
             m1: int = 3, m2: int = 3) -> list[dict]:
    """
    KDJ 随机指标
    klines: K线数据
    n: RSV 周期(默认9)
    m1/m2: K/D 平滑系数(默认3/3)
    返回: [{date, close, k, d, j}, ...]
    
    K/D > 80 超买,K/D < 20 超卖
    J > 100 或 J < 0 为极端信号
    金叉: K 上穿 D;死叉: K 下穿 D
    """
    k_val, d_val = 50.0, 50.0
    result = []
    
    for i, kline in enumerate(klines):
        if i < n - 1:
            result.append({"date": kline["date"], "close": kline["close"],
                           "k": None, "d": None, "j": None})
            continue
        
        window = klines[i - n + 1:i + 1]
        high_n = max(w["high"] for w in window)
        low_n = min(w["low"] for w in window)
        
        rsv = (kline["close"] - low_n) / (high_n - low_n) * 100 if high_n != low_n else 50.0
        k_val = (1 / m1) * rsv + (1 - 1 / m1) * k_val
        d_val = (1 / m2) * k_val + (1 - 1 / m2) * d_val
        j_val = 3 * k_val - 2 * d_val
        
        result.append({
            "date": kline["date"],
            "close": kline["close"],
            "k": round(k_val, 2),
            "d": round(d_val, 2),
            "j": round(j_val, 2),
        })
    return result

3.5 Bollinger Bands

def calc_boll(klines: list[dict], period: int = 20,
              num_std: float = 2.0) -> list[dict]:
    """
    布林带 (Bollinger Bands)
    klines: K线数据
    period: 中轨 MA 周期(默认20)
    num_std: 标准差倍数(默认2)
    返回: [{date, close, upper, middle, lower, bandwidth}, ...]
    
    价格触及 upper → 可能超买
    价格触及 lower → 可能超卖
    bandwidth 收窄 → 即将变盘
    """
    closes = [k["close"] for k in klines]
    result = []
    
    for i, k in enumerate(klines):
        if i < period - 1:
            result.append({"date": k["date"], "close": k["close"],
                           "upper": None, "middle": None, "lower": None,
                           "bandwidth": None})
            continue
        
        window = closes[i - period + 1:i + 1]
        ma = sum(window) / period
        std = (sum((x - ma) ** 2 for x in window) / period) ** 0.5
        upper = ma + num_std * std
        lower = ma - num_std * std
        
        result.append({
            "date": k["date"],
            "close": k["close"],
            "upper": round(upper, 4),
            "middle": round(ma, 4),
            "lower": round(lower, 4),
            "bandwidth": round((upper - lower) / ma * 100, 2) if ma else None,
        })
    return result

Layer 4: Fundamentals Layer

4.1 Three Financial Statements — Dongcai datacenter

Dongcai datacenter provides balance sheets, income statements, and cash flow statements for US/HK stocks, with Chinese field names, expanded by line item.

def financial_statements_eastmoney(secucode: str, statement: str = "balance",
                                     page_size: int = 200) -> list[dict]:
    """
    东财 datacenter 财报三表
    secucode: "AAPL.O" (NASDAQ) / "BABA.N" (NYSE) / "00700.HK" (港股)
    statement: "balance" / "income" / "cashflow"
    返回: [{ITEM_NAME, AMOUNT, YOY_RATIO, REPORT, REPORT_DATE, ...}, ...]
    
    注意: 数据按科目行展开,每行一个科目(如"流动资产合计"、"营业收入"等),
    同一期报告有多行。用 REPORT_DATE 分组可还原整张报表。
    """
    # 报表名映射(注意命名不统一:balance/income 用 F10,cashflow 用 SK)
    report_map = {
        "balance": {"us": "RPT_USF10_FN_BALANCE", "hk": "RPT_HKF10_FN_BALANCE"},
        "income":  {"us": "RPT_USF10_FN_INCOME",  "hk": "RPT_HKF10_FN_INCOME"},
        "cashflow": {"us": "RPT_USSK_FN_CASHFLOW", "hk": "RPT_HKSK_FN_CASHFLOW"},
    }
    
    market = "hk" if secucode.endswith(".HK") else "us"
    report_name = report_map[statement][market]
    
    return eastmoney_datacenter(
        report_name=report_name,
        filter_str=f'(SECUCODE="{secucode}")',
        page_size=page_size,
        sort_columns="REPORT_DATE",
        sort_types="-1",
    )
    # 每行字段:
    # SECUCODE, SECURITY_CODE, SECURITY_NAME_ABBR, REPORT_DATE,
    # STD_ITEM_CODE, ITEM_NAME (科目名), AMOUNT (金额),
    # YOY_RATIO (同比%), REPORT (如 "2026/Q2"), REPORT_TYPE,
    # ACCOUNT_STANDARD (如 "美国会计准则"/"国际会计准则"),
    # CURRENCY (如 "美元"/"人民币")

4.2 Key Financial Indicators (Chinese) — Dongcai GMAININDICATOR

The GMAININDICATOR report from Dongcai datacenter provides an overview of key financial indicators in Chinese. 49 fields for US stocks, 75 fields for HK stocks, including ROE/ROA/EPS/gross margin/debt-to-asset ratio/current ratio, etc., reported quarterly.

def key_indicators_eastmoney(secucode: str, page_size: int = 4) -> list[dict]:
    """
    东财 GMAININDICATOR 关键财务指标(中文)
    secucode: "AAPL.O" (NASDAQ) / "BABA.N" (NYSE) / "00700.HK" (港股)
    page_size: 返回最近几期报告(默认4期=一年)
    返回: [{REPORT_DATE, OPERATE_INCOME, BASIC_EPS, ROE_AVG, ROA, ...}, ...]
    
    美股核心字段(49): OPERATE_INCOME(营收), GROSS_PROFIT(毛利), GROSS_PROFIT_RATIO(毛利率%),
      PARENT_HOLDER_NETPROFIT(归母净利), NET_PROFIT_RATIO(净利率%), BASIC_EPS, DILUTED_EPS,
      ROE_AVG(平均ROE%), ROA(%), CURRENT_RATIO(流动比率), DEBT_ASSET_RATIO(资产负债率%),
      OPERATE_INCOME_YOY(营收同比%), BASIC_EPS_YOY(EPS同比%)
    
    港股额外字段(75): BPS(每股净资产), ROIC(投入资本回报率), EQUITY_RATIO(产权比率),
      HOLDER_PROFIT(股东应占溢利), OCF_SALES(经营现金流/营收%), DPS_HKD(每股股息),
      DIVI_RATIO(股息率%), PER_NETCASH_OPERATE(每股经营现金流)
    """
    market = "hk" if secucode.endswith(".HK") else "us"
    report_name = f"RPT_{'HK' if market == 'hk' else 'US'}F10_FN_GMAININDICATOR"
    
    return eastmoney_datacenter(
        report_name=report_name,
        filter_str=f'(SECUCODE="{secucode}")',
        page_size=page_size,
        sort_columns="REPORT_DATE",
        sort_types="-1",
    )

4.3 Key Financial Indicators (English) — Yahoo quoteSummary

The Yahoo quoteSummary financialData + defaultKeyStatistics module provides the most essential valuation metrics.

def key_statistics(symbol: str) -> dict:
    """
    Yahoo 关键财务指标
    symbol: "AAPL" (美股) 或 "0700.HK" (港股)
    返回: PE/PB/EV/EBITDA/利润率/目标价/ROE/Beta 等
    """
    data = yahoo_quote_summary(symbol, ["financialData", "defaultKeyStatistics", "summaryDetail"])
    
    fd = data.get("financialData", {})
    ks = data.get("defaultKeyStatistics", {})
    sd = data.get("summaryDetail", {})
    
    def _val(d, key):
        v = d.get(key, {})
        return v.get("raw") if isinstance(v, dict) else v
    
    return {
        # 价格相关
        "current_price": _val(fd, "currentPrice"),
        "target_high": _val(fd, "targetHighPrice"),
        "target_low": _val(fd, "targetLowPrice"),
        "target_mean": _val(fd, "targetMeanPrice"),
        "recommendation": fd.get("recommendationKey"),  # buy/hold/sell
        
        # 估值指标
        "trailing_pe": _val(sd, "trailingPE"),
        "forward_pe": _val(ks, "forwardPE"),
        "peg_ratio": _val(ks, "pegRatio"),
        "price_to_book": _val(ks, "priceToBook"),
        "enterprise_value": _val(ks, "enterpriseValue"),
        "ev_to_ebitda": _val(ks, "enterpriseToEbitda"),
        "ev_to_revenue": _val(ks, "enterpriseToRevenue"),
        
        # 盈利能力
        "profit_margin": _val(ks, "profitMargins"),
        "operating_margin": _val(fd, "operatingMargins"),
        "gross_margin": _val(fd, "grossMargins"),
        "return_on_equity": _val(fd, "returnOnEquity"),
        "return_on_assets": _val(fd, "returnOnAssets"),
        
        # 成长性
        "earnings_growth": _val(fd, "earningsGrowth"),
        "revenue_growth": _val(fd, "revenueGrowth"),
        
        # 风险
        "beta": _val(ks, "beta"),
        "short_ratio": _val(ks, "shortRatio"),
        
        # 股息
        "dividend_yield": _val(sd, "dividendYield"),
        "payout_ratio": _val(ks, "payoutRatio"),
        
        # 规模
        "market_cap": _val(sd, "marketCap"),
        "total_revenue": _val(fd, "totalRevenue"),
        "total_cash": _val(fd, "totalCash"),
        "total_debt": _val(fd, "totalDebt"),
    }

4.4 Analyst Expectations and Ratings — Yahoo quoteSummary

def analyst_estimates(symbol: str) -> dict:
    """
    Yahoo 分析师预期 — EPS预测/评级趋势/升降级历史
    symbol: "AAPL" 或 "0700.HK"
    """
    data = yahoo_quote_summary(symbol, [
        "earningsTrend", "recommendationTrend", "upgradeDowngradeHistory",
        "earnings", "earningsHistory",
    ])
    
    # EPS 趋势
    et = data.get("earningsTrend", {}).get("trend", [])
    eps_trend = []
    for t in et:
        eps_trend.append({
            "period": t.get("period"),
            "end_date": t.get("endDate"),
            "eps_estimate": t.get("earningsEstimate", {}).get("avg", {}).get("raw"),
            "eps_high": t.get("earningsEstimate", {}).get("high", {}).get("raw"),
            "eps_low": t.get("earningsEstimate", {}).get("low", {}).get("raw"),
            "revenue_estimate": t.get("revenueEstimate", {}).get("avg", {}).get("raw"),
            "num_analysts": t.get("earningsEstimate", {}).get("numberOfAnalysts", {}).get("raw"),
        })
    
    # 评级趋势 (最近4个月)
    rt = data.get("recommendationTrend", {}).get("trend", [])
    rating_trend = []
    for r_ in rt:
        rating_trend.append({
            "period": r_.get("period"),
            "strong_buy": r_.get("strongBuy"),
            "buy": r_.get("buy"),
            "hold": r_.get("hold"),
            "sell": r_.get("sell"),
            "strong_sell": r_.get("strongSell"),
        })
    
    # 升降级历史 (最近20条)
    udh = data.get("upgradeDowngradeHistory", {}).get("history", [])[:20]
    upgrades = []
    for u in udh:
        upgrades.append({
            "date": u.get("epochGradeDate"),
            "firm": u.get("firm"),
            "to_grade": u.get("toGrade"),
            "from_grade": u.get("fromGrade"),
            "action": u.get("action"),  # up/down/main/init
        })
    
    return {
        "eps_trend": eps_trend,
        "rating_trend": rating_trend,
        "upgrade_downgrade": upgrades,
    }

4.5 Institutional Holdings — Yahoo quoteSummary

def institutional_holders(symbol: str) -> dict:
    """
    Yahoo 机构持仓 — 前10大机构 + 内部人持股比例
    symbol: "AAPL" 或 "0700.HK"
    """
    data = yahoo_quote_summary(symbol, ["institutionOwnership", "majorHoldersBreakdown"])
    
    # 持股比例总览
    mhb = data.get("majorHoldersBreakdown", {})
    def _val(d, key):
        v = d.get(key, {})
        return v.get("raw") if isinstance(v, dict) else v
    
    overview = {
        "insiders_pct": _val(mhb, "insidersPercentHeld"),
        "institutions_pct": _val(mhb, "institutionsPercentHeld"),
        "institutions_float_pct": _val(mhb, "institutionsFloatPercentHeld"),
        "institutions_count": _val(mhb, "institutionsCount"),
    }
    
    # 前10大机构
    io = data.get("institutionOwnership", {}).get("ownershipList", [])
    top_holders = []
    for h in io[:10]:
        top_holders.append({
            "name": h.get("organization"),
            "shares": _val(h, "position"),
            "value": _val(h, "value"),
            "pct_held": _val(h, "pctHeld"),
            "report_date": h.get("reportDate", {}).get("fmt") if isinstance(h.get("reportDate"), dict) else None,
        })
    
    return {"overview": overview, "top_holders": top_holders}

4.6 Annual/Quarterly Financial Statement Details — Yahoo quoteSummary

Dongcai datacenter expands by line item, Yahoo directly returns the complete statement structure; the two are complementary.

def financial_statements_yahoo(symbol: str,
                                 quarterly: bool = False) -> dict:
    """
    Yahoo 财报三表 — 结构化完整报表
    symbol: "AAPL" 或 "0700.HK"
    quarterly: False=年度, True=季度
    返回: {"income": [...], "balance": [...], "cashflow": [...]}
    """
    suffix = "Quarterly" if quarterly else ""
    data = yahoo_quote_summary(symbol, [
        f"incomeStatementHistory{suffix}",
        f"balanceSheetHistory{suffix}",
        f"cashflowStatementHistory{suffix}",
    ])
    
    def _extract(statements):
        result = []
        for stmt in statements:
            row = {}
            for k, v in stmt.items():
                if isinstance(v, dict) and "raw" in v:
                    row[k] = v["raw"]
                elif isinstance(v, dict) and "fmt" in v:
                    row[k] = v["fmt"]
                else:
                    row[k] = v
            result.append(row)
        return result
    
    income_key = f"incomeStatementHistory{suffix}"
    balance_key = f"balanceSheetHistory{suffix}"
    cashflow_key = f"cashflowStatementHistory{suffix}"
    
    return {
        "income": _extract(data.get(income_key, {}).get("incomeStatementHistory", [])),
        "balance": _extract(data.get(balance_key, {}).get("balanceSheetStatements", [])),
        "cashflow": _extract(data.get(cashflow_key, {}).get("cashflowStatements", [])),
    }

Layer 5: Capital Flow Layer

5.1 Daily Capital Flow — Dongcai push2his

def fund_flow_daily(ticker_or_code: str, secid_prefix: int = 105,
                      limit: int = 100) -> list[dict]:
    """
    东财 push2his 日级资金流 — 主力/大单/中单/小单净流入
    美股: fund_flow_daily("AAPL", 105)  # NASDAQ
          fund_flow_daily("BABA", 106)  # NYSE
    港股: fund_flow_daily("00700", 116)
    返回: [{date, main_net, big_net, mid_net, small_net, main_pct, ...}, ...]
    """
    url = "https://push2his.eastmoney.com/api/qt/stock/fflow/daykline/get"
    params = {
        "secid": f"{secid_prefix}.{ticker_or_code}",
        "klt": 101,
        "fields1": "f1,f2,f3,f7",
        "fields2": "f51,f52,f53,f54,f55,f56,f57",
        "lmt": limit,
    }
    r = requests.get(url, params=params, timeout=15)
    d = r.json()
    data = d.get("data")
    if not data or not data.get("klines"):
        return []
    
    result = []
    for line in data["klines"]:
        parts = line.split(",")
        # f51=日期, f52=主力净流入, f53=小单净流入, f54=中单净流入, f55=大单净流入, f56=超大单净流入
        result.append({
            "date": parts[0],
            "main_net": float(parts[1]),       # 主力净流入(元)
            "small_net": float(parts[2]),       # 小单净流入
            "mid_net": float(parts[3]),         # 中单净流入
            "big_net": float(parts[4]),         # 大单净流入
            "super_big_net": float(parts[5]),   # 超大单净流入
            "main_pct": float(parts[6]) if len(parts) > 6 and parts[6] else 0,  # 主力净占比%
        })
    return result

Layer 6: Options Layer

6.1 Options Chain + Greeks + 0DTE Flow — CBOE Official (Primary ⭐ New in V2.0)

Data source cdn.cboe.com, zero authentication. A single symbol's full chain is returned in one request (tested NVDA 3,908 / TSLA 6,200 / AAPL 3,576 contracts), fields include bid/ask/volume/open_interest/iv/delta/gamma/vega/theta/rho。

⚠️ Compliance (Grade C): Cboe's Use of Content policy requires obtaining written approval and a license before use. The following codeis for personal research only; for commercial use or redistribution, authorization must first be requested from Cboe.

import re
from datetime import datetime, timezone, timedelta
# 依赖「官方源统一出口」的 official_get / assert_us_ticker

CBOE_BASE = "https://cdn.cboe.com/api/global/delayed_quotes"
# OCC 合约代码: 标的 + YYMMDD + C/P + 8位行权价(千分之一美元)
# root 允许含数字:拆股/分拆等公司行为会产生调整后合约(如 NVDA1、BRKB1)。
# 后面全是定宽组(6+1+8=15 字符),正则回溯能正确对齐,标准合约解析结果不变。
_OSI = re.compile(r"^(?P<root>[A-Z][A-Z0-9]*)(?P<y>\d{2})(?P<m>\d{2})(?P<d>\d{2})"
                  r"(?P<cp>[CP])(?P<strike>\d{8})$")


def parse_osi(symbol: str) -> dict:
    """解析 OCC 合约代码 → {expiry, type, strike};无法解析返回 {}"""
    m = _OSI.match(symbol)
    if not m:
        return {}
    g = m.groupdict()
    return {"expiry": f"20{g['y']}-{g['m']}-{g['d']}",
            "type": "call" if g["cp"] == "C" else "put",
            "strike": int(g["strike"]) / 1000.0}


def options_chain_cboe(ticker: str) -> dict:
    """
    CBOE 官方延时期权全链(仅美股)。
    返回 {"ticker","timestamp","spot","contracts":[{symbol,expiry,type,strike,bid,ask,
          volume,open_interest,iv,delta,gamma,vega,theta,rho,last_trade_price}]}
    """
    ticker = assert_us_ticker(ticker)
    raw = official_get(f"{CBOE_BASE}/options/{ticker}.json", as_json=True)
    data = raw.get("data") or {}
    contracts = []
    for o in data.get("options") or []:
        meta = parse_osi(o.get("option", ""))
        if not meta:
            continue
        contracts.append({
            "symbol": o["option"], **meta,
            "bid": o.get("bid"), "ask": o.get("ask"),
            "volume": o.get("volume") or 0,
            "open_interest": o.get("open_interest") or 0,
            "iv": o.get("iv"), "delta": o.get("delta"), "gamma": o.get("gamma"),
            "vega": o.get("vega"), "theta": o.get("theta"), "rho": o.get("rho"),
            "last_trade_price": o.get("last_trade_price"),
        })
    if not contracts:
        raise DataNotAvailable(f"{ticker} 未返回任何期权合约 —— 该标的可能无期权,"
                               f"或不在 CBOE 覆盖范围(CBOE 仅覆盖美股)")
    return {"ticker": ticker, "timestamp": raw.get("timestamp"),
            "spot": data.get("current_price"), "contracts": contracts}


try:
    from zoneinfo import ZoneInfo
    _ET_TZ = ZoneInfo("America/New_York")
except Exception:      # Windows 上 zoneinfo 可能缺 tzdata
    _ET_TZ = None


def _et_today() -> str:
    """
    美东今日 YYYY-MM-DD,用于 0DTE 判定。

    ⚠️ 必须区分 EDT(UTC-4) 与 EST(UTC-5):硬编码 UTC-4 会让冬令时
    UTC 04:00–05:00 这一小时算成次日,导致 0DTE 选错到期日。
    """
    now = datetime.now(timezone.utc)
    if _ET_TZ is not None:
        return now.astimezone(_ET_TZ).strftime("%Y-%m-%d")
    # 无 tzdata 时的回退:按美国 DST 规则(3月第2个周日 ~ 11月第1个周日)自算
    y = now.year
    # 美国 DST 在**当地时间 2:00** 切换,换算成 UTC:
    #   开始 = 3月第2个周日 02:00 EST = 07:00 UTC
    #   结束 = 11月第1个周日 02:00 EDT = 06:00 UTC
    # 用 00:00 UTC 当切换点会在切换日凌晨那几小时取错偏移。
    mar8 = datetime(y, 3, 8, tzinfo=timezone.utc)
    dst_start = (mar8 + timedelta(days=(6 - mar8.weekday()) % 7)
                 ).replace(hour=7)
    nov1 = datetime(y, 11, 1, tzinfo=timezone.utc)
    dst_end = (nov1 + timedelta(days=(6 - nov1.weekday()) % 7)
               ).replace(hour=6)
    offset = 4 if dst_start <= now < dst_end else 5
    return (now - timedelta(hours=offset)).strftime("%Y-%m-%d")


def filter_expiry(chain: dict, expiry: str = None, dte_max: int = None) -> list[dict]:
    """按到期日筛选。expiry='0DTE' 取当日到期;dte_max 取 N 天内到期"""
    cs = chain["contracts"]
    if expiry == "0DTE":
        return [c for c in cs if c["expiry"] == _et_today()]
    if expiry:
        return [c for c in cs if c["expiry"] == expiry]
    if dte_max is not None:
        today = datetime.strptime(_et_today(), "%Y-%m-%d")
        return [c for c in cs
                if 0 <= (datetime.strptime(c["expiry"], "%Y-%m-%d") - today).days <= dte_max]
    return cs


def unusual_activity(contracts: list[dict], min_volume: int = 500,
                     vol_oi_min: float = 1.0) -> list[dict]:
    """
    异动合约识别:成交量 >= min_volume 且 volume/open_interest >= vol_oi_min。
    vol/OI > 1 = 当日成交超过存量持仓 = 新建仓,是 options flow 的核心信号。
    """
    out = []
    for c in contracts:
        vol, oi = c["volume"], c["open_interest"]
        if vol < min_volume:
            continue
        ratio = vol / oi if oi > 0 else float("inf")
        if ratio >= vol_oi_min:
            out.append({**c, "vol_oi_ratio": round(ratio, 2) if oi > 0 else None})
    return sorted(out, key=lambda x: -x["volume"])


def chain_summary(contracts: list[dict]) -> dict:
    """链级聚合:put/call 量比与持仓比、成交量加权 IV、净 delta 敞口"""
    calls = [c for c in contracts if c["type"] == "call"]
    puts = [c for c in contracts if c["type"] == "put"]
    cv, pv = sum(c["volume"] for c in calls), sum(c["volume"] for c in puts)
    coi, poi = sum(c["open_interest"] for c in calls), sum(c["open_interest"] for c in puts)
    traded = [c for c in contracts if c["volume"] > 0 and c.get("iv")]
    tot_v = sum(c["volume"] for c in traded)
    vwiv = sum(c["iv"] * c["volume"] for c in traded) / tot_v if tot_v else None
    net_delta = sum((c.get("delta") or 0) * c["volume"] * 100 for c in contracts)
    return {"call_volume": cv, "put_volume": pv,
            "put_call_volume_ratio": round(pv / cv, 3) if cv else None,
            "call_oi": coi, "put_oi": poi,
            "put_call_oi_ratio": round(poi / coi, 3) if coi else None,
            "volume_weighted_iv": round(vwiv, 4) if vwiv else None,
            "net_delta_exposure_shares": round(net_delta),
            "contracts_total": len(contracts),
            "contracts_traded": len([c for c in contracts if c["volume"] > 0])}


def cboe_quote(ticker: str) -> dict:
    """CBOE 个股快照(含现价,可与期权链配合定 ATM)"""
    return official_get(f"{CBOE_BASE}/quotes/{assert_us_ticker(ticker)}.json",
                        as_json=True)["data"]

Usage

chain = options_chain_cboe("NVDA")
zero  = filter_expiry(chain, expiry="0DTE")      # 当日到期合约
near  = filter_expiry(chain, dte_max=7)          # 7 日内到期
flow  = unusual_activity(zero, min_volume=1000)  # 0DTE 异动
summ  = chain_summary(zero)                      # P/C 比、加权 IV、净 delta

Tested samples (2026-07-24)

Symbol Full chain 0DTE contracts P/C volume ratio Volume-weighted IV Net delta exposure
NVDA 3,908 168 0.542 43.1% +16,496,611 shares
TSLA 6,200 326 1.041 83.2% −37,364,065 shares

TSLA fell −14.52% that day; the three options-layer indicators (put dominance / IV 83% / negative net delta) independently pointed in the same direction, allowing cross-validation.

⚠️ Limitations: The CBOE endpoint only coversUS stocks(HK stock options require HKEX proprietary interfaces); the data isdelayed, not suitable for live order placement, suitable for research and flow analysis.


6.2 Options Chain — Yahoo Finance (Fallback · No Greeks)

def options_chain(symbol: str, expiration: int = None) -> dict:
    """
    Yahoo 期权链 — calls + puts 完整数据(仅美股)
    symbol: "AAPL", "TSLA" 等美股 ticker
    ⚠️ 港股(如0700.HK)期权不在Yahoo覆盖范围,调用会返回空列表
    expiration: Unix timestamp (不传则返回最近到期日 + 所有到期日列表)
    返回: {"expiration_dates": [...], "calls": [...], "puts": [...]}
    """
    s = get_yahoo_session()
    params = {"crumb": s._crumb}
    if expiration:
        params["date"] = expiration
    
    r = s.get(f"https://query2.finance.yahoo.com/v7/finance/options/{symbol}",
              params=params, timeout=15)
    r.raise_for_status()
    
    oc = r.json().get("optionChain", {}).get("result", [{}])[0]
    
    exp_dates = oc.get("expirationDates", [])
    options = oc.get("options", [{}])[0] if oc.get("options") else {}
    
    def _parse_options(opts):
        result = []
        for o in opts:
            def _val(key):
                v = o.get(key, {})
                return v.get("raw") if isinstance(v, dict) else v
            result.append({
                "strike": _val("strike"),
                "last_price": _val("lastPrice"),
                "bid": _val("bid"),
                "ask": _val("ask"),
                "volume": _val("volume"),
                "open_interest": _val("openInterest"),
                "implied_volatility": _val("impliedVolatility"),
                "in_the_money": o.get("inTheMoney"),
                "expiration": o.get("expiration", {}).get("fmt") if isinstance(o.get("expiration"), dict) else None,
                "contract_symbol": o.get("contractSymbol"),
            })
        return result
    
    return {
        "expiration_dates": exp_dates,  # Unix timestamps, 可依次传入获取各期
        "calls": _parse_options(options.get("calls", [])),
        "puts": _parse_options(options.get("puts", [])),
        "underlying_price": oc.get("quote", {}).get("regularMarketPrice"),
    }

Layer 7: SEC Filing Layer (US stocks only)

7.1 SEC Filing List — EDGAR submissions

SEC_HEADERS = {"User-Agent": "SimonLin global-stock-data/1.0 (contact@example.com)"}

def sec_filings(cik: str, form_type: str = None) -> dict:
    """
    SEC EDGAR Filing 列表
    cik: CIK号(10位补零),如 "0000320193" (Apple)
         可通过 ticker_to_cik() 从 ticker 转换
    form_type: 筛选类型,如 "10-K", "10-Q", "8-K"(不传返回全部)
    返回: {"company_name": ..., "filings": [{form, date, accession_number, primary_document}, ...]}
    """
    url = f"https://data.sec.gov/submissions/CIK{cik}.json"
    r = requests.get(url, headers=SEC_HEADERS, timeout=15)
    r.raise_for_status()
    
    data = r.json()
    recent = data.get("filings", {}).get("recent", {})
    
    forms = recent.get("form", [])
    dates = recent.get("filingDate", [])
    accessions = recent.get("accessionNumber", [])
    primary_docs = recent.get("primaryDocument", [])
    descriptions = recent.get("primaryDocDescription", [])
    
    filings = []
    for i in range(len(forms)):
        if form_type and forms[i] != form_type:
            continue
        filings.append({
            "form": forms[i],
            "date": dates[i],
            "accession_number": accessions[i],
            "primary_document": primary_docs[i] if i < len(primary_docs) else "",
            "description": descriptions[i] if i < len(descriptions) else "",
            "url": f"https://www.sec.gov/Archives/edgar/data/{int(cik)}/{accessions[i].replace('-', '')}/{primary_docs[i]}" if i < len(primary_docs) and primary_docs[i] else "",
        })
    
    return {
        "company_name": data.get("name"),
        "cik": cik,
        "ticker": data.get("tickers", [""])[0] if data.get("tickers") else "",
        "filings": filings[:50],  # 最近50条
    }

7.2 SEC XBRL Structured Financial Data — EDGAR companyfacts

Covers 503 GAAP metrics, enabling precise extraction of multi-year revenue/net income/EPS/assets/liabilities, etc.

def sec_xbrl_facts(cik: str, metrics: list[str] = None) -> dict:
    """
    SEC EDGAR XBRL 结构化财务数据
    cik: CIK号(10位补零)
    metrics: 要提取的指标名,如 ["RevenueFromContractWithCustomerExcludingAssessedTax",
             "NetIncomeLoss", "EarningsPerShareDiluted"]
             不传则返回所有可用指标名列表
    
    返回: {"company": ..., "metrics": {"Revenue": [{end, val, form, filed}, ...], ...}}
    """
    url = f"https://data.sec.gov/api/xbrl/companyfacts/CIK{cik}.json"
    r = requests.get(url, headers=SEC_HEADERS, timeout=15)
    r.raise_for_status()
    
    facts = r.json()
    us_gaap = facts.get("facts", {}).get("us-gaap", {})
    
    # 如果不传 metrics,返回所有可用指标
    if not metrics:
        available = []
        for k, v in us_gaap.items():
            label = v.get("label", k)
            units = list(v.get("units", {}).keys())
            available.append({"name": k, "label": label, "units": units})
        return {
            "company": facts.get("entityName"),
            "total_metrics": len(available),
            "available_metrics": available,
        }
    
    # 提取指定指标
    result = {}
    for metric_name in metrics:
        metric = us_gaap.get(metric_name, {})
        if not metric:
            result[metric_name] = []
            continue
        
        # 自动选择单位(USD 或 USD/shares)
        units = metric.get("units", {})
        unit_key = "USD" if "USD" in units else list(units.keys())[0] if units else None
        if not unit_key:
            result[metric_name] = []
            continue
        
        entries = units[unit_key]
        # 只取 10-K 和 10-Q
        filtered = [e for e in entries if e.get("form") in ("10-K", "10-Q")]
        result[metric_name] = [{
            "end": e.get("end"),
            "val": e.get("val"),
            "form": e.get("form"),
            "filed": e.get("filed"),
            "fy": e.get("fy"),
            "fp": e.get("fp"),
        } for e in filtered[-20:]]  # 最近20条
    
    return {
        "company": facts.get("entityName"),
        "metrics": result,
    }

Quick reference for common XBRL metric names:

Metric XBRL Name
Operating Revenue RevenueFromContractWithCustomerExcludingAssessedTax or Revenues
Net Income NetIncomeLoss
Diluted EPS EarningsPerShareDiluted
Basic EPS EarningsPerShareBasic
Total Assets Assets
Total Liabilities Liabilities
Shareholders' Equity StockholdersEquity
Operating Cash Flow NetCashProvidedByOperatingActivities
R&D Expenses ResearchAndDevelopmentExpense
Share Repurchases PaymentsForRepurchaseOfCommonStock
Dividend Payments PaymentsOfDividends

Layer 8: Tool Layer

8.1 Stock Search — Eastmoney search API

def stock_search(keyword: str, count: int = 10) -> list[dict]:
    """
    东财股票搜索 — 支持中英文,返回代码+市场+中文名
    keyword: "AAPL" / "苹果" / "Tencent" / "00700" / "特斯拉"
    返回: [{code, name, mkt_num, market_name, security_type}, ...]
    
    mkt_num 即 push2/push2his 的 secid 前缀:
    105=NASDAQ, 106=NYSE, 107=美股ETF, 116=港股
    """
    url = "https://searchapi.eastmoney.com/api/suggest/get"
    params = {
        "input": keyword,
        "type": 14,  # 14=全球市场
        "token": "D43BF722C8E33BDC906FB84D85E326E8",
        "count": count,
    }
    r = requests.get(url, params=params, timeout=10)
    d = r.json()
    
    suggestions = d.get("QuotationCodeTable", {}).get("Data", [])
    result = []
    for s in suggestions:
        mkt = s.get("MktNum", "")
        # 只保留美股和港股
        if str(mkt) not in ("105", "106", "107", "116"):
            continue
        
        market_map = {"105": "NASDAQ", "106": "NYSE", "107": "US_OTHER", "116": "HK"}
        result.append({
            "code": s.get("Code"),
            "name": s.get("Name"),
            "mkt_num": int(mkt),
            "market_name": market_map.get(str(mkt), str(mkt)),
            "security_type": s.get("SecurityTypeName"),
        })
    return result

8.2 Stock News — Yahoo Finance search

def stock_news(keyword: str, count: int = 10) -> list[dict]:
    """
    Yahoo Finance 新闻搜索
    keyword: 股票代码或关键词,如 "AAPL", "Tesla", "0700.HK"
    返回: [{title, publisher, link, publish_time, thumbnail}, ...]
    注意: 需要先获取 Yahoo cookie 才能调用,否则返回 400
    """
    s = requests.Session()
    s.headers["User-Agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
    s.get("https://fc.yahoo.com", timeout=10)  # 获取 cookie
    
    url = "https://query2.finance.yahoo.com/v1/finance/search"
    params = {"q": keyword, "quotesCount": 0, "newsCount": count}
    r = s.get(url, params=params, timeout=10)
    r.raise_for_status()
    
    news = r.json().get("news", [])
    result = []
    for n in news:
        result.append({
            "title": n.get("title"),
            "publisher": n.get("publisher"),
            "link": n.get("link"),
            "publish_time": n.get("providerPublishTime"),
            "thumbnail": n.get("thumbnail", {}).get("resolutions", [{}])[0].get("url") if n.get("thumbnail") else None,
        })
    return result

8.3 Ticker → CIK Mapping — SEC EDGAR (US stocks only)

_cik_cache = None

def ticker_to_cik(ticker: str) -> dict:
    """
    SEC EDGAR ticker → CIK 映射
    ticker: 如 "AAPL", "TSLA", "MSFT"
    返回: {"ticker": "AAPL", "cik": "0000320193", "company": "Apple Inc."}
    
    首次调用下载完整映射表(~10KB JSON, 10000+公司)并缓存。
    """
    global _cik_cache
    if not _cik_cache:
        r = requests.get("https://www.sec.gov/files/company_tickers.json",
                         headers=SEC_HEADERS, timeout=15)
        r.raise_for_status()
        _cik_cache = r.json()
    
    ticker_upper = ticker.upper()
    for _, v in _cik_cache.items():
        if v.get("ticker") == ticker_upper:
            cik_str = str(v["cik_str"]).zfill(10)
            return {
                "ticker": ticker_upper,
                "cik": cik_str,
                "company": v.get("title"),
            }
    return {}

8.4 Full-Market Stock List — Eastmoney push2

def market_stock_list(market: str = "us_nasdaq", sort_field: str = "f3",
                       sort_desc: bool = True, page: int = 1,
                       page_size: int = 20) -> dict:
    """
    东财 push2 全市场股票列表 — 涨跌幅/成交量/成交额排名
    market: "us_nasdaq" (m:105), "us_nyse" (m:106), "hk" (m:116)
    sort_field: 排序字段
      f3=涨跌幅, f5=成交量, f6=成交额, f2=最新价, f7=振幅, f15=最高, f16=最低
    sort_desc: True=降序(默认), False=升序
    page/page_size: 分页(默认第1页,每页20条)
    返回: {"total": 5925, "stocks": [{code, name, price, change_pct, volume, ...}, ...]}
    
    典型用途:
    - 今日涨幅 TOP 20: market_stock_list("us_nasdaq", "f3", True)
    - 今日跌幅 TOP 20: market_stock_list("us_nasdaq", "f3", False)
    - 成交量 TOP 20: market_stock_list("hk", "f5", True)
    - 遍历全市场: 循环 page=1..N, 每页100条做筛选
    """
    market_map = {"us_nasdaq": "m:105", "us_nyse": "m:106", "us_etf": "m:107", "hk": "m:116"}
    fs = market_map.get(market, market)
    
    url = "https://push2.eastmoney.com/api/qt/clist/get"
    params = {
        "fs": fs,
        "fields": "f2,f3,f4,f5,f6,f7,f12,f14,f15,f16,f17,f18",
        "pn": page,
        "pz": page_size,
        "fid": sort_field,
        "po": 1 if sort_desc else 0,
    }
    r = requests.get(url, params=params, timeout=15)
    d = r.json()
    data = d.get("data", {})
    
    total = data.get("total", 0)
    diff = data.get("diff", [])
    # 东财 push2 的 diff 有时是 list、有时是按序号为键的 dict(如 {"0":{...},"1":{...}})。
    # 直接 for item in diff 遇到 dict 会拿到字符串键 → AttributeError,统一成列表。
    if isinstance(diff, dict):
        diff = list(diff.values())
    
    stocks = []
    for item in diff:
        stocks.append({
            "code": item.get("f12"),         # 股票代码
            "name": item.get("f14"),         # 中文名
            "price": item.get("f2"),         # 最新价(原始值, 需÷10^小数位)
            "change_pct": round(item["f3"] / 100, 2) if item.get("f3") is not None else None,  # 涨跌幅(%)
            "change_amount": item.get("f4"), # 涨跌额(原始值)
            "volume": item.get("f5"),        # 成交量(股)
            "amount": item.get("f6"),        # 成交额
            "amplitude": round(item["f7"] / 100, 2) if item.get("f7") is not None else None,  # 振幅(%)
            "high": item.get("f15"),         # 最高(原始值)
            "low": item.get("f16"),          # 最低(原始值)
            "open": item.get("f17"),         # 开盘(原始值)
            "prev_close": item.get("f18"),   # 昨收(原始值)
        })
    
    return {"total": total, "stocks": stocks}

Layer 9: Short-Selling Layer — FINRA Reg SHO (Grade B ⭐ New in V2.0 · US stocks only)

Full MarketDailyShort-sale volume; there is no corresponding category for A-shares. A single file covers the entire market (measured: 12,112 stocks).

⚠️ Compliance (Grade B): The Reg SHO daily file is a regulatory disclosure file that FINRA proactively publishes for download, so downloading it directly is a routine use; however, its site terms prohibit bulk crawling of pages and state that the data is for non-commercial use.Before commercial use, please confirm with FINRA yourself.

from datetime import datetime, timedelta
# 依赖「官方源统一出口」的 official_get


def _recent_weekdays(days_back: int = 7) -> list[str]:
    d, out = datetime.now(), []
    while len(out) < days_back:
        if d.weekday() < 5:
            out.append(d.strftime("%Y%m%d"))
        d -= timedelta(days=1)
    return out


def short_volume_all(date: str = None, market: str = "CNMS") -> dict:
    """
    FINRA 全市场每日空头成交量。
    market: CNMS(合并全市场) / FNSQ(Nasdaq) / FNYX(NYSE) / FNRA(TRF)
    date: YYYYMMDD;不传则自动回退找最近有数据的交易日
    返回 {"date","market","count","data":{SYMBOL:{short,short_exempt,total,ratio}}}
    """
    for d in ([date] if date else _recent_weekdays(7)):
        try:
            raw = official_get(
                f"https://cdn.finra.org/equity/regsho/daily/{market}shvol{d}.txt")
        except DataNotAvailable:
            continue   # 该日无文件(非交易日/尚未发布),回退下一日
        # 其余异常(网络/限流/配置)直接抛出,不伪装成「没数据」
        rows = {}
        for line in raw.splitlines()[1:]:
            p = line.split("|")
            if len(p) < 5 or not p[1]:
                continue
            try:
                sv, se, tv = float(p[2]), float(p[3]), float(p[4])
            except ValueError:
                continue
            rows[p[1]] = {"short": sv, "short_exempt": se, "total": tv,
                          "ratio": round(sv / tv, 4) if tv else None}
        if rows:
            return {"date": d, "market": market, "count": len(rows), "data": rows}
    # 抛 DataNotAvailable 而非 RuntimeError:指定日期无数据时,
    # 调用方(如 short_volume_symbol 的多日循环)要能捕获并跳过这一天
    raise DataNotAvailable(f"未找到 {market} "
                           f"{'该日' if date else '近 7 个工作日'}的 Reg SHO 数据")


def short_volume_symbol(symbol: str, days: int = 5, market: str = "CNMS") -> list[dict]:
    """单只股票近 N 个交易日的空头成交占比时间序列"""
    out = []
    for d in _recent_weekdays(days * 2):
        if len(out) >= days:
            break
        try:
            snap = short_volume_all(date=d, market=market)
        except DataNotAvailable:
            continue
        rec = snap["data"].get(symbol.upper())
        if rec:
            out.append({"date": d, **rec})
    return out


def short_volume_ranking(snapshot: dict, min_total: float = 1_000_000,
                         top: int = 20) -> list[dict]:
    """空头占比排行(先按最小成交量过滤,避免小票噪音)"""
    rows = [{"symbol": s, **v} for s, v in snapshot["data"].items()
            if v["total"] >= min_total and v["ratio"] is not None]
    return sorted(rows, key=lambda x: -x["ratio"])[:top]

Measured (2026-07-23, covering 12,112 stocks): NVDA short volume ratio 37.5% / TSLA 48.3% / MU 49.6% / AAPL 50.7%; NVDA over the past three days 37.5% → 43.0% → 33.5%.

⚠️ How to read: short volume ≠ short interest. The former isthe portion of the day's sell transactions marked as short (including market-maker hedging, naturally on the high side, 40%~50% is common), while the latter isthe outstanding short position(disclosed bimonthly). Use it to look atdaily change trends, and do not draw conclusions from absolute values.


Layer 10: Filing Event Stream — SEC EDGAR (Grade S ⭐ New in V2.0 · US stocks only)

✅ Compliance (Grade S): This tool's only uncontroversial commercially usable source. Officially it explicitly allows scripted access and the data is free. 10 requests/second is the official hard limit, and the User-Agent must be declared (already handled by the unified egress, remember to change SEC_CONTACT)。

10.1 Daily Filing Stream (Form 4 insider / 8-K / 13F institutional holdings)

_FORM_LABEL = {"4": "内部人交易", "8-K": "重大事件", "13F-HR": "机构持仓",
               "144": "限售股拟出售", "10-K": "年报", "10-Q": "季报",
               "SC 13D": "举牌(主动)", "SC 13G": "举牌(被动)", "S-1": "IPO注册"}


def daily_filings(date: str = None, forms: list[str] = None) -> dict:
    """
    EDGAR 每日申报流。date=YYYYMMDD,不传自动回退找最近有数据的日子。
    forms: 只保留这些表单类型,如 ["4","8-K","13F-HR"];None=全部
    返回 {"date","total","by_form":{...},"filings":[{form,form_label,company,cik,date,url}]}
    """
    for d in ([date] if date else _recent_weekdays(7)):
        dt = datetime.strptime(d, "%Y%m%d")
        url = (f"https://www.sec.gov/Archives/edgar/daily-index/"
               f"{dt.year}/QTR{(dt.month - 1) // 3 + 1}/form.{d}.idx")
        try:
            raw = official_get(url)
        except DataNotAvailable:
            continue   # 该日无索引文件,回退下一日
        # 配置错误(SEC_CONTACT 未改)与网络错误在此直接抛出——
        # 否则会被 7 次循环吞掉,最终误报成「未找到 EDGAR 每日索引」
        lines = raw.splitlines()
        start = next((i + 1 for i, L in enumerate(lines) if L.startswith("---")), 11)
        filings, by_form = [], {}
        for L in lines[start:]:
            if len(L) < 98:
                continue
            form, company = L[:12].strip(), L[12:74].strip()
            cik, filed, path = L[74:86].strip(), L[86:98].strip(), L[98:].strip()
            if not form:
                continue
            by_form[form] = by_form.get(form, 0) + 1
            if forms and form not in forms:
                continue
            filings.append({"form": form, "form_label": _FORM_LABEL.get(form, ""),
                            "company": company, "cik": cik, "date": filed,
                            "url": f"https://www.sec.gov/Archives/{path}" if path else None})
        if by_form:
            return {"date": d, "total": sum(by_form.values()),
                    "by_form": dict(sorted(by_form.items(), key=lambda x: -x[1])),
                    "filings": filings}
    raise DataNotAvailable("未找到近 7 个工作日的 EDGAR 每日索引")

Measured (2026-07-23, 3,703 filings that day):424B2=627 / Form 4 insider=547 / 8-K=370 / 13F-HR=261 / D=204 / 144=118。

10.2 Full-Text Search (covering the full text of all filings from 2001 to present)

def fulltext_search(query: str, forms: str = None, date_from: str = None,
                    date_to: str = None, limit: int = 20) -> dict:
    """
    query: 加引号为精确短语,如 '"HBM4"'
    forms: "8-K" / "10-K" 等;date_from/to: YYYY-MM-DD
    """
    p = {"q": query, "from": 0, "size": limit}
    if forms:
        p["forms"] = forms
    if date_from:
        p["dateRange"], p["startdt"] = "custom", date_from
    if date_to:
        p["dateRange"], p["enddt"] = "custom", date_to
    j = official_get("https://efts.sec.gov/LATEST/search-index", params=p, as_json=True)
    hits = (j.get("hits") or {}).get("hits") or []
    return {"total": ((j.get("hits") or {}).get("total") or {}).get("value", 0),
            "results": [{"form": (h.get("_source") or {}).get("root_form"),
                         "company": ((h.get("_source") or {}).get("display_names") or [None])[0],
                         "filed": (h.get("_source") or {}).get("file_date"),
                         "id": h.get("_id")} for h in hits]}

Measured:fulltext_search('"HBM4"', forms="8-K") → 5 hits, including MICRON (MU) 2026-06-24 and AMD 2026-05-05. → Can be used to track in which company's formal filing a technical term/product code first appears.


Layer 11: Full-Market Cross-Section — EDGAR frames (Grade S ⭐ New in V2.0 · free screener)

Get in one requestall filing companies'value of a certain metric for a certain period. Measured "R&D Expenses CY2025Q1" covers 1,842 companies, and "Net Income CY2025Q1" covers 5,309 companies.

XBRL_TAGS = {
    "营业收入": "Revenues",
    "营业收入(合同)": "RevenueFromContractWithCustomerExcludingAssessedTax",
    "净利润": "NetIncomeLoss",
    "研发费用": "ResearchAndDevelopmentExpense",
    "毛利": "GrossProfit",
    "经营利润": "OperatingIncomeLoss",
    "总资产": "Assets",
    "股东权益": "StockholdersEquity",
    "现金及等价物": "CashAndCashEquivalentsAtCarryingValue",
    "经营现金流": "NetCashProvidedByUsedInOperatingActivities",
    "资本开支": "PaymentsToAcquirePropertyPlantAndEquipment",
    "长期负债": "LongTermDebtNoncurrent",
    "稀释EPS": "EarningsPerShareDiluted",
}

# ⚠️ 时点(instant)概念 —— 资产负债表科目描述的是「某一时刻的余额」,而非一段期间的发生额。
# SEC Frames 对这类概念**要求周期带 I 后缀**,且**没有纯年度周期**:
#   Assets/CY2025Q1  -> 404      Assets/CY2025Q1I -> 200 (5643 家)
#   Assets/CY2024    -> 404      Assets/CY2024Q4I -> 200 (6248 家)
# 期间(duration)概念(营收/净利/现金流等)则相反,用 CY2025Q1 / CY2024。
# 以上均为 2026-07-26 逐个实测结果。
_INSTANT_TAGS = {
    "Assets",
    "StockholdersEquity",
    "CashAndCashEquivalentsAtCarryingValue",
    "LongTermDebtNoncurrent",
}


def _frame_period(year: int, quarter, instant: bool) -> str:
    """时点概念没有纯年度周期,年度请求落到 Q4I。"""
    if instant:
        return f"CY{year}Q{quarter}I" if quarter else f"CY{year}Q4I"
    return f"CY{year}Q{quarter}" if quarter else f"CY{year}"


def market_frame(tag: str, year: int, quarter: int = None, unit: str = "USD",
                 instant=None) -> dict:
    """
    全市场横截面。tag 可用 XBRL_TAGS 的中文键或原始 XBRL 标签。
    quarter: 1-4 季度;None 为年度
    instant: 是否为时点(资产负债表)概念。None=自动判定。

    自动判定逻辑:先按 _INSTANT_TAGS 猜一种周期形式,404 再换另一种重试。
    这样**任意原始 XBRL 标签**(Liabilities / InventoryNet / AssetsCurrent …)
    都能正确取到数,而不必把所有时点概念都枚举进 _INSTANT_TAGS。
    已知类型时显式传 instant=True/False 可省掉一次探测请求。
    """
    tag = XBRL_TAGS.get(tag, tag)
    guess = (tag in _INSTANT_TAGS) if instant is None else instant
    attempts = [guess] if instant is not None else [guess, not guess]

    last_err = None
    for is_instant in attempts:
        period = _frame_period(year, quarter, is_instant)
        try:
            j = official_get(
                f"https://data.sec.gov/api/xbrl/frames/us-gaap/{tag}/{unit}/{period}.json",
                timeout=45, as_json=True)
        except DataNotAvailable as e:      # 周期形式不对时 SEC 返回 404
            last_err = e
            continue
        rows = [{"cik": d.get("cik"), "entity": d.get("entityName"),
                 "value": d.get("val"), "end": d.get("end")} for d in j.get("data", [])]
        return {"tag": tag, "period": period, "unit": unit,
                "instant": is_instant, "count": len(rows), "data": rows}
    raise last_err


def frame_ranking(frame: dict, top: int = 20, ascending: bool = False) -> list[dict]:
    return sorted(frame["data"], key=lambda x: x["value"], reverse=not ascending)[:top]


def frame_screen(frame: dict, min_value: float = None,
                 max_value: float = None) -> list[dict]:
    """按数值区间筛选全市场公司"""
    out = frame["data"]
    if min_value is not None:
        out = [r for r in out if r["value"] >= min_value]
    if max_value is not None:
        out = [r for r in out if r["value"] <= max_value]
    return out

Measured: R&D expenses CY2025Q1 → Alphabet $13.56B / Meta $12.15B / Apple $8.55B / Microsoft $8.20B / NVDA $3.99B; a total of 17 companies with R&D > $1 billion.

⚠️ The XBRL tag definitions used by different companies are not entirely consistent (e.g., revenue has Revenues and RevenueFromContractWithCustomerExcludingAssessedTax two kinds); when doing cross-sectional comparisons, you need to cross the two tags and take the union.


Layer 12: Macro / Calendar (mostly S-tier ⭐ New in V2.0)

import csv, io


def treasury_yield_curve(year: int = None) -> list[dict]:
    """美国国债收益率曲线(每日,1M~30Y)。政府数据,S 级。返回 [0] 为最新一日"""
    year = year or datetime.now().year
    url = ("https://home.treasury.gov/resource-center/data-chart-center/interest-rates/"
           f"daily-treasury-rates.csv/{year}/all?type=daily_treasury_yield_curve"
           f"&field_tdr_date_value={year}&page&_format=csv")
    return list(csv.DictReader(io.StringIO(official_get(url))))


def cftc_cot(limit: int = 20, market_contains: str = None) -> list[dict]:
    """CFTC 持仓报告(COT)。政府数据,S 级"""
    q = {"$limit": limit, "$order": "report_date_as_yyyy_mm_dd DESC"}
    if market_contains:
        q["$where"] = f"upper(contract_market_name) like upper('%{market_contains}%')"
    return official_get("https://publicreporting.cftc.gov/resource/6dca-aqww.json",
                        params=q, as_json=True)


def earnings_calendar(date: str = None) -> dict:
    """Nasdaq 财报日历。date=YYYY-MM-DD,不传取今天"""
    date = date or datetime.now().strftime("%Y-%m-%d")
    j = official_get("https://api.nasdaq.com/api/calendar/earnings",
                     params={"date": date}, headers={"Accept": "application/json"},
                     as_json=True)
    rows = ((j.get("data") or {}).get("rows")) or []
    return {"date": date, "count": len(rows),
            "rows": [{"symbol": r.get("symbol"), "name": r.get("name"),
                      "time": r.get("time"), "eps_forecast": r.get("epsForecast"),
                      "market_cap": r.get("marketCap")} for r in rows]}

Measured (2026-07-23/24): Yield curve 3M=3.95 / 2Y=4.37 / 10Y=4.71 / 30Y=5.17 (10Y−2Y=+0.34, not inverted); CFTC COT latest report date 2026-07-14; today's earnings calendar 41 companies (AXP pre-market EPS expected $4.41, VZ, NEE, HCA…).


Data source priority

Scenario First priority Alternative Description
US stock quotes Sina gb_XXXX Tencent / Eastmoney push2 Sina has Chinese name + EPS + PE
Hong Kong stock quotes Tencent r_hkXXXXX Sina / Eastmoney push2 Tencent has the most complete fields (78)
US stock K-line Sina Yahoo chart Sina goes back to 1984; Yahoo supports multiple periods
Hong Kong stock K-line Yahoo chart — Sina Hong Kong stock K-line is no longer valid; push2his does not return Hong Kong stock K-line
Financial statements three tables (Chinese) Eastmoney datacenter — Chinese account names, expanded by row
Financial statements three tables (structured) Yahoo quoteSummary — English, complete statement structure
Key indicators (Chinese) Eastmoney GMAININDICATOR — ROE/ROA/EPS/gross margin/debt-to-asset ratio (US 49/HK 75 fields)
Key indicators (English) Yahoo quoteSummary — PE/PB/EV/margin/target price
Analyst expectations Yahoo quoteSummary — EPS forecast + rating + upgrades/downgrades
Institutional holdings Yahoo quoteSummary — Top 10 institutions + insiders
Fund flow Eastmoney push2his — Daily main/large/medium/small orders
Options chain/Greeks/IV/0DTE CBOE official ⭐ Yahoo options CBOE includes delta/gamma/vega/theta/rho; Yahoo has no Greeks. US stocks only. ⚠️C-tier requires authorization
Unusual options flow CBOE official ⭐ — unusual_activity(): vol/OI>1 = new position
Daily short volume for the entire market FINRA Reg SHO ⭐ — Single file covers the entire market, US stocks only. B-tier
Same-day filing flow (Form4/8-K/13F) EDGAR daily index ⭐ — US stocks only.S-tier commercially usable
Full-text filing search EDGAR FTS ⭐ — Full text from 2001 to present.S-tier commercially usable
Full-market fundamental cross-section EDGAR frames ⭐ — Free screener.S-tier commercially usable
Yield curve / COT / earnings calendar Treasury / CFTC / Nasdaq ⭐ — Macro and event-driven
SEC Filing EDGAR — Official data, US stocks only
XBRL financials EDGAR — 503 GAAP indicators
Search Eastmoney search Yahoo search Eastmoney has secid mapping
News Yahoo search — The only stable news source
Full market list Eastmoney push2 clist — Gainers/losers and volume rankings, US stocks 5925+ and Hong Kong stocks 18000+

Data source summary

Data source Compliance level Protocol Authentication Coverage
SEC EDGAR S HTTPS None (requires a real UA) US stock Filing/XBRL/Daily filing stream/Full-text search/Full-market cross-section
US Treasury S HTTPS None Yield curve (1M–30Y)
CFTC S HTTPS None COT positioning report
FINRA B HTTPS None US stocks Daily short selling volume (full market)(commercial use requires your own confirmation)
CBOE C HTTPS None US stocks Full option chain + Greeks + IV + 0DTE(use requires prior authorization from Cboe)
Nasdaq C HTTPS None US stocks Earnings calendar(terms not verified)
Eastmoney push2 C HTTPS None US stocks + Hong Kong stocks real-time quotes + full market list
Eastmoney push2his C HTTPS None US stocks + Hong Kong stocks capital flow (K-line only for A-shares, does not cover US stocks/Hong Kong stocks)
Eastmoney datacenter C HTTPS None US stocks + Hong Kong stocks financial statements (three statements) + GMAININDICATOR key metrics
Eastmoney search API C HTTPS None Global stock search + secid mapping
Yahoo Finance C HTTPS cookie+crumb (automatic) US stocks + Hong Kong stocks all categories
Sina Finance C HTTP None US stocks + Hong Kong stocks quotes, US stock K-line
Tencent Finance C HTTPS None US stocks + Hong Kong stocks quotes

Level meanings:S= U.S. government data, can be used commercially and redistributed |B= proactively disclosed data files; commercial use requires your own confirmation |C= requires prior authorization or terms not verified, personal research only. For the original terms on which each level is based, see “Data Source Compliance Levels” at the top.

📦 https://github.com/simonlin1212/global-stock-data — Star ⭐ is the best support