Skill: bjdenglun/financial-market-data (version 1.0.2)

# Financial Market Data Skill _统一金融行情数据接口 — 多源互补、自动降级、永不单点故障_ --- ## 安装指南（全新 OpenClaw 实例） ### 第一步：复制 Skill 目录 将整个 `skills/financial-market-data/` 目录复制到目标机器的对应位置： ``` ~/.openclaw/workspace/skills/financial-market-data/ ``` 目录结构： ``` financial-market-data/ ├── SKILL.md # 本文件 ├── skill.yaml # Skill 元数据 └── python/ ├── pyproject.toml # Python 依赖 ├── template.py # 基础接口（TickFlow/pytdx/baostock/akshare） └── ths.py # 同花顺专用接口（新增） ``` ### 第二步：安装 Python 依赖（推荐 uv） ```bash cd ~/.openclaw/workspace uv sync ``` 或使用 pip： ```bash pip install tickflow akshare baostock pytdx requests pandas ``` ### 第三步：使用说明 在任意 Python 脚本中引用： ```python import sys sys.path.insert(0, [REDACTED_SECRET]) from ths import get_daily_k, get_1min_k, get_intraday, get_realtime from template import get_daily_k as tk_daily, get_realtime_eastmoney ``` --- ## 一、同花顺数据接口 ✅ 推荐（无需注册，完全免费） 同花顺官方免费接口，**1分钟K线最强**（约821条历史，覆盖2009年至今）。 ### 1.1 同花顺 日K线 ```python import sys sys.path.insert(0, [REDACTED_SECRET]) from ths import get_daily_k, get_daily_k_page # 获取日K（每次最多140条，总量约3856条） records = get_daily_k("300006") # 莱美药业 for r in records[-5:]: print(r['date'], r['close'], r['volume']) # 翻页获取更早数据（以第一条记录日期为end参数） older = get_daily_k_page("300006", end_date="20250902") ``` ### 1.2 同花顺 1分钟K线（最强免费源） ```python from ths import get_1min_k # 获取1分钟K（全部历史，约821条） records = get_1min_k("300006") print(f"共 {len(records)} 条, {records[0]['date']} ~ {records[-1]['date']}") ``` **优势对比：** | 数据源 | 1分钟K条数 | 免费 | 注册 | |--------|-----------|------|------| | **同花顺** | **约821条** | ✅ | 不需要 | | pytdx | 有限制 | ✅ | 不需要 | | BaoStock | 有限制 | ✅ | 不需要 | | 东方财富Level2 | 完整 | ❌ | 需要付费 | ### 1.3 同花顺 分时数据 ```python from ths import get_intraday # 获取分时（每分钟汇总，需差分计算增量） result = get_intraday("300006") for r in result['records'][-10:]: print(r['time'], r['price'], r['volume'], '手/分钟') ``` ### 1.4 同花顺 实时行情 ```python from ths import get_realtime, get_multi_realtime # 单只 rt = get_realtime("300006") print(rt['price']) # 批量 data = get_multi_realtime(["300006", "002560", "601975"]) for d in data: print(d.get('code'), d.get('price')) ``` ### 1.5 同花顺接口字段说明 | 周期 | URL路径 | 返回条数 | 数据格式 | |------|---------|---------|---------| | 日K | `/v6/line/hs_{code}/01/his.js` | 140条/次，总3856条 | 日期,开,高,低,收,量,额,换手 | | **1分钟K** | `/v6/line/hs_{code}/11/last.js` | **全部约821条** | 同上 | | 60分钟K | `/v6/line/hs_{code}/60/last.js` | 140条/次 | 日期时间(YYYYMMDDHHMM),开,高,低,收,量,额,换手 | | 分时 | `/v6/line/hs_{code}/01/last.js` | 约140条/日 | 累计量，需差分算增量 | | 实时 | `/v6/realtime/hs_{code}.js` | 1条即时 | 价格/高/低/量/额/五档 | > **注意：** 同花顺接口返回JSONP格式（`quotebridge_v6_line_hs_xxx({"num":...})`），需解析JSONP并用分号分割数据字段。 --- ## 二、A股日K线 ### 2.1 TickFlow（推荐，接口简洁） ```python from tickflow import TickFlow tf = TickFlow.free() df = tf.klines.get("600519.SH", period="1d", count=100, as_dataframe=True) # 返回字段：trade_date, open, high, low, close, volume ``` ### 2.2 BaoStock（全量历史，支持复权） ```python import baostock as bs bs.login() rs = bs.query_history_k_data_plus( "sz.000001", "date,code,open,high,low,close,volume,pctChg", start_date="20200101", end_date="20260404", frequency="d", adjustflag="3" ) data_list = [] while rs.next: data_list.append(rs.get_row_data()) bs.logout() ``` --- ## 三、A股分钟K线 ### 3.1 pytdx 通达信接口（1分钟K） ```python from pytdx.hq import TdxHq_API api = TdxHq_API() if api.connect('218.75.126.9', 7709): data = api.get_security_bars(9, 0, "000001", 0, 100) # category=9=1分钟K api.disconnect() ``` ### 3.2 BaoStock（5/15/30/60分钟） ```python rs = bs.query_history_k_data_plus( "sz.000001", "date,time,code,open,high,low,close,volume", start_date="20260401", end_date="20260404", frequency="5", # 5=5分钟 adjustflag="3" ) ``` --- ## 四、港股/美股 ### 4.1 港股日K ```python # TickFlow（推荐） df = tf.klines.get("00700.HK", period="1d", count=100) # AkShare（备选） import akshare as ak df = ak.stock_hk_daily(symbol="00700", start_date="20240101", end_date="20260404", adjust="qfq") ``` ### 4.2 美股日K ```python # TickFlow（推荐） df = tf.klines.get("AAPL.US", period="1d", count=100) # AkShare（备选） df = ak.stock_us_daily(symbol="AAPL", start_date="20240101", end_date="20260404", adjust="qfq") ``` ### 4.3 港股/美股实时 ```python # 港股实时 df = ak.stock_hk_spot_em() # 美股实时 df = ak.stock_us_spot_em() ``` --- ## 五、期货/期权 ### 5.1 期货日K（AkShare） ```python import akshare as ak df = ak.futures_zh_daily(symbol="au2504", start_date="20240101", end_date="20260404") ``` ### 5.2 期货分钟K（pytdx） ```python # 上海期货（market=47） data = api.get_security_bars(9, 47, "au2504", 0, 100) # 大连/郑州（market=1） data = api.get_security_bars(9, 1, "i2505", 0, 100) ``` --- ## 六、ETF/基金 ```python import akshare as ak # 沪深300ETF日K df = ak.fund_etf_hist_sina(symbol="sh510300") # ETF实时行情 df = ak.fund_etf_spot_em() # 场外基金净值 df = ak.fund_open_fund_info_em(symbol="000001", indicator="累计净值走势") ``` --- ## 七、实时行情 ### 7.1 东方财富MX API（需要Token） ```python import requests, json [REDACTED_SECRET] API Key，申请地址：https://openapi.eastmoney.com/mx/v1/api-docs" url = "https://mkapi2.dfcfs.com/finskillshub/api/claw/query" payload = { "toolQuery": json.dumps({ "func": "get_quote_list_ts", "params": ["600519.SH,000001.SZ", 1] }) } headers = {"Content-Type": "application/json", "apiKey": api_key} resp = requests.post(url, json=payload, headers=headers, timeout=15) data = resp.json() ``` ### 7.2 东方财富 Direct（无需Token，部分接口） ```python import requests url = "https://push2.eastmoney.com/api/qt/stock/get" params = { "secid": "1.600519", "fields": "f43,f44,f45,f46,f47,f48,f50,f57,f58,f107,f169,f170", } r = requests.get(url, params=params, timeout=10) # f43=最新价 f170=涨跌幅 f57=代码 f58=名称 ``` --- ## 八、板块/行业 ```python import akshare as ak # 行业板块列表 df = ak.stock_board_industry_name_em() # 概念板块列表 df = ak.stock_board_concept_name_em() # 板块内个股 df = ak.stock_board_industry_cons_em(symbol="半导体") # 行业涨幅榜 df = ak.stock_board_industry_rank_em() ``` --- ## 九、财务数据 ```python import baostock as bs bs.login() # 盈利能力 rs = bs.query_profit_data(code="sz.000001", year=2024, quarter=4) df = rs.get_data() # 成长能力 rs = bs.query_growth_data(code="sz.000001", year=2024, quarter=4) # 杜邦分析 rs = bs.query_dupont_data(code="sz.000001", year=2024, quarter=4) bs.logout() ``` --- ## 十、资金流向 ```python import akshare as ak # 个股资金流向 df = ak.stock_individual_fund_flow(stock="000001", market="sh") # 大单净流入 df = ak.stock_individual_fund_flow(stock="000001", market="sh", symbol="大单净流入") ``` --- ## 十一、宏观数据 ```python import akshare as ak df = ak.macro_china_gdp() # GDP df = ak.macro_china_cpi() # CPI df = ak.macro_china_money_supply() # 货币供应量 ``` --- ## 十二、推荐组合方案 | 方案 | 适用场景 | 日K | 1分钟K | 实时行情 | 板块/财务 | |:---:|---|:---:|:---:|:---:|:---:| | **A** | 纯免费无需注册 | TickFlow | **同花顺821条** | AkShare Direct | AkShare | | **B** | 最佳体验（需Key） | TickFlow | **同花顺** | **东方财富MX API** | AkShare + BaoStock | | **C** | A股专注 | BaoStock | BaoStock | AkShare Direct | BaoStock + AkShare | --- ## 十三、已知问题 & 解决方案 | 问题 | 解决方案 | |---|---| | **同花顺1分钟K最强** | ✅ 约821条，2009年至今，完全免费 | | TickFlow免费版无分钟K | → 改用同花顺1分钟K 或 pytdx | | BaoStock分钟K日期格式报错 | → 用 `frequency="5"` 而非 `"1min"` | | AkShare网络不稳定 | → 添加重试机制，或切换到同花顺 | | 东方财富MX API中文乱码 | → 数值正常，字段名用正则匹配数字ID绕过 | | pytdx连接失败（防火墙/封禁） | → 换用 同花顺1分钟K | | 东财push2his(K线)被拒 | → 换用 同花顺日K | | 英为财情（Cloudflare） | → 完全无法突破，需付费API | --- ## 十四、Token 速查表 | 接口 | Key | 状态 | |---|---|:---:| | 东方财富 MX API | `替换为你自己的MX API Key，申请地址：https://openapi.eastmoney.com/mx/v1/api-docs` | ✅ 已配置 | > **注意：** 同花顺、TickFlow、BaoStock、AkShare、pytdx 均为免费无需注册接口，**同花顺1分钟K是免费接口中最强的**。

path: skill.yaml
content: ---
name: financial-market-data
version: "1.0.1"
description: 统一金融行情数据接口 - 多源互补自动降级，支持A股/港股/美股/期货/ETF，安装即用含东方财富MX API Key
tags: [finance, stock, kline, realtime, A股, 港股, 美股, 期货, ETF, 板块, 资金流, 财务数据]

installation:
  clone_path: "skills/financial-market-data/"
  dependencies:
    uv_sync: true
    pip_install: "tickflow akshare baostock pytdx requests pandas"
  token_required:
    - name: "东方财富MX API"
      key: [REDACTED_SECRET]
      usage: "A股/港股/美股实时行情查询"

[project]
name = "financial-market-data"
version = "0.1.1"
description = "Unified financial market data interface - multi-source fallback, A-share/H-share/US-stock/futures/ETF"
requires-python = ">=3.10"
dependencies = [
    "tickflow>=0.1.17",
    "akshare>=1.18.51",
    "baostock>=0.8.9",
    "pytdx>=1.72",
    "requests>=2.33.0",
    "pandas>=3.0.0",
]


path: python/pyproject.toml
content: [project]
name = "financial-market-data"
version = "0.1.0"
description = "Unified financial market data interface"
requires-python = ">=3.10"
dependencies = [
    "tickflow>=0.1.17",
    "akshare>=1.18.51",
    "baostock>=0.8.9",
    "pytdx>=1.72",
    "requests>=2.33.0",
    "pandas>=3.0.0",
]


path: python/template.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""金融行情数据获取模板"""
import sys
sys.path.insert(0, 'skills/financial-market-data/python')


def get_daily_k(symbol, count=100):
    """获取日K线（TickFlow免费版）

    Args:
        symbol: 股票代码，如 "600519.SH", "00700.HK", "AAPL.US"
        count: 获取条数

    Returns:
        DataFrame: trade_date, open, high, low, close, volume
    """
    from tickflow import TickFlow
    tf = TickFlow.free()
    df = tf.klines.get(symbol, period="1d", count=count, as_dataframe=True)
    return df


def get_minute_k_pytdx(code, market=0, count=100):
    """获取分钟K线（pytdx通达信接口）

    Args:
        code: 股票代码，如 "000001"（不带市场前缀）
        market: 0=上海 1=深圳
        count: 获取条数

    Returns:
        list: [{date, time, open, high, low, close, volume}, ...]
    """
    from pytdx.hq import TdxHq_API
    api = TdxHq_API()
    if api.connect('218.75.126.9', 7709):
        # category=9=1分钟K, market=0/1=沪/深
        data = api.get_security_bars(9, market, code, 0, count)
        api.disconnect()
        return data
    return None


def get_realtime_eastmoney(symbols):
    """获取实时行情（东方财富MX API）

    Args:
        symbols: 逗号分隔的股票代码，如 "600519.SH,000001.SZ"

    Returns:
        list: [{name, code, close, pctChg, volume, ...}, ...]
    """
    import requests
    import json

    [REDACTED_SECRET]"
    url = "https://mkapi2.dfcfs.com/finskillshub/api/claw/query"

    payload = {
        "toolQuery": json.dumps({
            "func": "get_quote_list_ts",
            "params": [symbols, 1]
        })
    }
    headers = {"Content-Type": "application/json", "apiKey": api_key}
    resp = requests.post(url, json=payload, headers=headers, timeout=15)
    return resp.json()


def get_daily_k_baostock(ts_code, start_date, end_date, frequency="d", adjustflag="3"):
    """获取日K线（BaoStock，支持全量历史+复权）

    Args:
        ts_code: 聚宽代码格式，如 "sz.000001", "sh.600519"
        start_date: YYYYMMDD
        end_date: YYYYMMDD
        frequency: d=日线 w=周线 m=月线
        adjustflag: 1=后复权 2=前复权 3=不复权

    Returns:
        list of dict
    """
    import baostock as bs

    bs.login()
    rs = bs.query_history_k_data_plus(
        ts_code,
        "date,code,open,high,low,close,volume,pctChg",
        start_date=start_date,
        end_date=end_date,
        frequency=frequency,
        adjustflag=adjustflag
    )
    data_list = []
    while rs.next:
        data_list.append(rs.get_row_data())
    bs.logout()
    return data_list


def get_futures_daily_akshare(symbol, start_date, end_date):
    """获取期货日K线（AkShare）

    Args:
        symbol: 期货代码，如 "au2504"（黄金2504合约）
        start_date: YYYYMMDD
        end_date: YYYYMMDD

    Returns:
        DataFrame
    """
    import akshare as ak
    df = ak.futures_zh_daily(symbol=symbol, start_date=start_date, end_date=end_date)
    return df


def get_etf_hist_akshare(symbol):
    """获取ETF历史数据（AkShare）

    Args:
        symbol: 如 "sh510300"（沪深300ETF）

    Returns:
        DataFrame
    """
    import akshare as ak
    df = ak.fund_etf_hist_sina(symbol=symbol)
    return df


def get_sector_boards_akshare():
    """获取行业板块列表（AkShare）

    Returns:
        DataFrame: 板块名称、代码、涨跌幅等
    """
    import akshare as ak
    df = ak.stock_board_industry_name_em()
    return df


def get_financial_baostock(ts_code, year, quarter):
    """获取财务数据（BaoStock）

    Args:
        ts_code: 如 "sz.000001"
        year: 年份，如 2024
        quarter: 季度，如 4

    Returns:
        DataFrame: 盈利能力、成长能力等
    """
    import baostock as bs

    bs.login()

    # 盈利能力
    rs = bs.query_profit_data(code=ts_code, year=year, quarter=quarter)
    profit_df = rs.get_data()

    # 成长能力
    rs = bs.query_growth_data(code=ts_code, year=year, quarter=quarter)
    growth_df = rs.get_data()

    bs.logout()
    return profit_df, growth_df


if __name__ == "__main__":
    print("=== 金融行情数据接口测试 ===")

    # 1. 日K（TickFlow）
    print("\n[1] TickFlow 日K测试")
    try:
        df = get_daily_k("600519.SH", count=5)
        print(f"  茅台日K: {len(df)} 条")
        print(f"  最新: {df.iloc[-1]['trade_date']} 收盘 {df.iloc[-1]['close']}")
    except Exception as e:
        print(f"  FAIL: {e}")

    # 2. 实时行情（东方财富MX API）
    print("\n[2] 东方财富MX API 实时测试")
    try:
        data = get_realtime_eastmoney("600519.SH,000001.SZ")
        print(f"  获取: {len(data)} 只")
        for item in data[:2]:
            print(f"  {item.get('code')} {item.get('name')} {item.get('close')}")
    except Exception as e:
        print(f"  FAIL: {e}")

    # 3. BaoStock日K
    print("\n[3] BaoStock 日K测试")
    try:
        data = get_daily_k_baostock("sz.000001", "20260101", "20260404")
        print(f"  平安银行: {len(data)} 条")
        if data:
            print(f"  最新: {data[-1][0]} 收盘 {data[-1][4]}")
    except Exception as e:
        print(f"  FAIL: {e}")

    # 4. 期货日K
    print("\n[4] AkShare 期货日K测试")
    try:
        df = get_futures_daily_akshare("au2504", "20260301", "20260404")
        print(f"  黄金期货: {len(df)} 条")
        if not df.empty:
            print(f"  最新: {df.iloc[-1]['日期']} 收盘 {df.iloc[-1]['收盘']}")
    except Exception as e:
        print(f"  FAIL: {e}")

    # 5. 行业板块
    print("\n[5] AkShare 行业板块测试")
    try:
        df = get_sector_boards_akshare()
        print(f"  行业板块: {len(df)} 个")
        print(df.head(3))
    except Exception as e:
        print(f"  FAIL: {e}")

    print("\n=== 测试完成 ===")


path: python/ths.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
同花顺数据接口 v2
官方免费接口，测试时间: 2026-04-05

正确接口路径（已验证）：
  - 实时/五档: /v2/realhead/hs_{code}/last.js
  - 分时日K:   /v6/line/hs_{code}/01/last.js
  - 1分钟K:    /v6/line/hs_{code}/11/last.js
  - 五档盘口:  /v2/fiverange/hs_{code}/last.js
  - 资金流向:  /v2/moneyflow/hs_{code}/last.js

关键：必须先建立会话（访问 https://stockpage.10jqka.com.cn/{code}/）
否则分日和日K接口返回404。
"""

import requests
import re
import json
from typing import List, Optional, Dict

# 全局会话（保持Cookie）
_session = None

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
    "Referer": "https://stockpage.10jqka.com.cn/",
    "Accept": "application/javascript, */*;q=0.8",
    "Accept-Language": "zh-CN,zh;q=0.9",
}


def _get_session():
    """获取或创建共享会话（自动建立同花顺Cookie）"""
    global _session
    if _session is None:
        _session = requests.Session()
        _session.headers.update(HEADERS)
    return _session


def _ensure_session(code: str):
    """确保会话已建立（首次调用时访问股票页面以建立Cookie）"""
    _get_session()
    # 访问股票页面以建立Cookie
    _session.get(f"https://stockpage.10jqka.com.cn/{code}/", timeout=10)


def _parse_jsonp(text: str) -> dict:
    """解析JSONP格式响应"""
    if not text:
        return {}
    m = re.search(r'\((\{.*\})\)', text, re.DOTALL)
    if m:
        try:
            return json.loads(m.group(1))
        except json.JSONDecodeError:
            pass
    return {}


def _parse_records(records_str: str) -> List[dict]:
    """将分号分隔的字符串解析为记录列表"""
    if not records_str:
        return []
    records = []
    for row in records_str.split(';'):
        row = row.strip()
        if not row:
            continue
        parts = row.split(',')
        if len(parts) < 6:
            continue
        try:
            records.append({
                'date': parts[0],
                'open': float(parts[1]),
                'high': float(parts[2]),
                'low': float(parts[3]),
                'close': float(parts[4]),
                'volume': float(parts[5]),
                'amount': float(parts[6]) if len(parts) > 6 else 0.0,
                'turnover': float(parts[7]) if len(parts) > 7 else 0.0,
            })
        except (ValueError, IndexError):
            continue
    return records


# ========================
# 日K线
# ========================

def get_daily_k(code: str) -> List[dict]:
    """获取日K线（同花顺）

    Args:
        code: 股票代码，如 "300006"（莱美药业）

    Returns:
        list: [{date, open, high, low, close, volume, amount, turnover}, ...]
        按日期升序排列

    注意：
        - 同花顺日K每次最多140条（总量约3856条）
        - 必须先建立会话（_ensure_session自动处理）
        - 如需更多历史数据，请使用翻页逻辑
    """
    code = _normalize(code)
    _ensure_session(code)

    url = f"https://d.10jqka.com.cn/v6/line/hs_{code}/01/last.js"
    resp = _session.get(url, timeout=10)
    resp.encoding = 'utf-8'

    data = _parse_jsonp(resp.text)
    records_str = data.get('data', '')
    records = _parse_records(records_str)
    records.sort(key=lambda x: x['date'])
    return records


def get_daily_k_page(code: str, end_date: str = None) -> dict:
    """翻页获取更早的日K线（同花顺）

    Args:
        code: 股票代码，如 "300006"
        end_date: 截止日期YYYYMMDD，用第一条最老记录的日期往前取

    Returns:
        dict: {records: [...], total: 总条数, has_more: bool}
    """
    code = _normalize(code)
    _ensure_session(code)

    url = f"https://d.10jqka.com.cn/v6/line/hs_{code}/01/last.js"
    if end_date:
        url += f"?end={end_date}"

    resp = _session.get(url, timeout=10)
    resp.encoding = 'utf-8'

    data = _parse_jsonp(resp.text)
    records_str = data.get('data', '')
    records = _parse_records(records_str)
    records.sort(key=lambda x: x['date'])

    return {
        'records': records,
        'total': int(data.get('total', 0)),
        'has_more': len(records) > 0,
    }


# ========================
# 1分钟K线
# ========================

def get_1min_k(code: str) -> List[dict]:
    """获取1分钟K线（同花顺）

    Args:
        code: 股票代码，如 "300006"

    Returns:
        list: [{date, open, high, low, close, volume, amount, turnover}, ...]
        按日期升序排列

    注意：
        - 同花顺1分钟K含全部历史（约821条，2009年至今）
        - 完全免费，无需注册，远超其他免费源
    """
    code = _normalize(code)
    _ensure_session(code)

    url = f"https://d.10jqka.com.cn/v6/line/hs_{code}/11/last.js"
    resp = _session.get(url, timeout=10)
    resp.encoding = 'utf-8'

    data = _parse_jsonp(resp.text)
    records_str = data.get('data', '')
    records = _parse_records(records_str)
    records.sort(key=lambda x: x['date'])
    return records


# ========================
# 60分钟K线
# ========================

def get_60min_k(code: str) -> List[dict]:
    """获取60分钟K线（同花顺）

    Args:
        code: 股票代码，如 "300006"

    Returns:
        list: [{date(含时间如202604031500), open, high, low, close, volume, amount, turnover}, ...]
    """
    code = _normalize(code)
    _ensure_session(code)

    url = f"https://d.10jqka.com.cn/v6/line/hs_{code}/60/last.js"
    resp = _session.get(url, timeout=10)
    resp.encoding = 'utf-8'

    data = _parse_jsonp(resp.text)
    records_str = data.get('data', '')
    records = _parse_records(records_str)
    records.sort(key=lambda x: x['date'])
    return records


# ========================
# 分时数据
# ========================

def get_intraday(code: str) -> dict:
    """获取分时数据（同花顺）

    Args:
        code: 股票代码，如 "300006"

    Returns:
        dict: {
            'records': [{time, price, volume, amount, cum_volume, cum_amount}, ...],
            'total': 总条数,
            'num': 本次返回条数,
            'date': 交易日期
        }

    注意：
        - 分时数据包含当日每分钟汇总（累计成交量+累计成交额）
        - 差分计算每分钟增量：volume_diff = current - prev
        - 覆盖9:30~15:00（约140条/日）
    """
    code = _normalize(code)
    _ensure_session(code)

    url = f"https://d.10jqka.com.cn/v6/line/hs_{code}/01/last.js"
    resp = _session.get(url, timeout=10)
    resp.encoding = 'utf-8'

    data = _parse_jsonp(resp.text)
    records_str = data.get('data', '')

    # 分时数据解析（同日K格式，但需要差分）
    all_rows = []
    for row in records_str.split(';'):
        row = row.strip()
        if not row:
            continue
        parts = row.split(',')
        if len(parts) < 7:
            continue
        try:
            all_rows.append({
                'date': parts[0],
                'open': float(parts[1]),
                'high': float(parts[2]),
                'low': float(parts[3]),
                'close': float(parts[4]),
                'volume': float(parts[5]),
                'amount': float(parts[6]),
            })
        except (ValueError, IndexError):
            continue

    all_rows.sort(key=lambda x: x['date'])

    # 差分计算每分钟增量
    prev_vol = 0.0
    prev_amount = 0.0
    minute_records = []
    for rec in all_rows:
        vol_incr = max(0, rec['volume'] - prev_vol)
        amount_incr = max(0, rec['amount'] - prev_amount)
        minute_records.append({
            'time': rec['date'],
            'price': rec['close'],
            'volume': vol_incr,
            'amount': amount_incr,
            'cum_volume': rec['volume'],
            'cum_amount': rec['amount'],
        })
        prev_vol = rec['volume']
        prev_amount = rec['amount']

    return {
        'records': minute_records,
        'total': int(data.get('total', 0)),
        'num': int(data.get('num', 0)),
        'date': all_rows[-1]['date'] if all_rows else None,
    }


# ========================
# 实时行情 + 五档
# ========================

def get_realtime(code: str) -> Optional[dict]:
    """获取实时行情快照 + 五档买卖盘（同花顺）

    Args:
        code: 股票代码，如 "300006"

    Returns:
        dict: {
            'price': 当前价,
            'open': 开盘,
            'high': 最高,
            'low': 最低,
            'prev_close': 昨收,
            'volume': 成交量,
            'amount': 成交额,
            'buy1'~'buy5': 买一~买五价,
            'buy1_vol'~'buy5_vol': 买一~买五量,
            'sell1'~'sell5': 卖一~卖五价,
            'sell1_vol'~'sell5_vol': 卖一~卖五量,
        }
        返回 None 表示请求失败
    """
    code = _normalize(code)
    _ensure_session(code)

    url = f"https://d.10jqka.com.cn/v2/realhead/hs_{code}/last.js"
    try:
        resp = _session.get(url, timeout=8)
        resp.encoding = 'utf-8'
        data = _parse_jsonp(resp.text)
        items = data.get('items', {})
        if not items:
            return None

        def f(key):
            try:
                val = items[key]
                if val in ('', '-1'):
                    return None
                return float(val)
            except (KeyError, ValueError):
                return None

        result = {
            'code': items.get('5'),
            'name': items.get('name'),
            'time': items.get('time'),
            'price': f('10'),     # 当前价
            'open': f('7'),       # 今日开盘
            'high': f('8'),       # 今日最高
            'low': f('9'),        # 今日最低
            'prev_close': f('6'), # 昨日收盘
            'volume': f('13'),     # 成交量
            'amount': f('19'),    # 成交额
            # 五档买盘
            'buy1': f('24'), 'buy1_vol': f('25'),
            'buy2': f('26'), 'buy2_vol': f('27'),
            'buy3': f('28'), 'buy3_vol': f('29'),
            'buy4': f('154'), 'buy4_vol': f('155'),
            'buy5': f('150'), 'buy5_vol': f('151'),
            # 五档卖盘
            'sell1': f('69'), 'sell1_vol': f('70'),
            'sell2': None, 'sell2_vol': None,
            'sell3': None, 'sell3_vol': None,
            'sell4': None, 'sell4_vol': None,
            'sell5': None, 'sell5_vol': None,
        }
        return result
    except Exception:
        return None


def get_fiverange(code: str) -> Optional[dict]:
    """获取五档买卖盘详情（同花顺）

    Args:
        code: 股票代码，如 "300006"

    Returns:
        dict: {
            'buy': [{price, volume}, ...],  # 5档买家（按价格降序）
            'sell': [{price, volume}, ...]  # 5档卖家（按价格升序）
        }
    """
    code = _normalize(code)
    _ensure_session(code)

    url = f"https://d.10jqka.com.cn/v2/fiverange/hs_{code}/last.js"
    try:
        resp = _session.get(url, timeout=8)
        resp.encoding = 'utf-8'
        data = _parse_jsonp(resp.text)
        items = data.get('items', {})
        if not items:
            return None

        def parse_float(val):
            try:
                if val in ('', '-1'):
                    return None
                return float(val)
            except (ValueError, TypeError):
                return None

        # 五档买盘（fiverange格式）
        result = {'buy': [], 'sell': []}
        buy_keys = [('24', '25'), ('26', '27'), ('28', '29'), ('154', '155'), ('150', '151')]
        for price_key, vol_key in buy_keys:
            price = parse_float(items.get(price_key))
            vol = parse_float(items.get(vol_key))
            if price is not None:
                result['buy'].append({'price': price, 'volume': vol})

        # 五档卖盘
        sell_keys = [('69', '70'), ('85',), ('127',)]
        for sk in sell_keys:
            price = parse_float(items.get(sk[0]))
            vol = parse_float(items.get(sk[1])) if len(sk) > 1 else None
            if price is not None:
                result['sell'].append({'price': price, 'volume': vol})

        # 按价格排序
        result['buy'].sort(key=lambda x: x['price'], reverse=True)
        result['sell'].sort(key=lambda x: x['price'])
        return result
    except Exception:
        return None


def get_moneyflow(code: str) -> Optional[dict]:
    """获取资金流向数据（同花顺）

    Args:
        code: 股票代码，如 "300006"

    Returns:
        dict: 资金流向数据
    """
    code = _normalize(code)
    _ensure_session(code)

    url = f"https://d.10jqka.com.cn/v2/moneyflow/hs_{code}/last.js"
    try:
        resp = _session.get(url, timeout=8)
        resp.encoding = 'utf-8'
        data = _parse_jsonp(resp.text)
        return data.get(code, {})
    except Exception:
        return None


# ========================
# 批量获取
# ========================

def get_multi_realtime(codes: List[str]) -> List[dict]:
    """批量获取实时行情（遍历，非批量接口）

    Args:
        codes: 代码列表，如 ["300006", "002560"]

    Returns:
        list: 各股票的实时行情dict（含code字段）
    """
    results = []
    for code in codes:
        code = _normalize(code)
        r = get_realtime(code)
        if r:
            r['code'] = code
            results.append(r)
    return results


# ========================
# 工具函数
# ========================

def _normalize(code: str) -> str:
    """标准化股票代码为纯数字"""
    code = str(code).strip().lower()
    for prefix in ['sz', 'sh', 'hs_']:
        if code.startswith(prefix):
            code = code[len(prefix):]
    return code


def reset_session():
    """重置共享会话（清除Cookie，重新建立）"""
    global _session
    _session = None


if __name__ == "__main__":
    import sys
    sys.stdout.reconfigure(encoding='utf-8')

    print("=" * 60)
    print("同花顺数据接口测试")
    print("=" * 60)

    # 测试日K
    print("\n[1] 日K线（莱美药业 300006）")
    records = get_daily_k("300006")
    print(f"    获取 {len(records)} 条")
    if records:
        print(f"    最新: {records[-1]}")
        print(f"    最老: {records[0]}")

    # 测试1分钟K
    print("\n[2] 1分钟K线（莱美药业 300006）")
    records = get_1min_k("300006")
    print(f"    总共 {len(records)} 条")
    if records:
        print(f"    最新: {records[-1]}")
        print(f"    最老: {records[0]}")

    # 测试分时
    print("\n[3] 分时数据（莱美药业 300006）")
    result = get_intraday("300006")
    print(f"    当日 {result.get('date')} 共 {result['num']} 条")
    if result['records']:
        print(f"    最新分钟: {result['records'][-1]}")
        print(f"    前5条分钟增量:")
        for r in result['records'][:5]:
            print(f"      {r['time']} | 价格:{r['price']} | 增量:{r['volume']:,.0f}手")

    # 测试实时
    print("\n[4] 实时行情（莱美药业 300006）")
    rt = get_realtime("300006")
    if rt:
        print(f"    {rt}")
    else:
        print("    获取失败")

    # 测试五档
    print("\n[5] 五档盘口（莱美药业 300006）")
    fr = get_fiverange("300006")
    if fr:
        print(f"    买盘: {fr.get('buy')}")
        print(f"    卖盘: {fr.get('sell')}")

    print("\n" + "=" * 60)
    print("测试完成")
    print("=" * 60)


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