Claude Skill

ftshare-market-data

FTShare 市场数据技能集。根据用户对股票、ETF、指数、基金、可转债、期货、港股、美股、资金流、财务数据或宏观数据的查询意图,匹配并执行对应子 skill。

LLM Mart · 0 points · 0 views 29 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download ftshare-lab-ftshare-skill-ftshare-market-data-53e8a69.zip · 624 KB

Install

skills CLI npx skills add https://github.com/FTShare-Lab/FTShare-skill/tree/main/ftshare-market-data
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install ftshare-lab-ftshare-skill@llmmart
Git git clone https://github.com/FTShare-Lab/FTShare-skill.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole ftshare-lab/ftshare-skill collection as a plugin from our marketplace. Git is the plain clone.

README

ftshare-market-data

License: MIT Python 3.9+

ftshare-market-data 的当前可用范围不包含暂不支持的美股资产负债表、美股现金流、美股利润表、美股复权因子、港股资产负债表、港股现金流量表、港股利润表、港股估值分析、港股市值和炸板池接口。当前 README 及 sub-skills/ 仅介绍和提供仍在支持范围内的接口。

在 ftshare 生态中的位置

ftshare-market-data 处于 ftshare 生态的数据 Skill 层。它连接 FTShare 数据服务 market.ft.tech,为投研任务、MCP 工具和 Agent 应用提供结构化、可直接调用的行情、财报和宏观数据。

FTShare 数据服务 (market.ft.tech)
    ↓  HTTP GET(Python 标准库 urllib)
ftshare-market-data        # run.py 统一路由 + sub-skills 目录中的实际子 skill
    ↓
Claude Code / Codex / OpenClaw   # Agent 运行时加载本 Skill
    ↓
用户                       # 自然语言提问 → JSON 结果

这是一份 Skill(给 Agent 运行时消费),不是给人 import 的 Python 库。如果你需要在数据分析脚本里编程调用、想要 pandas DataFrame,请使用独立维护的 ftshare-python-sdk;本次 Skill 调整不修改 SDK。

作为 Skill 加载

本目录已包含标准 Skill 描述文件 SKILL.md(带 name / description frontmatter),把它作为一个 Skill 放进你的 Agent 运行时即可,无需安装任何包。

Claude Code:将本目录放入 skills 路径——项目级 .claude/skills/ftshare-market-data/,或用户级 ~/.claude/skills/ftshare-market-data/。Claude Code 会自动读取 SKILL.md,在用户提问匹配到行情 / 财报 / 宏观等数据需求时触发。

Codex / OpenClaw:同样将本目录作为一个 Skill 加载,运行时读取 SKILL.md 的 frontmatter 完成路由(各家具体加载命令请以对应运行时文档为准)。

获取仓库:

git clone https://github.com/ftshare-lab/ftshare-skills.git

运行时只需要 Python 3:子 skill 仅使用标准库 urllibjson零第三方依赖,不需要 pandasrequests

快速开始

加载 Skill 后,用户用自然语言提问即可。运行时根据 SKILL.md 匹配子 skill、执行 run.py,并将返回的 JSON 交回用户。

python <RUN_PY> stock-list-all-stocks
python <RUN_PY> stock-realtime-minute-kline --symbols 600519.SH 000001.SZ
python <RUN_PY> hk-candlesticks --trade-code 00700.HK --interval-unit day --until-date 2026-03-24

<RUN_PY> 是本目录下 run.py 的绝对路径。当前子 skill 仅对应《FTShare 四档套餐全量接口表》及其活动源文档;已下线、未发布和暂不支持的接口不在范围内。

{
    "items": [
        { "stock_code": "000001.SZ", "stock_name": "平安银行" },
        { "stock_code": "000002.SZ", "stock_name": "万科A" }
    ]
}

调用方式(唯一规则)

run.py 是统一调度入口,与 SKILL.md 同级。执行时:

  1. SKILL.md 的绝对路径,将末尾 /SKILL.md 替换为 /run.py,得到 <RUN_PY>
  2. 调用:python <RUN_PY> <子skill名> [参数...]
# 示例
python <RUN_PY> stock-list-all-stocks
python <RUN_PY> stock-ipos --page 1 --page_size 20
python <RUN_PY> semantic-search-news --query 人工智能
python <RUN_PY> etf-pcfs --date 20260309
python <RUN_PY> index-weight-summary --index-code 000300 --page 1 --page-size 20
python <RUN_PY> index-weight-list --index-code 000300 --page 1 --page-size 20
python <RUN_PY> hk-candlesticks --trade-code 00700.HK --interval-unit day --until-date 2026-03-24
python <RUN_PY> nth-trade-date --n 5
python <RUN_PY> eastmoney-shareholder-changes --symbol 股东增持 --page 1 --page-size 20
python <RUN_PY> ashare-code-change --trade-code 001872.SZ
python <RUN_PY> ashare-status-change --trade-code 600848.SH --change-type 上市
python <RUN_PY> economic-china-cpi-monthly

所有 market.ft.tech handler 默认使用 https://market.ft.tech/gateway。认证 key 从环境变量 FTSHARE_API_KEY 读取,并作为 FTSHARE_API_KEY 请求头发送;同时发送 Content-Type: application/json。缺少凭据时 handler 不会发起请求。本地或内网服务可通过环境变量切换 API 地址:

FTSHARE_BASE_URL=http://127.0.0.1:8000/ python <RUN_PY> stock-list-all-stocks

run.py 内部通过 __file__ 自定位,无论安装在何处都能正确找到各子 skill 的脚本。

返回类型

所有子 skill 一律向 标准输出打印 JSON,运行时(Agent)会直接读取这份 JSON,再决定如何以表格 / 要点形式展示给用户。

需要手工解析时(如冒烟测试、定时任务):Shell 中配合 jq

python run.py stock-list-all-stocks | jq '.items[0:3]'

响应中常见的表格数据信封包括:data.recordsdata.items、顶层 items、顶层数组。各子 skill 的 SKILL.md 会标注其具体响应结构与字段含义。

分页

分页接口同时支持传统 --page / --page_size 和更方便的 --all 自动翻页。

取单页:

python run.py stock-ipos --page 1 --page_size 20

自动翻页拉取全量数据:

python run.py stock-ipos --all

--all 仅在具体子 skill 的 SKILL.md 明确说明时可用;分页参数也以对应源文档为准。

能力总览

子 skill 以《FTShare 四档套餐全量接口表》为范围依据,按股票、ETF、指数、基金、可转债、期货、港股、板块、资金流、财务、公司治理、新闻公告研报和宏观经济等领域组织。每个接口的参数、响应字段和限制见对应子目录的 SKILL.md 及活动源文档;api-doc/已下线api-doc/未发布 不计入范围。

代表子 skill
交易日 / 财经日历 / 新闻公告研报 nth-trade-datetrading-calendarfinancial-calendarsemantic-search-newsnews-reaction-snapshotstock-announcementsstock-reportsstock-prospectuses
A 股行情 / 基础 stock-list-all-stocksstock-description-allstock-quotes-liststock-iposstock-intradayeastmoney-all-board-daily-ohlcblock-tradesmargin-trading-detailscontinuous-auction-volumeintraday-auction-volume
A 股财报 / 业绩 stock-income-*stock-balance-*stock-cashflow-*stock-performance-express-*stock-performance-forecast-*
A 股股东 / 质押 / 增减持 stock-holder-tenstock-holder-ftenstock-holder-numspledge-summarypledge-detailstock-share-chgexecutive-holdings-changeseastmoney-shareholder-changes
A 股公司行动 / 代码与状态 shareholder-meetingstock-dividendsstock-dividends-effectivemajor-contract-by-datemajor-contract-by-symbolmajor-contract-summaryashare-code-changeashare-status-changestock-st-history
A 股估值 / 千股千评 / 热度 / 资金流 eastmoney-stock-valuationeastmoney-market-valuationstock-comment-index/score/org-participate/desire/focusstock-rank-xueqiustock-rank-eastmoneystock-capital-flows
A 股涨跌停 limit-up-poollimit-up-pool-yesterdaylimit-down-pool
A 股商誉 stock-goodwill-detailstock-goodwill-impairmentstock-goodwill-industrystock-goodwill-market-overviewstock-goodwill-predict
可转债 cb-listscb-base-dataconvertible-bond-candlesticksconvertible-bond-candlesticks-batchconvertible-bond-minutesconvertible-bond-realtime-day-klineconvertible-bond-realtime-minute-klineconvertible-bond-szse-matching-trades 等深交所成交明细
ETF etf-description-alletf-components-alletf-component-detailsetf-pre-singleetf-pcfsetf-pcf-infosetf-shareetf-net-valueetf-announcementsetf-adjust-factoretf-candlesticksetf-candlesticks-batchetf-minutesetf-minutes-batchetf-realtime-minute-klineetf-realtime-day-kline
基金 fund-basicinfo-single-fundfund-cal-return-...fund-nav-single-fund-paginatedfund-overview-all-funds-paginatedfund-support-symbols-all-funds-paginated
指数 index-detailindex-list-paginatedindex-ohlcsindex-pricesindex-candlesticksindex-candlesticks-batchindex-minutesindex-minutes-batchsw-index-history-minutesindex-realtime-minute-klineindex-realtime-day-klineindex-description-all/paginated/downloadindex-weight-summary/list/download
板块(东财 / 同花顺) eastmoney-concept-boardseastmoney-board-constituents/daily-ohlc/latest-ohlc10jqk-board-list/kline/all-klineths-industry-constituents
港股 company-hkhk-candlesticksnorthboundsouthboundeastmoney-hk-index-daily-klinehsi-daily-weight
美股 eastmoney-us-stock-listeastmoney-us-stock-daily-ohlcus-basic
期货 futures-base-datafutures-listsfutures-limitfutures-settlefutures-weekly-detailfutures-warehouse-receiptfutures-contract-klineeastmoney-futures-positioneastmoney-futures-strangemember-build-processmember-position-ranking
宏观经济(中国 + 美国) economic-china-gdp/cpi/ppi/pmi/lpr/...-monthly(15 项)、economic-us-economic-by-type(16 类,按 --type

名称 → 代码映射

部分接口只接受代码而非名称。具体代码格式与映射方式以对应子 skill 文档为准。

查看可用接口

不带参数运行 run.py 会打印用法并列出全部可用子 skill:

python run.py

查看某个接口的详细参数、响应结构与字段说明:

cat sub-skills/stock-list-all-stocks/SKILL.md

Base URL 配置

market.ft.tech 接口默认以 https://market.ft.tech/gateway 为基础地址,使用 HTTP GET;每个请求均需在请求头携带 FTSHARE_API_KEYContent-Type: application/json

/api/v1/market/data/<接口路径>

如需切到本地或内网服务,设置 FTSHARE_BASE_URL。变量里是否带 /gateway 会原样保留:

FTSHARE_BASE_URL=http://127.0.0.1:8000/ python <RUN_PY> stock-list-all-stocks
FTSHARE_BASE_URL=http://127.0.0.1:8000/gateway/ python <RUN_PY> stock-list-all-stocks
  • 所有 market.ft.tech 接口都通过 FTSHARE_BASE_URL 切换基础地址。

安全与约束

  • 域名白名单:使用 safe_urlopen 的 handler 会校验请求协议和主机匹配当前基础地址;设置 FTSHARE_BASE_URL 后按该地址校验。
  • 子 skill 白名单run.py 仅允许 sub-skills/<名称>/scripts/handler.py 形态的子 skill,防止路径遍历。
  • 下载落盘限制:含 --output 的下载类接口仅允许写入当前工作目录下的路径。
  • 依赖前序接口的参数:下载类接口的 url_hash / filename 须先由对应的列表接口取得,勿硬编码。

项目结构

ftshare-market-data/
  SKILL.md                # Skill 入口文档:frontmatter(name/description)+ 能力总览 + 调用规则
  run.py                  # 统一调度入口:校验并执行子 skill 的 handler
  README.md               # 本文档
  sub-skills/
    <子skill名>/
      SKILL.md            # 该接口的参数、响应结构、字段说明
      scripts/
        handler.py        # 具体实现:HTTP GET → 打印 JSON

与 ftshare-python-sdk 的关系

本仓库是 Skill(命令行驱动,给 Agent 运行时)ftshare-python-sdkPython 库(给开发者编程)。两者覆盖同一批 FTShare 数据接口,按使用形态区分:

ftshare-market-data(本仓库) ftshare-python-sdk
形态 Skill(被运行时加载) pip 包
入口 python run.py <子skill> ft.market_api().<方法>()
返回 原始 JSON(stdout) pandas DataFrame
消费者 Claude Code / Codex / OpenClaw 数据分析脚本 / 量化研究
依赖 仅 Python 标准库 pandasrequests

Skill manifest

FTShare Market Data Skills

本 skill 是统一路由入口。子 skill 位于 sub-skills/,由同目录的 run.py 动态发现和执行。

调用

python <RUN_PY> <子skill名> [参数...]

<RUN_PY> 是本文件同级的 run.py 绝对路径。不带参数运行会列出当前可用子 skill。

认证与请求约束

  • 所有请求必须提供 FTSHARE_API_KEY 环境变量。
  • handler 将其作为 FTSHARE_API_KEY 请求头发送,并设置 Content-Type: application/json
  • 未设置凭据时,handler 在发起请求前以非零状态退出。
  • 默认基础地址为 https://market.ft.tech/gateway,可通过 FTSHARE_BASE_URL 覆盖。
  • 请求使用各子 skill 文档声明的 /api/v1/api/v2/api/v3/api/v4 路由;handler 会限制请求 scheme 和 host 与基础地址一致。

常用示例

python <RUN_PY> stock-description-all --symbol-id 600000.SH --page 1 --page-size 1
python <RUN_PY> stock-realtime-minute-kline --symbols 600519.SH 000001.SZ
python <RUN_PY> stock-minutes --symbol 600519.SH --since-ts-millis 1787189400000 --until-ts-millis 1787191200000
python <RUN_PY> etf-realtime-day-kline --symbols 510300.SH
python <RUN_PY> index-minutes --symbol 000300.SH --since-ts-millis 1787189400000 --until-ts-millis 1787191200000
python <RUN_PY> stock-reports --stock-code 600036.SH --page 1 --page-size 20
python <RUN_PY> stock-announcements --stock-code 600000 --page 1 --page-size 20
python <RUN_PY> stock-prospectuses --stock-code 600000.SH --page 1 --page-size 20
python <RUN_PY> stock-candlesticks-batch --symbols 600519.SH,510300.SH --interval-unit Day --since-ts-millis 1787000000000 --until-ts-millis 1787191200000
python <RUN_PY> etf-candlesticks-batch --symbols 510300.XSHG,159915.XSHE --interval-unit Day --since-ts-millis 1787000000000 --until-ts-millis 1787191200000
python <RUN_PY> index-candlesticks-batch --symbols 000300.XSHG,399001.XSHE --interval-unit Day --since-ts-millis 1787000000000 --until-ts-millis 1787191200000
python <RUN_PY> futures-contract-kline --symbol A2605.DCE --interval daily --limit 5
python <RUN_PY> etf-pcf-infos --symbol 510300.SH --trade-date 20260909
python <RUN_PY> etf-net-value --etf-code 510300 --nav-date 20260909
python <RUN_PY> etf-share --etf-code 510300 --page 1 --page-size 5
python <RUN_PY> etf-announcements --etf-code 159915 --page 1 --page-size 5
python <RUN_PY> etf-component-details --symbol 510300.SH
python <RUN_PY> stock-dividends-effective --symbol 600519.XSHG --page 1 --page-size 20
python <RUN_PY> executive-holdings-changes --stock-code 600519 --page 1 --page-size 20
python <RUN_PY> eastmoney-shareholder-changes --symbol 股东增持 --page 1 --page-size 20
python <RUN_PY> ashare-code-change --trade-code 001872.SZ
python <RUN_PY> ashare-status-change --trade-code 600848.SH --change-type 上市
python <RUN_PY> stock-st-history --symbol 600735.SH,000004.SZ
python <RUN_PY> nth-trade-date --n 5
python <RUN_PY> continuous-auction-volume --trade-date 20260620 --page 1 --page-size 50
python <RUN_PY> news-reaction-snapshot --symbol 600519.SH --start-date 20260818 --end-date 20260828 --lookback-hours 48 --page 1 --page-size 5
python <RUN_PY> semantic-search-news --query 人工智能

能力范围

当前子 skill 仅覆盖《FTShare 四档套餐全量接口表》中的接口;接口详细参数、响应字段和限制以对应子目录的 SKILL.mdftshare-doc/api-doc/ 活动源文档为准。api-doc/已下线api-doc/未发布 不属于当前范围。

输出与错误

  • 成功响应以格式化 JSON 输出到 stdout。
  • HTTP、网络、参数和认证诊断输出到 stderr。
  • 请求失败时返回非零退出状态。
  • 下载类子 skill 只允许将文件写入当前工作目录及其子目录。

运行时发现

run.py 仅执行 sub-skills/<名称>/scripts/handler.py 形式的动态发现结果,不维护重复的静态注册表。新增或移除子 skill 时,应同步其目录中的 SKILL.md 和 handler,并以套餐接口表和源接口文档进行核对。

本包是 Agent Skill,不是 Python SDK;SDK 的同步由独立任务负责。

Files (ftshare-skill)
  • sub-skills
    • 10jqk-board-list
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """查询同花顺板块列表(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.request
          import os
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询同花顺板块列表")
              parser.add_argument("--module", choices=["concept", "csrc", "industry", "region"],
                                  help="板块类型过滤:concept/csrc/industry/region")
              parser.add_argument("--search", default=None, help="搜索板块名称或代码")
              args = parser.parse_args()
          
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              url = f"{BASE_URL}/api/v1/market/data/ths-board-list"
              req = urllib.request.Request(url, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key}, method="GET")
          
              try:
                  with urllib.request.urlopen(req, timeout=30) as resp:
                      data = json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()[:500] if e.fp else ""
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Connection error: {e.reason}", file=sys.stderr)
                  sys.exit(1)
          
              if not isinstance(data, dict) or not isinstance(data.get("data"), list):
                  print("Unexpected response format", file=sys.stderr)
                  sys.exit(1)
          
              rows = data["data"]
              if args.module:
                  rows = [b for b in rows if b.get("module") == args.module]
          
              if args.search:
                  q = args.search.lower()
                  rows = [b for b in rows if q in b.get("name", "").lower() or q in b.get("code", "")]
          
              data["data"] = rows
              print(json.dumps(data, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.8 KB
        ---
        name: 10jqk-board-list
        description: Get all THS board list (同花顺板块列表). Use when user asks about 同花顺板块列表、概念板块、行业板块、地域板块、证监会板块、有哪些板块.
        ---
        
        # 同花顺板块列表
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询同花顺板块列表 |
        | 外部接口 | `GET /api/v1/market/data/ths-board-list` |
        | 请求方式 | GET |
        | 适用场景 | 查询同花顺全部板块(概念/证监会/行业/地域)的代码与名称列表 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | 无 | - | - | - | - | 接口返回全量板块列表 |
        
        ## 3. 响应说明
        
        成功响应外层为 `code`、`message`、`data`,板块列表位于 `data` 数组中。
        |--------|------|------|
        | module | string | 板块类型:`concept`(概念)/ `csrc`(证监会)/ `industry`(行业)/ `region`(地域) |
        | code | string | 板块代码 |
        | name | string | 板块名称 |
        
        示例响应:
        ```json
        {
          "code": 200,
          "message": "success",
          "data": [
            { "module": "concept", "code": "885311", "name": "5G概念" },
            { "module": "industry", "code": "881001", "name": "银行" },
            { "module": "region", "code": "883001", "name": "北京" }
          ]
        }
        ```
        
        ## 4. 用法
        
        ```bash
        python <RUN_PY> 10jqk-board-list
        python <RUN_PY> 10jqk-board-list --module concept
        python <RUN_PY> 10jqk-board-list --module industry
        python <RUN_PY> 10jqk-board-list --search 人工智能
        ```
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/ths-board-list
        ```
        
        ## 6. 注意事项
        
        - 返回四类板块(concept/csrc/industry/region),客户端按 `module` 字段过滤
        - 板块代码用于后续查询 K 线等数据
        
    • abnormal-trading-details
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/abnormal-trading-details'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='龙虎榜明细')
              parser.add_argument("--date")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              args = parser.parse_args()
              params = {}
              if args.date is not None: params["date"] = args.date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 655 B
        ---
        name: abnormal-trading-details
        description: 查询龙虎榜明细。接口:GET /api/v1/market/data/abnormal-trading-details。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 龙虎榜明细
        
        接口:GET `/api/v1/market/data/abnormal-trading-details`。参数和响应以 `ftshare-doc/api-doc/股票数据/打板专题数据/龙虎榜明细.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> abnormal-trading-details --date 20260828 --page 1 --page_size 5
        ```
        
    • abnormal-trading-overview
      • scripts
        • handler.py 3.2 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/abnormal-trading-overview'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='龙虎榜总览')
              parser.add_argument("--date")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--total", required=False)
              parser.add_argument("--symbol", required=True)
              parser.add_argument("--symbol_name")
              parser.add_argument("--change_rate", required=False)
              parser.add_argument("--close", required=False)
              parser.add_argument("--turnover", required=False)
              args = parser.parse_args()
              params = {}
              if args.date is not None: params["date"] = args.date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.symbol is not None: params["symbol"] = args.symbol
              if args.symbol_name is not None: params["symbol_name"] = args.symbol_name
              if args.change_rate is not None: params["change_rate"] = args.change_rate
              if args.close is not None: params["close"] = args.close
              if args.turnover is not None: params["turnover"] = args.turnover
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 648 B
        ---
        name: abnormal-trading-overview
        description: 查询龙虎榜总览。接口:GET /api/v1/market/data/abnormal-trading-overview。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 龙虎榜总览
        
        接口:GET `/api/v1/market/data/abnormal-trading-overview`。参数和响应以 `ftshare-doc/api-doc/股票数据/打板专题数据/龙虎榜总览.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> abnormal-trading-overview --page 1 --symbol 600519.SH
        ```
        
    • ashare-code-change
      • scripts
        • handler.py 2.2 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL=os.environ.get("FTSHARE_BASE_URL","https://market.ft.tech/gateway").rstrip("/"); ENDPOINT="/api/v1/market/data/stk-code-change"
          _REQUEST_HEADERS={"Content-Type":"application/json"}
          def safe_urlopen(request):
           url=request.full_url if isinstance(request,urllib.request.Request) else str(request)
           parsed=urllib.parse.urlparse(url); base=urllib.parse.urlparse(BASE_URL)
           if (parsed.scheme,parsed.netloc)!=(base.scheme,base.netloc): print("Invalid URL for safe_urlopen",file=sys.stderr); raise SystemExit(1)
           if not isinstance(request,urllib.request.Request): request=urllib.request.Request(url,headers=_REQUEST_HEADERS,method="GET")
           return urllib.request.urlopen(request,timeout=30)
          def fetch(params):
           key=os.environ.get("FTSHARE_API_KEY")
           if not key: print("FTSHARE_API_KEY environment variable is required",file=sys.stderr); raise SystemExit(2)
           url=BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params); b,p=urllib.parse.urlparse(BASE_URL),urllib.parse.urlparse(url)
           if (b.scheme,b.netloc)!=(p.scheme,p.netloc): print("Invalid URL for safe_urlopen",file=sys.stderr); raise SystemExit(1)
           try:
            with safe_urlopen(urllib.request.Request(url,headers={"FTSHARE_API_KEY":key,"Content-Type":"application/json"},method="GET")) as r:return json.loads(r.read().decode())
           except urllib.error.HTTPError as e: print(f"HTTP {e.code}: {e.read().decode()}",file=sys.stderr); raise SystemExit(1)
           except urllib.error.URLError as e: print(f"请求失败: {e.reason}",file=sys.stderr); raise SystemExit(1)
          def main():
           p=argparse.ArgumentParser(description="查询 A 股代码变更"); p.add_argument("--trade-code",required=True); p.add_argument("--start-date"); p.add_argument("--end-date"); a=p.parse_args()
           if not a.trade_code.strip(): p.error("trade-code 不能为空")
           if a.start_date and a.end_date and a.start_date>a.end_date: p.error("start-date 不得晚于 end-date")
           params={"trade_code":a.trade_code};
           if a.start_date: params["start_date"]=a.start_date
           if a.end_date: params["end_date"]=a.end_date
           print(json.dumps(fetch(params),ensure_ascii=False,indent=2))
          if __name__=="__main__":main()
          
        • test_handler.py 1022 B
          import importlib.util
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          
          _DIR = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_DIR, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          spec.loader.exec_module(handler)
          
          class TestHandler(unittest.TestCase):
              @patch.object(handler, "fetch")
              def test_required_code_and_dates(self, mock_fetch):
                  mock_fetch.return_value = {"code": 200, "data": []}
                  with patch.object(sys, "argv", ["handler.py", "--trade-code", "001872.SZ,920000.BJ", "--start-date", "20180101", "--end-date", "20241231"]), patch("sys.stdout", new_callable=StringIO) as out:
                      handler.main()
                  self.assertEqual(mock_fetch.call_args.args[0], {"trade_code": "001872.SZ,920000.BJ", "start_date": "20180101", "end_date": "20241231"})
                  self.assertEqual(json.loads(out.getvalue())["code"], 200)
          
          if __name__ == "__main__": unittest.main()
          
      • SKILL.md 941 B
        ---
        name: ashare-code-change
        description: 查询 A 股代码变更历史。Use when user asks about 股票代码变更、改名换码、代码迁移或北交所升板代码历史。
        ---
        
        # A 股代码变更
        
        接口:`GET /api/v1/market/data/stk-code-change`。`trade_code` 必填,支持逗号分隔多个带市场后缀的代码;不分页,返回全部变更记录。
        
        ```bash
        python <RUN_PY> ashare-code-change --trade-code 001872.SZ
        python <RUN_PY> ashare-code-change --trade-code 001872.SZ,920000.BJ --start-date 20180101 --end-date 20241231
        ```
        
        - `--trade-code`:必填,如 `600848.SH`,可逗号分隔多个。
        - `--start-date`、`--end-date`:可选,格式 `YYYYMMDD`;同时提供时起始日不得晚于截止日。
        
        成功响应为 `code/message/data`,`data` 为记录数组或对象中的记录数据,记录包括最新 `trade_code`、实际使用的 `code`、`name`、`start_date` 和可为空的 `end_date`。
        
    • ashare-interactions
      • scripts
        • handler.py 3.2 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/ashare-interactions'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='e互动')
              parser.add_argument("--start_date", required=True)
              parser.add_argument("--end_date", required=True)
              parser.add_argument("--trade_code")
              parser.add_argument("--company_name")
              parser.add_argument("--industry_code")
              parser.add_argument("--industry_name")
              parser.add_argument("--data_source")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              args = parser.parse_args()
              params = {}
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.trade_code is not None: params["trade_code"] = args.trade_code
              if args.company_name is not None: params["company_name"] = args.company_name
              if args.industry_code is not None: params["industry_code"] = args.industry_code
              if args.industry_name is not None: params["industry_name"] = args.industry_name
              if args.data_source is not None: params["data_source"] = args.data_source
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 631 B
        ---
        name: ashare-interactions
        description: 查询e互动。接口:GET /api/v2/market/data/ashare-interactions。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # e互动
        
        接口:GET `/api/v2/market/data/ashare-interactions`。参数和响应以 `ftshare-doc/api-doc/股票数据/特色数据/e互动.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> ashare-interactions --start_date 20260828 --end_date 20260828 --page 1 --page_size 5
        ```
        
    • ashare-news-sentiment-factors
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v3/market/data/ashare-news-sentiment-factors'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description='查询 A 股新闻情绪因子')
              parser.add_argument("--trade-code", required=True)
              parser.add_argument("--start-date")
              parser.add_argument("--end-date")
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", type=int, default=50)
          
              args = parser.parse_args()
              params = {}
              if args.trade_code is not None:
                  params['trade_code'] = args.trade_code
              if args.start_date is not None:
                  params['start_date'] = args.start_date
              if args.end_date is not None:
                  params['end_date'] = args.end_date
              if args.page is not None:
                  params['page'] = args.page
              if args.page_size is not None:
                  params['page_size'] = args.page_size
          
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 730 B
        ---
        name: ashare-news-sentiment-factors
        description: 查询指定 A 股的新闻情绪因子。必填 --trade-code;可选 --start-date、--end-date、--page、--page-size。接口:GET /api/v3/market/data/ashare-news-sentiment-factors。
        ---
        
        # A股新闻情绪因子
        
        查询指定 A 股的新闻情绪因子。必填 --trade-code;可选 --start-date、--end-date、--page、--page-size。接口:GET /api/v3/market/data/ashare-news-sentiment-factors。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> ashare-news-sentiment-factors --trade-code 600000.SH
        ```
        
    • ashare-rating-factor-snapshot
      • scripts
        • handler.py 2.2 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v3/market/data/ashare-rating-factor-snapshot"
          _REQUEST_HEADERS = {"X-Client-Name": "ft-claw", "Content-Type": "application/json"}
          if os.environ.get("FTSHARE_API_KEY"):
              _REQUEST_HEADERS["FTSHARE_API_KEY"] = os.environ["FTSHARE_API_KEY"]
          OPENER = urllib.request.build_opener()
          
          
          def safe_urlopen(request):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed = urllib.parse.urlparse(url)
              base = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, headers=_REQUEST_HEADERS, method="GET")
              return OPENER.open(request, timeout=60)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser()
              parser.add_argument("--trade-code", required=True)
              parser.add_argument("--date")
              parser.add_argument("--top-k", type=int)
              args = parser.parse_args()
              params = {"trade_code": args.trade_code}
              if args.date is not None:
                  params["date"] = args.date
              if args.top_k is not None:
                  params["top_k"] = args.top_k
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params)
              request = urllib.request.Request(url, headers=_REQUEST_HEADERS, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      result = json.loads(response.read().decode())
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 623 B
        ---
        name: ashare-rating-factor-snapshot
        description: 查询指定 A 股的 Top-K 相关公司。用户询问股票相关性、相关公司排名或相关性评分时使用。
        ---
        
        # A股相关性 Top-K
        
        外部接口:`GET /api/v3/market/data/ashare-rating-factor-snapshot`
        
        必填:`--trade-code`。可选:`--date`(YYYYMMDD)、`--top-k`(1–500,默认由服务端设为 20)。不分页。
        
        ```bash
        python <RUN_PY> ashare-rating-factor-snapshot --trade-code 600000.SH --date 20260819 --top-k 10
        ```
        
        返回 `code`、`message`、`data`,其中 `data.related_securities` 按相关性分数返回相关证券。
        
    • ashare-status-change
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL=os.environ.get("FTSHARE_BASE_URL","https://market.ft.tech/gateway").rstrip("/"); ENDPOINT="/api/v1/market/data/stk-status-change"
          _REQUEST_HEADERS={"Content-Type":"application/json"}
          def safe_urlopen(request):
           url=request.full_url if isinstance(request,urllib.request.Request) else str(request)
           parsed=urllib.parse.urlparse(url); base=urllib.parse.urlparse(BASE_URL)
           if (parsed.scheme,parsed.netloc)!=(base.scheme,base.netloc): print("Invalid URL for safe_urlopen",file=sys.stderr); raise SystemExit(1)
           if not isinstance(request,urllib.request.Request): request=urllib.request.Request(url,headers=_REQUEST_HEADERS,method="GET")
           return urllib.request.urlopen(request,timeout=30)
          def fetch(params):
           key=os.environ.get("FTSHARE_API_KEY")
           if not key: print("FTSHARE_API_KEY environment variable is required",file=sys.stderr); raise SystemExit(2)
           url=BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params); b,p=urllib.parse.urlparse(BASE_URL),urllib.parse.urlparse(url)
           if (b.scheme,b.netloc)!=(p.scheme,p.netloc): print("Invalid URL for safe_urlopen",file=sys.stderr); raise SystemExit(1)
           try:
            with safe_urlopen(urllib.request.Request(url,headers={"FTSHARE_API_KEY":key,"Content-Type":"application/json"},method="GET")) as r:return json.loads(r.read().decode())
           except urllib.error.HTTPError as e: print(f"HTTP {e.code}: {e.read().decode()}",file=sys.stderr); raise SystemExit(1)
           except urllib.error.URLError as e: print(f"请求失败: {e.reason}",file=sys.stderr); raise SystemExit(1)
          def main():
           p=argparse.ArgumentParser(description="查询 A 股状态变更"); p.add_argument("--trade-code"); p.add_argument("--change-date"); p.add_argument("--change-type"); a=p.parse_args()
           params={k:v for k,v in (("trade_code",a.trade_code),("change_date",a.change_date),("change_type",a.change_type)) if v is not None}
           print(json.dumps(fetch(params),ensure_ascii=False,indent=2))
          if __name__=="__main__":main()
          
        • test_handler.py 999 B
          import importlib.util
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          
          _DIR = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_DIR, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          spec.loader.exec_module(handler)
          
          class TestHandler(unittest.TestCase):
              @patch.object(handler, "fetch")
              def test_optional_filters(self, mock_fetch):
                  mock_fetch.return_value = {"code": 200, "data": []}
                  with patch.object(sys, "argv", ["handler.py", "--trade-code", "600848.SH", "--change-date", "20240101", "--change-type", "上市"]), patch("sys.stdout", new_callable=StringIO) as out:
                      handler.main()
                  self.assertEqual(mock_fetch.call_args.args[0], {"trade_code": "600848.SH", "change_date": "20240101", "change_type": "上市"})
                  self.assertEqual(json.loads(out.getvalue())["code"], 200)
          
          if __name__ == "__main__": unittest.main()
          
      • SKILL.md 936 B
        ---
        name: ashare-status-change
        description: 查询 A 股状态变更记录。Use when user asks about 上市、退市、暂停上市或股票状态变更。
        ---
        
        # A 股状态变更
        
        接口:`GET /api/v1/market/data/stk-status-change`。支持代码、变更日期和变更类型任意组合过滤,不分页。
        
        ```bash
        python <RUN_PY> ashare-status-change --trade-code 600848.SH
        python <RUN_PY> ashare-status-change --trade-code 600848.SH --change-type 上市
        python <RUN_PY> ashare-status-change --change-date 20240101
        ```
        
        - `--trade-code`:可选,支持逗号分隔多个 `.SZ`/`.SH` 代码。
        - `--change-date`:可选,精确日期过滤,格式 `YYYYMMDD`。
        - `--change-type`:可选,精确类型过滤,如 `上市`、`退市`、`暂停上市`。
        
        成功响应为 `code/message/data`,记录包括 `trade_code`、`name`、`change_date`、`change_type`、`change_details`,按代码升序、日期降序返回。
        
    • auction-results
      • scripts
        • handler.py 3.8 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/auction-results'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='集合竞价结果')
              parser.add_argument("--ts_code")
              parser.add_argument("--trade_date")
              parser.add_argument("--start_date")
              parser.add_argument("--end_date")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--total", required=False)
              parser.add_argument("--symbol", required=True)
              parser.add_argument("--close", required=False)
              parser.add_argument("--open", required=False)
              parser.add_argument("--high", required=False)
              parser.add_argument("--low", required=False)
              parser.add_argument("--volume", required=False)
              parser.add_argument("--amount", required=False)
              parser.add_argument("--vwap", required=False)
              args = parser.parse_args()
              params = {}
              if args.ts_code is not None: params["ts_code"] = args.ts_code
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.symbol is not None: params["symbol"] = args.symbol
              if args.close is not None: params["close"] = args.close
              if args.open is not None: params["open"] = args.open
              if args.high is not None: params["high"] = args.high
              if args.low is not None: params["low"] = args.low
              if args.volume is not None: params["volume"] = args.volume
              if args.amount is not None: params["amount"] = args.amount
              if args.vwap is not None: params["vwap"] = args.vwap
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 611 B
        ---
        name: auction-results
        description: 查询集合竞价结果。接口:GET /api/v2/market/data/auction-results。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 集合竞价结果
        
        接口:GET `/api/v2/market/data/auction-results`。参数和响应以 `ftshare-doc/api-doc/股票数据/特色数据/集合竞价结果.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> auction-results --page 1 --symbol 600519.SH
        ```
        
    • block-trades
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """查询 A 股大宗交易列表"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/block-trades"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.1 KB
        ---
        name: block-trades
        description: "查询大宗交易列表。当用户需要获取 A 股大宗交易记录,包含买卖方营业部、成交价、成交量、溢价率等,或了解大宗交易列表时使用。"
        ---
        
        # 查询大宗交易列表
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 查询大宗交易列表 |
        | 外部接口 | `/api/v1/market/data/block-trades` |
        | 请求方式 | GET |
        | 适用场景 | 获取 A 股大宗交易记录,包含买卖方营业部、成交价、成交量、溢价率等 |
        
        ## 请求参数
        
        无需任何参数。
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> block-trades
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        返回值为大宗交易记录数组(直接返回数组,非对象包装):
        
        ```json
        [
            {
                "symbol": "000001.SZ",
                "name": "平安银行",
                "date": "2025-03-10",
                "price": "10.50",
                "close": "10.48",
                "volume": 1000000,
                "premium_rate": 0.19,
                "buyer_name": "某某营业部",
                "seller_name": "某某证券营业部"
            }
        ]
        ```
        
        ### 字段说明(BlockTrade)
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |---|---|---|---|---|
        | `symbol` | String | 否 | 标的代码,带市场后缀 | - |
        | `name` | String | 否 | 标的名称 | - |
        | `date` | String | 否 | 成交日期,格式 `YYYY-MM-DD` | - |
        | `price` | Number | 否 | 成交价 | 元 |
        | `close` | Number | 否 | 收盘价 | 元 |
        | `volume` | int | 否 | 成交量 | 股 |
        | `premium_rate` | Number | 否 | 溢价率(小数形式,如 `0.19` 表示 19%) | - |
        | `buyer_name` | String | 否 | 买方营业部名称 | - |
        | `seller_name` | String | 否 | 卖方营业部名称 | - |
        
        ## 注意事项
        
        - 接口直接返回数组,无分页包装结构
        - `premium_rate` 为小数形式,展示时乘以 100 转为百分比
        - `price` 和 `close` 在文档中标注为 Number,但部分情况可能以字符串返回,比较前注意类型转换
        
    • bse-mapping
      • scripts
        • handler.py 2.3 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/bse-mapping'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description='查询北交所代码映射')
              parser.add_argument("--o-code", dest="o_code")
          
              args = parser.parse_args()
              params = {}
              if args.o_code is not None:
                  params['o_code'] = args.o_code
          
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 480 B
        ---
        name: bse-mapping
        description: 查询北交所代码映射。可选 --o-code。接口:GET /api/v1/market/data/bse-mapping。
        ---
        
        # 北交所映射
        
        查询北交所代码映射。可选 --o-code。接口:GET /api/v1/market/data/bse-mapping。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> bse-mapping
        ```
        
    • bullion-price
      • scripts
        • handler.py 3.1 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/bullion/price'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='贵金属价格')
              parser.add_argument("--symbol", required=True)
              parser.add_argument("--start_date", required=True)
              parser.add_argument("--end_date", required=True)
              parser.add_argument("--page", required=True)
              parser.add_argument("--page_size", required=True)
              parser.add_argument("--total", required=False)
              parser.add_argument("--date", required=True)
              parser.add_argument("--value", required=False)
              args = parser.parse_args()
              params = {}
              if args.symbol is not None: params["symbol"] = args.symbol
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.date is not None: params["date"] = args.date
              if args.value is not None: params["value"] = args.value
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 653 B
        ---
        name: bullion-price
        description: 查询贵金属价格。接口:GET /api/v1/market/data/bullion/price。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 贵金属价格
        
        接口:GET `/api/v1/market/data/bullion/price`。参数和响应以 `ftshare-doc/api-doc/现货数据/贵金属价格.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> bullion-price --symbol 600519.SH --start_date 20260828 --end_date 20260828 --page 1 --page_size 5 --date 20260828
        ```
        
    • cashflow-stock-code
      • scripts
        • handler.py 2.6 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/finance/cashflow-stock-code'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='现金流支持股票代码')
              parser.add_argument("--items", required=False)
              parser.add_argument("--stock_code", required=True)
              parser.add_argument("--stock_name", required=True)
              args = parser.parse_args()
              params = {}
              if args.items is not None: params["items"] = args.items
              if args.stock_code is not None: params["stock_code"] = args.stock_code
              if args.stock_name is not None: params["stock_name"] = args.stock_name
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 677 B
        ---
        name: cashflow-stock-code
        description: 查询现金流支持股票代码。接口:GET /api/v2/market/data/finance/cashflow-stock-code。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 现金流支持股票代码
        
        接口:GET `/api/v2/market/data/finance/cashflow-stock-code`。参数和响应以 `ftshare-doc/api-doc/股票数据/财务数据/现金流支持股票代码.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> cashflow-stock-code --stock_code 600519 --stock_name 1
        ```
        
    • cb-lists
      • scripts
        • handler.py 1.6 KB
          #!/usr/bin/env python3
          """获取可转债全量列表(market.ft.tech)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              url = BASE_URL + "/api/v1/market/data/cb/cb-lists"
              req = urllib.request.Request(url, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key}, method="GET")
          
              try:
                  with safe_urlopen(req) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.9 KB
        ---
        name: cb-lists
        description: 可转债全量列表(market.ft.tech)。用户问可转债列表、全部可转债、转债代码列表、有哪些可转债时使用。
        ---
        
        # 可转债列表 - 获取可转债列表
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 获取可转债列表 |
        | 外部接口 | `GET /api/v1/market/data/cb/cb-lists` |
        | 请求方式 | GET |
        | 适用场景 | 获取当前可转债全量列表(全称、债券代码、正股代码、交易所);仅包含通过可转债号段判断的标的,数据为前一交易日 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 响应说明
        
        返回可转债列表;无数据时 `items` 为空数组 `[]`。
        
        ```json
        {
          "items": [
            {
              "full_name": "恒逸石化股份有限公司可转换公司债券",
              "cb_id": 110070,
              "stock_id": 703,
              "exchange": 1
            },
            {
              "full_name": "浙江伟明环保股份有限公司可转换公司债券",
              "cb_id": 113643,
              "stock_id": 603568,
              "exchange": 1
            }
          ]
        }
        ```
        
        ### CbListsItem 字段
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | full_name | string | 否 | 债券全称 | - |
        | cb_id | int | 否 | 债券代码 | - |
        | stock_id | int | 否 | 转债对应正股代码 | - |
        | exchange | int | 否 | 交易所 | - |
        
        ## 4. 用法
        
        通过主目录 `run.py` 调用(无需参数):
        
        ```bash
        python <RUN_PY> cb-lists
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。脚本输出 JSON;本接口无需额外请求头。
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/cb/cb-lists
        ```
        
        ## 6. 注意事项
        
        - 数据为前一交易日可转债列表,具体以接口返回为准
        - 无数据时返回 `{"items": []}`
        
    • chinabond-yield-daily
      • scripts
        • handler.py 2.9 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/chinabond-yield-daily'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='中债收益率曲线日度')
              parser.add_argument("--start_date", required=True)
              parser.add_argument("--end_date", required=True)
              parser.add_argument("--curve_name")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--trade_date", required=True)
              args = parser.parse_args()
              params = {}
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.curve_name is not None: params["curve_name"] = args.curve_name
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.curve_name is not None: params["curve_name"] = args.curve_name
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 707 B
        ---
        name: chinabond-yield-daily
        description: 查询中债收益率曲线日度。接口:GET /api/v1/market/data/chinabond-yield-daily。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 中债收益率曲线日度
        
        接口:GET `/api/v1/market/data/chinabond-yield-daily`。参数和响应以 `ftshare-doc/api-doc/宏观经济/国内宏观/中债收益率曲线日度.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> chinabond-yield-daily --start_date 20260828 --end_date 20260828 --page 1 --trade_date 20260828
        ```
        
    • company-list
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/company-list'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description='查询公司列表')
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", dest="page_size", type=int, default=50)
          
              args = parser.parse_args()
              params = {}
              if args.page is not None:
                  params['page'] = args.page
              if args.page_size is not None:
                  params['page_size'] = args.page_size
          
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 487 B
        ---
        name: company-list
        description: 查询公司列表。可选 --page、--page-size。接口:GET /api/v1/market/data/company-list。
        ---
        
        # 公司列表
        
        查询公司列表。可选 --page、--page-size。接口:GET /api/v1/market/data/company-list。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> company-list
        ```
        
    • continuous-auction-volume
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v2/market/data/intraday-auction-volume"
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ.get("FTSHARE_API_KEY", ""), "Content-Type": "application/json"}
          
          
          def fetch(params):
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if (parsed.scheme, parsed.netloc) != (base.scheme, base.netloc):
                  print("Invalid URL for safe_urlopen", file=sys.stderr)
                  raise SystemExit(1)
              request = urllib.request.Request(url, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key}, method="GET")
              try:
                  with urllib.request.urlopen(request, timeout=30) as response:
                      return json.loads(response.read().decode())
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询连续竞价成交量")
              parser.add_argument("--trade-date", dest="trade_date")
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", dest="page_size", type=int, default=50)
              parser.add_argument("--all", action="store_true", dest="fetch_all")
              args = parser.parse_args()
              if args.page < 1 or not 1 <= args.page_size <= 200:
                  parser.error("page 须大于等于 1,page-size 须在 1~200 之间")
              params = {"page": 1 if args.fetch_all else args.page, "page_size": args.page_size}
              if args.trade_date is not None:
                  params["trade_date"] = args.trade_date
              result = fetch(params)
              if args.fetch_all:
                  data = result.get("data") or {}
                  records = list(data.get("records", []))
                  for page in range(2, int(data.get("pages", 1)) + 1):
                      records.extend((fetch({**params, "page": page}).get("data") or {}).get("records", []))
                  result["data"] = {**data, "records": records}
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 733 B
        ---
        name: continuous-auction-volume
        description: 查询全市场连续竞价成交量和成交额占比。用户询问全市场、沪深北市场的分时成交占比时使用。
        ---
        
        # 连续竞价成交量
        
        接口:`GET /api/v2/market/data/intraday-auction-volume`。可选 `--trade-date`(YYYYMMDD)、`--page` 和 `--page-size`,每页最多 200 条;支持 `--all` 自动翻页。
        
        ```bash
        python <RUN_PY> continuous-auction-volume --trade-date 20260620 --page 1 --page-size 50
        ```
        
        不传交易日查询当日实时全市场聚合数据,传入历史交易日查询历史数据;响应为 `code/message/data` 分页信封,分钟记录位于 `data.records`,包含 overall、xshg、xshe 和 bjse 四个市场维度。
        
    • convertible-bond-candlesticks
      • scripts
        • handler.py 5 KB
          #!/usr/bin/env python3
          """查询单只可转债历史日/周/月/年 K 线(GET /api/v1/market/data/convertible-bond-candlesticks)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/convertible-bond-candlesticks"
          
          INTERVAL_UNITS = ("Day", "Week", "Month", "Year")
          ADJUST_KINDS = ("None", "Forward", "Backward")
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def build_body(symbol, interval_unit, interval_value, adjust_kind,
                         since_ts_millis, until_ts_millis, limit):
              body = {
                  "symbol": symbol,
                  "interval_unit": interval_unit,
                  "since_ts_millis": since_ts_millis,
                  "until_ts_millis": until_ts_millis,
              }
              if interval_value is not None:
                  body["interval_value"] = interval_value
              if adjust_kind and adjust_kind != "None":
                  body["adjust_kind"] = adjust_kind
              if limit is not None:
                  body["limit"] = limit
              return body
          
          
          def fetch(
          symbol, interval_unit, interval_value, adjust_kind,
                    since_ts_millis, until_ts_millis, limit):
              body = build_body(symbol, interval_unit, interval_value, adjust_kind,
                                since_ts_millis, until_ts_millis, limit)
              query = urllib.parse.urlencode(body, doseq=True)
              url = f"{BASE_URL}{ENDPOINT}?{query}"
              req = urllib.request.Request(
                  url,
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  msg = e.read().decode()
                  print(f"HTTP {e.code}: {msg}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询单只可转债历史日/周/月/年 K 线(GET 查询参数,不支持分钟周期)")
              parser.add_argument("--symbol", required=True, help="可转债代码,如 113042.SH、123107.SZ")
              parser.add_argument("--interval-unit", dest="interval_unit", required=True, type=str.capitalize,
                                  choices=INTERVAL_UNITS, help="K 线周期:Day/Week/Month/Year(大小写不敏感,不支持 Minute)")
              parser.add_argument("--interval-value", dest="interval_value", type=int, default=None,
                                  help="间隔数值,周期查询无需设置")
              parser.add_argument("--adjust-kind", dest="adjust_kind", default="None",
                                  choices=ADJUST_KINDS, help="复权:None(默认)/Forward/Backward")
              parser.add_argument("--since-ts-millis", dest="since_ts_millis", required=True, type=int,
                                  help="开始时间戳(毫秒);与结束时间跨度不得超过 12 个自然月")
              parser.add_argument("--until-ts-millis", dest="until_ts_millis", required=True, type=int,
                                  help="结束时间戳(毫秒)")
              parser.add_argument("--limit", type=int, default=None,
                                  help="返回条数上限;省略时返回窗口内全部记录")
              args = parser.parse_args()
          
              if args.since_ts_millis > args.until_ts_millis:
                  print("--since-ts-millis 不能晚于 --until-ts-millis", file=sys.stderr)
                  raise SystemExit(2)
          
              data = fetch(args.symbol, args.interval_unit, args.interval_value,
                           args.adjust_kind, args.since_ts_millis, args.until_ts_millis, args.limit)
              print(json.dumps(data, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 3.5 KB
          #!/usr/bin/env python3
          """Tests for convertible-bond-candlesticks handler"""
          import json
          import sys
          import unittest
          import urllib.error
          from io import BytesIO, StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          SINCE = 1786291200000
          UNTIL = 1786377599999
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_get_to_cb_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("113042.SH", "Day", None, "None", SINCE, UNTIL, 1)
                  req = mock_open.call_args[0][0]
                  self.assertEqual(req.get_method(), "GET")
                  self.assertIn("/api/v1/market/data/convertible-bond-candlesticks", req.full_url)
                  self.assertIsNone(req.data)
                  self.assertIn("symbol=113042.SH", req.full_url)
                  self.assertIn(f"since_ts_millis={SINCE}", req.full_url)
                  self.assertEqual(req.headers.get("X-client-name"), "ft-claw")
          
              @patch.object(handler, "safe_urlopen")
              def test_http_error_exits(self, mock_open):
                  mock_open.side_effect = urllib.error.HTTPError(
                      "https://fake", 500, "Internal Error", {}, BytesIO(b"server error")
                  )
                  with self.assertRaises(SystemExit):
                      handler.fetch("113042.SH", "Day", None, "None", SINCE, UNTIL, None)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", [
                          "handler.py", "--symbol", "113042.SH", "--interval-unit", "Day",
                          "--since-ts-millis", str(SINCE), "--until-ts-millis", str(UNTIL)
                      ]):
                          with patch("sys.stdout", new_callable=StringIO) as fake_out:
                              handler.main()
                              self.assertEqual(json.loads(fake_out.getvalue()), [])
          
              def test_main_requires_since(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", [
                          "handler.py", "--symbol", "113042.SH", "--interval-unit", "Day",
                          "--until-ts-millis", str(UNTIL)
                      ]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              def test_main_rejects_minute_interval(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", [
                          "handler.py", "--symbol", "113042.SH", "--interval-unit", "Minute",
                          "--since-ts-millis", str(SINCE), "--until-ts-millis", str(UNTIL)
                      ]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              def test_main_rejects_since_after_until(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", [
                          "handler.py", "--symbol", "113042.SH", "--interval-unit", "Day",
                          "--since-ts-millis", str(UNTIL), "--until-ts-millis", str(SINCE)
                      ]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
          
          if __name__ == "__main__":
              unittest.main()
      • SKILL.md 4.1 KB
        ---
        name: convertible-bond-candlesticks
        description: 单只可转债历史日/周/月/年 K 线(convertible_bond_candlesticks,GET /api/v1/market/data/convertible-bond-candlesticks)。用户问某只可转债的日/周/月/年 K 线、开高低收、前/后复权时使用。必填 --symbol、--interval-unit、--since-ts-millis、--until-ts-millis;可选 --interval-value、--adjust-kind、--limit。分钟 K 线请改用 convertible-bond-minutes。
        ---
        
        # 可转债历史K线 - 查询单只可转债日/周/月/年 K 线(convertible_bond_candlesticks)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 可转债历史K线(convertible_bond_candlesticks) |
        | 外部接口 | `GET /api/v1/market/data/convertible-bond-candlesticks` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 获取单只可转债的历史日、周、月、年 K 线(开高低收、成交量、成交额),支持前复权/后复权/不复权 |
        | 数据范围 | 以各标的实际历史行情覆盖为准,不保证从发行日起逐日有记录;**不含当日盘中实时行情** |
        | 单次限量 | 无分页;所有周期单次时间跨度不超过 12 个自然月 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 是 | 可转债代码(带市场后缀) | 113042.SH | 也接受 `.XSHG`/`.SH`、`.XSHE`/`.SZ` |
        | interval_unit | string | 是 | 周期单位 | Day | Day/Week/Month/Year,大小写不敏感;**不支持 Minute** |
        | interval_value | int | 否 | 间隔数值 | 可省略 | 周期查询无需设置,省略即按一个周期返回 |
        | adjust_kind | string | 否 | 复权类型 | Forward | None(默认)/Forward(前复权)/Backward(后复权) |
        | since_ts_millis | int | 是 | 开始时间戳(毫秒) | 1786291200000 | 不得晚于 `until_ts_millis`,不能用 `until` + `limit` 替代 |
        | until_ts_millis | int | 是 | 结束时间戳(毫秒) | 1786377599999 | 与起始时间相差不超过 12 个自然月 |
        | limit | int | 否 | 返回条数上限 | 1 | 省略时返回窗口内全部记录;设置后保留最新若干根 |
        
        ## 3. 响应说明
        
        外层固定为 `code`(成功 200)/ `message`(成功 `success`)/ `data`。`data` 为 K 线数组,无数据时为 `[]`;每根 K 线字段:
        
        | 字段名 | 类型 | 说明 | 单位 |
        |--------|------|------|------|
        | open / high / low / close | string | 开/高/低/收盘价;进行中的 K 线 `close` 为最新价 | 元 |
        | ts_millis | int | K 线结束时间戳 | 毫秒 |
        | ts_millis_open | int | K 线开始时间戳 | 毫秒 |
        | turnover | string | 成交额 | 元 |
        | volume | int | 成交量 | - |
        
        注:价格与成交额在 JSON 中为字符串(避免精度丢失),不是 number;`ts_millis` 为数字。
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> convertible-bond-candlesticks --symbol 113042.SH --interval-unit Day --since-ts-millis 1786291200000 --until-ts-millis 1786377599999 --limit 1
        python <RUN_PY> convertible-bond-candlesticks --symbol 113042.SH --interval-unit Week --adjust-kind Forward --since-ts-millis 1783000000000 --until-ts-millis 1786377599999
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - `symbol`、`interval_unit`、`since_ts_millis`、`until_ts_millis` 必填;缺 `since` 会返回 `缺少必填参数:since_ts_millis`。
        - 仅接受可转债标的;传入股票等其它类别返回 `symbols [...] are not convertible bonds`。
        - 分钟 K 线不在本接口:本接口传 `interval_unit=Minute` 会返回 400 并指向 `convertible-bond-minute-candlesticks`,请改用 `convertible-bond-minutes` 子 skill。
        - 周、月、年 K 按请求范围内的历史日 K 聚合;窗口未覆盖完整周期时,该周期不是完整周期数据。
        - 结果按时间升序返回;设置 `limit` 时保留最新若干根。
        - 单次时间跨度上限 12 个自然月,更长区间需分段调用。
        - 复权参数是当前接口的兼容参数,不表示每只可转债都存在复权事件。
        
    • convertible-bond-candlesticks-batch
      • scripts
        • handler.py 5.5 KB
          #!/usr/bin/env python3
          """批量查询多只可转债历史日/周/月/年 K 线(GET /api/v2/market/data/convertible-bond-candlesticks/batch)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v2/market/data/convertible-bond-candlesticks/batch"
          
          INTERVAL_UNITS = ("Day", "Week", "Month", "Year")
          ADJUST_KINDS = ("None", "Forward", "Backward")
          MAX_SYMBOLS = 20
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def parse_symbols(raw):
              syms = [s.strip() for s in raw.split(",") if s.strip()]
              if not syms:
                  print("--symbols 不能为空", file=sys.stderr)
                  sys.exit(1)
              if len(syms) > MAX_SYMBOLS:
                  print(f"--symbols 最多 {MAX_SYMBOLS} 个标的,当前 {len(syms)} 个", file=sys.stderr)
                  sys.exit(1)
              return syms
          
          
          def build_query(symbols, interval_unit, interval_value, adjust_kind,
                          since_ts_millis, until_ts_millis, limit):
              body = {
                  "symbols": symbols,
                  "interval_unit": interval_unit,
                  "since_ts_millis": since_ts_millis,
                  "until_ts_millis": until_ts_millis,
              }
              if interval_value is not None:
                  body["interval_value"] = interval_value
              if adjust_kind and adjust_kind != "None":
                  body["adjust_kind"] = adjust_kind
              if limit is not None:
                  body["limit"] = limit
              return body
          
          
          def fetch(symbols, interval_unit, interval_value, adjust_kind,
                    since_ts_millis, until_ts_millis, limit):
              body = build_query(symbols, interval_unit, interval_value, adjust_kind,
                                 since_ts_millis, until_ts_millis, limit)
              query = urllib.parse.urlencode(body, doseq=True)
              req = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}?{query}",
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="批量获取多只可转债的历史日/周/月/年 K 线(不支持分钟周期)")
              parser.add_argument("--symbols", required=True,
                                  help="可转债代码列表,逗号分隔,1~20 只,如 113042.SH,123107.SZ;也接受 .XSHG/.XSHE 长后缀")
              parser.add_argument("--interval-unit", dest="interval_unit", required=True, type=str.capitalize,
                                  choices=INTERVAL_UNITS, help="K 线周期:Day/Week/Month/Year(大小写不敏感,不支持 Minute)")
              parser.add_argument("--interval-value", dest="interval_value", type=int, default=None,
                                  help="间隔数值,周期查询无需设置")
              parser.add_argument("--adjust-kind", dest="adjust_kind", default="None",
                                  choices=ADJUST_KINDS, help="复权:None(默认)/Forward/Backward")
              parser.add_argument("--since-ts-millis", dest="since_ts_millis", required=True, type=int,
                                  help="开始时间戳(毫秒);与结束时间跨度不得超过 12 个自然月")
              parser.add_argument("--until-ts-millis", dest="until_ts_millis", required=True, type=int,
                                  help="结束时间戳(毫秒)")
              parser.add_argument("--limit", type=int, default=None,
                                  help="每个标的返回条数上限;省略时返回窗口内全部记录")
              args = parser.parse_args()
          
              if args.since_ts_millis > args.until_ts_millis:
                  print("--since-ts-millis 不能晚于 --until-ts-millis", file=sys.stderr)
                  raise SystemExit(2)
          
              symbols = parse_symbols(args.symbols)
              data = fetch(symbols, args.interval_unit, args.interval_value, args.adjust_kind,
                           args.since_ts_millis, args.until_ts_millis, args.limit)
              print(json.dumps(data, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
        • test_handler.py 5.4 KB
          #!/usr/bin/env python3
          """Tests for convertible-bond-candlesticks-batch handler"""
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          SINCE = "1786291200000"
          UNTIL = "1786377599999"
          
          
          class TestBuildQuery(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_required_only(self):
                  body = handler.build_query(["113042.SH"], "Day", None, "None", 1786291200000, 1786377599999, None)
                  self.assertEqual(body["symbols"], ["113042.SH"])
                  self.assertEqual(body["interval_unit"], "Day")
                  self.assertEqual(body["since_ts_millis"], 1786291200000)
                  self.assertEqual(body["until_ts_millis"], 1786377599999)
                  self.assertNotIn("adjust_kind", body)
                  self.assertNotIn("limit", body)
                  self.assertNotIn("interval_value", body)
          
              def test_optional_fields(self):
                  body = handler.build_query(["113042.SH", "123107.SZ"], "Week", 1, "Forward",
                                             1786291200000, 1786377599999, 2)
                  self.assertEqual(body["symbols"], ["113042.SH", "123107.SZ"])
                  self.assertEqual(body["adjust_kind"], "Forward")
                  self.assertEqual(body["interval_value"], 1)
                  self.assertEqual(body["limit"], 2)
          
              def test_rejects_too_many_symbols(self):
                  with self.assertRaises(SystemExit):
                      handler.parse_symbols(",".join(f"11304{i}.SH" for i in range(21)))
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_query_string_expands_symbols(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch(["113042.SH", "123107.SZ"], "Day", None, "None", 1786291200000, 1786377599999, 1)
                  req = mock_open.call_args[0][0]
                  self.assertEqual(req.get_method(), "GET")
                  self.assertIn("/api/v2/market/data/convertible-bond-candlesticks/batch", req.full_url)
                  query = req.full_url.split("?", 1)[1]
                  self.assertIn("symbols=113042.SH", query)
                  self.assertIn("symbols=123107.SZ", query)
                  self.assertIn("interval_unit=Day", query)
                  self.assertIn("since_ts_millis=1786291200000", query)
                  self.assertEqual(req.headers.get("X-client-name"), "ft-claw")
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = (
                      b'{"code":200,"message":"success","data":[["113042.SH",'
                      b'[{"open":"117.1980","close":"116.8380","ts_millis":1786345200000}]]]}'
                  )
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "113042.SH,123107.SZ",
                                                      "--interval-unit", "Day",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL, "--limit", "1"]):
                          with patch("sys.stdout", new_callable=StringIO) as out:
                              handler.main()
                              data = json.loads(out.getvalue())
                              self.assertEqual(data["data"][0][0], "113042.SH")
          
              def test_main_rejects_minute_interval(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "113042.SH",
                                                      "--interval-unit", "Minute",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              def test_main_requires_since(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "113042.SH",
                                                      "--interval-unit", "Day",
                                                      "--until-ts-millis", UNTIL]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              def test_main_rejects_since_after_until(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "113042.SH",
                                                      "--interval-unit", "Day",
                                                      "--since-ts-millis", UNTIL,
                                                      "--until-ts-millis", SINCE]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              def test_main_requires_symbols(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--interval-unit", "Day",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
          
          if __name__ == "__main__":
              unittest.main()
      • SKILL.md 4.1 KB
        ---
        name: convertible-bond-candlesticks-batch
        description: 批量查询多只可转债历史日/周/月/年 K 线(convertible_bond_candlesticks_batch,GET /api/v2/market/data/convertible-bond-candlesticks/batch)。用户问多只可转债的日/周/月/年 K 线、批量可转债开高低收、多只转债对比 K 线时使用。必填 --symbols、--interval-unit、--since-ts-millis、--until-ts-millis;可选 --interval-value、--adjust-kind、--limit。分钟 K 线请改用 convertible-bond-minutes。
        ---
        
        # 批量可转债历史K线
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 批量可转债历史K线(convertible_bond_candlesticks_batch) |
        | 外部接口 | `GET /api/v2/market/data/convertible-bond-candlesticks/batch` |
        | 请求方式 | GET(query 参数,`symbols` 以重复参数发送) |
        | 适用场景 | 一次批量获取 1~20 只可转债的历史日/周/月/年 K 线(开高低收、成交量、成交额),支持前复权/后复权/不复权 |
        | 数据范围 | 以各标的实际历史行情覆盖为准;**不含当日盘中实时行情** |
        | 单次限量 | 每次 1~20 个标的;无分页;所有周期单次时间跨度不超过 12 个自然月 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbols | string[] | 是 | 可转债代码列表,逗号分隔传给 CLI | 113042.SH,123107.SZ | 1~20 只;也接受 `.XSHG`/`.SH`、`.XSHE`/`.SZ`;接口侧以重复参数发送 |
        | interval_unit | string | 是 | 周期单位 | Day | Day/Week/Month/Year,大小写不敏感;**不支持 Minute** |
        | interval_value | int | 否 | 间隔数值 | 可省略 | 周期查询无需设置 |
        | adjust_kind | string | 否 | 复权类型 | Forward | None(默认)/Forward(前复权)/Backward(后复权) |
        | since_ts_millis | int | 是 | 开始时间戳(毫秒) | 1786291200000 | 不得晚于 `until_ts_millis` |
        | until_ts_millis | int | 是 | 结束时间戳(毫秒) | 1786377599999 | 与起始时间相差不超过 12 个自然月 |
        | limit | int | 否 | **每个标的**返回条数上限 | 1 | 省略时返回窗口内全部记录 |
        
        ## 3. 响应说明
        
        外层固定为 `code`(成功 200)/ `message`(成功 `success`)/ `data`。`data` 为非分页嵌套数组,外层每项为 `[symbol, K线数组]`;某只标的无数据时其 K 线数组为 `[]`。每根 K 线字段:
        
        | 字段名 | 类型 | 说明 | 单位 |
        |--------|------|------|------|
        | open / high / low / close | string | 开/高/低/收盘价;进行中的 K 线 `close` 为最新价 | 元 |
        | ts_millis | int | K 线结束时间戳 | 毫秒 |
        | ts_millis_open | int | K 线开始时间戳 | 毫秒 |
        | turnover | string | 成交额 | 元 |
        | volume | int | 成交量 | - |
        
        注:价格与成交额在 JSON 中为字符串(避免精度丢失),不是 number;`ts_millis` 为数字。响应中的 symbol 使用 `.SH`/`.SZ` 短后缀。
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> convertible-bond-candlesticks-batch --symbols 113042.SH,123107.SZ --interval-unit Day --since-ts-millis 1786291200000 --until-ts-millis 1786377599999 --limit 1
        python <RUN_PY> convertible-bond-candlesticks-batch --symbols 113042.XSHG,123107.XSHE --interval-unit Week --adjust-kind Forward --since-ts-millis 1783000000000 --until-ts-millis 1786377599999
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - `symbols`、`interval_unit`、`since_ts_millis`、`until_ts_millis` 必填;空 `symbols` 或超过 20 个标的会请求失败。
        - 所有标的共用同一周期与复权参数,不能逐只指定。
        - 仅接受可转债标的;混入股票等其它类别时整个请求失败,不静默过滤。
        - 分钟 K 线不在本接口,请改用 `convertible-bond-minutes` 子 skill。
        - 单次时间跨度上限 12 个自然月;更长历史需分段调用。
        - 周、月、年 K 按请求范围内的历史日 K 聚合;窗口未覆盖完整周期时,该周期不是完整周期数据。
        - 无数据不等于请求失败,应同时检查 `code` 与 `data`。
    • convertible-bond-minutes
      • scripts
        • handler.py 5.1 KB
          #!/usr/bin/env python3
          """查询单只或批量可转债历史分钟 K 线(GET /api/v2/market/data/convertible-bond-minute-candlesticks)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v2/market/data/convertible-bond-minute-candlesticks"
          
          INTERVAL_VALUES = (1, 5, 15)
          MAX_SYMBOLS = 20
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def parse_symbols(raw):
              syms = [s.strip() for s in raw.split(",") if s.strip()]
              if not syms:
                  print("--symbols 不能为空", file=sys.stderr)
                  sys.exit(1)
              if len(syms) > MAX_SYMBOLS:
                  print(f"--symbols 最多 {MAX_SYMBOLS} 个标的,当前 {len(syms)} 个", file=sys.stderr)
                  sys.exit(1)
              return syms
          
          
          def build_query(symbol, symbols, interval_value, since_ts_millis, until_ts_millis, limit):
              body = {}
              if symbol is not None:
                  body["symbol"] = symbol
              if symbols is not None:
                  body["symbols"] = symbols
              if interval_value is not None:
                  body["interval_value"] = interval_value
              body["since_ts_millis"] = since_ts_millis
              body["until_ts_millis"] = until_ts_millis
              if limit is not None:
                  body["limit"] = limit
              return body
          
          
          def fetch(symbol, symbols, interval_value, since_ts_millis, until_ts_millis, limit):
              body = build_query(symbol, symbols, interval_value, since_ts_millis, until_ts_millis, limit)
              query = urllib.parse.urlencode(body, doseq=True)
              req = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}?{query}",
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询单只或批量可转债历史分钟 K 线(GET 查询参数)")
              target = parser.add_mutually_exclusive_group(required=True)
              target.add_argument("--symbol", help="单只可转债代码,如 113042.SH")
              target.add_argument("--symbols", help="可转债代码列表,逗号分隔,1~20 只,如 113042.SH,123107.SZ")
              parser.add_argument("--interval-value", dest="interval_value", type=int, default=None,
                                  choices=INTERVAL_VALUES, help="分钟周期:1/5/15,默认 1")
              parser.add_argument("--since-ts-millis", dest="since_ts_millis", required=True, type=int,
                                  help="起始时间戳(毫秒);与结束时间相差不超过 3 个北京时间自然日")
              parser.add_argument("--until-ts-millis", dest="until_ts_millis", required=True, type=int,
                                  help="结束时间戳(毫秒)")
              parser.add_argument("--limit", type=int, default=None,
                                  help="每只标的聚合后返回条数上限,范围 1~1000;省略返回窗口内全部记录")
              args = parser.parse_args()
          
              if args.since_ts_millis > args.until_ts_millis:
                  print("--since-ts-millis 不能晚于 --until-ts-millis", file=sys.stderr)
                  raise SystemExit(2)
              if args.limit is not None and not 1 <= args.limit <= 1000:
                  print("--limit 须在 1~1000 之间", file=sys.stderr)
                  raise SystemExit(2)
          
              symbols = parse_symbols(args.symbols) if args.symbols is not None else None
              data = fetch(args.symbol, symbols, args.interval_value,
                           args.since_ts_millis, args.until_ts_millis, args.limit)
              print(json.dumps(data, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
        • test_handler.py 5.4 KB
          #!/usr/bin/env python3
          """Tests for convertible-bond-minutes handler"""
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          SINCE = "1786291200000"
          UNTIL = "1786377599999"
          
          
          class TestBuildQuery(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_single_symbol_query(self):
                  body = handler.build_query("113042.SH", None, None, 1786291200000, 1786377599999, 1)
                  self.assertEqual(body["symbol"], "113042.SH")
                  self.assertNotIn("symbols", body)
                  self.assertNotIn("interval_value", body)
                  self.assertEqual(body["limit"], 1)
          
              def test_batch_symbols_query(self):
                  body = handler.build_query(None, ["113042.SH", "123107.SZ"], 5,
                                             1786291200000, 1786377599999, None)
                  self.assertNotIn("symbol", body)
                  self.assertEqual(body["symbols"], ["113042.SH", "123107.SZ"])
                  self.assertEqual(body["interval_value"], 5)
                  self.assertNotIn("limit", body)
          
              def test_rejects_too_many_symbols(self):
                  with self.assertRaises(SystemExit):
                      handler.parse_symbols(",".join(f"11304{i}.SH" for i in range(21)))
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_query_string_expands_symbols(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch(None, ["113042.SH", "123107.SZ"], 1, 1786291200000, 1786377599999, 1)
                  req = mock_open.call_args[0][0]
                  self.assertEqual(req.get_method(), "GET")
                  self.assertIn("/api/v2/market/data/convertible-bond-minute-candlesticks", req.full_url)
                  query = req.full_url.split("?", 1)[1]
                  self.assertIn("symbols=113042.SH", query)
                  self.assertIn("symbols=123107.SZ", query)
                  self.assertIn("interval_value=1", query)
                  self.assertIn("since_ts_millis=1786291200000", query)
                  self.assertEqual(req.headers.get("X-client-name"), "ft-claw")
          
              @patch.object(handler, "safe_urlopen")
              def test_single_symbol_uses_symbol_key(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("113042.SH", None, None, 1786291200000, 1786377599999, None)
                  query = mock_open.call_args[0][0].full_url.split("?", 1)[1]
                  self.assertIn("symbol=113042.SH", query)
                  self.assertNotIn("symbols=", query)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_single_symbol(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbol", "113042.SH",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          with patch("sys.stdout", new_callable=StringIO) as out:
                              handler.main()
                              self.assertEqual(json.loads(out.getvalue()), [])
          
              def test_main_requires_target(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              def test_main_rejects_symbol_and_symbols_together(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py",
                                                      "--symbol", "113042.SH",
                                                      "--symbols", "123107.SZ",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              def test_main_rejects_bad_interval_value(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbol", "113042.SH",
                                                      "--interval-value", "7",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              def test_main_rejects_limit_out_of_range(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbol", "113042.SH",
                                                      "--limit", "1001",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
          
          if __name__ == "__main__":
              unittest.main()
      • SKILL.md 4.7 KB
        ---
        name: convertible-bond-minutes
        description: 可转债历史分钟 K 线,单只或批量(convertible_bond_minute_candlesticks,GET /api/v2/market/data/convertible-bond-minute-candlesticks)。用户问可转债分钟行情、1/5/15 分钟 K 线、多只可转债分钟走势时使用。必填 --since-ts-millis、--until-ts-millis 与 --symbol / --symbols 之一;可选 --interval-value、--limit。
        ---
        
        # 可转债历史分钟K线
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 可转债历史分钟K线(convertible_bond_minute_candlesticks) |
        | 外部接口 | `GET /api/v2/market/data/convertible-bond-minute-candlesticks` |
        | 请求方式 | GET(query 参数,`symbols` 以重复参数发送) |
        | 适用场景 | 获取单只或批量可转债的分钟 K 线;同一路径通过 `symbol` / `symbols` 区分查询形式 |
        | 数据范围 | 按时间窗口返回可用历史分钟行情,窗口包含当日时可包含当日已生成分钟行情 |
        | 单次限量 | 批量最多 20 个标的;单次最多 3 个北京时间自然日;`limit` 为每只标的 1~1000 条 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 二选一 | 单只可转债代码 | 113042.SH | 与 `symbols` 不能同时传 |
        | symbols | string[] | 二选一 | 1~20 个可转债代码,逗号分隔传给 CLI | 113042.SH,123107.SZ | 接口侧以重复参数发送;也支持 JSON 字符串数组 |
        | interval_value | int | 否 | 分钟周期 | 1 | 仅支持 1、5、15;不传等同于 1 |
        | since_ts_millis | int | 是 | 起始时间戳(毫秒) | 1786291200000 | 单只、批量都必须提供;不得晚于 `until` |
        | until_ts_millis | int | 是 | 结束时间戳(毫秒) | 1786377599999 | 与起始时间相差不超过 3 个北京时间自然日 |
        | limit | int | 否 | **每只标的**聚合后返回条数上限 | 1 | 范围 1~1000;省略返回窗口内全部记录 |
        
        ## 3. 响应说明
        
        外层固定为 `code`(成功 200)/ `message`(成功 `success`)/ `data`。`data` 结构随查询形式变化:
        
        - **单只查询**(`--symbol`):`data` 直接为 K 线数组,无数据为 `[]`。
        - **批量查询**(`--symbols`):`data` 为分组对象数组;**即使只传 1 个标的也采用分组格式**,每项:
        
        | 字段名 | 类型 | 说明 |
        |--------|------|------|
        | symbol | string | 可转债代码,响应使用 `.SH`/`.SZ` 短后缀 |
        | items | array | 该标的 K 线数组,按时间升序排列 |
        | total | int | `items` 的条数,不是分页总量 |
        
        K 线字段:
        
        | 字段名 | 类型 | 说明 | 单位 |
        |--------|------|------|------|
        | open / high / low / close | string | 开/高/低/收盘价;进行中的 K 线 `close` 为最新价 | 元 |
        | ts_millis | int | K 线结束时间戳 | 毫秒 |
        | ts_millis_open | int | K 线开始时间戳 | 毫秒 |
        | turnover | string | 成交额 | 元 |
        | volume | int | 成交量 | - |
        
        注:价格与成交额在 JSON 中为字符串(避免精度丢失),不是 number;`ts_millis` 为数字。
        
        ## 4. 调用方式
        
        ```bash
        # 单只:2026-08-10 最后 1 根 1 分钟 K
        python <RUN_PY> convertible-bond-minutes --symbol 113042.SH --since-ts-millis 1786291200000 --until-ts-millis 1786377599999 --limit 1
        
        # 批量:每只最后 1 根 1 分钟 K
        python <RUN_PY> convertible-bond-minutes --symbols 113042.SH,123107.SZ --since-ts-millis 1786291200000 --until-ts-millis 1786377599999 --limit 1
        
        # 5 分钟聚合
        python <RUN_PY> convertible-bond-minutes --symbol 113042.SH --interval-value 5 --since-ts-millis 1786291200000 --until-ts-millis 1786377599999
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - `--symbol` / `--symbols` 缺省或同时传入、非可转债、缺少开始时间、时间倒序、超过 3 个自然日均会请求失败。
        - `interval_value` 仅支持 1、5、15,其它取值请求失败;`limit` 必须在 1~1000,批量超过 20 个标的请求失败(CLI 会提前拦截标的数量)。
        - 返回不复权分钟行情,接口不提供 `interval_unit` 与 `adjust_kind` 参数(CLI 也未暴露)。
        - 自然日范围包含首尾日期:周一至周三为 3 个自然日,不是按交易日计数。
        - 多分钟 K 在交易日内聚合,OHLC 分别取区间首根开盘、区间最高、区间最低、末根收盘,成交量和成交额求和。
        - 设置 `limit` 时截取最新若干根,并按时间升序返回。
        - 错误应同时检查 HTTP 状态与业务 `code`,不应仅凭 HTTP 200 判断成功。
        - 实时分钟 K 线请使用 `convertible-bond-realtime-minute-kline`;日/周/月/年 K 线请使用 `convertible-bond-candlesticks`。
    • convertible-bond-realtime-day-kline
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          """查询单只或批量可转债当前交易日实时日 K 线(GET /api/v4/market/data/convertible-bond-realtime-day-kline)"""
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v4/market/data/convertible-bond-realtime-day-kline"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          MAX_SYMBOLS = 20
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              for key, value in _REQUEST_HEADERS.items():
                  request.add_unredirected_header(key, value)
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询单只或批量可转债实时日 K 线")
              parser.add_argument("--symbols", nargs="+", required=True,
                                  help="可转债代码,空格分隔,1~20 只,如 113042.SH 123107.SZ")
              args = parser.parse_args()
          
              symbols = args.symbols
              if len(symbols) > MAX_SYMBOLS:
                  print(f"--symbols 最多 {MAX_SYMBOLS} 个标的,当前 {len(symbols)} 个", file=sys.stderr)
                  raise SystemExit(2)
          
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode({"symbols": json.dumps(symbols, ensure_ascii=False)})
              request = urllib.request.Request(url, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
      • SKILL.md 2.2 KB
        ---
        name: convertible-bond-realtime-day-kline
        description: 查询单只或批量可转债当前交易日实时日 K 线(GET /api/v4/market/data/convertible-bond-realtime-day-kline)。用户问可转债实时日 K、当下转债开高低收、多只可转债今日行情时使用。必填 --symbols,按空格分隔,1~20 只。
        ---
        
        # 可转债实时日K线
        
        查询单只或批量可转债当前交易日实时日 K 线。必填 `--symbols`,按空格分隔,1~20 只。
        
        外部接口:`GET /api/v4/market/data/convertible-bond-realtime-day-kline`。
        
        | 项目 | 说明 |
        |------|------|
        | 输入参数 | `symbols`(必填):JSON 字符串数组,例如 `["113042.SH","123107.SZ"]`;单只也必须是单元素数组 |
        | 响应结构 | `code` / `message` / `data`;`data` 为分组数组,每项含 `symbol`、`items`、`total` |
        | 每根 K 线字段 | open / high / low / close / ts_millis / ts_millis_open / turnover / volume |
        | 单次限量 | 无分页;1~20 个标的 |
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> convertible-bond-realtime-day-kline --symbols 113042.SH 123107.SZ
        ```
        
        ## 注意事项
        
        - 仅接受可转债标的,支持 `.SH` / `.XSHG`、`.SZ` / `.XSHE` 后缀;代码可被解析不代表属于接口支持的证券类别。
        - 自动选择交易日:北京时间交易日 09:00 起选择当日,09:00 前及非交易日选择上一交易日,实际返回以行情可用性为准。
        - 不提供 `symbol`、日期范围、`interval_unit`、`interval_value`、`adjust_kind`、`limit` 或分页参数;不能通过本接口指定历史日期。
        - 每个有数据的标的返回 1 条实时日 K;盘中 OHLC、成交量和成交额会继续变化,不应当作盘中已确定的最终收盘数据。
        - 无可用行情时仍返回对应标的分组,`items` 为 `[]`、`total` 为 0;停牌可能导致不返回 K 线。
        - 时间戳单位为毫秒;价格与成交额在 JSON 中为字符串,不是 number。响应中的 symbol 使用 `.SH`/`.SZ` 短后缀。
        - 历史日 K 请使用 `convertible-bond-candlesticks`,历史分钟 K 请使用 `convertible-bond-minutes`。
    • convertible-bond-realtime-minute-kline
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          """查询单只或批量可转债当前交易日实时 1 分钟 K 线(GET /api/v4/market/data/convertible-bond-realtime-minute-kline)"""
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v4/market/data/convertible-bond-realtime-minute-kline"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          MAX_SYMBOLS = 20
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              for key, value in _REQUEST_HEADERS.items():
                  request.add_unredirected_header(key, value)
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询单只或批量可转债实时分钟 K 线")
              parser.add_argument("--symbols", nargs="+", required=True,
                                  help="可转债代码,空格分隔,1~20 只,如 113042.SH 123107.SZ")
              args = parser.parse_args()
          
              symbols = args.symbols
              if len(symbols) > MAX_SYMBOLS:
                  print(f"--symbols 最多 {MAX_SYMBOLS} 个标的,当前 {len(symbols)} 个", file=sys.stderr)
                  raise SystemExit(2)
          
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode({"symbols": json.dumps(symbols, ensure_ascii=False)})
              request = urllib.request.Request(url, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
      • SKILL.md 2.3 KB
        ---
        name: convertible-bond-realtime-minute-kline
        description: 查询单只或批量可转债当前交易日实时 1 分钟 K 线(GET /api/v4/market/data/convertible-bond-realtime-minute-kline)。用户问可转债实时分钟行情、当下转债分钟走势、多只可转债盘中分钟 K 线时使用。必填 --symbols,按空格分隔,1~20 只。
        ---
        
        # 可转债实时分钟K线
        
        查询单只或批量可转债当前交易日实时 1 分钟 K 线。必填 `--symbols`,按空格分隔,1~20 只。
        
        外部接口:`GET /api/v4/market/data/convertible-bond-realtime-minute-kline`。
        
        | 项目 | 说明 |
        |------|------|
        | 输入参数 | `symbols`(必填):JSON 字符串数组,例如 `["113042.SH","123107.SZ"]`;单只也必须是单元素数组 |
        | 响应结构 | `code` / `message` / `data`;`data` 为分组数组,每项含 `symbol`、`items`、`total` |
        | 每根 K 线字段 | open / high / low / close / ts_millis / ts_millis_open / turnover / volume |
        | 单次限量 | 无分页;1~20 个标的 |
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> convertible-bond-realtime-minute-kline --symbols 113042.SH 123107.SZ
        ```
        
        ## 注意事项
        
        - 仅接受可转债标的,支持 `.SH` / `.XSHG`、`.SZ` / `.XSHE` 后缀;代码可被解析不代表属于接口支持的证券类别。
        - 自动选择交易日:北京时间交易日 09:00 起选择当日,09:00 前及非交易日选择上一交易日,实际返回以行情可用性为准。
        - 不提供 `symbol`、日期范围、`interval_unit`、`interval_value`、`adjust_kind`、`limit` 或分页参数;不能通过本接口指定历史日期。
        - 固定 1 分钟周期;结果按时间升序返回,可包含 09:30 记录,不应硬编码每日必须为 240 条。
        - 无可用行情时仍返回对应标的分组,`items` 为 `[]`、`total` 为 0;停牌可能导致不返回 K 线。
        - 时间戳单位为毫秒;价格与成交额在 JSON 中为字符串,不是 number。响应中的 symbol 使用 `.SH`/`.SZ` 短后缀。
        - `total` 是 `items` 条数,不是分页总量。本接口不提供历史翻页,历史区间请使用 `convertible-bond-minutes`。
    • convertible-bond-szse-declaration-snapshots
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/convertible-bond/szse/declaration-snapshots'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key=os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url=request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed,base=urllib.parse.urlparse(url),urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request=urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key=_require_api_key(); parser=argparse.ArgumentParser(description='深交所可转债申报快照')
              parser.add_argument("--security-code", dest="security_code")
              parser.add_argument("--trade-date", dest="trade_date", type=int)
              parser.add_argument("--page", dest="page", type=int)
              parser.add_argument("--page-size", dest="page_size", type=int)
              ns=parser.parse_args(); params={}
              for name,value in vars(ns).items():
                  if value is not None: params[name.replace("_","-") if False else name]=value
              request=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params), headers={"FTSHARE_API_KEY":key,"X-Client-Name":"ft-claw","Content-Type":"application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload=json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 663 B
        ---
        name: convertible-bond-szse-declaration-snapshots
        description: 深交所可转债申报快照。调用 /api/v1/market/data/convertible-bond/szse/declaration-snapshots,所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 深交所可转债申报快照
        
        外部接口:`GET /api/v1/market/data/convertible-bond/szse/declaration-snapshots`。
        
        支持源接口文档列出的查询参数,handler 将参数作为 query 发送,并以 JSON 输出响应。所有请求必须设置环境变量 `FTSHARE_API_KEY`;缺失凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> convertible-bond-szse-declaration-snapshots --page 1 --page-size 5
        ```
        
    • convertible-bond-szse-directed-trades
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/convertible-bond/szse/directed-trades'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key=os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url=request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed,base=urllib.parse.urlparse(url),urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request=urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key=_require_api_key(); parser=argparse.ArgumentParser(description='深交所可转债定向成交')
              parser.add_argument("--security-code", dest="security_code")
              parser.add_argument("--trade-date", dest="trade_date", type=int)
              parser.add_argument("--page", dest="page", type=int)
              parser.add_argument("--page-size", dest="page_size", type=int)
              ns=parser.parse_args(); params={}
              for name,value in vars(ns).items():
                  if value is not None: params[name.replace("_","-") if False else name]=value
              request=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params), headers={"FTSHARE_API_KEY":key,"X-Client-Name":"ft-claw","Content-Type":"application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload=json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 639 B
        ---
        name: convertible-bond-szse-directed-trades
        description: 深交所可转债定向成交。调用 /api/v1/market/data/convertible-bond/szse/directed-trades,所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 深交所可转债定向成交
        
        外部接口:`GET /api/v1/market/data/convertible-bond/szse/directed-trades`。
        
        支持源接口文档列出的查询参数,handler 将参数作为 query 发送,并以 JSON 输出响应。所有请求必须设置环境变量 `FTSHARE_API_KEY`;缺失凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> convertible-bond-szse-directed-trades --page 1 --page-size 5
        ```
        
    • convertible-bond-szse-matching-trades
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/convertible-bond/szse/matching-trades'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key=os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url=request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed,base=urllib.parse.urlparse(url),urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request=urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key=_require_api_key(); parser=argparse.ArgumentParser(description='深交所可转债匹配成交')
              parser.add_argument("--security-code", dest="security_code")
              parser.add_argument("--trade-date", dest="trade_date", type=int)
              parser.add_argument("--page", dest="page", type=int)
              parser.add_argument("--page-size", dest="page_size", type=int)
              ns=parser.parse_args(); params={}
              for name,value in vars(ns).items():
                  if value is not None: params[name.replace("_","-") if False else name]=value
              request=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params), headers={"FTSHARE_API_KEY":key,"X-Client-Name":"ft-claw","Content-Type":"application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload=json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 639 B
        ---
        name: convertible-bond-szse-matching-trades
        description: 深交所可转债匹配成交。调用 /api/v1/market/data/convertible-bond/szse/matching-trades,所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 深交所可转债匹配成交
        
        外部接口:`GET /api/v1/market/data/convertible-bond/szse/matching-trades`。
        
        支持源接口文档列出的查询参数,handler 将参数作为 query 发送,并以 JSON 输出响应。所有请求必须设置环境变量 `FTSHARE_API_KEY`;缺失凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> convertible-bond-szse-matching-trades --page 1 --page-size 5
        ```
        
    • convertible-bond-szse-negotiated-trades
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/convertible-bond/szse/negotiated-trades'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key=os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url=request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed,base=urllib.parse.urlparse(url),urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request=urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key=_require_api_key(); parser=argparse.ArgumentParser(description='深交所可转债协议成交')
              parser.add_argument("--security-code", dest="security_code")
              parser.add_argument("--trade-date", dest="trade_date", type=int)
              parser.add_argument("--page", dest="page", type=int)
              parser.add_argument("--page-size", dest="page_size", type=int)
              ns=parser.parse_args(); params={}
              for name,value in vars(ns).items():
                  if value is not None: params[name.replace("_","-") if False else name]=value
              request=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params), headers={"FTSHARE_API_KEY":key,"X-Client-Name":"ft-claw","Content-Type":"application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload=json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 647 B
        ---
        name: convertible-bond-szse-negotiated-trades
        description: 深交所可转债协议成交。调用 /api/v1/market/data/convertible-bond/szse/negotiated-trades,所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 深交所可转债协议成交
        
        外部接口:`GET /api/v1/market/data/convertible-bond/szse/negotiated-trades`。
        
        支持源接口文档列出的查询参数,handler 将参数作为 query 发送,并以 JSON 输出响应。所有请求必须设置环境变量 `FTSHARE_API_KEY`;缺失凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> convertible-bond-szse-negotiated-trades --page 1 --page-size 5
        ```
        
    • daec-history-prices
      • scripts
        • handler.py 3.1 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v4/market/data/daec/history/prices'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='标的分时数据')
              parser.add_argument("--symbol", required=True)
              parser.add_argument("--range")
              parser.add_argument("--days")
              parser.add_argument("--ts_ms")
              parser.add_argument("--price", required=False)
              parser.add_argument("--avg_price")
              parser.add_argument("--volume", required=False)
              parser.add_argument("--turnover", required=False)
              args = parser.parse_args()
              params = {}
              if args.symbol is not None: params["symbol"] = args.symbol
              if args.range is not None: params["range"] = args.range
              if args.days is not None: params["days"] = args.days
              if args.ts_ms is not None: params["ts_ms"] = args.ts_ms
              if args.ts_ms is not None: params["ts_ms"] = args.ts_ms
              if args.price is not None: params["price"] = args.price
              if args.avg_price is not None: params["avg_price"] = args.avg_price
              if args.volume is not None: params["volume"] = args.volume
              if args.turnover is not None: params["turnover"] = args.turnover
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 618 B
        ---
        name: daec-history-prices
        description: 查询标的分时数据。接口:GET /api/v4/market/data/daec/history/prices。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 标的分时数据
        
        接口:GET `/api/v4/market/data/daec/history/prices`。参数和响应以 `ftshare-doc/api-doc/股票数据/行情数据/标的分时数据.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> daec-history-prices --symbol 600519.SH
        ```
        
    • daec-market-snapshot
      • scripts
        • handler.py 3 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/daec/market/snapshot'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='市场行情快照')
              parser.add_argument("--scope")
              parser.add_argument("--status", required=False)
              parser.add_argument("--change_rate", required=False)
              parser.add_argument("--volume", required=False)
              parser.add_argument("--turnover", required=False)
              parser.add_argument("--prev_turnover")
              parser.add_argument("--distribution", required=False)
              args = parser.parse_args()
              params = {}
              if args.scope is not None: params["scope"] = args.scope
              if args.status is not None: params["status"] = args.status
              if args.change_rate is not None: params["change_rate"] = args.change_rate
              if args.volume is not None: params["volume"] = args.volume
              if args.turnover is not None: params["turnover"] = args.turnover
              if args.prev_turnover is not None: params["prev_turnover"] = args.prev_turnover
              if args.distribution is not None: params["distribution"] = args.distribution
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 603 B
        ---
        name: daec-market-snapshot
        description: 查询市场行情快照。接口:GET /api/v1/market/data/daec/market/snapshot。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 市场行情快照
        
        接口:GET `/api/v1/market/data/daec/market/snapshot`。参数和响应以 `ftshare-doc/api-doc/股票数据/行情数据/市场行情快照.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> daec-market-snapshot
        ```
        
    • daec-prev-closes
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/daec/history/prev-closes'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='标的昨收价')
              parser.add_argument("--symbol", required=True)
              parser.add_argument("--since", required=True)
              parser.add_argument("--until", required=True)
              args = parser.parse_args()
              params = {}
              if args.symbol is not None: params["symbol"] = args.symbol
              if args.since is not None: params["since"] = args.since
              if args.until is not None: params["until"] = args.until
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 649 B
        ---
        name: daec-prev-closes
        description: 查询标的昨收价。接口:GET /api/v1/market/data/daec/history/prev-closes。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 标的昨收价
        
        接口:GET `/api/v1/market/data/daec/history/prev-closes`。参数和响应以 `ftshare-doc/api-doc/股票数据/行情数据/标的昨收价.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> daec-prev-closes --symbol 600000.XSHG --since 20240501 --until 20240531
        ```
        
    • eastmoney-all-board-daily-ohlc
      • scripts
        • handler.py 2.9 KB
          #!/usr/bin/env python3
          """分页查询东方财富全部板块历史日线 OHLC,按板块代码、日期排序"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v1/market/data/eastmoney-all-board-daily-ohlc"
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def fetch_page(page: int, page_size: int, **extra) -> dict:
              params = {"page": page, "page_size": page_size}
              params.update({k: v for k, v in extra.items() if v is not None})
              qs = urllib.parse.urlencode(params)
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询东方财富全板块日线 OHLC")
              parser.add_argument("--start_date", default=None, help="起始日期(含),YYYY-MM-DD 或 YYYYMMDD")
              parser.add_argument("--end_date", default=None, help="截止日期(含),YYYY-MM-DD 或 YYYYMMDD")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始,默认 1;传 0 时按 1 处理")
              parser.add_argument("--page_size", type=int, default=50, help="每页数量,默认 50;传 0 时按 1 处理,最大 200")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量数据")
              args = parser.parse_args()
          
              extra = dict(start_date=args.start_date, end_date=args.end_date)
          
              if args.fetch_all:
                  first = fetch_page(1, args.page_size, **extra)
                  data = first.get("data") or {}
                  records = list(data.get("records", []))
                  pages = data.get("pages", 1)
                  for p in range(2, pages + 1):
                      d = fetch_page(p, args.page_size, **extra).get("data") or {}
                      records.extend(d.get("records", []))
                  result = {
                      "code": first.get("code"), "message": first.get("message"),
                      "data": {"pageNum": 1, "pageSize": args.page_size,
                               "total": data.get("total", len(records)), "pages": pages, "records": records},
                  }
              else:
                  result = fetch_page(args.page, args.page_size, **extra)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3.5 KB
        ---
        name: eastmoney-all-board-daily-ohlc
        description: 分页查询东方财富全部板块历史日线 OHLC(板块代码/名称/市场/日期/开高低收/成交量/成交额/振幅/涨跌幅/涨跌额/换手率)。用户提到「东财全板块日线」「全部板块 OHLC」「eastmoney all board daily ohlc」时使用。结果按板块代码、日期排序,分页返回;起止日期同时给定时跨度不得超过 3 个自然日,page_size 最大 200,支持 --all 翻页。
        ---
        
        # 查询东方财富全板块日线 OHLC
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询东方财富全板块日线 OHLC |
        | 外部接口 | GET /api/v1/market/data/eastmoney-all-board-daily-ohlc |
        | 请求方式 | GET |
        | 适用场景 | 获取东方财富全部板块的历史日线 OHLC,结果按板块代码、日期排序并分页返回 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | start_date | string | 否 | 起始日期(含) | `20251127` | YYYY-MM-DD 或 YYYYMMDD |
        | end_date | string | 否 | 截止日期(含) | `2025-11-28` | YYYY-MM-DD 或 YYYYMMDD |
        | page | uint | 否 | 页码 | `1` | 从 1 开始,默认 1;传 0 时按 1 处理 |
        | page_size | uint | 否 | 每页数量 | `50` | 默认 50;传 0 时按 1 处理,最大 200 |
        
        > `start_date` 与 `end_date` 同时给出时,结束日期与开始日期相差不得超过 3 个自然日。
        
        ## 执行方式
        
        ```bash
        # 取一页
        python <RUN_PY> eastmoney-all-board-daily-ohlc --start_date 2025-11-27 --end_date 2025-11-28 --page 1 --page_size 1
        # 翻全量
        python <RUN_PY> eastmoney-all-board-daily-ohlc --all
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        外层 `code/message/data`,分页数据位于 `data.records`。
        
        ```json
        {
          "code": 200,
          "message": "success",
          "data": {
            "pageNum": 1, "pageSize": 1, "total": 3, "pages": 3,
            "records": [
              {
                "板块代码": "BK1024",
                "板块名称": "绿色电力",
                "市场": "90",
                "日期": "2025-11-27",
                "开盘": "1001.53",
                "收盘": "1037.67",
                "最高": "1041.82",
                "最低": "1001.53",
                "成交量": "52669555",
                "成交额": "43012845568.00",
                "振幅": "4.03",
                "涨跌幅": "3.77",
                "涨跌额": "37.67",
                "换手率": "1.03"
              }
            ]
          }
        }
        ```
        
        ### data 字段
        
        | 字段 | 类型 | 说明 |
        |------|------|------|
        | pageNum | int | 当前页码 |
        | pageSize | int | 每页数量 |
        | total | int | 命中总记录数 |
        | pages | int | 总页数 |
        | records | array | 当前页行情记录 |
        
        ### records 元素字段(中文键)
        
        | 字段 | 类型 | 说明 |
        |------|------|------|
        | 板块代码 | string | 板块代码 |
        | 板块名称 | string | 板块名称 |
        | 市场 | string | 东方财富市场代码 |
        | 日期 | string | 日期,YYYY-MM-DD |
        | 开盘 / 收盘 / 最高 / 最低 | string | 开 / 收 / 高 / 低 |
        | 成交量 / 成交额 | string | 成交量 / 成交额 |
        | 振幅 / 涨跌幅 / 涨跌额 / 换手率 | string | 振幅(%) / 涨跌幅(%) / 涨跌额 / 换手率(%) |
        
        ## 注意事项
        
        - 数据范围 2013-04-09 至今,不同板块的起始日期可能不同。
        - `start_date`、`end_date` 均可选;仅在二者同时传入时校验 3 个自然日的跨度。
        - `page`、`page_size` 必须是非负整数;值为 0 时按 1 处理,`page_size` 大于 200 时按 200 处理。
        - 行情记录的字段名及数值均按字符串返回;**字段名为中文**。
        
    • eastmoney-board-constituents
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          """查询东财板块成分股"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/eastmoney-board-constituents"
          
          
          def fetch(
          board_code: str) -> dict:
              params = urllib.parse.urlencode({"board_code": board_code})
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询东财板块成分股")
              parser.add_argument("--board_code", required=True, help="板块代码(BK 前缀),如 BK1024")
              args = parser.parse_args()
          
              result = fetch(args.board_code)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.1 KB
        ---
        name: eastmoney-board-constituents
        description: "查询东财板块成分股。当用户需要查询指定东财板块的全部成分股代码和名称,或了解东财板块成分股时使用。"
        ---
        
        # 查询东财板块成分股
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 查询东财板块成分股 |
        | 外部接口 | `/api/v1/market/data/eastmoney-board-constituents` |
        | 请求方式 | GET |
        | 适用场景 | 查询指定东财板块的全部成分股代码和名称 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |---|---|---|---|---|---|
        | `board_code` | string | 是 | 板块代码 | `BK1024` | BK 前缀,可通过 `eastmoney-concept-boards` 或 `eastmoney-board-latest-ohlc` 获取 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        # 查询绿色电力板块成分股
        python <RUN_PY> eastmoney-board-constituents --board_code BK1024
        
        # 查询工程建设板块成分股
        python <RUN_PY> eastmoney-board-constituents --board_code BK0425
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        ```json
        {
            "board_code": "BK1024",
            "board_name": "绿色电力",
            "constituents": [
                {
                    "stock_code": "600905",
                    "stock_name": "三峡能源"
                },
                {
                    "stock_code": "601016",
                    "stock_name": "节能风电"
                }
            ]
        }
        ```
        
        ### 字段说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |---|---|---|---|
        | `board_code` | String | 否 | 板块代码 |
        | `board_name` | String | 否 | 板块名称 |
        | `constituents` | Array | 否 | 成分股列表 |
        
        ### constituents 元素字段说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |---|---|---|---|
        | `stock_code` | String | 否 | 股票代码(不含市场后缀) |
        | `stock_name` | String | 否 | 股票名称 |
        
        ## 注意事项
        
        - 接口返回指定板块的全量成分股,无分页
        - `stock_code` 不含市场后缀(如 `600905` 而非 `600905.SH`)
        - 板块代码对行业板块和概念板块通用
        
    • eastmoney-board-daily-ohlc
      • scripts
        • handler.py 3.8 KB
          #!/usr/bin/env python3
          """查询东财单板块历史 OHLC 数据,支持日期范围过滤与分页"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/eastmoney-board-daily-ohlc"
          
          
          def fetch_page(board_code: str, page: int, page_size: int, start_date: str = None, end_date: str = None) -> dict:
              params = {"board_code": board_code, "page": page, "page_size": page_size}
              if start_date:
                  params["start_date"] = start_date
              if end_date:
                  params["end_date"] = end_date
              qs = urllib.parse.urlencode(params)
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询东财单板块历史 OHLC 数据")
              parser.add_argument("--board_code", required=True, help="板块代码,如 BK1024")
              parser.add_argument("--start_date", default=None, help="起始日期(含),格式 YYYY-MM-DD 或 YYYYMMDD")
              parser.add_argument("--end_date", default=None, help="截止日期(含),格式 YYYY-MM-DD 或 YYYYMMDD")
              parser.add_argument("--page", type=int, default=1, help="页码(从 1 开始)")
              parser.add_argument("--page_size", type=int, default=50, help="每页记录数")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量数据")
              args = parser.parse_args()
          
              if args.fetch_all:
                  first = fetch_page(args.board_code, 1, args.page_size, args.start_date, args.end_date)
                  all_items = list(first.get("items", []))
                  total_pages = first.get("total_pages", 1)
                  for p in range(2, total_pages + 1):
                      page_data = fetch_page(args.board_code, p, args.page_size, args.start_date, args.end_date)
                      all_items.extend(page_data.get("items", []))
                  result = {
                      "items": all_items,
                      "total_pages": total_pages,
                      "total_items": first.get("total_items", len(all_items)),
                  }
              else:
                  result = fetch_page(args.board_code, args.page, args.page_size, args.start_date, args.end_date)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3.5 KB
        ---
        name: eastmoney-board-daily-ohlc
        description: "查询东财单板块历史 OHLC。当用户需要查询指定东财板块历史 OHLC 数据,支持日期范围过滤与分页,或了解东财单板块历史 OHLC时使用。"
        ---
        
        # 查询东财单板块历史 OHLC
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 查询东财单板块历史 OHLC |
        | 外部接口 | `/api/v1/market/data/eastmoney-board-daily-ohlc` |
        | 请求方式 | GET |
        | 适用场景 | 查询指定东财板块历史 OHLC 数据,支持日期范围过滤与分页 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |---|---|---|---|---|---|
        | `board_code` | string | 是 | 板块代码 | `BK1024` | BK 前缀 |
        | `start_date` | string | 否 | 起始日期(含) | `2021-01-01` | 格式 `YYYY-MM-DD` 或 `YYYYMMDD`;不传则从最早开始 |
        | `end_date` | string | 否 | 截止日期(含) | `2021-12-31` | 格式 `YYYY-MM-DD` 或 `YYYYMMDD`;不传则到最晚为止 |
        | `page` | int | 否 | 页码,从 1 开始 | `1` | 默认 1 |
        | `page_size` | int | 否 | 每页记录数 | `20` | 默认 50 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        # 查询板块全部历史 OHLC
        python <RUN_PY> eastmoney-board-daily-ohlc --board_code BK1024 --page 1 --page_size 20
        
        # 指定日期范围
        python <RUN_PY> eastmoney-board-daily-ohlc --board_code BK1024 --start_date 2021-01-01 --end_date 2021-12-31 --page 1 --page_size 20
        
        # 自动翻页获取全量数据
        python <RUN_PY> eastmoney-board-daily-ohlc --board_code BK1024 --all
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        ```json
        {
            "items": [
                {
                    "board_code": "BK1024",
                    "board_name": "绿色电力",
                    "market": "90",
                    "date": "2021-10-18",
                    "open": "1001.53",
                    "close": "1037.67",
                    "high": "1041.82",
                    "low": "1001.53",
                    "volume": "52669555",
                    "turnover": "43012845568",
                    "amplitude": "4.03",
                    "change_rate": "3.77",
                    "change": "37.67",
                    "turnover_rate": "1.03"
                }
            ],
            "total_pages": 1,
            "total_items": 1
        }
        ```
        
        ### 顶层字段说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |---|---|---|---|
        | `items` | Array | 否 | 当前页 OHLC 数据列表 |
        | `total_pages` | int | 否 | 总页数 |
        | `total_items` | int | 否 | 总记录数 |
        
        ### items 元素字段说明(BoardDailyOhlc)
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |---|---|---|---|---|
        | `board_code` | String | 否 | 板块代码 | - |
        | `board_name` | String | 否 | 板块名称 | - |
        | `market` | String | 否 | 市场编码 | - |
        | `date` | String | 否 | 交易日期,格式 `YYYY-MM-DD` | - |
        | `open` | String | 否 | 开盘价 | - |
        | `close` | String | 否 | 收盘价 | - |
        | `high` | String | 否 | 最高价 | - |
        | `low` | String | 否 | 最低价 | - |
        | `volume` | String | 否 | 成交量 | 股 |
        | `turnover` | String | 否 | 成交额 | 元 |
        | `amplitude` | String | 否 | 振幅 | % |
        | `change_rate` | String | 否 | 涨跌幅 | % |
        | `change` | String | 否 | 涨跌额 | - |
        | `turnover_rate` | String | 否 | 换手率 | % |
        
        ## 注意事项
        
        - `board_code` 为必填参数,可通过 `eastmoney-concept-boards` 获取板块代码
        - 历史 OHLC 中多数数值字段以字符串返回
        - 板块代码对行业板块和概念板块通用
        
    • eastmoney-concept-boards
      • scripts
        • handler.py 2.1 KB
          #!/usr/bin/env python3
          """查询东财概念板块列表,返回全量板块基础信息"""
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/eastmoney-concept-boards"
          
          
          def fetch():
              url = f"{BASE_URL}{ENDPOINT}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              result = fetch()
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.6 KB
        ---
        name: eastmoney-concept-boards
        description: "查询东财概念板块列表。当用户需要查询东财概念板块基础信息与成分代码列表,返回全量板块,或了解东财概念板块列表时使用。"
        ---
        
        # 查询东财概念板块列表
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 查询东财概念板块列表 |
        | 外部接口 | `/api/v1/market/data/eastmoney-concept-boards` |
        | 请求方式 | GET |
        | 适用场景 | 查询东财概念板块基础信息与成分代码列表,返回全量板块 |
        
        ## 请求参数
        
        无需任何参数,接口返回全量概念板块列表。
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> eastmoney-concept-boards
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        返回值为概念板块数组(直接返回数组,非对象包装):
        
        ```json
        [
            {
                "code": "BK1024",
                "name": "绿色电力",
                "constituents": ["600089", "600905", "000591"]
            }
        ]
        ```
        
        ### 字段说明(ConceptBoard)
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |---|---|---|---|
        | `code` | String | 否 | 板块代码(BK 前缀) |
        | `name` | String | 否 | 板块名称 |
        | `constituents` | Array[String] | 否 | 成分标的代码列表 |
        
        ## 注意事项
        
        - 接口直接返回数组,无分页包装结构
        - 板块代码以 `BK` 开头,可用于 `eastmoney-board-daily-ohlc` 和 `eastmoney-board-latest-ohlc` 查询K线
        - 成分代码不含市场后缀(如 `600089` 而非 `600089.SH`)
        
    • eastmoney-dapan-flow
      • scripts
        • handler.py 4.7 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/eastmoney-dapan-flow'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='东方财富大盘资金流')
              parser.add_argument("--trade_date")
              parser.add_argument("--start_date")
              parser.add_argument("--end_date")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--total", required=False)
              parser.add_argument("--sh_close", required=False)
              parser.add_argument("--sh_change_pct", required=False)
              parser.add_argument("--sz_close", required=False)
              parser.add_argument("--sz_change_pct", required=False)
              parser.add_argument("--main_net", required=False)
              parser.add_argument("--main_pct", required=False)
              parser.add_argument("--xlarge_net", required=False)
              parser.add_argument("--xlarge_pct", required=False)
              parser.add_argument("--large_net", required=False)
              parser.add_argument("--large_pct", required=False)
              parser.add_argument("--mid_net", required=False)
              parser.add_argument("--mid_pct", required=False)
              parser.add_argument("--small_net", required=False)
              parser.add_argument("--small_pct", required=False)
              parser.add_argument("--name", required=True)
              args = parser.parse_args()
              params = {}
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.sh_close is not None: params["sh_close"] = args.sh_close
              if args.sh_change_pct is not None: params["sh_change_pct"] = args.sh_change_pct
              if args.sz_close is not None: params["sz_close"] = args.sz_close
              if args.sz_change_pct is not None: params["sz_change_pct"] = args.sz_change_pct
              if args.main_net is not None: params["main_net"] = args.main_net
              if args.main_pct is not None: params["main_pct"] = args.main_pct
              if args.xlarge_net is not None: params["xlarge_net"] = args.xlarge_net
              if args.xlarge_pct is not None: params["xlarge_pct"] = args.xlarge_pct
              if args.large_net is not None: params["large_net"] = args.large_net
              if args.large_pct is not None: params["large_pct"] = args.large_pct
              if args.mid_net is not None: params["mid_net"] = args.mid_net
              if args.mid_pct is not None: params["mid_pct"] = args.mid_pct
              if args.small_net is not None: params["small_net"] = args.small_net
              if args.small_pct is not None: params["small_pct"] = args.small_pct
              if args.name is not None: params["name"] = args.name
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 654 B
        ---
        name: eastmoney-dapan-flow
        description: 查询东方财富大盘资金流。接口:GET /api/v1/market/data/eastmoney-dapan-flow。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 东方财富大盘资金流
        
        接口:GET `/api/v1/market/data/eastmoney-dapan-flow`。参数和响应以 `ftshare-doc/api-doc/股票数据/资金流向数据/东方财富大盘资金流.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> eastmoney-dapan-flow --page 1 --name 1
        ```
        
    • eastmoney-futures-position
      • scripts
        • handler.py 2.7 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/eastmoney-futures-position'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key=os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url=request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed,base=urllib.parse.urlparse(url),urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request=urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key=_require_api_key(); parser=argparse.ArgumentParser(description='东方财富期货持仓')
              parser.add_argument("--exchange", dest="exchange")
              parser.add_argument("--variety-code", dest="variety_code")
              parser.add_argument("--contract-code", dest="contract_code")
              parser.add_argument("--trade-date", dest="trade_date", type=int)
              parser.add_argument("--start-date", dest="start_date", type=int)
              parser.add_argument("--end-date", dest="end_date", type=int)
              parser.add_argument("--member-name-abbr", dest="member_name_abbr")
              parser.add_argument("--page", dest="page", type=int)
              parser.add_argument("--page-size", dest="page_size", type=int)
              ns=parser.parse_args(); params={}
              for name,value in vars(ns).items():
                  if value is not None: params[name.replace("_","-") if False else name]=value
              request=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params), headers={"FTSHARE_API_KEY":key,"X-Client-Name":"ft-claw","Content-Type":"application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload=json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 583 B
        ---
        name: eastmoney-futures-position
        description: 东方财富期货持仓。调用 /api/v1/market/data/eastmoney-futures-position,所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 东方财富期货持仓
        
        外部接口:`GET /api/v1/market/data/eastmoney-futures-position`。
        
        支持源接口文档列出的查询参数,handler 将参数作为 query 发送,并以 JSON 输出响应。所有请求必须设置环境变量 `FTSHARE_API_KEY`;缺失凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> eastmoney-futures-position --page 1 --page-size 5
        ```
        
    • eastmoney-futures-strange
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """查询东方财富期货龙虎榜面板(成交量/持仓量多空排名等),按交易所/品种/合约/交易日"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v1/market/data/eastmoney-futures-strange"
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询东方财富期货龙虎榜面板")
              parser.add_argument("--exchange", required=True, help="交易所代码:shfe/dce/czce/cffex/ine/gfe")
              parser.add_argument("--variety", required=True, help="品种名称,如 多晶硅")
              parser.add_argument("--contract", required=True, help="合约代码,如 ps2609")
              parser.add_argument("--trade_date", required=True, help="交易日 YYYYMMDD")
              args = parser.parse_args()
          
              params = {
                  "exchange": args.exchange,
                  "variety": args.variety,
                  "contract": args.contract,
                  "trade_date": args.trade_date,
              }
              qs = urllib.parse.urlencode(params)
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.6 KB
        ---
        name: eastmoney-futures-strange
        description: 查询东方财富期货龙虎榜面板(成交量/持仓量多空排名等)。用户提到「东财期货龙虎榜」「期货龙虎榜面板」「期货会员排名」「eastmoney futures strange」时使用。按交易所/品种/合约/交易日返回完整面板数组,不分页。
        ---
        
        # 查询东方财富期货龙虎榜
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询东方财富期货龙虎榜 |
        | 外部接口 | GET /api/v1/market/data/eastmoney-futures-strange |
        | 请求方式 | GET |
        | 适用场景 | 查询指定交易所、品种、合约和交易日的东方财富期货龙虎榜面板,包括占比切片、会员排名和汇总数据 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | exchange | string | 是 | 交易所代码 | `dce` | `shfe`/`dce`/`czce`/`cffex`/`ine`/`gfe` |
        | variety | string | 是 | 品种名称 | `多晶硅` | 中文名称,URL 编码后传入 |
        | contract | string | 是 | 合约代码 | `ps2609` | 小写合约代码 |
        | trade_date | string | 是 | 交易日 | `20260721` | `YYYYMMDD` |
        
        ## 执行方式
        
        ```bash
        python <RUN_PY> eastmoney-futures-strange --exchange dce --variety 多晶硅 --contract ps2609 --trade_date 20260721
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        返回统一结构 `code/message/data`,`data` 为面板数组(不分页)。
        
        ```json
        {
          "code": 200,
          "message": "success",
          "data": [
            {
              "key": "volume_rank",
              "title": "成交量龙虎榜",
              "slices": null,
              "rows": [{"rank": 1, "member_name": "东证期货(代客)", "value": 24952, "change": -4961}],
              "summary": {"today_total": 175902, "previous_total": 218790, "total_change": -42888}
            }
          ]
        }
        ```
        
        ### data 元素字段
        
        | 字段 | 类型 | 是否可为空 | 说明 |
        |------|------|------------|------|
        | key | string | 否 | 面板标识 |
        | title | string | 否 | 面板标题 |
        | slices | array/null | 是 | 饼图切片;每项含 `name`/`value`/`percent` |
        | rows | array/null | 是 | 排名记录;每项含 `rank`/`member_id`/`member_name`/`value`/`change` |
        | summary | object/null | 是 | 汇总;含 `today_total`/`previous_total`/`total_change` |
        
        ## 注意事项
        
        - 所有参数均为必填,缺一返回 400。
        - 单次返回该合约对应的完整面板数组,不分页。
        - `variety` 为中文品种名称,调用时通过 URL 编码传入。
        - HTTP 恒为 200,业务错误通过 `code`/`message` 携带。
        
    • eastmoney-market-valuation
      • scripts
        • handler.py 4.5 KB
          #!/usr/bin/env python3
          """查询东财市场日估值数据(主要市场指数 PE/市值/点位),支持单市场、单日、区间查询与分页"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/eastmoney-market-valuation"
          
          
          def fetch(
          market_code: str = None, trade_date: str = None, start_date: str = None,
                    end_date: str = None, page: int = 1, page_size: int = 50) -> dict:
              params = {"page": page, "page_size": page_size}
              if market_code:
                  params["market_code"] = market_code
              if trade_date:
                  params["trade_date"] = trade_date
              if start_date:
                  params["start_date"] = start_date
              if end_date:
                  params["end_date"] = end_date
              qs = urllib.parse.urlencode(params)
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询东财市场日估值数据")
              parser.add_argument("--market_code", default=None, help="市场代码,如 000001=上证指数, 000300=沪深300, 399001=深证成指, 399006=创业板指, 000688=科创50, 899050=北证50")
              parser.add_argument("--trade_date", default=None, help="交易日,格式 YYYY-MM-DD")
              parser.add_argument("--start_date", default=None, help="区间起始日,格式 YYYY-MM-DD")
              parser.add_argument("--end_date", default=None, help="区间结束日,格式 YYYY-MM-DD")
              parser.add_argument("--page", type=int, default=1, help="页码(从 1 开始,默认 1)")
              parser.add_argument("--page_size", type=int, default=50, help="每页条数(默认 50,最大 500)")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量数据")
              args = parser.parse_args()
          
              if args.fetch_all:
                  first = fetch(args.market_code, args.trade_date, args.start_date, args.end_date, 1, args.page_size)
                  data = first.get("data") or {}
                  records = list(data.get("records", []))
                  total_pages = data.get("pages", 1)
                  for p in range(2, total_pages + 1):
                      page_data = fetch(args.market_code, args.trade_date, args.start_date, args.end_date, p, args.page_size)
                      pdata = page_data.get("data") or {}
                      records.extend(pdata.get("records", []))
                  result = {
                      "code": first.get("code", 0),
                      "message": first.get("message", "success"),
                      "data": {
                          "pageNum": 1,
                          "pageSize": args.page_size,
                          "total": len(records),
                          "pages": 1,
                          "records": records,
                      },
                  }
              else:
                  result = fetch(args.market_code, args.trade_date, args.start_date, args.end_date, args.page, args.page_size)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 5.1 KB
        ---
        name: eastmoney-market-valuation
        description: "查询东财市场日估值。当用户需要查询 A 股主要市场指数(上证指数、沪深300、深证成指、创业板指、科创50、北证50)的每日估值数据,包括市盈率、总市值、流通市值、收盘点位等;支持单日、区间查询,或了解东财市场日估值时使用。"
        ---
        
        # 查询东财市场日估值
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 查询东财市场日估值 |
        | 外部接口 | `/api/v1/market/data/eastmoney-market-valuation` |
        | 请求方式 | GET |
        | 适用场景 | 查询 A 股主要市场指数(上证指数、沪深300、深证成指、创业板指、科创50、北证50)的每日估值数据,包括市盈率、总市值、流通市值、收盘点位等;支持单日、区间查询 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | `market_code` | string | 否 | 市场代码 | `000300` | 000001=上证指数, 000300=沪深300, 399001=深证成指, 399006=创业板指, 000688=科创50, 899050=北证50 |
        | `trade_date` | string | 否 | 交易日 | `2026-06-08` | 格式 `YYYY-MM-DD`;与 `start_date`/`end_date` 互斥 |
        | `start_date` | string | 否 | 区间起始日 | `2026-06-01` | 格式 `YYYY-MM-DD`;与 `end_date` 同时提供 |
        | `end_date` | string | 否 | 区间结束日 | `2026-06-09` | 格式 `YYYY-MM-DD`;与 `start_date` 同时提供 |
        | `--page` | int | 否 | 页码 | `1` | 从 1 开始,默认 1 |
        | `--page_size` | int | 否 | 每页条数 | `50` | 默认 50,最大 500 |
        
        ### 查询模式
        
        | 模式 | 参数 | 说明 |
        |------|------|------|
        | 全量查询 | 无 | 返回全部市场全部日期估值 |
        | 单市场全部历史 | `--market_code` | 返回指定市场全部历史估值 |
        | 单日全市场 | `--trade_date` | 返回指定交易日全部市场估值 |
        | 单市场单日 | `--market_code --trade_date` | 返回指定市场指定交易日估值 |
        | 单市场区间 | `--market_code --start_date --end_date` | 返回指定市场日期区间估值 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用:
        
        ```bash
        python <RUN_PY> eastmoney-market-valuation --market_code 000300 --page 1 --page_size 10
        python <RUN_PY> eastmoney-market-valuation --market_code 000001 --start_date 2026-06-01 --end_date 2026-06-09
        python <RUN_PY> eastmoney-market-valuation --start_date 2026-06-01 --end_date 2026-06-09 --page 1 --page_size 50
        python <RUN_PY> eastmoney-market-valuation --market_code 000300 --trade_date 2026-06-08
        python <RUN_PY> eastmoney-market-valuation --start_date 2026-06-01 --end_date 2026-06-09 --all
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        顶层为分页包装对象:
        
        | 字段名 | 类型 | 描述 |
        |--------|------|------|
        | `code` | int | 业务状态码,成功为 `0` |
        | `message` | string | 业务消息 |
        | `data` | object/null | 分页数据体;无数据时可能为 `null` |
        | `data.pageNum` | int | 当前页码 |
        | `data.pageSize` | int | 每页条数 |
        | `data.total` | int | 总记录数 |
        | `data.pages` | int | 总页数 |
        | `data.records` | array | 当前页记录列表 |
        
        `data.records` 中每项字段:
        
        | 字段名 | 类型 | 描述 |
        |--------|------|------|
        | `market_code` | string | 市场代码,如 `000001`=上证指数 |
        | `market_name` | string | 市场名称,如 `上证指数` |
        | `trade_date` | string | 交易日期,格式 `YYYY-MM-DD` |
        | `pe_ttm` | string | 市盈率 TTM,无数据时为空 |
        | `total_shares` | string | 总股本,无数据时为空 |
        | `free_shares` | string | 流通股本,无数据时为空 |
        | `trade_market_value` | string | 市场总市值(元),无数据时为空 |
        | `free_market_cap` | string | 流通市值(元),无数据时为空 |
        | `listing_org_num` | string | 上市公司家数,无数据时为空 |
        | `close_price` | string | 收盘点位,无数据时为空 |
        | `change_rate` | string | 涨跌幅(%),无数据时为空 |
        
        ### 响应示例
        
        ```json
        {
          "code": 0,
          "message": "success",
          "data": {
            "pageNum": 1,
            "pageSize": 3,
            "total": 36,
            "pages": 12,
            "records": [
              {
                "market_code": "000001",
                "market_name": "上证指数",
                "trade_date": "2026-06-08",
                "pe_ttm": "13.95",
                "total_shares": "484989600",
                "free_shares": "462381100",
                "trade_market_value": "5276746800",
                "free_market_cap": "5065018100",
                "listing_org_num": "1708",
                "close_price": "3959.3378",
                "change_rate": "-1.6982"
              }
            ]
          }
        }
        ```
        
        ## 注意事项
        
        - 覆盖 6 个 A 股主要市场指数
        - 市场代码参考:上证指数(000001)、沪深300(000300)、深证成指(399001)、创业板指(399006)、科创50(000688)、北证50(899050)
        - 所有数值字段序列化为字符串,避免 JSON 浮点精度问题
        - 无数据的字段输出为空字符串 `""`,而非 `null`
        - 推荐使用 `YYYY-MM-DD` 日期格式;`YYYYMMDD` 会返回空列表
        - 当前单日查询可能异常,建议优先使用区间查询
        
    • eastmoney-rank
      • scripts
        • handler.py 4.2 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/eastmoney-rank'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='东方财富股票排名')
              parser.add_argument("--rank_group")
              parser.add_argument("--market")
              parser.add_argument("--trade_date")
              parser.add_argument("--display_name", required=True)
              parser.add_argument("--metric_name", required=True)
              parser.add_argument("--total", required=False)
              parser.add_argument("--items", required=False)
              parser.add_argument("--rank_no", required=True)
              parser.add_argument("--rank_change", required=False)
              parser.add_argument("--normalized_symbol", required=False)
              parser.add_argument("--stock_name", required=True)
              parser.add_argument("--hot_score", required=False)
              parser.add_argument("--latest_price", required=False)
              parser.add_argument("--change_amount", required=False)
              parser.add_argument("--change_pct", required=False)
              parser.add_argument("--raw_symbol", required=False)
              args = parser.parse_args()
              params = {}
              if args.rank_group is not None: params["rank_group"] = args.rank_group
              if args.market is not None: params["market"] = args.market
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.display_name is not None: params["display_name"] = args.display_name
              if args.metric_name is not None: params["metric_name"] = args.metric_name
              if args.total is not None: params["total"] = args.total
              if args.items is not None: params["items"] = args.items
              if args.rank_no is not None: params["rank_no"] = args.rank_no
              if args.rank_change is not None: params["rank_change"] = args.rank_change
              if args.normalized_symbol is not None: params["normalized_symbol"] = args.normalized_symbol
              if args.stock_name is not None: params["stock_name"] = args.stock_name
              if args.hot_score is not None: params["hot_score"] = args.hot_score
              if args.latest_price is not None: params["latest_price"] = args.latest_price
              if args.change_amount is not None: params["change_amount"] = args.change_amount
              if args.change_pct is not None: params["change_pct"] = args.change_pct
              if args.raw_symbol is not None: params["raw_symbol"] = args.raw_symbol
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 657 B
        ---
        name: eastmoney-rank
        description: 查询东方财富股票排名。接口:GET /api/v1/market/data/eastmoney-rank。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 东方财富股票排名
        
        接口:GET `/api/v1/market/data/eastmoney-rank`。参数和响应以 `ftshare-doc/api-doc/股票数据/行情数据/东方财富股票排名.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> eastmoney-rank --display_name 1 --metric_name 1 --rank_no 1 --stock_name 1
        ```
        
    • eastmoney-sector-flow
      • scripts
        • handler.py 4.4 KB
          #!/usr/bin/env python3
          """东方财富板块资金流(GET /api/v1/market/data/eastmoney-sector-flow)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/eastmoney-sector-flow"
          
          BOARD_TYPES = ("industry", "concept", "regional")
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def build_params(args):
              params = {}
              if args.board_code is not None:
                  params["board_code"] = args.board_code
              if args.board_type is not None:
                  params["board_type"] = args.board_type
              if args.board_level is not None:
                  params["board_level"] = args.board_level
              if args.trade_date is not None:
                  params["trade_date"] = args.trade_date
              if args.start_date is not None:
                  params["start_date"] = args.start_date
              if args.end_date is not None:
                  params["end_date"] = args.end_date
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
              return params
          
          
          def fetch(params):
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              req = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}{query}",
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="东方财富板块(行业/概念/地域)日资金流")
              parser.add_argument("--board-code", dest="board_code", default=None,
                                  help="板块代码,如 BK0488")
              parser.add_argument("--board-type", dest="board_type", default=None, choices=BOARD_TYPES,
                                  help="板块类型:industry(行业)/concept(概念)/regional(地域)")
              parser.add_argument("--board-level", dest="board_level", type=int, default=None,
                                  help="行业层级:1=一级、2=二级、3=三级;不传返回全部层级,仅匹配 industry")
              parser.add_argument("--trade-date", dest="trade_date", default=None, help="交易日 YYYYMMDD")
              parser.add_argument("--start-date", dest="start_date", default=None, help="区间起始日 YYYYMMDD")
              parser.add_argument("--end-date", dest="end_date", default=None, help="区间结束日 YYYYMMDD")
              parser.add_argument("--page", type=int, default=None, help="页码,从 1 开始,默认 1")
              parser.add_argument("--page-size", dest="page_size", type=int, default=None,
                                  help="每页条数,默认 50,最大 500")
              args = parser.parse_args()
          
              print(json.dumps(fetch(build_params(args)), ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.9 KB
          #!/usr/bin/env python3
          """Tests for eastmoney-sector-flow handler (board_* parameter rename)"""
          import os
          import sys
          import unittest
          from unittest.mock import patch
          import importlib.util
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          def _run(argv):
              with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                  with patch.object(sys, "argv", ["handler.py"] + argv):
                      handler.main()
          
          
          class TestBoardParams(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_board_params_sent(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"{}"
                  _run(["--board-code", "BK0488", "--board-type", "industry",
                        "--board-level", "2", "--page", "1", "--page-size", "5"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/eastmoney-sector-flow", req.full_url)
                  self.assertIn("board_code=BK0488", req.full_url)
                  self.assertIn("board_type=industry", req.full_url)
                  self.assertIn("board_level=2", req.full_url)
                  self.assertNotIn("sector_code", req.full_url)
                  self.assertNotIn("sector_type", req.full_url)
                  self.assertNotIn("sector_level", req.full_url)
          
              def test_rejects_unknown_board_type(self):
                  with self.assertRaises(SystemExit):
                      _run(["--board-type", "unknown"])
          
              @patch.object(handler, "safe_urlopen")
              def test_all_params_optional(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"{}"
                  _run([])
                  req = mock_open.call_args[0][0]
                  self.assertTrue(req.full_url.endswith("/api/v1/market/data/eastmoney-sector-flow"))
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 2.9 KB
        ---
        name: eastmoney-sector-flow
        description: 查询东方财富板块资金流(行业/概念/地域日资金流)。用户问东财板块资金流、板块主力净流入、行业/概念/地域资金流向、BK 板块代码资金流、超大单大单净流入时使用。
        ---
        
        # 东方财富板块资金流
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 东方财富板块资金流(get_eastmoney_sector_flow) |
        | 外部接口 | `GET /api/v1/market/data/eastmoney-sector-flow` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 获取东方财富板块(行业/概念/地域)日资金流,含主力/超大单/大单/中单/小单净流入及净占比;支持按板块代码、板块类型、行业层级、交易日或日期区间过滤 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | board_code | string | 否 | 板块代码 | BK0488 | 东财板块代码,BK 开头 |
        | board_type | string | 否 | 板块类型 | industry | industry(行业)/ concept(概念)/ regional(地域) |
        | board_level | int | 否 | 行业层级 | 2 | 1=一级、2=二级、3=三级;不传返回全部层级,仅匹配 industry |
        | trade_date | string | 否 | 交易日 | 20260623 | YYYYMMDD |
        | start_date | string | 否 | 区间起始日 | 20260601 | YYYYMMDD |
        | end_date | string | 否 | 区间结束日 | 20260630 | YYYYMMDD |
        | page | int | 否 | 页码 | 1 | 从 1 开始,默认 1 |
        | page_size | int | 否 | 每页条数 | 50 | 默认 50,最大 500 |
        
        ## 3. 响应说明
        
        外层固定为 `code` / `message` / `data`。`data` 为分页对象:`pageNum` / `pageSize` / `total` / `pages` / `records`。
        
        records 元素核心字段:`board_code`、`board_name`、`board_type`(industry/concept/regional)、`board_level`(industry 为 1/2/3,concept/regional 为 0)、`trade_date`(YYYYMMDD),以及 `main_net`/`main_pct`、`super_large_net`/`super_large_pct`、`large_net`/`large_pct`、`medium_net`/`medium_pct`、`small_net`/`small_pct`(净流入单位亿元,净占比单位 %)。
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> eastmoney-sector-flow --board-type industry --board-level 2 --page 1 --page-size 5
        python <RUN_PY> eastmoney-sector-flow --board-code BK0488 --trade-date 20260623
        python <RUN_PY> eastmoney-sector-flow --board-type concept --start-date 20260601 --end-date 20260630
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - 请求参数为 `board_code` / `board_type` / `board_level`(原 `sector_code` / `sector_type` / `sector_level` 已更名,不再使用旧参数名)。
        - 概念和地域板块的层级为 0,传入 `board_level` 时不会匹配 concept/regional 数据。
        - `trade_date`、`start_date`、`end_date` 可同时使用,按全部条件过滤。
        
    • eastmoney-shareholder-changes
      • scripts
        • handler.py 2.7 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v1/market/data/holder/stock-ggcg-em"
          _REQUEST_HEADERS = {"Content-Type": "application/json"}
          def safe_urlopen(request):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              b, p = urllib.parse.urlparse(BASE_URL), urllib.parse.urlparse(url)
              if (b.scheme, b.netloc) != (p.scheme, p.netloc): print("Invalid URL for safe_urlopen", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, headers=_REQUEST_HEADERS, method="GET")
              return urllib.request.urlopen(request, timeout=30)
          
          def fetch(params):
              key = os.environ.get("FTSHARE_API_KEY")
              if not key: print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params)
              b, p = urllib.parse.urlparse(BASE_URL), urllib.parse.urlparse(url)
              if (b.scheme, b.netloc) != (p.scheme, p.netloc): print("Invalid URL for safe_urlopen", file=sys.stderr); raise SystemExit(1)
              req = urllib.request.Request(url, headers={"FTSHARE_API_KEY": key, "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(req) as r: return json.loads(r.read().decode())
              except urllib.error.HTTPError as e: print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as e: print(f"请求失败: {e.reason}", file=sys.stderr); raise SystemExit(1)
          def main():
              p=argparse.ArgumentParser(description="查询东方财富股东增减持"); p.add_argument("--symbol", default="全部"); p.add_argument("--page", type=int, default=1); p.add_argument("--page-size", dest="page_size", type=int, default=50); p.add_argument("--all", action="store_true", dest="fetch_all"); a=p.parse_args()
              if a.symbol not in ("全部", "股东增持", "股东减持"): p.error("symbol 仅支持 全部、股东增持、股东减持")
              if a.page < 1 or not 1 <= a.page_size <= 200: p.error("page 须大于等于 1,page-size 须在 1~200 之间")
              params={"symbol":a.symbol,"page":1 if a.fetch_all else a.page,"page_size":a.page_size}; result=fetch(params)
              if a.fetch_all:
                  data=result.get("data") or {}; records=list(data.get("records", []))
                  for page in range(2,int(data.get("pages",1))+1): records.extend((fetch({**params,"page":page}).get("data") or {}).get("records", []))
                  result["data"]={**data,"records":records}
              print(json.dumps(result, ensure_ascii=False, indent=2))
          if __name__ == "__main__": main()
          
        • test_handler.py 979 B
          import importlib.util
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          
          _DIR = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_DIR, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          spec.loader.exec_module(handler)
          
          class TestHandler(unittest.TestCase):
              @patch.object(handler, "fetch")
              def test_symbol_and_paging(self, mock_fetch):
                  mock_fetch.return_value = {"code": 200, "data": {"pages": 1, "records": []}}
                  with patch.object(sys, "argv", ["handler.py", "--symbol", "股东减持", "--page", "2", "--page-size", "20"]), patch("sys.stdout", new_callable=StringIO) as out:
                      handler.main()
                  self.assertEqual(mock_fetch.call_args.args[0], {"symbol": "股东减持", "page": 2, "page_size": 20})
                  self.assertEqual(json.loads(out.getvalue())["code"], 200)
          
          if __name__ == "__main__": unittest.main()
          
      • SKILL.md 945 B
        ---
        name: eastmoney-shareholder-changes
        description: 查询东方财富股东增减持。Use when user asks about 股东增持、股东减持或东方财富股东变动。
        ---
        
        # 东方财富股东增减持
        
        接口:`GET /api/v1/market/data/holder/stock-ggcg-em`。通过 `symbol` 筛选全部、股东增持或股东减持,返回分页明细。
        
        ```bash
        python <RUN_PY> eastmoney-shareholder-changes --symbol 全部 --page 1 --page-size 20
        python <RUN_PY> eastmoney-shareholder-changes --symbol 股东减持 --all
        ```
        
        - `--symbol`:可选,`全部`、`股东增持` 或 `股东减持`,默认全部。
        - `--page`:页码,默认 1。
        - `--page-size`:每页条数,默认 50,最大 200。
        - `--all`:自动翻页合并全部记录。
        
        响应为 `code/message/data`,记录位于 `data.records`,包括 `stock_code`、`stock_name`、`holder_name`、变动数量/比例、变动后持股及起止日期、公告日期等字段。
        
    • eastmoney-stock-flow
      • scripts
        • handler.py 4.7 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/eastmoney-stock-flow'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='东方财富个股资金流')
              parser.add_argument("--symbol")
              parser.add_argument("--trade_date")
              parser.add_argument("--start_date")
              parser.add_argument("--end_date")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--total", required=False)
              parser.add_argument("--name", required=True)
              parser.add_argument("--market", required=True)
              parser.add_argument("--close_price", required=False)
              parser.add_argument("--change_pct", required=False)
              parser.add_argument("--main_net", required=False)
              parser.add_argument("--main_pct", required=False)
              parser.add_argument("--super_large_net", required=True)
              parser.add_argument("--super_large_pct", required=True)
              parser.add_argument("--large_net", required=False)
              parser.add_argument("--large_pct", required=False)
              parser.add_argument("--medium_net", required=True)
              parser.add_argument("--medium_pct", required=True)
              parser.add_argument("--small_net", required=False)
              parser.add_argument("--small_pct", required=False)
              args = parser.parse_args()
              params = {}
              if args.symbol is not None: params["symbol"] = args.symbol
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.name is not None: params["name"] = args.name
              if args.market is not None: params["market"] = args.market
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.close_price is not None: params["close_price"] = args.close_price
              if args.change_pct is not None: params["change_pct"] = args.change_pct
              if args.main_net is not None: params["main_net"] = args.main_net
              if args.main_pct is not None: params["main_pct"] = args.main_pct
              if args.super_large_net is not None: params["super_large_net"] = args.super_large_net
              if args.super_large_pct is not None: params["super_large_pct"] = args.super_large_pct
              if args.large_net is not None: params["large_net"] = args.large_net
              if args.large_pct is not None: params["large_pct"] = args.large_pct
              if args.medium_net is not None: params["medium_net"] = args.medium_net
              if args.medium_pct is not None: params["medium_pct"] = args.medium_pct
              if args.small_net is not None: params["small_net"] = args.small_net
              if args.small_pct is not None: params["small_pct"] = args.small_pct
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 754 B
        ---
        name: eastmoney-stock-flow
        description: 查询东方财富个股资金流。接口:GET /api/v1/market/data/eastmoney-stock-flow。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 东方财富个股资金流
        
        接口:GET `/api/v1/market/data/eastmoney-stock-flow`。参数和响应以 `ftshare-doc/api-doc/股票数据/资金流向数据/东方财富个股资金流.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> eastmoney-stock-flow --symbol 600519.SH --page 1 --name 1 --market A --super_large_net 1 --super_large_pct 1 --medium_net 1 --medium_pct 1
        ```
        
    • eastmoney-stock-valuation
      • scripts
        • handler.py 4.3 KB
          #!/usr/bin/env python3
          """查询东财个股日估值数据,支持单票、单日、区间查询与分页"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/eastmoney-stock-valuation"
          
          
          def fetch(
          symbol: str = None, trade_date: str = None, start_date: str = None,
                    end_date: str = None, page: int = 1, page_size: int = 50) -> dict:
              params = {"page": page, "page_size": page_size}
              if symbol:
                  params["symbol"] = symbol
              if trade_date:
                  params["trade_date"] = trade_date
              if start_date:
                  params["start_date"] = start_date
              if end_date:
                  params["end_date"] = end_date
              qs = urllib.parse.urlencode(params)
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询东财个股日估值数据")
              parser.add_argument("--symbol", default=None, help="股票代码,6位数字,如 000001")
              parser.add_argument("--trade_date", default=None, help="交易日,格式 YYYY-MM-DD")
              parser.add_argument("--start_date", default=None, help="区间起始日,格式 YYYY-MM-DD")
              parser.add_argument("--end_date", default=None, help="区间结束日,格式 YYYY-MM-DD")
              parser.add_argument("--page", type=int, default=1, help="页码(从 1 开始,默认 1)")
              parser.add_argument("--page_size", type=int, default=50, help="每页条数(默认 50,最大 500)")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量数据")
              args = parser.parse_args()
          
              if args.fetch_all:
                  first = fetch(args.symbol, args.trade_date, args.start_date, args.end_date, 1, args.page_size)
                  data = first.get("data") or {}
                  records = list(data.get("records", []))
                  total_pages = data.get("pages", 1)
                  for p in range(2, total_pages + 1):
                      page_data = fetch(args.symbol, args.trade_date, args.start_date, args.end_date, p, args.page_size)
                      pdata = page_data.get("data") or {}
                      records.extend(pdata.get("records", []))
                  result = {
                      "code": first.get("code", 0),
                      "message": first.get("message", "success"),
                      "data": {
                          "pageNum": 1,
                          "pageSize": args.page_size,
                          "total": len(records),
                          "pages": 1,
                          "records": records,
                      },
                  }
              else:
                  result = fetch(args.symbol, args.trade_date, args.start_date, args.end_date, args.page, args.page_size)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 5.4 KB
        ---
        name: eastmoney-stock-valuation
        description: "查询东财个股日估值。当用户需要查询全部 A 股个股的每日估值数据,包括市盈率(TTM/LYR)、市净率、市现率、市销率、PEG、总市值、流通市值、收盘价等;支持单票单日、单票历史区间及全市场查询,或了解东财个股日估值时使用。"
        ---
        
        # 查询东财个股日估值
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 查询东财个股日估值 |
        | 外部接口 | `/api/v1/market/data/eastmoney-stock-valuation` |
        | 请求方式 | GET |
        | 适用场景 | 查询全部 A 股个股的每日估值数据,包括市盈率(TTM/LYR)、市净率、市现率、市销率、PEG、总市值、流通市值、收盘价等;支持单票单日、单票历史区间及全市场查询 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | `symbol` | string | 否 | 股票代码 | `000001` | 6位数字代码,不传返回全部股票 |
        | `trade_date` | string | 否 | 交易日 | `2026-06-08` | 格式 `YYYY-MM-DD`;与 `start_date`/`end_date` 互斥 |
        | `start_date` | string | 否 | 区间起始日 | `2026-06-01` | 格式 `YYYY-MM-DD`;与 `end_date` 同时提供 |
        | `end_date` | string | 否 | 区间结束日 | `2026-06-09` | 格式 `YYYY-MM-DD`;与 `start_date` 同时提供 |
        | `--page` | int | 否 | 页码 | `1` | 从 1 开始,默认 1 |
        | `--page_size` | int | 否 | 每页条数 | `50` | 默认 50,最大 500 |
        
        ### 查询模式
        
        | 模式 | 参数 | 说明 |
        |------|------|------|
        | 全量查询 | 无 | 返回全部股票全部日期(⚠️ 数据量极大,建议配合分页) |
        | 单票全部历史 | `--symbol` | 返回指定股票全部历史估值 |
        | 单日全市场 | `--trade_date` | 返回指定交易日全部股票估值 |
        | 单票单日 | `--symbol --trade_date` | 返回指定股票指定交易日估值 |
        | 单票历史区间 | `--symbol --start_date --end_date` | 返回指定股票日期区间估值走势 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用:
        
        ```bash
        python <RUN_PY> eastmoney-stock-valuation --symbol 000001 --page 1 --page_size 10
        python <RUN_PY> eastmoney-stock-valuation --symbol 000001 --start_date 2026-06-01 --end_date 2026-06-09
        python <RUN_PY> eastmoney-stock-valuation --start_date 2026-06-01 --end_date 2026-06-09 --page 1 --page_size 50
        python <RUN_PY> eastmoney-stock-valuation --symbol 000001 --trade_date 2026-06-08
        python <RUN_PY> eastmoney-stock-valuation --start_date 2026-06-01 --end_date 2026-06-09 --all
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        顶层为分页包装对象:
        
        | 字段名 | 类型 | 描述 |
        |--------|------|------|
        | `code` | int | 业务状态码,成功为 `0` |
        | `message` | string | 业务消息 |
        | `data` | object/null | 分页数据体;无数据时可能为 `null` |
        | `data.pageNum` | int | 当前页码 |
        | `data.pageSize` | int | 每页条数 |
        | `data.total` | int | 总记录数 |
        | `data.pages` | int | 总页数 |
        | `data.records` | array | 当前页记录列表 |
        
        `data.records` 中每项字段:
        
        | 字段名 | 类型 | 描述 |
        |--------|------|------|
        | `stock_code` | string | 股票代码,如 `000001` |
        | `stock_name` | string | 股票简称,如 `平安银行` |
        | `trade_date` | string | 交易日期,格式 `YYYY-MM-DD` |
        | `pe_ttm` | string | 市盈率 TTM,无数据时为空 |
        | `pe_lar` | string | 市盈率 LYR(静态),无数据时为空 |
        | `pb_mrq` | string | 市净率 MRQ,无数据时为空 |
        | `pcf_ocf_ttm` | string | 市现率 TTM,无数据时为空 |
        | `pcf_ocf_lar` | string | 市现率 LYR,无数据时为空 |
        | `ps_ttm` | string | 市销率 TTM,无数据时为空 |
        | `peg_car` | string | PEG 指标,无数据时为空 |
        | `total_market_cap` | string | 总市值(元),无数据时为空 |
        | `notlimited_marketcap_a` | string | 流通市值(元),无数据时为空 |
        | `close_price` | string | 收盘价(元/股),无数据时为空 |
        | `change_rate` | string | 涨跌幅(%),无数据时为空 |
        | `total_shares` | string | 总股本,无数据时为空 |
        | `free_shares_a` | string | 流通股本,无数据时为空 |
        
        ### 响应示例
        
        ```json
        {
          "code": 0,
          "message": "success",
          "data": {
            "pageNum": 1,
            "pageSize": 3,
            "total": 33142,
            "pages": 11048,
            "records": [
              {
                "stock_code": "000001",
                "stock_name": "平安银行",
                "trade_date": "2026-06-08",
                "pe_ttm": "4.9709",
                "pe_lar": "5.0207",
                "pb_mrq": "0.4612",
                "pcf_ocf_ttm": "1.1223",
                "pcf_ocf_lar": "0.6777",
                "ps_ttm": "1.6093",
                "peg_car": "-1.1934",
                "total_market_cap": "214047277723.94",
                "notlimited_marketcap_a": "214043775202.59",
                "close_price": "11.03",
                "change_rate": "0.4554",
                "total_shares": "19405918198",
                "free_shares_a": "19405600653"
              }
            ]
          }
        }
        ```
        
        ## 注意事项
        
        - 所有数值字段序列化为字符串,避免 JSON 浮点精度问题
        - 无数据的字段输出为空字符串 `""`,而非 `null`
        - 部分估值指标(如 PEG、市现率 LYR)可能因财务数据缺失而为空
        - 负值指标(如负市盈率)会正常返回负数字符串
        - 推荐使用 `YYYY-MM-DD` 日期格式;`YYYYMMDD` 会返回空列表
        - 当前单日查询可能异常,建议优先使用区间查询
        
    • eastmoney-us-stock-daily-ohlc
      • scripts
        • handler.py 7.2 KB
          #!/usr/bin/env python3
          """查询东财美股历史日K线。
          
          有日期范围时:按 3 天窗口分批请求(避免服务端大范围 500),合并去重后客户端分页。
          无日期范围时:全量拉取(不传日期参数)。
          """
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          from datetime import datetime, timedelta
          import os
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/eastmoney-us-stock-daily-ohlc"
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          MAX_WINDOW_DAYS = 3  # 每次请求日期跨度不超过 3 天
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def _fetch_page(stock_code: str, start_date: str = None, end_date: str = None,
                          page: int = 1, page_size: int = 100) -> dict:
              params = {"stock_code": stock_code, "page": page, "page_size": page_size}
              if start_date:
                  params["start_date"] = start_date
              if end_date:
                  params["end_date"] = end_date
              qs = urllib.parse.urlencode(params)
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              with safe_urlopen(url) as resp:
                  return json.loads(resp.read().decode())
          
          
          def _fetch_window_pages(stock_code: str, start: str, end: str, page_size: int = 100) -> list:
              """请求一个日期窗口内所有页的数据。"""
              first = _fetch_page(stock_code, start, end, 1, page_size)
              data = first.get("data") or {}
              records = list(data.get("records", []))
              total_pages = data.get("pages", 1)
              for p in range(2, total_pages + 1):
                  page_data = _fetch_page(stock_code, start, end, p, page_size)
                  pdata = page_data.get("data") or {}
                  records.extend(pdata.get("records", []))
              return records
          
          
          def _fetch_full(stock_code: str, page_size: int = 100) -> list:
              """全量拉取(不传日期参数)。"""
              first = _fetch_page(stock_code, page=1, page_size=page_size)
              data = first.get("data") or {}
              records = list(data.get("records", []))
              total_pages = data.get("pages", 1)
              for p in range(2, total_pages + 1):
                  page_data = _fetch_page(stock_code, page=p, page_size=page_size)
                  pdata = page_data.get("data") or {}
                  records.extend(pdata.get("records", []))
              return records
          
          
          def _normalize_date(d: str) -> str:
              d = d.strip().replace("-", "")
              if len(d) == 8:
                  return f"{d[:4]}-{d[4:6]}-{d[6:8]}"
              return d
          
          
          def _date_range_windows(start: str, end: str, max_days: int = 3) -> list:
              """将日期区间拆分为 max_days 天的小窗口。"""
              fmt = "%Y-%m-%d"
              s = datetime.strptime(start, fmt)
              e = datetime.strptime(end, fmt)
              windows = []
              cur = s
              while cur <= e:
                  w_end = min(cur + timedelta(days=max_days - 1), e)
                  windows.append((cur.strftime(fmt), w_end.strftime(fmt)))
                  cur = w_end + timedelta(days=1)
              return windows
          
          
          def fetch_by_windows(stock_code: str, start_date: str, end_date: str) -> list:
              """按 3 天窗口分批请求,合并去重。"""
              windows = _date_range_windows(start_date, end_date, MAX_WINDOW_DAYS)
              total_wins = len(windows)
              print(f"info: 日期区间 {start_date}~{end_date} 拆分为 {total_wins} 个 {MAX_WINDOW_DAYS} 天窗口", file=sys.stderr)
          
              all_records = []
              seen = set()
              for i, (ws, we) in enumerate(windows):
                  records = _fetch_window_pages(stock_code, ws, we)
                  for r in records:
                      d = r.get("date", "")
                      if d and d not in seen:
                          seen.add(d)
                          all_records.append(r)
                  if total_wins > 1:
                      print(f"info: 窗口 {i + 1}/{total_wins} ({ws}~{we}) 获取 {len(records)} 条", file=sys.stderr)
          
              # 按日期排序
              all_records.sort(key=lambda r: r.get("date", ""))
              return all_records
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询东财美股历史日K线(3天窗口分批)")
              parser.add_argument("--stock_code", required=True, help="股票代码,如 AAL")
              parser.add_argument("--start_date", default=None, help="起始日期(含),格式 YYYY-MM-DD 或 YYYYMMDD")
              parser.add_argument("--end_date", default=None, help="截止日期(含),格式 YYYY-MM-DD 或 YYYYMMDD")
              parser.add_argument("--page", type=int, default=1, help="页码(从 1 开始,默认 1)")
              parser.add_argument("--page_size", type=int, default=50, help="每页条数(默认 50)")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="返回全量数据(不分页)")
              args = parser.parse_args()
          
              has_date_filter = bool(args.start_date or args.end_date)
          
              if has_date_filter:
                  start = _normalize_date(args.start_date) if args.start_date else ""
                  end = _normalize_date(args.end_date) if args.end_date else ""
                  # 确保至少有一个边界
                  if not start:
                      start = "2000-01-01"
                  if not end:
                      end = "2099-12-31"
                  records = fetch_by_windows(args.stock_code, start, end)
              else:
                  records = _fetch_full(args.stock_code)
          
              total = len(records)
          
              if total == 0:
                  result = {"code": 0, "message": "success", "data": None}
              elif args.fetch_all:
                  result = {
                      "code": 0,
                      "message": "success",
                      "data": {
                          "pageNum": 1,
                          "pageSize": total,
                          "total": total,
                          "pages": 1,
                          "records": records,
                      },
                  }
              else:
                  start_idx = (args.page - 1) * args.page_size
                  end_idx = start_idx + args.page_size
                  result = {
                      "code": 0,
                      "message": "success",
                      "data": {
                          "pageNum": args.page,
                          "pageSize": args.page_size,
                          "total": total,
                          "pages": max(1, (total + args.page_size - 1) // args.page_size),
                          "records": records[start_idx:end_idx],
                      },
                  }
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 4.6 KB
        ---
        name: eastmoney-us-stock-daily-ohlc
        description: "查询东财美股历史日 K 线。当用户需要查询东财美股历史日 K 线;有日期范围时按 3 天窗口分批请求,无日期范围时全量拉取,或了解东财美股历史 OHLC、东财美股历史日 K 线时使用。"
        ---
        
        # 查询东财美股历史 OHLC
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 查询东财美股历史日 K 线 |
        | 外部接口 | `/api/v1/market/data/eastmoney-us-stock-daily-ohlc` |
        | 请求方式 | GET |
        | 适用场景 | 查询东财美股历史日 K 线;有日期范围时按 3 天窗口分批请求,无日期范围时全量拉取 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | `--stock_code` | string | 是 | 股票代码 | `AAL` | 东财美股代码 |
        | `--start_date` | string | 否 | 起始日期(含) | `2026-06-01` | 格式 `YYYY-MM-DD` 或 `YYYYMMDD` |
        | `--end_date` | string | 否 | 截止日期(含) | `2026-06-10` | 格式 `YYYY-MM-DD` 或 `YYYYMMDD` |
        | `--page` | int | 否 | 页码 | `1` | 从 1 开始,默认 1 |
        | `--page_size` | int | 否 | 每页条数 | `50` | 默认 50 |
        | `--all` | flag | 否 | 全量返回 | — | 返回全量数据(不分页) |
        
        > **执行策略**:有日期范围时,将区间拆分为最多 3 天的窗口分批请求,合并去重排序;无日期范围时全量拉取(不传日期参数)。
        
        ### 查询模式
        
        | 模式 | 参数 | 说明 |
        |------|------|------|
        | 全部历史 | `--stock_code` | 全量拉取全部历史 K 线(分页) |
        | 日期区间 | `--stock_code --start_date --end_date` | 按 3 天窗口分批请求,合并后分页 |
        | 日期区间全量 | `--stock_code --start_date --end_date --all` | 按 3 天窗口分批请求,合并后返回全量(不分页) |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用:
        
        ```bash
        python <RUN_PY> eastmoney-us-stock-daily-ohlc --stock_code AAL --page 1 --page_size 10
        python <RUN_PY> eastmoney-us-stock-daily-ohlc --stock_code AAL --start_date 2026-05-01 --end_date 2026-06-10 --page 1 --page_size 50
        python <RUN_PY> eastmoney-us-stock-daily-ohlc --stock_code AAL --start_date 2026-05-01 --end_date 2026-06-10 --all
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        顶层为分页包装对象:
        
        | 字段名 | 类型 | 描述 |
        |--------|------|------|
        | `code` | int | 业务状态码,成功为 `0` |
        | `message` | string | 业务消息 |
        | `data` | object/null | 分页数据体;无数据时可能为 `null` |
        | `data.pageNum` | int | 当前页码 |
        | `data.pageSize` | int | 每页条数 |
        | `data.total` | int | 总记录数 |
        | `data.pages` | int | 总页数 |
        | `data.records` | array | 当前页 K 线列表 |
        
        `data.records[]` 单条记录字段(**全部字段按字符串返回**):
        
        | 字段名 | 类型 | 描述 |
        |--------|------|------|
        | `secid` | string | 证券 ID,格式 `{market}.{code}`,如 `105.ADV` |
        | `code` | string | 股票代码 |
        | `name` | string | 股票名称 |
        | `market` | string | 市场编号 |
        | `date` | string | 日期,格式 `YYYY-MM-DD` |
        | `open` | string | 开盘价 |
        | `close` | string | 收盘价 |
        | `high` | string | 最高价 |
        | `low` | string | 最低价 |
        | `volume` | string | 成交量 |
        | `amount` | string | 成交额 |
        | `amplitude` | string | 振幅 |
        | `klt` | string | K 线类型 |
        | `fqt` | string | 复权类型 |
        
        ### 响应示例
        
        ```json
        {
          "code": 0,
          "message": "success",
          "data": {
            "pageNum": 1,
            "pageSize": 50,
            "total": 503,
            "pages": 11,
            "records": [
              {
                "secid": "105.ADV",
                "code": "ADV",
                "name": "Advantage Solutions Inc",
                "market": "105",
                "date": "2021-01-05",
                "open": "12.34",
                "close": "12.56",
                "high": "12.78",
                "low": "12.10",
                "volume": "1234567",
                "amount": "9876543.21",
                "amplitude": "5.5",
                "klt": "101",
                "fqt": "1"
              }
            ]
          }
        }
        ```
        
        ## 注意事项
        
        - 有日期范围时,区间拆分为最多 3 天的窗口分别请求,避免服务端大范围 500 错误
        - 无日期范围时,全量拉取全部历史 K 线(单只股票数据量较小,如 AAL ~1365 条)
        - `start_date` / `end_date` 支持 `YYYY-MM-DD` 与 `YYYYMMDD` 两种格式
        - 记录字段为 snake_case,所有数值字段均为字符串
        - `data` 在错误或无数据时可能为 `null`
        - `stock_code` 为东财美股代码(如 `AAL`)
        - 可先通过 `eastmoney-us-stock-list` 获取有效的美股代码列表
        
    • eastmoney-us-stock-list
      • scripts
        • handler.py 3 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v1/market/data/eastmoney-us-stock-list"
          
          _REQUEST_HEADERS = {"Content-Type": "application/json"}
          
          def safe_urlopen(request):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              base = urllib.parse.urlparse(BASE_URL)
              parsed = urllib.parse.urlparse(url)
              if (parsed.scheme, parsed.netloc) != (base.scheme, base.netloc):
                  print("Invalid URL for safe_urlopen", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, headers=_REQUEST_HEADERS, method="GET")
              return urllib.request.urlopen(request, timeout=30)
          
          def fetch(params):
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params)
              base = urllib.parse.urlparse(BASE_URL)
              parsed = urllib.parse.urlparse(url)
              if (parsed.scheme, parsed.netloc) != (base.scheme, base.netloc):
                  print("Invalid URL for safe_urlopen", file=sys.stderr)
                  raise SystemExit(1)
              request = urllib.request.Request(url, headers={"FTSHARE_API_KEY": key, "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      return json.loads(response.read().decode())
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          def main():
              parser = argparse.ArgumentParser(description="查询东方财富美股列表")
              parser.add_argument("--refresh", action="store_true")
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", dest="page_size", type=int)
              parser.add_argument("--all", action="store_true", dest="fetch_all")
              args = parser.parse_args()
              if args.page < 1 or (args.page_size is not None and args.page_size < 1):
                  parser.error("page 和 page-size 必须大于等于 1")
              params = {"page": 1 if args.fetch_all else args.page}
              if args.refresh:
                  params["refresh"] = "true"
              if args.page_size is not None:
                  params["page_size"] = args.page_size
              result = fetch(params)
              if args.fetch_all:
                  data = result.get("data") or {}
                  records = list(data.get("records", []))
                  for page in range(2, int(data.get("pages", 1)) + 1):
                      records.extend((fetch({**params, "page": page}).get("data") or {}).get("records", []))
                  result["data"] = {**data, "records": records}
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1012 B
          import importlib.util
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          
          _DIR = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_DIR, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          spec.loader.exec_module(handler)
          
          class TestHandler(unittest.TestCase):
              @patch.object(handler, "fetch")
              def test_contract_and_json(self, mock_fetch):
                  mock_fetch.return_value = {"code": 200, "data": {"pages": 1, "records": []}}
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}), patch.object(sys, "argv", ["handler.py", "--refresh", "--page", "2", "--page-size", "5"]), patch("sys.stdout", new_callable=StringIO) as out:
                      handler.main()
                  self.assertEqual(mock_fetch.call_args.args[0], {"page": 2, "refresh": "true", "page_size": 5})
                  self.assertEqual(json.loads(out.getvalue())["code"], 200)
          
          if __name__ == "__main__": unittest.main()
          
      • SKILL.md 1010 B
        ---
        name: eastmoney-us-stock-list
        description: 查询东方财富美股列表。Use when user asks for 美股列表、东财美股股票清单或美股代码。
        ---
        
        # 东方财富美股列表
        
        接口:`GET /api/v1/market/data/eastmoney-us-stock-list`。获取东方财富美股最新快照,支持刷新和分页。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> eastmoney-us-stock-list --page 1 --page-size 5
        python <RUN_PY> eastmoney-us-stock-list --refresh --page 1 --page-size 10
        python <RUN_PY> eastmoney-us-stock-list --all
        ```
        
        ## 参数
        
        - `--refresh`:刷新列表缓存,可选。
        - `--page`:页码,从 1 开始,默认 1。
        - `--page-size`:每页数量,可选。
        - `--all`:自动翻页并合并全部记录。
        
        响应为 `code/message/data` 信封,分页记录位于 `data.records`,字段包括 `secid`、`market`、`code`、`name`、`market_value_usd`、`latest_price`、`change_pct`、`volume`、`amount`、`pe_ttm`。数值字段按字符串返回;`secid` 示例为 `105.ADV`。
        
    • economic-china-cpi-monthly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国居民消费价格指数 CPI(月度)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-cpi"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.4 KB
        ---
        name: economic-china-cpi-monthly
        description: Get China CPI monthly data (居民消费价格指数 月度). Use when user asks about CPI, 居民消费价格指数, 消费价格指数, 中国CPI, 全国CPI, 城市农村CPI, China CPI.
        ---
        
        # 中国经济 - 居民消费价格指数(月度)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 居民消费价格指数(月度汇总计算结果) |
        | 外部接口 | GET /api/v1/market/data/economic/china-cpi |
        | 请求方式 | GET |
        | 适用场景 | 获取中国居民消费价格指数 CPI 月度汇总数据,含全国/城市/农村当月值及同比、环比等 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `month`(如 2025年03月份)、`national_cpi`/`national_yoy`/`national_mom`(全国 CPI 及同比、环比)、`cumulative`(累计)、`city_cpi`/`city_yoy`/`city_mom`(城市)、`rural_cpi`/`rural_yoy`/`rural_mom`(农村),以表格展示给用户。
        
        ## 4. 响应说明
        
        返回值为居民消费价格指数月度计算结果列表,按时间倒序。
        
        ### ComputedCpi 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | month | String | 否 | 月份,格式如 2025年03月份 | - |
        | national_cpi | float | 是 | 全国 CPI 当月值 | - |
        | national_yoy | float | 是 | 全国同比 | % |
        | national_mom | float | 是 | 全国环比 | % |
        | cumulative | float | 是 | 累计值 | - |
        | city_cpi | float | 是 | 城市 CPI 当月值 | - |
        | city_yoy | float | 是 | 城市同比 | % |
        | city_mom | float | 是 | 城市环比 | % |
        | rural_cpi | float | 是 | 农村 CPI 当月值 | - |
        | rural_yoy | float | 是 | 农村同比 | % |
        | rural_mom | float | 是 | 农村环比 | % |
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/economic/china-cpi
        ```
        
        ## 6. 注意事项
        
        - 返回按月份汇总,格式如「2025年03月份」,列表已按时间倒序,最新月份在前。
        - CPI 为指数(基期 100),同比/环比单位为 %,各数值字段可为 null。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-cpi-monthly
        ```
        
    • economic-china-credit-loans-monthly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国信贷数据(月度)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-credit-loans"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.2 KB
        ---
        name: economic-china-credit-loans-monthly
        description: Get China credit/loans monthly data (中国信贷数据 月度). Use when user asks about 信贷, 新增信贷, 信贷数据, 中国信贷, 贷款增量, China credit loans, 社会融资.
        ---
        
        # 中国经济 - 信贷(月度)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 信贷数据(月度汇总计算结果) |
        | 外部接口 | GET /api/v1/market/data/economic/china-credit-loans |
        | 请求方式 | GET |
        | 适用场景 | 获取中国信贷数据月度汇总,含新增信贷、同比、环比、当年累计及累计同比等 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `month`(如 2025年03月份)、`new_loans`(本月新增信贷)、`yoy`(同比 %)、`mom`(环比 %)、`cumulative`(当年累计)、`cumulative_yoy`(累计同比 %)、`unit`(亿元)、`currency`(CNY),以表格展示给用户。
        
        ## 4. 响应说明
        
        返回值为信贷月度计算结果列表,按时间倒序。
        
        ### CreditComputed 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | month | String | 否 | 月份,格式如 2025年03月份 | - |
        | new_loans | float | 是 | 本月新增信贷 | 见 unit |
        | yoy | float | 是 | 同比增长 | % |
        | mom | float | 是 | 环比增长 | % |
        | cumulative | float | 是 | 当年累计(1 月到当前月之和) | 见 unit |
        | cumulative_yoy | float | 是 | 累计同比增长 | % |
        | unit | String | 否 | 货币单位 | - |
        | currency | String | 否 | 货币种类 | - |
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/economic/china-credit-loans
        ```
        
        ## 6. 注意事项
        
        - 返回按月份汇总,格式如「2025年03月份」,列表已按时间倒序,最新月份在前。
        - 金额单位见 `unit`(通常为亿元),同比/环比单位为 %,各数值字段可为 null。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-credit-loans-monthly
        ```
        
    • economic-china-customs-trade-monthly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国海关进出口(月度)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-customs-trade"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.7 KB
        ---
        name: economic-china-customs-trade-monthly
        description: Get China customs import/export monthly data (海关进出口 月度). Use when user asks about 海关进出口, 出口, 进口, 进出口, 外贸, 中国出口进口, China customs trade.
        ---
        
        # 中国经济 - 海关进出口(月度)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 海关进出口增减情况(月度汇总计算结果) |
        | 外部接口 | GET /api/v1/market/data/economic/china-customs-trade |
        | 请求方式 | GET |
        | 适用场景 | 获取中国海关进出口月度数据,含出口/进口当月值、同比、环比及累计与累计同比等 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `month`(如 2025年03月份)、`export_current`/`export_yoy`/`export_mom`(当月出口及同比、环比)、`import_current`/`import_yoy`/`import_mom`(当月进口)、`cumulative_export`/`cumulative_export_yoy`、`cumulative_import`/`cumulative_import_yoy`(累计及同比)、`unit`、`currency`,以表格展示给用户。
        
        ## 4. 响应说明
        
        返回值为海关进出口月度计算结果列表,按时间倒序。
        
        ### CustomsTradeComputed 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | month | String | 否 | 月份,格式如 2025年03月份 | - |
        | export_current | float | 是 | 当月出口额 | 见 unit |
        | export_yoy | float | 是 | 当月出口额同比 | % |
        | export_mom | float | 是 | 当月出口额环比 | % |
        | import_current | float | 是 | 当月进口额 | 见 unit |
        | import_yoy | float | 是 | 当月进口额同比 | % |
        | import_mom | float | 是 | 当月进口额环比 | % |
        | cumulative_export | float | 是 | 累计出口额 | 见 unit |
        | cumulative_export_yoy | float | 是 | 累计出口额同比 | % |
        | cumulative_import | float | 是 | 累计进口额 | 见 unit |
        | cumulative_import_yoy | float | 是 | 累计进口额同比 | % |
        | unit | String | 否 | 货币单位 | - |
        | currency | String | 否 | 货币种类 | - |
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/economic/china-customs-trade
        ```
        
        ## 6. 注意事项
        
        - 返回按月份汇总,格式如「2025年03月份」,列表已按时间倒序,最新月份在前。
        - 金额单位见 `unit`(通常为亿美元),同比/环比单位为 %,各数值字段可为 null。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-customs-trade-monthly
        ```
        
    • economic-china-fiscal-revenue-monthly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国财政收入月度数据"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-fiscal-revenue"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1002 B
        ---
        name: economic-china-fiscal-revenue-monthly
        description: Get China fiscal revenue monthly data (中国财政收入月度). Use when user asks about 中国财政收入, 财政收入月度, 财政收支, fiscal revenue, 当月同比环比, 累计财政收入.
        ---
        
        # 中国经济 - 财政收入(月度)
        
        ## 参数
        
        无需任何参数。
        
        ## 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `month`(如 2025年03月份)、`revenue_current`(当月财政收入)、`yoy`(当月同比 %)、`mom`(当月环比 %)、`cumulative`(累计)、`cumulative_yoy`(累计同比 %)、`unit`(亿元)、`currency`(CNY),以表格展示给用户。
        
        ## 注意
        
        - 返回按月份汇总,格式如「2025年03月份」
        - 数值单位见 `unit`(亿元),同比/环比单位为 %
        - 列表已按时间倒序,最新月份在前
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-fiscal-revenue-monthly
        ```
        
    • economic-china-fixed-asset-investment-monthly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国城镇固定资产投资(月度)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-fixed-asset-investment"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.2 KB
        ---
        name: economic-china-fixed-asset-investment-monthly
        description: Get China urban fixed asset investment monthly data (城镇固定资产投资 月度). Use when user asks about 城镇固定资产投资, 固定资产投资, 固投, 中国投资, China fixed asset investment.
        ---
        
        # 中国经济 - 城镇固定资产投资(月度)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 城镇固定资产投资(月度汇总计算结果) |
        | 外部接口 | GET /api/v1/market/data/economic/china-fixed-asset-investment |
        | 请求方式 | GET |
        | 适用场景 | 获取中国城镇固定资产投资月度数据,含本月投资、同比、环比、累计值等 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `month`(如 2025年03月份)、`investment`(本月投资)、`yoy`(同比 %)、`mom`(环比 %)、`cumulative`(累计值)、`unit`(亿元)、`currency`(CNY),以表格展示给用户。
        
        ## 4. 响应说明
        
        返回值为城镇固定资产投资月度计算结果列表,按时间倒序。
        
        ### FixedAssetInvestmentComputed 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | month | String | 否 | 月份,格式如 2025年03月份 | - |
        | investment | float | 是 | 本月投资(本月累计减上月累计) | 见 unit |
        | yoy | float | 是 | 同比增长 | % |
        | mom | float | 是 | 环比增长 | % |
        | cumulative | float | 是 | 累计值 | 见 unit |
        | unit | String | 否 | 货币单位 | - |
        | currency | String | 否 | 货币种类 | - |
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/economic/china-fixed-asset-investment
        ```
        
        ## 6. 注意事项
        
        - 返回按月份汇总,格式如「2025年03月份」,列表已按时间倒序,最新月份在前。
        - 金额单位见 `unit`(通常为亿元),同比/环比单位为 %,各数值字段可为 null。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-fixed-asset-investment-monthly
        ```
        
    • economic-china-gdp-quarterly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国 GDP 季度数据"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-gdp"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 982 B
        ---
        name: economic-china-gdp-quarterly
        description: Get China GDP quarterly data (中国 GDP 季度数据). Use when user asks about 中国 GDP, 季度 GDP, 三次产业, 中国经济 GDP, China GDP quarterly, GDP 同比, 第一二三产业.
        ---
        
        # 中国经济 - GDP(季度)
        
        ## 参数
        
        无需任何参数。
        
        ## 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `period`(如 2025年第1-4季度)、`gdp`、`gdp_yoy`(GDP 同比 %)、`primary`/`secondary`/`tertiary`(三次产业值)及对应累计同比、`unit`(亿元)、`currency`(人民币),以表格展示给用户。
        
        ## 注意
        
        - 返回按 `period` 季度汇总,如「2025年第1季度」「2025年第1-3季度」「2025年第1-4季度」
        - 数值单位见 `unit`(亿元),同比类字段单位为 %
        - 列表已按时间倒序,最新季度在前
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-gdp-quarterly
        ```
        
    • economic-china-industrial-added-value-monthly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国工业增加值增长(月度)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-industrial-added-value"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.9 KB
        ---
        name: economic-china-industrial-added-value-monthly
        description: Get China industrial added value growth monthly data (工业增加值增长 月度). Use when user asks about 工业增加值, 工业增加值同比, 工业增长, 中国工业, China industrial added value.
        ---
        
        # 中国经济 - 工业增加值(月度)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 中国工业增加值增长(月度汇总计算结果) |
        | 外部接口 | GET /api/v1/market/data/economic/china-industrial-added-value |
        | 请求方式 | GET |
        | 适用场景 | 获取中国工业增加值月度数据,含同比增长、累计同比增长等 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `month`(如 2025年03月份)、`yoy`(同比增长 %)、`cumulative_yoy`(累计同比增长 %),以表格展示给用户。
        
        ## 4. 响应说明
        
        返回值为工业增加值月度计算结果列表,按时间倒序。
        
        ### IndustrialAddedValueComputed 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | month | String | 否 | 月份,格式如 2025年03月份 | - |
        | yoy | float | 是 | 同比增长 | % |
        | cumulative_yoy | float | 是 | 累计同比增长 | % |
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/economic/china-industrial-added-value
        ```
        
        ## 6. 注意事项
        
        - 返回按月份汇总,格式如「2025年03月份」,列表已按时间倒序,最新月份在前。
        - 同比单位为 %,各数值字段可为 null。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-industrial-added-value-monthly
        ```
        
    • economic-china-lpr-monthly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国 LPR 贷款市场报价利率(月度)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-lpr"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.8 KB
        ---
        name: economic-china-lpr-monthly
        description: Get China LPR (Loan Prime Rate) monthly data (LPR 贷款市场报价利率 月度). Use when user asks about LPR, 贷款市场报价利率, 1年期LPR, 5年期LPR, 中国 LPR, 房贷利率, China LPR.
        ---
        
        # 中国经济 - LPR 贷款市场报价利率(月度)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | LPR 贷款市场报价利率(月度汇总计算结果) |
        | 外部接口 | GET /api/v1/market/data/economic/china-lpr |
        | 请求方式 | GET |
        | 适用场景 | 获取中国 LPR 1 年期与 5 年期利率按日期汇总数据 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `date`(如 2025-12-22)、`lpr_1y`(1 年期 LPR %)、`lpr_5y`(5 年期 LPR %),以表格展示给用户。
        
        ## 4. 响应说明
        
        返回值为 LPR 按日期计算结果列表,按时间倒序。
        
        ### LprComputed 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | date | String | 否 | 日期,格式如 2025-12-22 | - |
        | lpr_1y | float | 是 | LPR 1 年期利率 | % |
        | lpr_5y | float | 是 | LPR 5 年期利率 | % |
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/economic/china-lpr
        ```
        
        ## 6. 注意事项
        
        - 返回按报价日期(通常为每月 20 日左右)汇总,列表已按时间倒序,最新日期在前。
        - `lpr_1y`、`lpr_5y` 单位为 %,可为 null。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-lpr-monthly
        ```
        
    • economic-china-money-supply-monthly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国货币供应量 M0/M1/M2(月度)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-money-supply"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.4 KB
        ---
        name: economic-china-money-supply-monthly
        description: Get China money supply monthly data (货币供应量 M0/M1/M2 月度). Use when user asks about M0, M1, M2, 货币供应量, 广义货币, 狭义货币, 中国货币供应, China money supply.
        ---
        
        # 中国经济 - 货币供应量(月度)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 货币供应量(月度汇总计算结果) |
        | 外部接口 | GET /api/v1/market/data/economic/china-money-supply |
        | 请求方式 | GET |
        | 适用场景 | 获取中国 M0、M1、M2 货币供应量月度数据,含当月值、同比、环比等 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `month`(如 2025年03月份)、`m2_current`/`m2_yoy`/`m2_mom`(M2 当月及同比、环比)、`m1_current`/`m1_yoy`/`m1_mom`(M1)、`m0_current`/`m0_yoy`/`m0_mom`(M0)、`unit`(亿元)、`currency`(CNY),以表格展示给用户。
        
        ## 4. 响应说明
        
        返回值为货币供应量月度计算结果列表,按时间倒序。
        
        ### MoneySupplyComputed 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | month | String | 否 | 月份,格式如 2025年03月份 | - |
        | m2_current | float | 是 | M2 当月数量 | 见 unit |
        | m2_yoy | float | 是 | M2 同比增长 | % |
        | m2_mom | float | 是 | M2 环比增长 | % |
        | m1_current | float | 是 | M1 当月数量 | 见 unit |
        | m1_yoy | float | 是 | M1 同比增长 | % |
        | m1_mom | float | 是 | M1 环比增长 | % |
        | m0_current | float | 是 | M0 当月数量 | 见 unit |
        | m0_yoy | float | 是 | M0 同比增长 | % |
        | m0_mom | float | 是 | M0 环比增长 | % |
        | unit | String | 否 | 货币单位 | - |
        | currency | String | 否 | 货币种类 | - |
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/economic/china-money-supply
        ```
        
        ## 6. 注意事项
        
        - 返回按月份汇总,格式如「2025年03月份」,列表已按时间倒序,最新月份在前。
        - 金额单位见 `unit`(通常为亿元),同比/环比单位为 %,各数值字段可为 null。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-money-supply-monthly
        ```
        
    • economic-china-pmi-monthly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国 PMI 数据(月度)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-pmi"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.2 KB
        ---
        name: economic-china-pmi-monthly
        description: Get China PMI monthly data (中国 PMI 月度). Use when user asks about PMI, 制造业PMI, 非制造业PMI, 中国PMI, 采购经理指数, China PMI, manufacturing PMI.
        ---
        
        # 中国经济 - PMI(月度)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | PMI 数据(月度汇总计算结果) |
        | 外部接口 | GET /api/v1/market/data/economic/china-pmi |
        | 请求方式 | GET |
        | 适用场景 | 获取中国制造业与非制造业 PMI 月度数据,含当月值、同比、环比等 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `month`(如 2025年03月份)、`manufacturing_pmi`(制造业 PMI)、`manufacturing_yoy`/`manufacturing_mom`(制造业同比/环比 %)、`non_manufacturing_pmi`(非制造业 PMI)、`non_manufacturing_yoy`/`non_manufacturing_mom`(非制造业同比/环比 %),以表格展示给用户。
        
        ## 4. 响应说明
        
        返回值为 PMI 月度计算结果列表,按时间倒序。
        
        ### PmiComputed 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | month | String | 否 | 月份,格式如 2025年03月份 | - |
        | manufacturing_pmi | float | 是 | 制造业 PMI | - |
        | manufacturing_yoy | float | 是 | 制造业同比 | % |
        | manufacturing_mom | float | 是 | 制造业环比 | % |
        | non_manufacturing_pmi | float | 是 | 非制造业 PMI | - |
        | non_manufacturing_yoy | float | 是 | 非制造业同比 | % |
        | non_manufacturing_mom | float | 是 | 非制造业环比 | % |
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/economic/china-pmi
        ```
        
        ## 6. 注意事项
        
        - 返回按月份汇总,格式如「2025年03月份」,列表已按时间倒序,最新月份在前。
        - PMI 为指数(通常 50 为荣枯线),同比/环比单位为 %,各字段可为 null。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-pmi-monthly
        ```
        
    • economic-china-ppi-monthly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国 PPI 工业品出厂价格指数(月度)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-ppi"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2 KB
        ---
        name: economic-china-ppi-monthly
        description: Get China PPI monthly data (中国 PPI 工业品出厂价格指数 月度). Use when user asks about PPI, 工业品出厂价格指数, 生产者价格指数, 中国PPI, China PPI, 工业品价格.
        ---
        
        # 中国经济 - PPI(月度)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | PPI 数据(月度汇总计算结果) |
        | 外部接口 | GET /api/v1/market/data/economic/china-ppi |
        | 请求方式 | GET |
        | 适用场景 | 获取中国工业品出厂价格指数 PPI 月度数据,含当月值、同比、环比及当年累计平均等 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `month`(如 2025年03月份)、`ppi`(当月工业品出厂价格指数)、`yoy`(同比 %)、`mom`(环比 %)、`cumulative_avg`(当年累计平均),以表格展示给用户。
        
        ## 4. 响应说明
        
        返回值为 PPI 月度计算结果列表,按时间倒序。
        
        ### PpiComputed 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | month | String | 否 | 月份,格式如 2025年03月份 | - |
        | ppi | float | 是 | 当月工业品出厂价格指数 | - |
        | yoy | float | 是 | 同比增长 | % |
        | mom | float | 是 | 环比增长 | % |
        | cumulative_avg | float | 是 | 当年累计平均(1 月到当前月平均) | - |
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/economic/china-ppi
        ```
        
        ## 6. 注意事项
        
        - 返回按月份汇总,格式如「2025年03月份」,列表已按时间倒序,最新月份在前。
        - PPI 为指数(基期 100),同比/环比单位为 %,各字段可为 null。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-ppi-monthly
        ```
        
    • economic-china-reserve-ratio-monthly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国存款准备金率(月度)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-reserve-ratio"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2 KB
        ---
        name: economic-china-reserve-ratio-monthly
        description: Get China reserve requirement ratio monthly data (存款准备金率 月度). Use when user asks about 存款准备金率, 准备金率, 大型金融机构准备金率, 中小型金融机构准备金率, China reserve ratio.
        ---
        
        # 中国经济 - 存款准备金率(月度)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 准备金率(月度汇总计算结果) |
        | 外部接口 | GET /api/v1/market/data/economic/china-reserve-ratio |
        | 请求方式 | GET |
        | 适用场景 | 获取中国人民币存款准备金率月度数据,含大型与中小型金融机构准备金率等 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `date`(如 2025年12月)、`reserve_ratio_small_medium`(中小型金融机构准备金率 %)、`reserve_ratio_large`(大型金融机构准备金率 %),以表格展示给用户。
        
        ## 4. 响应说明
        
        返回值为准备金率按日期计算结果列表,按时间倒序。
        
        ### ReserveRatioComputed 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | date | String | 否 | 日期,格式如 2025年12月 | - |
        | reserve_ratio_small_medium | float | 是 | 准备金率中小型金融机构 | % |
        | reserve_ratio_large | float | 是 | 准备金率大型金融机构 | % |
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/economic/china-reserve-ratio
        ```
        
        ## 6. 注意事项
        
        - 返回按日期汇总(通常为调准日所在月份),格式如「2025年12月」,列表已按时间倒序,最新在前。
        - 准备金率单位为 %,各数值字段可为 null;仅当央行调整时会有新记录。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-reserve-ratio-monthly
        ```
        
    • economic-china-retail-sales-monthly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国社会消费品零售总额(月度)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-retail-sales"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.2 KB
        ---
        name: economic-china-retail-sales-monthly
        description: Get China retail sales of consumer goods monthly data (社会消费品零售总额 月度). Use when user asks about 社会消费品零售总额, 零售总额, 消费零售, 中国消费, China retail sales, 社零.
        ---
        
        # 中国经济 - 社会消费品零售总额(月度)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 社会消费品零售总额(月度汇总计算结果) |
        | 外部接口 | GET /api/v1/market/data/economic/china-retail-sales |
        | 请求方式 | GET |
        | 适用场景 | 获取中国社会消费品零售总额月度数据,含当月值、同比、累计及累计同比等 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `month`(如 2025年03月份)、`current`(当月值)、`yoy`(同比 %)、`cumulative`(累计值)、`cumulative_yoy`(累计同比 %)、`unit`(亿元)、`currency`(CNY),以表格展示给用户。
        
        ## 4. 响应说明
        
        返回值为社会消费品零售总额月度计算结果列表,按时间倒序。
        
        ### RetailSalesComputed 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | month | String | 否 | 月份,格式如 2025年03月份 | - |
        | current | float | 是 | 当月值 | 见 unit |
        | yoy | float | 是 | 同比增长 | % |
        | cumulative | float | 是 | 累计值 | 见 unit |
        | cumulative_yoy | float | 是 | 累计同比增长 | % |
        | unit | String | 否 | 货币单位 | - |
        | currency | String | 否 | 货币种类 | - |
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/economic/china-retail-sales
        ```
        
        ## 6. 注意事项
        
        - 返回按月份汇总,格式如「2025年03月份」,列表已按时间倒序,最新月份在前。
        - 金额单位见 `unit`(通常为亿元),同比单位为 %,各数值字段可为 null。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-retail-sales-monthly
        ```
        
    • economic-china-tax-revenue-monthly
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """中国全国税收收入(月度)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/economic/china-tax-revenue"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.3 KB
        ---
        name: economic-china-tax-revenue-monthly
        description: Get China tax revenue monthly data (全国税收收入 月度). Use when user asks about 全国税收收入, 税收收入, 税收月度, 中国税收, China tax revenue.
        ---
        
        # 中国经济 - 全国税收收入(月度)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 全国税收收入(月度汇总计算结果) |
        | 外部接口 | GET /api/v1/market/data/economic/china-tax-revenue |
        | 请求方式 | GET |
        | 适用场景 | 获取中国全国税收收入月度数据,含当月值、累计值、累计同比、月度环比、月度同比等 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 用法
        
        直接执行:
        
        ```bash
        python script/handler.py
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `month`(如 2025年05月)、`monthly_revenue`(当月税收,亿元)、`cumulative_revenue`(累计税收,亿元)、`yoy_growth`(累计同比 %)、`mom_growth`(月度环比 %)、`monthly_yoy_growth`(月度同比 %),以表格展示给用户。
        
        ## 4. 响应说明
        
        返回值为全国税收收入月度计算结果列表,按时间倒序。
        
        ### TaxRevenueComputed 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | month | String | 否 | 月份,格式如 2025年05月 | - |
        | monthly_revenue | float | 是 | 税收收入合计当月值 | 亿元 |
        | cumulative_revenue | float | 是 | 税收收入合计累计值 | 亿元 |
        | yoy_growth | float | 是 | 税收收入合计累计同比 | % |
        | mom_growth | float | 是 | 月度环比((当月值 - 上月值) / 上月值 × 100) | % |
        | monthly_yoy_growth | float | 是 | 月度同比((当月值 - 去年同月值) / 去年同月值 × 100) | % |
        
        注:金额单位为亿元。
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/economic/china-tax-revenue
        ```
        
        ## 6. 注意事项
        
        - 返回按月份汇总,格式如「2025年05月」,列表已按时间倒序,最新月份在前。
        - 金额单位为亿元,同比/环比单位为 %,各数值字段可为 null。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-china-tax-revenue-monthly
        ```
        
    • economic-us-economic-by-type
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          """美国经济指标按 type 查询(统一接口)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          VALID_TYPES = [
              "ism-manufacturing",
              "ism-non-manufacturing",
              "nonfarm-payroll",
              "trade-balance",
              "unemployment-rate",
              "ppi-mom",
              "cpi-mom",
              "cpi-yoy",
              "core-cpi-mom",
              "core-cpi-yoy",
              "housing-starts",
              "existing-home-sales",
              "durable-goods-orders-mom",
              "cb-consumer-confidence",
              "gdp-yoy-preliminary",
              "fed-funds-rate-upper",
          ]
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(
                  description="按 type 查询美国经济指标,返回时间序列(前值、现值、发布日期)"
              )
              parser.add_argument(
                  "--type",
                  required=True,
                  choices=VALID_TYPES,
                  help="指标类型,如 ism-manufacturing, nonfarm-payroll, cpi-mom 等",
              )
              args = parser.parse_args()
          
              params = {"type": args.type}
              url = f"{BASE_URL}/api/v1/market/data/economic/us-economic?" + urllib.parse.urlencode(params)
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3.8 KB
        ---
        name: economic-us-economic-by-type
        description: Get US economic indicator time series by type (美国经济指标按类型查询). Use when user asks about 美国 ISM, 美国非农, 美国 CPI/PPI, 美国失业率, 美国贸易帐, 美国新屋开工, 美国成屋销售, 美国耐用品订单, 美国咨商会信心指数, 美国 GDP 年率, 美国联邦基金利率, US economic data.
        ---
        
        # 美国经济指标 - 按类型查询
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 按类型查询美国经济指标(统一接口) |
        | 外部接口 | GET /api/v1/market/data/economic/us-economic |
        | 请求方式 | GET |
        | 适用场景 | 通过参数 `type` 指定指标类型,获取对应美国经济指标的时间序列(时间、前值、现值、发布日期);支持 16 类指标 |
        
        ## 2. 请求参数
        
        说明:`type` 为必填项,指定要查询的指标类型。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | type | string | 是 | 指标类型 | ism-manufacturing | 见下方「type 取值列表」 |
        
        ### type 取值列表
        
        | type 值 | 说明 | 单位/备注 |
        |---------|------|-----------|
        | ism-manufacturing | 美国 ISM 制造业指数 | 百分比 |
        | ism-non-manufacturing | 美国 ISM 非制造业指数 | 百分比 |
        | nonfarm-payroll | 美国非农就业人数变化 | 万人(接口已换算) |
        | trade-balance | 美国贸易帐 | 亿美元(接口已换算) |
        | unemployment-rate | 美国失业率 | 百分比 |
        | ppi-mom | 美国生产者物价指数月率 | 百分比 |
        | cpi-mom | 美国消费者物价指数月率 | 百分比 |
        | cpi-yoy | 美国消费者物价指数年率 | 百分比 |
        | core-cpi-mom | 美国核心消费者物价指数月率 | 百分比 |
        | core-cpi-yoy | 美国核心消费者物价指数年率 | 百分比 |
        | housing-starts | 美国新屋开工 | 万户(接口已换算) |
        | existing-home-sales | 美国成屋销售 | 万套 |
        | durable-goods-orders-mom | 美国耐用品订单月率 | 百分比 |
        | cb-consumer-confidence | 美国咨商会消费者信心指数 | 无单位 |
        | gdp-yoy-preliminary | 美国 GDP 年率初值 | 百分比;季度数据,时间为「YYYY年第N季度」 |
        | fed-funds-rate-upper | 美国央行公布利率决议(上限) | 百分比 |
        
        ## 3. 用法
        
        从用户问题中识别要查的美国经济指标类型,对应到上表 `type` 值,执行:
        
        ```bash
        python script/handler.py --type <type值>
        ```
        
        示例:
        
        ```bash
        python script/handler.py --type ism-manufacturing
        python script/handler.py --type nonfarm-payroll
        python script/handler.py --type cpi-mom
        python script/handler.py --type fed-funds-rate-upper
        ```
        
        脚本输出 JSON 数组,按时间倒序,每项含 `month`(或季度格式)、`prev_value`、`current_value`、`release_date`,以表格展示给用户。
        
        ## 4. 响应说明
        
        返回值为该指标的时间序列数组,按时间倒序。
        
        ### 单条记录结构(UsEconomicItem)
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | month | String | 否 | 时间;月度为「YYYY年MM月」,季度(如 GDP)为「YYYY年第N季度」 | - |
        | prev_value | Number/String | 是 | 上一期数值 | 见各 type 说明 |
        | current_value | Number/String | 是 | 当期数值,最新期可能为 null | 见各 type 说明 |
        | release_date | String | 否 | 发布日期,YYYY-MM-DD | - |
        
        说明:部分指标为季度(如 gdp-yoy-preliminary),其余为月度。
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/economic/us-economic?type=ism-manufacturing
        ```
        
        ## 6. 注意事项
        
        - 必须传入合法的 `type`,否则接口可能报错或返回空。
        - 数组按时间倒序,最新在前;数值单位见各 type 说明。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> economic-us-economic-by-type --type cpi-yoy
        ```
        
    • etf-adjust-factor
      • scripts
        • handler.py 3 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v1/market/data/etf-adjust-factor"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed = urllib.parse.urlparse(url)
              base = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询 ETF 复权因子")
              parser.add_argument("--symbol")
              parser.add_argument("--trade-date")
              parser.add_argument("--start-date")
              parser.add_argument("--end-date")
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", type=int, default=50)
              args = parser.parse_args()
              if args.page < 1 or not 1 <= args.page_size <= 2000:
                  parser.error("page 须 >= 1,page-size 须在 1~2000 之间")
              if bool(args.start_date) != bool(args.end_date):
                  parser.error("start-date 与 end-date 必须同时提供")
              if args.start_date and not args.symbol:
                  parser.error("区间扫描必须提供 symbol")
              params = {"page": args.page, "page_size": args.page_size}
              for name in ("symbol", "trade_date", "start_date", "end_date"):
                  value = getattr(args, name)
                  if value:
                      params[name] = value
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params)
              request = urllib.request.Request(url, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 692 B
        ---
        name: etf-adjust-factor
        description: 查询 ETF 复权因子。支持 --symbol、--trade-date 或带 symbol 的 --start-date/--end-date 区间查询,以及 --page/--page-size。接口:GET /api/v1/market/data/etf-adjust-factor。
        ---
        
        # ETF复权因子
        
        查询 ETF 复权因子。支持 --symbol、--trade-date 或带 symbol 的 --start-date/--end-date 区间查询,以及 --page/--page-size。接口:GET /api/v1/market/data/etf-adjust-factor。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> etf-adjust-factor
        ```
        
    • etf-announcements
      • scripts
        • handler.py 4.5 KB
          #!/usr/bin/env python3
          """ETF 公告列表(GET /api/v2/market/data/announcements/etf-announcements)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v2/market/data/announcements/etf-announcements"
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def build_params(args):
              params = {"page": args.page, "page_size": args.page_size}
              if args.etf_code is not None:
                  params["etf_code"] = args.etf_code
              if args.start_date is not None:
                  params["start_date"] = args.start_date
              if args.end_date is not None:
                  params["end_date"] = args.end_date
              return params
          
          
          def fetch_page(params):
              query = urllib.parse.urlencode(params)
              req = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}?{query}",
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(
                  description="ETF 公告列表:按标的(--etf-code)或按单日日期(--start-date)查询"
              )
              parser.add_argument("--etf-code", dest="etf_code", default=None,
                                  help="ETF 代码,支持裸代码/短后缀/长后缀,如 159915、159915.SZ、510300.XSHG")
              parser.add_argument("--start-date", dest="start_date", default=None,
                                  help="日期 YYYYMMDD(按日期查询时必填,仅支持单日)")
              parser.add_argument("--end-date", dest="end_date", default=None,
                                  help="日期 YYYYMMDD;不填默认等于 start_date,且必须等于 start_date")
              parser.add_argument("--page", type=int, required=True, help="页码(必填)")
              parser.add_argument("--page-size", dest="page_size", type=int, required=True, help="每页条数(必填)")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量数据")
              args = parser.parse_args()
          
              if args.etf_code is None and args.start_date is None:
                  print("必须提供 --etf-code(按标的查)或 --start-date(按日期查)", file=sys.stderr)
                  raise SystemExit(2)
          
              params = build_params(args)
              if args.fetch_all:
                  first = fetch_page(params)
                  data = first.get("data") or {}
                  records = list(data.get("records", []))
                  total_pages = data.get("pages") or 1
                  for page in range(2, total_pages + 1):
                      page_data = fetch_page({**params, "page": page})
                      records.extend((page_data.get("data") or {}).get("records", []))
                  result = {
                      "records": records,
                      "pages": total_pages,
                      "total": data.get("total", len(records)),
                  }
              else:
                  result = fetch_page(params)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2.9 KB
          #!/usr/bin/env python3
          """Tests for etf-announcements handler"""
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          def _run(argv):
              with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                  with patch.object(sys, "argv", ["handler.py"] + argv):
                      with patch("sys.stdout", new_callable=StringIO) as out:
                          handler.main()
                          return json.loads(out.getvalue())
          
          
          class TestModes(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_requires_code_or_date(self):
                  with self.assertRaises(SystemExit):
                      _run(["--page", "1", "--page-size", "5"])
          
              @patch.object(handler, "safe_urlopen")
              def test_by_symbol(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = (
                      b'{"code":200,"data":{"records":[],"pages":1,"total":0}}'
                  )
                  _run(["--etf-code", "159915", "--page", "1", "--page-size", "5"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("/api/v2/market/data/announcements/etf-announcements", req.full_url)
                  self.assertIn("etf_code=159915", req.full_url)
                  self.assertIn("page=1", req.full_url)
                  self.assertIn("page_size=5", req.full_url)
          
              @patch.object(handler, "safe_urlopen")
              def test_by_single_date(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = (
                      b'{"code":200,"data":{"records":[],"pages":1,"total":0}}'
                  )
                  _run(["--start-date", "20260831", "--page", "1", "--page-size", "5"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("start_date=20260831", req.full_url)
                  self.assertNotIn("end_date", req.full_url)
          
          
          class TestFetchAll(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_all_aggregates_pages(self, mock_open):
                  page1 = {"code": 200, "data": {"records": [{"announcement_id": "1"}], "pages": 2, "total": 2}}
                  page2 = {"code": 200, "data": {"records": [{"announcement_id": "2"}], "pages": 2, "total": 2}}
                  mock_open.return_value.__enter__.return_value.read.side_effect = [
                      json.dumps(page1).encode(), json.dumps(page2).encode(),
                  ]
                  result = _run(["--etf-code", "159915", "--page", "1", "--page-size", "1", "--all"])
                  self.assertEqual([r["announcement_id"] for r in result["records"]], ["1", "2"])
                  self.assertEqual(result["total"], 2)
                  second_req = mock_open.call_args_list[1][0][0]
                  self.assertIn("page=2", second_req.full_url)
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 3 KB
        ---
        name: etf-announcements
        description: 查询 ETF 公告列表(etf_announcements)。用户问 ETF 公告、基金公告、ETF 中期报告/年报公告、按日期查全市场 ETF 公告、公告 PDF 下载地址时使用。
        ---
        
        # ETF 公告列表
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | ETF 公告列表(etf_announcements) |
        | 外部接口 | `GET /api/v2/market/data/announcements/etf-announcements` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 按标的查单只 ETF 全部公告,或按单日日期查全市场 ETF 公告;公告正文 PDF 可按返回的 `url_hash` 另行下载 |
        
        两种模式二选一:
        
        1. **按标的**:传 `etf_code`,查单只 ETF 所有公告。
        2. **按日期**:传 `start_date`(必须等于 `end_date`,仅支持单日),查指定日期全市场 ETF 公告。
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | etf_code | string | 二选一 | ETF 代码 | 159915 | 支持裸代码/短后缀/长后缀(`159915.SZ`、`510300.XSHG`),大小写不敏感 |
        | start_date | string | 二选一 | 日期 | 20260831 | YYYYMMDD;按日期查询时必填,仅支持单日 |
        | end_date | string | 否 | 日期 | 20260831 | 不填默认等于 `start_date`,且必须等于 `start_date` |
        | page | int | 是 | 页码 | 1 | 必填 |
        | page_size | int | 是 | 每页条数 | 5 | 必填 |
        | --all | - | 否 | 自动翻页拉全量 | - | 仅本子 skill 扩展参数 |
        
        ## 3. 响应说明
        
        外层固定为 `code` / `message` / `data`。`data` 为分页对象:`pageNum` / `pageSize` / `total` / `pages` / `records`。
        
        records 元素:
        
        | 字段名 | 类型 | 说明 |
        |--------|------|------|
        | etf_code | string | ETF 代码,6 位裸代码,不带交易所后缀 |
        | etf_name | string | ETF 名称 |
        | announcement_id | string | 公告 id |
        | announcement_title | string | 公告标题 |
        | announcement_time | string | 公告时间,格式 YYYY-MM-DD HH:MM:SS |
        | url_hash | string | 公告文件 URL 哈希,用于下载正文文件 |
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> etf-announcements --etf-code 159915 --page 1 --page-size 5
        python <RUN_PY> etf-announcements --start-date 20260831 --page 1 --page-size 20
        python <RUN_PY> etf-announcements --etf-code 159915 --page 1 --page-size 20 --all
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - 必须提供 `etf_code` 或 `start_date` 之一,否则服务端报错(handler 会本地校验并拒绝)。
        - 按日期查询仅支持单日:`end_date` 不填默认等于 `start_date`,填了则必须相等。
        - ETF 范围为场内 ETF,不含联接基金、LOF。
        - 公告正文下载:`GET /api/v2/market/data/announcements/etf-announcements/{url_hash}`,响应为 PDF 二进制附件;`url_hash` 须取自本接口返回值,勿硬编码。
        - 当前数据源仅覆盖深市 ETF(`159` 开头);查询沪市代码会返回空列表。
        
    • etf-candlesticks
      • scripts
        • handler.py 4.7 KB
          #!/usr/bin/env python3
          """查询单只 ETF 历史 K 线(GET /api/v1/market/data/etf-candlesticks)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/etf-candlesticks"
          
          INTERVAL_UNITS = ("Minute", "Day", "Week", "Month", "Year")
          ADJUST_KINDS = ("None", "Forward", "Backward")
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def build_body(symbol, interval_unit, interval_value, adjust_kind,
                         since_ts_millis, until_ts_millis, limit):
              body = {
                  "symbol": symbol,
                  "interval_unit": interval_unit,
                  "until_ts_millis": until_ts_millis,
              }
              if interval_value is not None and interval_value != 1:
                  body["interval_value"] = interval_value
              if adjust_kind and adjust_kind != "None":
                  body["adjust_kind"] = adjust_kind
              if since_ts_millis is not None:
                  body["since_ts_millis"] = since_ts_millis
              if limit is not None:
                  body["limit"] = limit
              return body
          
          
          def fetch(
          symbol, interval_unit, interval_value, adjust_kind,
                    since_ts_millis, until_ts_millis, limit):
              body = build_body(symbol, interval_unit, interval_value, adjust_kind,
                                since_ts_millis, until_ts_millis, limit)
              query = urllib.parse.urlencode(body, doseq=True)
              url = f"{BASE_URL}{ENDPOINT}?{query}"
              req = urllib.request.Request(
                  url,
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  msg = e.read().decode()
                  print(f"HTTP {e.code}: {msg}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询单只 ETF 历史 K 线(GET 查询参数)")
              parser.add_argument("--symbol", required=True, help="ETF 代码,如 510300.XSHG、159915.XSHE")
              parser.add_argument("--interval-unit", dest="interval_unit", required=True,
                                  choices=INTERVAL_UNITS, help="K 线周期:Minute/Day/Week/Month/Year")
              parser.add_argument("--interval-value", dest="interval_value", type=int, default=1,
                                  help="间隔数值,默认 1(Minute+5 表示 5 分钟 K)")
              parser.add_argument("--adjust-kind", dest="adjust_kind", default="None",
                                  choices=ADJUST_KINDS, help="复权:None(默认)/Forward/Backward")
              parser.add_argument("--since-ts-millis", dest="since_ts_millis", type=int, default=None,
                                  help="开始时间戳(毫秒);分钟 K 与 until 跨度 ≤3 天")
              parser.add_argument("--until-ts-millis", dest="until_ts_millis", required=True, type=int,
                                  help="结束时间戳(毫秒)")
              parser.add_argument("--limit", type=int, default=None, help="返回条数上限")
              args = parser.parse_args()
          
              data = fetch(args.symbol, args.interval_unit, args.interval_value,
                           args.adjust_kind, args.since_ts_millis, args.until_ts_millis, args.limit)
              print(json.dumps(data, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 3.9 KB
          #!/usr/bin/env python3
          """Tests for etf-candlesticks handler"""
          import json
          import sys
          import unittest
          import urllib.error
          from io import BytesIO, StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestBuildBody(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_required_fields(self):
                  b = handler.build_body("510300.XSHG", "Day", 1, "None", None, 1756791000000, None)
                  self.assertEqual(b["symbol"], "510300.XSHG")
                  self.assertEqual(b["interval_unit"], "Day")
                  self.assertEqual(b["until_ts_millis"], 1756791000000)
                  self.assertNotIn("interval_value", b)
                  self.assertNotIn("adjust_kind", b)
                  self.assertNotIn("since_ts_millis", b)
                  self.assertNotIn("limit", b)
          
              def test_optional_fields(self):
                  b = handler.build_body("510300.XSHG", "Minute", 5, "Forward",
                                         1756700000000, 1756791000000, 100)
                  self.assertEqual(b["interval_value"], 5)
                  self.assertEqual(b["adjust_kind"], "Forward")
                  self.assertEqual(b["since_ts_millis"], 1756700000000)
                  self.assertEqual(b["limit"], 100)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_post_body_sent(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("510300.XSHG", "Day", 1, "None", None, 1756791000000, 5)
                  req = mock_open.call_args[0][0]
                  self.assertEqual(req.get_method(), "GET")
                  self.assertIn("/api/v1/market/data/etf-candlesticks", req.full_url)
                  self.assertIsNone(req.data)
                  self.assertEqual(req.full_url.split("?", 1)[1], "symbol=510300.XSHG&interval_unit=Day&until_ts_millis=1756791000000&limit=5")
                  self.assertEqual(req.headers.get("X-client-name"), "ft-claw")
                  self.assertEqual(req.headers.get("Content-type"), "application/json")
          
              @patch.object(handler, "safe_urlopen")
              def test_http_error_exits(self, mock_open):
                  mock_open.side_effect = urllib.error.HTTPError(
                      "https://fake", 500, "Internal Error", {}, BytesIO(b"server error")
                  )
                  with self.assertRaises(SystemExit):
                      handler.fetch("510300.XSHG", "Day", 1, "None", None, 1756791000000, None)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = (
                      b'[{"open":"4.5","high":"4.6","low":"4.4","close":"4.55","ts_millis":1756710000000,'
                      b'"ts_millis_open":1756690200000,"turnover":"100","volume":1000}]'
                  )
                  with patch.object(sys, "argv", [
                      "handler.py", "--symbol", "510300.XSHG", "--interval-unit", "Day",
                      "--until-ts-millis", "1756791000000", "--limit", "5"
                  ]):
                      with patch("sys.stdout", new_callable=StringIO) as fake_out:
                          handler.main()
                          data = json.loads(fake_out.getvalue())
                          self.assertEqual(len(data), 1)
                          self.assertEqual(data[0]["close"], "4.55")
          
              def test_main_missing_until_exits(self):
                  with patch.object(sys, "argv", [
                      "handler.py", "--symbol", "510300.XSHG", "--interval-unit", "Day"
                  ]):
                      with self.assertRaises(SystemExit):
                          handler.main()
          
          
          class TestSafeUrlopen(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_rejects_wrong_host(self):
                  with self.assertRaises(SystemExit):
                      handler.safe_urlopen("https://evil.com/api")
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 3.9 KB
        ---
        name: etf-candlesticks
        description: 单只 ETF 历史 K 线 GET 接口(market.ft.tech,etf-candlesticks)。用户问某只 ETF 的分/日/周/月/年 K 线、开高低收、前/后复权、分钟级 K 线、5 分钟/日 K/周 K/月 K/年 K 时使用。必填 --symbol、--interval-unit、--until-ts-millis;可选 --interval-value、--adjust-kind、--since-ts-millis、--limit。
        ---
        
        # ETF K 线 - 查询单只 ETF 历史 K 线(etf-candlesticks)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询单只 ETF 历史 K 线 |
        | 外部接口 | `GET /api/v1/market/data/etf-candlesticks` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 获取指定 ETF 的分/日/周/月/年 K 线,含开高低收、成交量、成交额;支持前复权 / 后复权 / 不复权。仅接受 ETF 标的 |
        
        > 与 `etf-ohlcs`(`GET daec/history/ohlcs`,YYYYMMDD 日期区间,仅日/周/月)区别:本接口走 GET query 参数,参数为毫秒时间戳,支持分钟级与年 K,是统一的 candlesticks 契约。
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 是 | ETF 代码(带市场后缀) | 510300.XSHG、159915.XSHE | 也接受 `.SH`/`.SZ` 短后缀;非 ETF 标的当前返回系统错误 |
        | interval_unit | string | 是 | 周期单位 | Day | Minute/Day/Week/Month/Year |
        | interval_value | int | 否 | 间隔数值 | 1 | 默认 1;Minute+5 表示 5 分钟 K 线 |
        | adjust_kind | string | 否 | 复权类型 | Forward | None(默认)/Forward(前复权)/Backward(后复权) |
        | since_ts_millis | int | 否 | 开始时间戳(毫秒) | 1756700000000 | 分钟 K 线与 until 跨度 ≤3 天,其余周期不受 3 天限制 |
        | until_ts_millis | int | 是 | 结束时间戳(毫秒) | 1756791000000 | - |
        | limit | int | 否 | 返回条数上限 | 5 | 未传 since 和 limit 时默认最多返回 50 根 |
        
        ## 3. 响应说明
        
        返回裸数组,每根 K 线包含:
        
        | 字段名 | 类型 | 说明 | 单位 |
        |--------|------|------|------|
        | open | string | 开盘价 | 元 |
        | high | string | 最高价 | 元 |
        | low | string | 最低价 | 元 |
        | close | string | 收盘价(或最新价) | 元 |
        | ts_millis | int | 收盘时间戳 | 毫秒 |
        | ts_millis_open | int | 开盘时间戳 | 毫秒 |
        | turnover | string | 成交额 | 元 |
        | volume | int64 | 成交量 | 份 |
        
        ## 4. 调用方式
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> etf-candlesticks --symbol 510300.XSHG --interval-unit Day --since-ts-millis 1756700000000 --until-ts-millis 1756791000000 --limit 5
        python <RUN_PY> etf-candlesticks --symbol 510300.XSHG --interval-unit Minute --interval-value 5 --adjust-kind Forward --since-ts-millis 1756700000000 --until-ts-millis 1756791000000 --limit 100
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON,请求头已内置 `X-Client-Name: ft-claw`。
        
        ### 直接执行 handler(调试)
        
        ```bash
        python scripts/handler.py --symbol 510300.XSHG --interval-unit Day --until-ts-millis 1756791000000 --limit 5
        ```
        
        ## 5. 注意事项
        
        - `symbol`、`interval_unit`、`until_ts_millis` 必填。
        - `symbol` 必须是 ETF 代码,格式 `{代码}.{市场}`;非 ETF 标的当前外部接口返回系统错误。
        - 分钟 K 线(`interval_unit=Minute`)的 `since/until` 跨度硬限制 ≤3 天,超过需分段调用。
        - `interval_value` 仅在 `interval_unit=Minute` 时生效:不传或传 1 为 1 分钟 K,传 5/15/30/60/120 为对应多分钟 K;其他周期忽略该字段。
        - 多分钟 K 按北京时间的每个交易日分别聚合,不跨交易日;以 5 分钟 K 为例,首根为 09:30—09:35,开高低收取区间首根开盘价、最高价、最低价、末根收盘价,成交量和成交额按区间求和。
        - 默认不复权(None),`Forward` 前复权、`Backward` 后复权。
        - 价格字段 JSON 中为字符串以避免精度丢失。
        
    • etf-candlesticks-batch
      • scripts
        • handler.py 4.9 KB
          #!/usr/bin/env python3
          """批量查询多只 ETF K 线(GET /api/v2/market/data/etf-candlesticks/batch)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v2/market/data/etf-candlesticks/batch"
          
          INTERVAL_UNITS = ("Day", "Week", "Month", "Year")
          ADJUST_KINDS = ("None", "Forward", "Backward")
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def parse_symbols(raw):
              syms = [s.strip() for s in raw.split(",") if s.strip()]
              if not syms:
                  print("--symbols 不能为空", file=sys.stderr)
                  sys.exit(1)
              return syms
          
          
          def build_query(symbols, interval_unit, adjust_kind, since_ts_millis, until_ts_millis, limit):
              body = {
                  "symbols": symbols,
                  "interval_unit": interval_unit,
                  "until_ts_millis": until_ts_millis,
              }
              if adjust_kind and adjust_kind != "None":
                  body["adjust_kind"] = adjust_kind
              if since_ts_millis is not None:
                  body["since_ts_millis"] = since_ts_millis
              if limit is not None:
                  body["limit"] = limit
              return body
          
          
          def fetch(symbols, interval_unit, adjust_kind, since_ts_millis, until_ts_millis, limit):
              body = build_query(symbols, interval_unit, adjust_kind, since_ts_millis, until_ts_millis, limit)
              query = urllib.parse.urlencode(body, doseq=True)
              req = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}?{query}",
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="批量获取多只 ETF 的历史 K 线(不支持分钟周期)")
              parser.add_argument("--symbols", required=True,
                                  help="ETF 代码列表,逗号分隔,如 510300.XSHG,159915.XSHE;也接受 .SH/.SZ 短后缀")
              parser.add_argument("--interval-unit", dest="interval_unit", required=True, type=str.capitalize,
                                  choices=INTERVAL_UNITS, help="K 线周期:Day/Week/Month/Year(大小写不敏感,不支持 Minute)")
              parser.add_argument("--adjust-kind", dest="adjust_kind", default="None",
                                  choices=ADJUST_KINDS, help="复权:None(默认)/Forward/Backward")
              parser.add_argument("--since-ts-millis", dest="since_ts_millis", required=True, type=int,
                                  help="开始时间戳(毫秒);与结束时间跨度不得超过 12 个日历月")
              parser.add_argument("--until-ts-millis", dest="until_ts_millis", required=True, type=int,
                                  help="结束时间戳(毫秒)")
              parser.add_argument("--limit", type=int, default=None,
                                  help="每个标的返回条数上限;不传时返回请求时间范围内的全部数据")
              args = parser.parse_args()
          
              if args.since_ts_millis > args.until_ts_millis:
                  print("--since-ts-millis 不能晚于 --until-ts-millis", file=sys.stderr)
                  raise SystemExit(2)
          
              symbols = parse_symbols(args.symbols)
              data = fetch(symbols, args.interval_unit, args.adjust_kind,
                           args.since_ts_millis, args.until_ts_millis, args.limit)
              print(json.dumps(data, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 5 KB
          #!/usr/bin/env python3
          """Tests for etf-candlesticks-batch handler"""
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          SINCE = "1756431000000"
          UNTIL = "1756791000000"
          
          
          class TestBuildQuery(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_required_only(self):
                  body = handler.build_query(["510300.SH"], "Day", "None", 1756431000000, 1756791000000, None)
                  self.assertEqual(body["symbols"], ["510300.SH"])
                  self.assertEqual(body["interval_unit"], "Day")
                  self.assertEqual(body["since_ts_millis"], 1756431000000)
                  self.assertEqual(body["until_ts_millis"], 1756791000000)
                  self.assertNotIn("adjust_kind", body)
                  self.assertNotIn("limit", body)
                  self.assertNotIn("interval_value", body)
          
              def test_optional_fields(self):
                  body = handler.build_query(["510300.SH", "159915.SZ"], "Week", "Forward",
                                             1756700000000, 1756791000000, 5)
                  self.assertEqual(body["symbols"], ["510300.SH", "159915.SZ"])
                  self.assertEqual(body["adjust_kind"], "Forward")
                  self.assertEqual(body["limit"], 5)
          
              def test_minute_not_allowed(self):
                  with self.assertRaises(SystemExit):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "510300.SH",
                                                      "--interval-unit", "Minute",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          handler.main()
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_query_string_expands_symbols(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch(["510300.SH", "159915.SZ"], "Day", "None", 1756431000000, 1756791000000, 2)
                  req = mock_open.call_args[0][0]
                  self.assertEqual(req.get_method(), "GET")
                  self.assertIn("/api/v2/market/data/etf-candlesticks/batch", req.full_url)
                  query = req.full_url.split("?", 1)[1]
                  self.assertIn("symbols=510300.SH", query)
                  self.assertIn("symbols=159915.SZ", query)
                  self.assertIn("interval_unit=Day", query)
                  self.assertIn("since_ts_millis=1756431000000", query)
                  self.assertEqual(req.headers.get("X-client-name"), "ft-claw")
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = (
                      b'{"code":200,"message":"success","data":[["510300.SH",'
                      b'[{"open":4.55,"close":4.601,"ts_millis":"1756450800000"}]]]}'
                  )
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "510300.SH",
                                                      "--interval-unit", "Day",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL, "--limit", "2"]):
                          with patch("sys.stdout", new_callable=StringIO) as out:
                              handler.main()
                              data = json.loads(out.getvalue())
                              self.assertEqual(data["data"][0][0], "510300.SH")
          
              def test_main_requires_since(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "510300.SH",
                                                      "--interval-unit", "Day",
                                                      "--until-ts-millis", UNTIL]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              def test_main_rejects_since_after_until(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "510300.SH",
                                                      "--interval-unit", "Day",
                                                      "--since-ts-millis", UNTIL,
                                                      "--until-ts-millis", SINCE]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              def test_main_requires_symbols(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--interval-unit", "Day",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 3.7 KB
        ---
        name: etf-candlesticks-batch
        description: 批量查询多只 ETF 的历史 K 线(etf_candlesticks_batch)。用户问多只 ETF 日/周/月/年 K 线、批量 ETF 开高低收、ETF 批量行情、多 ETF 对比 K 线时使用。
        ---
        
        # 批量ETFK线
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 批量ETFK线(etf_candlesticks_batch) |
        | 外部接口 | `GET /api/v2/market/data/etf-candlesticks/batch` |
        | 请求方式 | GET(query 参数,`symbols` 可重复传入) |
        | 适用场景 | 一次批量获取多只 ETF 的历史 K 线(开高低收、成交量、成交额、换手率),支持日/周/月/年周期与前复权/后复权 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbols | string[] | 是 | ETF 代码列表,逗号分隔传给 CLI | 510300.SH,159915.SZ | 沪市支持 `.XSHG`/`.SH`,深市支持 `.XSHE`/`.SZ`;接口侧以重复 query 参数发送 |
        | interval_unit | string | 是 | 周期单位 | Day | Day/Week/Month/Year,大小写不敏感;**不支持 Minute** |
        | adjust_kind | string | 否 | 复权类型 | Forward | None(默认)/Forward(前复权)/Backward(后复权) |
        | since_ts_millis | int | 是 | 开始时间戳(毫秒) | 1756431000000 | 与 until 的跨度不得超过 12 个日历月;不得晚于 until |
        | until_ts_millis | int | 是 | 结束时间戳(毫秒) | 1756791000000 | - |
        | limit | int | 否 | 每个标的返回条数上限 | 2 | 不传时返回请求时间范围内的全部数据 |
        
        ## 3. 响应说明
        
        外层固定为 `code`(成功 200)/ `message`(成功 `success`)/ `data`(失败时为 `null`)。`data` 为非分页嵌套数组,外层每项为 `[symbol, K线数组]`,每根 K 线字段:
        
        | 字段名 | 类型 | 说明 | 单位 |
        |--------|------|------|------|
        | symbol | string | ETF 代码,响应统一使用 `.SH`、`.SZ` 短后缀 | - |
        | open / high / low / close | number | 开/高/低/收盘价 | 元 |
        | ts_millis | string | 收盘时间戳 | 毫秒 |
        | ts_millis_open | string | 开盘时间戳 | 毫秒 |
        | turnover | number | 成交额 | 元 |
        | volume | integer | 成交量 | - |
        | turnover_rate | number | 换手率;ETF 标的当前为 `null` | % |
        
        注:`open/high/low/close`、`turnover` 在 JSON 中实际以字符串返回(避免精度丢失);`ts_millis` 为数字。
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> etf-candlesticks-batch --symbols 510300.SH,159915.SZ --interval-unit Day --since-ts-millis 1756431000000 --until-ts-millis 1756791000000 --limit 2
        python <RUN_PY> etf-candlesticks-batch --symbols 510300.XSHG,159915.XSHE --interval-unit Week --adjust-kind Forward --since-ts-millis 1754092800000 --until-ts-millis 1756791000000
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - `symbols`、`interval_unit`、`since_ts_millis`、`until_ts_millis` 必填;所有 `symbols` 使用相同周期。
        - 接口仅支持 GET;`symbols` 在查询参数中以重复参数形式发送(`symbols=510300.SH&symbols=159915.SZ`)。
        - 时间跨度最多 12 个日历月;需要更长历史时按窗口分段多次调用。
        - 不支持分钟 K 线;分钟数据请使用 `etf-minutes-batch` 子 skill。
        - `symbols` 中每项必须是 ETF 标的:若混入非 ETF(如股票),整个批量请求失败,不静默过滤(当前返回系统错误)。
        - 输入 `.XSHG`/`.XSHE` 长后缀时,响应中的 symbol 会规范化为 `.SH`、`.SZ` 短后缀。
        - 默认不复权(None);仅使用历史日 K 数据计算,不含实时行情,实际起始日期以行情数据源覆盖为准。
        
    • etf-component-details
      • scripts
        • handler.py 3 KB
          #!/usr/bin/env python3
          """ETF 成分证券明细(GET /api/v2/market/data/etf-component-details)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v2/market/data/etf-component-details"
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def build_params(args):
              params = {"symbol": args.symbol}
              if args.trade_date is not None:
                  params["trade_date"] = args.trade_date
              return params
          
          
          def fetch(params):
              query = urllib.parse.urlencode(params)
              req = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}?{query}",
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="按 ETF 和交易日查询申赎清单全部成分证券及数量")
              parser.add_argument("--symbol", required=True,
                                  help="ETF 代码,如 510300.SH、159915.SZ")
              parser.add_argument("--trade-date", dest="trade_date", type=int, default=None,
                                  help="交易日 YYYYMMDD;不传时查询该 ETF 最新可用交易日")
              args = parser.parse_args()
          
              print(json.dumps(fetch(build_params(args)), ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.7 KB
        ---
        name: etf-component-details
        description: 查询 ETF 成分证券明细(etf_component_details)。用户问 ETF 成分股明细、申赎清单成分证券、成分数量、现金替代标志、必须/禁止现金替代、ETF PCF 成分明细时使用。
        ---
        
        # ETF 成分证券明细
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | ETF 成分证券明细(etf_component_details) |
        | 外部接口 | `GET /api/v2/market/data/etf-component-details` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 按 ETF 和交易日返回申赎清单中的全部成分证券及数量、成分类型、现金替代标志与替代金额 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 是 | ETF 代码 | 510300.SH | 也支持 `159915.SZ` |
        | trade_date | int | 否 | 交易日 | 20260908 | YYYYMMDD;不传时查询该 ETF 最新可用交易日 |
        
        ## 3. 响应说明
        
        外层固定为 `code` / `message` / `data`。`data` 为成分证券明细数组(无分页,一次返回全部)。
        
        数组元素:
        
        | 字段名 | 类型 | 说明 |
        |--------|------|------|
        | symbol | string | ETF 代码 |
        | trade_date | int | 实际返回的交易日 YYYYMMDD |
        | component_code | string | 成分证券代码,带 `.SH`/`.SZ`/`.BJ`/`.HK` 后缀 |
        | component_name | string | 成分证券名称 |
        | component_type | string / null | `stock`/`bond`/`commodity`/`cash`;无法识别时为 `null` |
        | quantity | int / null | 成分证券数量(股) |
        | cash_substitution_flag | int / null | 现金替代原始编码 |
        | cash_substitution_type | string / null | `prohibited`/`allowed`/`mandatory`/`refund` |
        | creation_substitution_amount | number / null | 申购替代金额 |
        | redemption_substitution_amount | number / null | 赎回替代金额 |
        | general_substitution_amount | number / null | 通用替代金额 |
        
        现金替代编码:`0`=prohibited(禁止现金替代)、`1`=allowed(允许)、`2/4/6/8`=mandatory(必须)、`3/5/7`=refund(退补)。
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> etf-component-details --symbol 510300.SH
        python <RUN_PY> etf-component-details --symbol 510300.SH --trade-date 20260908
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - 数据范围 20250501 年至今。
        - `trade_date` 为精确查询条件:该日期没有数据时成功返回空数组,不向前回退。
        - 现金占位成分会保留,`component_type` 标记为 `cash`。
        - 不同交易所的现金替代编码语义可能存在差异;需要精确区分时以 `cash_substitution_flag` 为准。
        - 本接口不返回成分权重。
        
    • etf-components-all
      • scripts
        • handler.py 2.2 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v2/market/data/etf-components-all"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=60):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询全部或单只 ETF 成份列表")
              parser.add_argument("--symbol")
              args = parser.parse_args()
              url = BASE_URL + ENDPOINT
              if args.symbol:
                  url += "?" + urllib.parse.urlencode({"symbol": args.symbol})
              request = urllib.request.Request(url, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 580 B
        ---
        name: etf-components-all
        description: 查询全部或单只 ETF 成份列表。可选 --symbol;不传返回全部 ETF。接口:GET /api/v2/market/data/etf-components-all。
        ---
        
        # ETF成份列表
        
        查询全部或单只 ETF 成份列表。可选 --symbol;不传返回全部 ETF。接口:GET /api/v2/market/data/etf-components-all。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> etf-components-all
        ```
        
    • etf-description-all
      • scripts
        • handler.py 2.1 KB
          #!/usr/bin/env python3
          """查询全部 ETF 基础信息(market.ft.tech)"""
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              _require_api_key()
              url = BASE_URL + "/api/v2/market/data/etf-description-all"
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
              req.add_header("Content-Type", "application/json")
          
              try:
                  with safe_urlopen(req) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.5 KB
        ---
        name: etf-description-all
        description: 查询全部 ETF 基础信息(market.ft.tech)。用户问 ETF 列表、全部 ETF、ETF 代码与名称映射、按名称找 ETF 代码时使用。
        ---
        
        # ETF-查询全部ETF基础信息
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询全部ETF基础信息 |
        | 外部接口 | `/api/v2/market/data/etf-description-all` |
        | 请求方式 | GET |
        | 适用场景 | 获取 ETF 列表(代码与名称)及部分基础属性信息,用于列表展示、筛选与检索 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 响应说明
        
        返回值为 ETF 基础信息数组,数据模型如下:
        
        ```json
        [
          EtfDescriptionItem
        ]
        ```
        
        ### EtfDescriptionItem 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | asset_class | String | 否 | 基金类型,当前外部返回小写字符串(如 `stock`、`bond`、`commodity`、`currency`) | - |
        | custodian | String | 否 | 基金托管人(通常为托管银行) | - |
        | float_shares | int | 是 | 流通份额,无数据时为 null | 份 |
        | inception_date | String | 否 | 成立日期,格式 `YYYY-MM-DD` | - |
        | management_company | String | 否 | 基金管理人 | - |
        | name | String | 否 | ETF 名称 | - |
        | symbol | String | 否 | ETF 标的代码,带交易所后缀 | - |
        
        ## 4. 用法
        
        通过主目录 `run.py` 调用(无需参数):
        
        ```bash
        python <RUN_PY> etf-description-all
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。脚本输出 JSON 数组,可直接用于“名称 -> symbol”映射。
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/etf-description-all
        ```
        
        ## 6. 响应示例
        
        ```json
        [
          {
            "asset_class": "currency",
            "custodian": "交通银行",
            "float_shares": 3955933,
            "inception_date": "2013-03-29",
            "management_company": "易方达基金",
            "name": "货币ETF易方达",
            "symbol": "159001.XSHE"
          }
        ]
        ```
        
        ## 7. 数据更新时间与注意事项
        
        - 数据更新时间以接口/数据源为准。
        - 当用户只给 ETF 名称或简称时,建议先调用本接口做名称匹配,拿到唯一 `symbol` 后再调用 `etf-detail`、`etf-ohlcs`、`etf-prices`、`etf-components-all`、`etf-pre-single`。
        - 若名称匹配到多个 `symbol`,先让用户确认目标标的,再继续查询指标。
        
    • etf-minutes
      • scripts
        • handler.py 3 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v2/market/data/etf_minutes"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询 ETF 历史分钟行情")
              parser.add_argument("--symbol", required=True)
              parser.add_argument("--interval-value", dest="interval_value", type=int, default=1)
              parser.add_argument("--adjust-kind", dest="adjust_kind")
              parser.add_argument("--since-ts-millis", dest="since_ts_millis", required=True, type=int)
              parser.add_argument("--until-ts-millis", dest="until_ts_millis", required=True, type=int)
              parser.add_argument("--limit", type=int, default=50)
              args = parser.parse_args()
              if args.interval_value <= 0 or not 1 <= args.limit <= 1000:
                  parser.error("interval-value 须大于 0,limit 须在 1~1000 之间")
              params = {"symbol": args.symbol, "interval_value": args.interval_value,
                        "since_ts_millis": args.since_ts_millis, "until_ts_millis": args.until_ts_millis,
                        "limit": args.limit}
              if args.adjust_kind is not None:
                  params["adjust_kind"] = args.adjust_kind
              request = urllib.request.Request(BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params),
                                               headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 689 B
        ---
        name: etf-minutes
        description: ETF历史分钟行情。必填 --symbol;可选日期和分页参数。 用户询问ETF历史分钟行情时使用。
        ---
        
        # ETF历史分钟行情
        
        外部接口:`GET /api/v2/market/data/etf_minutes`。
        
        必填 --symbol;可选日期和分页参数。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> etf-minutes --symbol 510300.SH --since-ts-millis 1787189400000 --until-ts-millis 1787191200000 --limit 5
        ```
        
        接口返回 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
    • etf-minutes-batch
      • scripts
        • handler.py 1.7 KB
          #!/usr/bin/env python3
          import argparse,json,os,sys,urllib.parse,urllib.request
          BASE_URL=os.environ.get("FTSHARE_BASE_URL","https://market.ft.tech/gateway").rstrip("/");_REQUEST_HEADERS={"FTSHARE_API_KEY":os.environ.get("FTSHARE_API_KEY", ""),"Content-Type":"application/json"}; ENDPOINT="/api/v2/market/data/etf_minutes/batch"
          def main():
           p=argparse.ArgumentParser(); p.add_argument("--symbols",required=True); p.add_argument("--since-ts-millis",required=True,type=int); p.add_argument("--until-ts-millis",required=True,type=int); p.add_argument("--interval-value",type=int,default=1); p.add_argument("--adjust-kind",choices=["None","Forward","Backward"],default="None"); p.add_argument("--limit",type=int,default=50); a=p.parse_args()
           if a.interval_value<1 or not 1<=a.limit<=1000:p.error("interval-value 须大于 0,limit 须在 1~1000 之间")
           key=os.environ.get("FTSHARE_API_KEY")
           if not key: print("FTSHARE_API_KEY environment variable is required",file=sys.stderr); raise SystemExit(2)
           params=[("symbols",s.strip()) for s in a.symbols.split(",") if s.strip()]+[("since_ts_millis",a.since_ts_millis),("until_ts_millis",a.until_ts_millis),("interval_value",a.interval_value),("limit",a.limit)]
           if a.adjust_kind!="None":params.append(("adjust_kind",a.adjust_kind))
           req=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params),headers={"FTSHARE_API_KEY":key,"Content-Type":"application/json"},method="GET")
           try:
            with urllib.request.urlopen(req,timeout=30) as r: print(json.dumps(json.loads(r.read().decode()),ensure_ascii=False,indent=2))
           except Exception as e: print(f"请求失败: {e}",file=sys.stderr); raise SystemExit(1)
          if __name__=="__main__":main()
          
      • SKILL.md 719 B
        ---
        name: etf-minutes-batch
        description: 批量查询 ETF 历史分钟 K 线。用户询问多只 ETF 的分钟行情时使用。
        ---
        
        # 批量 ETF 历史分钟行情
        
        接口:`GET /api/v2/market/data/etf_minutes/batch`。必填 `--symbols`、`--since-ts-millis`、`--until-ts-millis`;`--symbols` 使用逗号分隔,最多 20 只;可选 `--interval-value`、`--adjust-kind`、`--limit`。时间跨度不超过 3 天,`limit` 范围 1~1000。
        
        ```bash
        python <RUN_PY> etf-minutes-batch --symbols 510300.SH,159915.SZ --since-ts-millis 1787189400000 --until-ts-millis 1787191200000 --limit 5
        ```
        
        返回 `code/message/data`,`data` 为每只标的的 `symbol`、`total` 和 `items` 列表;不是分页接口。
        
    • etf-net-value
      • scripts
        • handler.py 4.9 KB
          #!/usr/bin/env python3
          """ETF 历史净值(GET /api/v2/market/data/etf-net-value)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v2/market/data/etf-net-value"
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def build_params(args):
              params = {"etf_code": args.etf_code}
              if args.nav_date is not None:
                  params["nav_date"] = args.nav_date
              if args.start_date is not None:
                  params["start_date"] = args.start_date
              if args.end_date is not None:
                  params["end_date"] = args.end_date
              params["page"] = args.page
              params["page_size"] = args.page_size
              return params
          
          
          def fetch_page(params):
              query = urllib.parse.urlencode(params)
              req = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}?{query}",
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="按 ETF 代码分页查询历史净值")
              parser.add_argument("--etf-code", dest="etf_code", required=True,
                                  help="ETF 代码,如 510300")
              parser.add_argument("--nav-date", dest="nav_date", type=int, default=None,
                                  help="净值日期 YYYYMMDD;与日期区间参数互斥,二者必须二选一")
              parser.add_argument("--start-date", dest="start_date", type=int, default=None,
                                  help="净值开始日期 YYYYMMDD;须与 --end-date 同时提供")
              parser.add_argument("--end-date", dest="end_date", type=int, default=None,
                                  help="净值结束日期 YYYYMMDD;须与 --start-date 同时提供")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始,默认 1")
              parser.add_argument("--page-size", dest="page_size", type=int, default=50,
                                  help="每页条数,默认 50,最大 200")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量数据")
              args = parser.parse_args()
          
              if args.nav_date is not None and (args.start_date is not None or args.end_date is not None):
                  print("--nav-date 不能与 --start-date/--end-date 同时使用", file=sys.stderr)
                  raise SystemExit(2)
              if args.nav_date is None and (args.start_date is None or args.end_date is None):
                  print("必须提供 --nav-date,或同时提供 --start-date 和 --end-date", file=sys.stderr)
                  raise SystemExit(2)
          
              params = build_params(args)
              if args.fetch_all:
                  first = fetch_page(params)
                  data = first.get("data") or {}
                  items = list(data.get("items", []))
                  total_pages = data.get("total_pages") or 1
                  for page in range(2, total_pages + 1):
                      page_data = fetch_page({**params, "page": page})
                      items.extend((page_data.get("data") or {}).get("items", []))
                  result = {
                      "items": items,
                      "total_pages": total_pages,
                      "total_items": data.get("total_items", len(items)),
                  }
              else:
                  result = fetch_page(params)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2.1 KB
          #!/usr/bin/env python3
          """Tests for etf-net-value handler"""
          import os
          import sys
          import unittest
          from unittest.mock import patch
          import importlib.util
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          def _run(argv):
              with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                  with patch.object(sys, "argv", ["handler.py"] + argv):
                      handler.main()
          
          
          class TestDateModes(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_requires_nav_date_or_range(self):
                  with self.assertRaises(SystemExit):
                      _run(["--etf-code", "510300", "--page", "1", "--page-size", "5"])
          
              def test_rejects_nav_date_with_range(self):
                  with self.assertRaises(SystemExit):
                      _run(["--etf-code", "510300", "--nav-date", "20260909",
                            "--start-date", "20260901", "--end-date", "20260909"])
          
              def test_rejects_partial_range(self):
                  with self.assertRaises(SystemExit):
                      _run(["--etf-code", "510300", "--start-date", "20260901"])
          
              @patch.object(handler, "safe_urlopen")
              def test_accepts_nav_date(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"{}"
                  _run(["--etf-code", "510300", "--nav-date", "20260909"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("/api/v2/market/data/etf-net-value", req.full_url)
                  self.assertIn("etf_code=510300", req.full_url)
                  self.assertIn("nav_date=20260909", req.full_url)
          
              @patch.object(handler, "safe_urlopen")
              def test_accepts_range(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"{}"
                  _run(["--etf-code", "510300", "--start-date", "20260901", "--end-date", "20260909"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("start_date=20260901", req.full_url)
                  self.assertIn("end_date=20260909", req.full_url)
                  self.assertNotIn("nav_date", req.full_url)
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 3.1 KB
        ---
        name: etf-net-value
        description: 查询 ETF 历史净值(etf_net_value)。用户问 ETF 净值、单位净值、累计净值、净值增长率、复权净值、每万份收益、7 日年化收益率时使用。
        ---
        
        # ETF 历史净值
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | ETF 净值(etf_net_value) |
        | 外部接口 | `GET /api/v2/market/data/etf-net-value` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 按 ETF 代码分页查询历史净值:发布日、净值日期、资产净值、单位净值、累计净值、净值增长率、复权净值、每万份收益、7 日年化收益率;支持单日或日期区间查询 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | etf_code | string | 是 | ETF 代码 | 510300 | 兼容参数名 `fund_code`,建议使用 `etf_code` |
        | nav_date | int | 二选一 | 净值日期 | 20260909 | YYYYMMDD;与日期区间参数互斥 |
        | start_date | int | 二选一 | 净值开始日期 | 20260901 | 须与 `end_date` 同时提供 |
        | end_date | int | 二选一 | 净值结束日期 | 20260909 | 须与 `start_date` 同时提供 |
        | page | int | 否 | 页码 | 1 | 从 1 开始,默认 1 |
        | page_size | int | 否 | 每页条数 | 50 | 默认 50,最大 200 |
        | --all | - | 否 | 自动翻页拉全量 | - | 仅本子 skill 扩展参数 |
        
        ## 3. 响应说明
        
        外层固定为 `code` / `message` / `data`。`data` 为分页对象:`items`(列表)、`total_pages`、`total_items`。
        
        items 元素:
        
        | 字段名 | 类型 | 说明 |
        |--------|------|------|
        | trade_code | string | ETF 交易代码 |
        | publish_date | int | 发布日期 YYYYMMDD |
        | nav_date | int | 净值日期 YYYYMMDD |
        | net_asset | string | 资产净值 |
        | unit_nav | string | 单位净值 |
        | unit_nav_growth | string | 单位净值增长率(%) |
        | accumulated_nav | string | 累计单位净值 |
        | adjustment_factor | string | 复权因子 |
        | adjusted_unit_nav | string | 复权单位净值 |
        | adjusted_unit_nav_growth | string | 复权单位净值增长率(%) |
        | daily_profit | string | 每万份基金收益 |
        | seven_day_annualized_return | string | 7 日年化收益率(%) |
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> etf-net-value --etf-code 510300 --nav-date 20260909
        python <RUN_PY> etf-net-value --etf-code 510300 --start-date 20260901 --end-date 20260909 --page 1 --page-size 5
        python <RUN_PY> etf-net-value --etf-code 510300 --start-date 20260801 --end-date 20260909 --all
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - 必须提供 `nav_date`,或同时提供 `start_date` 和 `end_date`;二者互斥(handler 会在本地校验并拒绝)。
        - 日期格式 YYYYMMDD;区间查询包含开始和结束日期,`start_date` 不得晚于 `end_date`。
        - 数值字段以字符串形式返回,以保留小数精度。
        - `--all` 会按 `total_pages` 自动翻页,把所有 `items` 合并为一个数组返回。
        
    • etf-pcf-infos
      • scripts
        • handler.py 4.3 KB
          #!/usr/bin/env python3
          """ETF 申赎清单 PCF 汇总信息(GET /api/v2/market/data/etf-pcf/etf-pcf-infos)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v2/market/data/etf-pcf/etf-pcf-infos"
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def build_query(args):
              params = {}
              if args.symbol is not None:
                  params["symbol"] = args.symbol
              if args.trade_date is not None:
                  params["trade_date"] = args.trade_date
              if args.start_date is not None:
                  params["start_date"] = args.start_date
              if args.end_date is not None:
                  params["end_date"] = args.end_date
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
              return params
          
          
          def fetch(params):
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              req = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}{query}",
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(
                  description="ETF 申赎清单(PCF 汇总信息):单标的单日 / 全市场单日分页 / 单标的日期区间"
              )
              parser.add_argument("--symbol", default=None, help="ETF 代码,如 510300.SH 或 510300.XSHG")
              parser.add_argument("--trade-date", dest="trade_date", type=int, default=None,
                                  help="交易日 YYYYMMDD;单日查询时必填,不能与日期区间同用")
              parser.add_argument("--start-date", dest="start_date", type=int, default=None,
                                  help="区间开始日期 YYYYMMDD;须与 --end-date、--symbol 同时提供")
              parser.add_argument("--end-date", dest="end_date", type=int, default=None,
                                  help="区间结束日期 YYYYMMDD;须与 --start-date、--symbol 同时提供")
              parser.add_argument("--page", type=int, default=None, help="页码,从 1 开始,默认 1;分页查询有效")
              parser.add_argument("--page-size", dest="page_size", type=int, default=None,
                                  help="每页条数,默认 50,最大 500;分页查询有效")
              args = parser.parse_args()
          
              if args.trade_date is not None and (args.start_date is not None or args.end_date is not None):
                  print("--trade-date 不能与 --start-date/--end-date 同时使用", file=sys.stderr)
                  raise SystemExit(2)
          
              print(json.dumps(fetch(build_query(args)), ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2.3 KB
          #!/usr/bin/env python3
          """Tests for etf-pcf-infos handler"""
          import os
          import sys
          import unittest
          from unittest.mock import patch
          import importlib.util
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          def _run(argv):
              with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                  with patch.object(sys, "argv", ["handler.py"] + argv):
                      handler.main()
          
          
          class TestModes(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_rejects_trade_date_with_range(self):
                  with self.assertRaises(SystemExit):
                      _run(["--symbol", "510300.SH", "--trade-date", "20260909",
                            "--start-date", "20260901", "--end-date", "20260909"])
          
              @patch.object(handler, "safe_urlopen")
              def test_single_symbol_single_day(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"{}"
                  _run(["--symbol", "510300.SH", "--trade-date", "20260909"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("/api/v2/market/data/etf-pcf/etf-pcf-infos", req.full_url)
                  self.assertIn("symbol=510300.SH", req.full_url)
                  self.assertIn("trade_date=20260909", req.full_url)
          
              @patch.object(handler, "safe_urlopen")
              def test_omits_unset_optionals(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"{}"
                  _run(["--trade-date", "20260909"])
                  req = mock_open.call_args[0][0]
                  self.assertNotIn("symbol", req.full_url)
                  self.assertNotIn("page", req.full_url)
                  self.assertNotIn("page_size", req.full_url)
          
              @patch.object(handler, "safe_urlopen")
              def test_range_mode_keeps_paging(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"{}"
                  _run(["--symbol", "510300.SH", "--start-date", "20260901",
                        "--end-date", "20260909", "--page", "2", "--page-size", "5"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("start_date=20260901", req.full_url)
                  self.assertIn("page=2", req.full_url)
                  self.assertIn("page_size=5", req.full_url)
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 3.5 KB
        ---
        name: etf-pcf-infos
        description: 查询 ETF 申赎清单(PCF 汇总信息,etf_pcf_infos)。用户问 ETF 申赎清单、PCF 汇总、申购赎回单位、现金替代金额、预估现金差额、成分证券数量、ETF PCF 信息时使用。
        ---
        
        # ETF 申赎清单(PCF 汇总信息)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | ETF 申赎清单(etf_pcf_infos) |
        | 外部接口 | `GET /api/v2/market/data/etf-pcf/etf-pcf-infos` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 查询 ETF 的 PCF(申购赎回清单)汇总信息:申购赎回单位、现金替代金额、预估现金差额、现金替代比例上限、成分证券数量 |
        
        支持三种查询模式:
        
        1. **单标的单日**:同时传 `symbol` + `trade_date`,`data` 直接返回一条 PCF 信息对象。
        2. **全市场单日**:只传 `trade_date`,`data` 返回分页对象。
        3. **单标的区间**:同时传 `symbol` + `start_date` + `end_date`,`data` 返回分页对象(区间最长 30 个日历日)。
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 视模式 | ETF 代码 | 510300.SH | 支持短/长市场后缀(`510300.XSHG`) |
        | trade_date | int | 视模式 | 交易日 | 20260909 | YYYYMMDD;与日期区间参数互斥 |
        | start_date | int | 视模式 | 区间开始日期 | 20260901 | 须与 `end_date`、`symbol` 同时提供 |
        | end_date | int | 视模式 | 区间结束日期 | 20260909 | 须与 `start_date`、`symbol` 同时提供 |
        | page | int | 否 | 页码 | 1 | 从 1 开始,默认 1;分页查询有效 |
        | page_size | int | 否 | 每页条数 | 50 | 默认 50,最大 500;分页查询有效 |
        
        ## 3. 响应说明
        
        外层固定为 `code` / `message` / `data`。`data` 在单标的单日模式下为对象,分页模式下为分页对象(`pageNum`/`pageSize`/`total`/`pages`/`records`)。
        
        PCF 信息字段:
        
        | 字段名 | 类型 | 说明 |
        |--------|------|------|
        | symbol | string | ETF 代码(短市场后缀) |
        | trade_date | int | 交易日 YYYYMMDD |
        | creation_redemption_unit | int / null | 申购赎回单位(份) |
        | cash_substitution | string / null | 现金替代金额 |
        | estimated_cash | string / null | 预估现金差额 |
        | iopv | string / null | 基金份额参考净值;当前为 `null`(保留字段) |
        | min_creation_unit | int / null | 最小申购单位(份) |
        | max_cash_ratio | string / null | 现金替代比例上限 |
        | component_count | int / null | 成分证券数量 |
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> etf-pcf-infos --symbol 510300.SH --trade-date 20260909
        python <RUN_PY> etf-pcf-infos --trade-date 20260909 --page 1 --page-size 5
        python <RUN_PY> etf-pcf-infos --symbol 510300.SH --start-date 20260901 --end-date 20260909 --page 1 --page-size 5
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - `trade_date` 不能与 `start_date`、`end_date` 同时使用(handler 会在本地校验并拒绝)。
        - `start_date` 必须早于 `end_date`,区间最长 30 个日历日。
        - 单标的单日查询未命中数据时,仍返回对应标的和日期,其余业务字段为 `null`。
        - `iopv` 为保留字段,当前返回 `null`。
        - 金额和比例字段以字符串形式返回,以保留小数精度。
        - 需要指定日期的 PCF 文件清单(XML 文件名)请使用 `etf-pcfs` 子 skill;本接口返回的是汇总指标。
        
    • etf-pcfs
      • scripts
        • handler.py 2.7 KB
          #!/usr/bin/env python3
          """获取指定日期 ETF PCF 列表(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="获取指定日期 ETF PCF 列表")
              parser.add_argument(
                  "--date",
                  required=True,
                  type=int,
                  help="日期,YYYYMMDD 整型,如 20260309",
              )
              parser.add_argument(
                  "--page",
                  type=int,
                  default=1,
                  help="页码,从 1 开始,默认 1",
              )
              parser.add_argument(
                  "--page_size",
                  type=int,
                  default=20,
                  help="每页记录数,默认 20,最大 100",
              )
              args = parser.parse_args()
          
              params = {"date": args.date, "page": args.page, "page_size": args.page_size}
              url = BASE_URL + "/api/v2/market/data/etf-pcf/etf-pcfs?" + urllib.parse.urlencode(params)
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
          
              try:
                  with safe_urlopen(req) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.1 KB
        ---
        name: etf-pcfs
        description: 指定日期 ETF PCF 列表(market.ft.tech)。用户问 ETF PCF、申购赎回清单、指定日期 PCF 列表时使用。
        ---
        
        # 指定日期 ETF PCF 列表 - 获取指定日期 ETF PCF 列表
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 获取指定日期 ETF PCF 列表 |
        | 外部接口 | `GET /api/v1/market/data/etf-pcf/etf-pcfs` |
        | 请求方式 | GET |
        | 适用场景 | 获取指定交易日 ETF 申购赎回清单(PCF)文件列表,支持分页,数据来自外部 PCF 服务 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | date | int | 是 | 日期 | 20260309 | YYYYMMDD 整型 |
        | page | int | 否 | 页码,从 1 开始 | 1 | 默认 1 |
        | page_size | int | 否 | 每页记录数 | 20 | 默认 20,最大 100 |
        
        ## 3. 响应说明
        
        返回分页的 PCF 文件列表。
        
        ```json
        {
          "total": 850,
          "page": 1,
          "page_size": 20,
          "items": [
            {
              "etf_code": 159003,
              "date": 20260309,
              "filename": "pcf_159003_20260309.xml"
            },
            {
              "etf_code": 159005,
              "date": 20260309,
              "filename": "pcf_159005_20260309.xml"
            }
          ]
        }
        ```
        
        ### EtfPcfItem 字段
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | etf_code | int | 否 | ETF 代码 |
        | date | int | 否 | 日期 YYYYMMDD |
        | filename | string | 否 | PCF 文件名,如 pcf_159152_20260316.xml |
        
        ## 4. 用法
        
        通过主目录 `run.py` 调用(必填 `--date`,可选 `--page`、`--page_size`):
        
        ```bash
        python <RUN_PY> etf-pcfs --date 20260309
        python <RUN_PY> etf-pcfs --date 20260309 --page 1 --page_size 20
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。脚本输出 JSON;本接口无需额外请求头。
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/etf-pcf/etf-pcfs?date=20260309&page=1&page_size=20
        ```
        
        ## 6. 注意事项
        
        - `date` 为 YYYYMMDD 整型(如 20260309),需为交易日
        - `page_size` 最大 100
        - 无数据时 `items` 为空数组,`total` 为 0
        
    • etf-pre-data
      • scripts
        • handler.py 2.3 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/etf-pre-data'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='ETF盘前数据')
              parser.add_argument("--date")
              args = parser.parse_args()
              params = {}
              if args.date is not None: params["date"] = args.date
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 546 B
        ---
        name: etf-pre-data
        description: 查询ETF盘前数据。接口:GET /api/v2/market/data/etf-pre-data。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # ETF盘前数据
        
        接口:GET `/api/v2/market/data/etf-pre-data`。参数和响应以 `ftshare-doc/api-doc/ETF专题/ETF盘前数据.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> etf-pre-data
        ```
        
    • etf-pre-single
      • scripts
        • handler.py 2.7 KB
          #!/usr/bin/env python3
          """查询单只 ETF 盘前数据(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询单只 ETF 盘前数据")
              parser.add_argument(
                  "--symbol",
                  required=True,
                  help="ETF 标的代码,带交易所后缀,如 510300.XSHG、159915.XSHE",
              )
              parser.add_argument(
                  "--date",
                  type=int,
                  default=None,
                  help="交易日 YYYYMMDD,如 20260316;不传则使用当日(CST)",
              )
              args = parser.parse_args()
          
              params: dict = {"symbol": args.symbol}
              if args.date is not None:
                  params["date"] = args.date
          
              path = "/api/v2/market/data/etf-pre-single?" + urllib.parse.urlencode(params)
              url = BASE_URL + path
          
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
              req.add_header("Content-Type", "application/json")
          
              try:
                  with safe_urlopen(req) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(body, file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3.7 KB
        ---
        name: etf-pre-single
        description: 查询单只 ETF 盘前数据。用户问某只 ETF 盘前、申购赎回单位、净值、现金差额、IOPV、510300 盘前时使用。
        ---
        
        # 查询单只 ETF 盘前数据
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询单只 ETF 盘前数据 |
        | 外部接口 | `GET /api/v1/market/data/etf-pre-single` |
        | 请求方式 | GET |
        | 适用场景 | 根据标的代码查询单只 ETF 的盘前信息(申购赎回单位、净值、现金差额等);不传 date 时使用当日(CST);未找到或报错时接口返回相应错误信息 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 是 | ETF 标的代码,带交易所后缀 | 510300.XSHG、159915.XSHE | - |
        | date | int | 否 | 交易日 | 20260316 | YYYYMMDD;不传则使用当日(CST) |
        
        ## 3. 响应说明
        
        返回单条 ETF 盘前信息,结构与全量接口 `etf-pre-data` 的 `items` 中单条一致。
        
        ### EtfPreItem 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | etf_symbol_id | int | 否 | ETF 代码 | - |
        | etf_market_id | int | 否 | ETF 交易所编号 | - |
        | creation_redemption_unit | long | 是 | 最小申购、赎回单位 | 份 |
        | max_cash_ratio | float | 是 | 现金替代比例上限 | - |
        | publish_ipov | int | 否 | 是否需要公布 IOPV:1=需要公布 | - |
        | creation_redemption_flag | int | 否 | 申购赎回允许情况:1=允许申购 2=允许赎回,通过 & 判断 | - |
        | record_num | int | 是 | 成分股数量 | - |
        | estimate_cash_component | float | 是 | 最小申购、赎回单位的预估现金部分 | 元 |
        | trade_date | int | 是 | 交易日 | YYYYMMDD |
        | cash_component | float | 是 | 现金差额 | 元 |
        | nav_per_cu | float | 是 | 最小申购、赎回单位净值 | 元 |
        | nav | float | 是 | 基金份额净值 | 元 |
        | member_market_type | int | 否 | 成分股类型位掩码:1=上交所 2=深交所 4=港交所 8=北交所 16=外汇 32=其他 | - |
        
        ### 响应示例
        
        ```json
        {
          "etf_symbol_id": 510300,
          "etf_market_id": 1,
          "creation_redemption_unit": 1000000,
          "max_cash_ratio": 0.15,
          "publish_ipov": 1,
          "creation_redemption_flag": 3,
          "record_num": 301,
          "estimate_cash_component": 12345.67,
          "trade_date": 20260316,
          "cash_component": 0.0,
          "nav_per_cu": 3.8921,
          "nav": 3.8921,
          "member_market_type": 1
        }
        ```
        
        ## 4. 用法
        
        通过主目录 `run.py` 调用(必填 `--symbol`,可选 `--date`):
        
        ```bash
        # 当日盘前(CST)
        python <RUN_PY> etf-pre-single --symbol 510300.XSHG
        
        # 指定交易日
        python <RUN_PY> etf-pre-single --symbol 510300.XSHG --date 20260316
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。脚本输出 JSON;接口报错或未找到时,将接口返回的错误信息原样输出到 stderr 并退出码 1。
        
        ## 5. 请求示例
        
        当日:
        
        ```
        GET /api/v1/market/data/etf-pre-single?symbol=510300.XSHG
        ```
        
        指定日期:
        
        ```
        GET /api/v1/market/data/etf-pre-single?symbol=510300.XSHG&date=20260316
        ```
        
        ## 6. 数据更新时间与注意事项
        
        - 数据更新时间以接口/数据源为准;不传 `date` 时使用当日(CST)交易日。
        - 净值、现金差额等金额类字段以接口返回为准;展示时注意单位(元、份)。
        - `creation_redemption_flag` 为位掩码:1=允许申购,2=允许赎回,3 表示申购与赎回均允许。
        - **时段**:盘前数据在**交易日盘前时段**更易返回成功;非盘前、周末、节假日或当日数据尚未就绪时,接口可能返回系统错误或空数据,**属服务端行为而非脚本故障**。
        
    • etf-realtime-day-kline
      • scripts
        • handler.py 2.2 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v4/market/data/etf-realtime-day-kline"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询 ETF 实时日 K 线")
              parser.add_argument("--symbols", nargs="+", required=True)
              args = parser.parse_args()
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode({"symbols": json.dumps(args.symbols, ensure_ascii=False)})
              request = urllib.request.Request(url, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 651 B
        ---
        name: etf-realtime-day-kline
        description: 查询 ETF 当前交易日实时日 K 线。必填 --symbols,按空格分隔。接口:GET /api/v4/market/data/etf-realtime-day-kline。
        ---
        
        # ETF实时日K线
        
        查询 ETF 当前交易日实时日 K 线。必填 --symbols,按空格分隔。接口:GET /api/v4/market/data/etf-realtime-day-kline。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> etf-realtime-day-kline --symbols 510300.SH 159915.SZ --symbols 510300.SH 159915.SZ
        ```
        
    • etf-realtime-minute-kline
      • scripts
        • handler.py 2.3 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v4/market/data/etf-realtime-minute-kline"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询 ETF 实时分钟 K 线")
              parser.add_argument("--symbols", nargs="+", required=True)
              args = parser.parse_args()
              if len(args.symbols) > 20:
                  parser.error("symbols 单次最多 20 个")
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode({"symbols": json.dumps(args.symbols, ensure_ascii=False)})
              request = urllib.request.Request(url, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 722 B
        ---
        name: etf-realtime-minute-kline
        description: 查询 ETF 当前交易日实时 1 分钟 K 线。必填 --symbols,按空格分隔,单次最多 20 个。接口:GET /api/v4/market/data/etf-realtime-minute-kline。
        ---
        
        # ETF实时分钟K线
        
        查询 ETF 当前交易日实时 1 分钟 K 线。必填 --symbols,按空格分隔,单次最多 20 个。接口:GET /api/v4/market/data/etf-realtime-minute-kline。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> etf-realtime-minute-kline --symbols 510300.SH 159915.SZ --symbols 510300.SH 159915.SZ
        ```
        
    • etf-share
      • scripts
        • handler.py 4.5 KB
          #!/usr/bin/env python3
          """ETF 份额变动(GET /api/v2/market/data/etf-share)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v2/market/data/etf-share"
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          STATI_PERD = ("日", "季度", "年度", "截止时点", "半年", "全部")
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def build_params(args):
              params = {"etf_code": args.etf_code}
              if args.stati_perd is not None:
                  params["stati_perd"] = args.stati_perd
              if args.start_date is not None:
                  params["start_date"] = args.start_date
              if args.end_date is not None:
                  params["end_date"] = args.end_date
              params["page"] = args.page
              params["page_size"] = args.page_size
              return params
          
          
          def fetch_page(params):
              query = urllib.parse.urlencode(params)
              req = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}?{query}",
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="按 ETF 代码分页查询份额变动")
              parser.add_argument("--etf-code", dest="etf_code", required=True,
                                  help="ETF 代码,如 510300")
              parser.add_argument("--stati-perd", dest="stati_perd", default=None, choices=STATI_PERD,
                                  help="统计周期:日/季度/年度/截止时点/半年/全部;不传默认全部")
              parser.add_argument("--start-date", dest="start_date", type=int, default=None,
                                  help="开始日期 YYYYMMDD,按 trade_date 过滤")
              parser.add_argument("--end-date", dest="end_date", type=int, default=None,
                                  help="结束日期 YYYYMMDD,按 trade_date 过滤")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始,默认 1")
              parser.add_argument("--page-size", dest="page_size", type=int, default=50,
                                  help="每页条数,默认 50,最大 200")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量数据")
              args = parser.parse_args()
          
              params = build_params(args)
              if args.fetch_all:
                  first = fetch_page(params)
                  data = first.get("data") or {}
                  items = list(data.get("items", []))
                  total_pages = data.get("total_pages") or 1
                  for page in range(2, total_pages + 1):
                      page_data = fetch_page({**params, "page": page})
                      items.extend((page_data.get("data") or {}).get("items", []))
                  result = {
                      "items": items,
                      "total_pages": total_pages,
                      "total_items": data.get("total_items", len(items)),
                  }
              else:
                  result = fetch_page(params)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.8 KB
        ---
        name: etf-share
        description: 查询 ETF 份额变动(etf_share)。用户问 ETF 份额、期末份额、申购赎回份额、份额净变动、份额变动率、ETF 份额历史时使用。
        ---
        
        # ETF 份额变动
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | ETF 份额(etf_share) |
        | 外部接口 | `GET /api/v2/market/data/etf-share` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 按 ETF 代码分页查询份额变动:统计周期、统计日期、期末/期初份额、申购/赎回份额、份额净变动和份额变动率,支持按统计周期和日期范围筛选 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | etf_code | string | 是 | ETF 代码 | 510300 | 兼容参数名 `fund_code`,建议使用 `etf_code` |
        | stati_perd | string | 否 | 统计周期 | 全部 | `日`/`季度`/`年度`/`截止时点`/`半年`/`全部`;不传默认 `全部` |
        | start_date | int | 否 | 开始日期 | 20250101 | YYYYMMDD,按 `trade_date` 过滤 |
        | end_date | int | 否 | 结束日期 | 20260909 | YYYYMMDD,按 `trade_date` 过滤;不早于 `start_date` |
        | page | int | 否 | 页码 | 1 | 从 1 开始,默认 1 |
        | page_size | int | 否 | 每页条数 | 50 | 默认 50,最大 200 |
        | --all | - | 否 | 自动翻页拉全量 | - | 仅本子 skill 扩展参数 |
        
        ## 3. 响应说明
        
        外层固定为 `code` / `message` / `data`。`data` 为分页对象:`items`(列表)、`total_pages`、`total_items`。
        
        items 元素:
        
        | 字段名 | 类型 | 说明 |
        |--------|------|------|
        | trade_code | string | ETF 交易代码 |
        | statistics_period | string | 统计周期 |
        | trade_date | int | 统计日期 YYYYMMDD |
        | fund_share | string | 期末份额(份) |
        | begin_shares | string / null | 期初份额(份) |
        | purchase_shares | string / null | 申购份额(份) |
        | redemption_shares | string / null | 赎回份额(份) |
        | shares_change | string / null | 份额净变动(份) |
        | shares_change_ratio | string / null | 份额变动率(%) |
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> etf-share --etf-code 510300 --page 1 --page-size 5
        python <RUN_PY> etf-share --etf-code 510300 --stati-perd 日 --start-date 20250101 --end-date 20260909
        python <RUN_PY> etf-share --etf-code 510300 --all
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - 份额字段单位为份;数值字段以字符串形式返回,以保留小数精度。
        - `start_date` / `end_date` 可单独提供;同时提供时 `start_date` 不得晚于 `end_date`。
        - `--all` 会按 `total_pages` 自动翻页,把所有 `items` 合并为一个数组返回。
        
    • exchange-margin-summaries
      • scripts
        • handler.py 2.9 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v1/market/data/exchange-margin-summaries"
          _REQUEST_HEADERS = {"X-Client-Name": "ft-claw", "Content-Type": "application/json"}
          if os.environ.get("FTSHARE_API_KEY"):
              _REQUEST_HEADERS["FTSHARE_API_KEY"] = os.environ["FTSHARE_API_KEY"]
          OPENER = urllib.request.build_opener()
          
          
          def safe_urlopen(request):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed = urllib.parse.urlparse(url)
              base = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, headers=_REQUEST_HEADERS, method="GET")
              return OPENER.open(request, timeout=60)
          
          
          def fetch(
          page, page_size, start_date, end_date, exchange=None):
              params = {"start_date": start_date, "end_date": end_date, "page": page, "page_size": page_size}
              if exchange:
                  params["exchange"] = exchange
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params)
              request = urllib.request.Request(url, headers=_REQUEST_HEADERS, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      return json.loads(response.read().decode())
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser()
              parser.add_argument("--start-date", required=True)
              parser.add_argument("--end-date", required=True)
              parser.add_argument("--exchange")
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", type=int, default=100)
              parser.add_argument("--all", action="store_true", dest="fetch_all")
              args = parser.parse_args()
              result = fetch(args.page, args.page_size, args.start_date, args.end_date, args.exchange)
              if args.fetch_all:
                  records = list(result.get("records", result.get("items", [])))
                  pages = result.get("pages", result.get("total_pages", 1))
                  for page in range(args.page + 1, pages + 1):
                      next_page = fetch(page, args.page_size, args.start_date, args.end_date, args.exchange)
                      records.extend(next_page.get("records", next_page.get("items", [])))
                  result["records"] = records
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 700 B
        ---
        name: exchange-margin-summaries
        description: 查询交易所融资融券汇总日度数据。用户询问沪深北交易所融资融券余额、买入额、偿还额或融券数据时使用。
        ---
        
        # 交易所融资融券汇总日度
        
        外部接口:`GET /api/v1/market/data/exchange-margin-summaries`
        
        必填:`--start-date`、`--end-date`(YYYYMMDD)。可选:`--exchange`、`--page`、`--page-size`、`--all`。
        
        ```bash
        python <RUN_PY> exchange-margin-summaries --start-date 20260701 --end-date 20260731 --page 1 --page-size 100
        ```
        
        返回分页 JSON,记录位于 `records`,包含 `exchange`、`trade_date`、`rzye`、`rzmre`、`rzche`、`rqyl`、`rqylje`、`rqmcl`、`rzrqjyzl`。
        
    • executive-holdings-changes
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v1/market/data/holder/stock-ggmx"
          _REQUEST_HEADERS = {
              "FTSHARE_API_KEY": os.environ.get("FTSHARE_API_KEY", ""),
              "Content-Type": "application/json",
          }
          
          
          def fetch(params):
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if (parsed.scheme, parsed.netloc) != (base.scheme, base.netloc):
                  print("Invalid URL for safe_urlopen", file=sys.stderr)
                  raise SystemExit(1)
              request = urllib.request.Request(
                  url,
                  headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key},
                  method="GET",
              )
              try:
                  with urllib.request.urlopen(request, timeout=30) as response:
                      return json.loads(response.read().decode())
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询董监高持股变动")
              parser.add_argument("--stock-code", dest="stock_code")
              parser.add_argument("--change-direction", dest="change_direction")
              parser.add_argument("--start-date", dest="start_date")
              parser.add_argument("--end-date", dest="end_date")
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", dest="page_size", type=int, default=50)
              parser.add_argument("--all", action="store_true", dest="fetch_all")
              args = parser.parse_args()
              if args.page < 1 or not 1 <= args.page_size <= 200:
                  parser.error("page 须大于等于 1,page-size 须在 1~200 之间")
              params = {
                  "page": 1 if args.fetch_all else args.page,
                  "page_size": args.page_size,
              }
              for name in ("stock_code", "change_direction", "start_date", "end_date"):
                  value = getattr(args, name)
                  if value is not None:
                      params[name] = value
              result = fetch(params)
              if args.fetch_all:
                  data = result.get("data") or {}
                  records = list(data.get("records", []))
                  for page in range(2, int(data.get("pages", 1)) + 1):
                      records.extend((fetch({**params, "page": page}).get("data") or {}).get("records", []))
                  result["data"] = {**data, "records": records}
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2 KB
          #!/usr/bin/env python3
          """Tests for executive-holdings-changes handler"""
          import importlib.util
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "fetch")
              def test_fetch_contract(self, mock_fetch):
                  mock_fetch.return_value = {
                      "code": 200,
                      "message": "ok",
                      "data": {"pages": 1, "records": []},
                  }
                  with patch.object(sys, "argv", [
                      "handler.py",
                      "--stock-code", "600848",
                      "--change-direction", "增持",
                      "--start-date", "20260101",
                      "--end-date", "20260601",
                      "--page", "2",
                      "--page-size", "20",
                  ]):
                      handler.main()
                  params = mock_fetch.call_args.args[0]
                  self.assertEqual(params, {
                      "stock_code": "600848",
                      "change_direction": "增持",
                      "start_date": "20260101",
                      "end_date": "20260601",
                      "page": 2,
                      "page_size": 20,
                  })
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "fetch")
              def test_main_emits_json(self, mock_fetch):
                  mock_fetch.return_value = {
                      "code": 200,
                      "message": "ok",
                      "data": {"pages": 1, "records": []},
                  }
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--stock-code", "600848"]):
                          with patch("sys.stdout", new_callable=StringIO) as fake_out:
                              handler.main()
                              self.assertEqual(json.loads(fake_out.getvalue())["code"], 200)
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 1.8 KB
        ---
        name: executive-holdings-changes
        description: 查询董监高持股变动明细。用户询问董监高、高管持股变动、增持减持、变动股数或变动日期时使用。
        ---
        
        # 董监高持股变动
        
        接口:`GET /api/v1/market/data/holder/stock-ggmx`。支持按股票代码、变动方向和变动日期范围筛选,返回分页数据。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> executive-holdings-changes --stock-code 600001 --change-direction 增持 --start-date 20260101 --end-date 20260601 --page 1 --page-size 20
        python <RUN_PY> executive-holdings-changes --stock-code 600519 --all
        ```
        
        ## 参数
        
        - `--stock-code`:股票代码,如 `600001`,可选。
        - `--change-direction`:变动方向,可填 `增持` 或 `减持`,可选。
        - `--start-date`:变动日期起始日,可使用 `YYYYMMDD` 或 `YYYY-MM-DD`,可选。
        - `--end-date`:变动日期截止日,可使用 `YYYYMMDD` 或 `YYYY-MM-DD`,可选。
        - `--page`:页码,默认 1。
        - `--page-size`:每页条数,默认 50,范围为 1~200。
        - `--all`:自动翻页并合并所有记录。
        
        所有筛选参数均可省略。响应为 `code/message/data` 信封,分页记录位于 `data.records`。
        
        ## 记录字段
        
        记录包含 `stock_code`、`stock_name`、`change_date`、`notice_date`、`register_date`、`changer`、`executive_name`、`relation`、`position`、`change_direction`、`change_shares`、`change_quantity`、`avg_price`、`change_amount`、`change_reason`、`shares_after`、`change_ratio`、`close_price`、`price_change`、`total_share`、`data_time`、`quote_price`、`quote_change`、`crawl_date`、`source` 和 `crawl_batch_ts` 等字段。
        
        日期参数按变动日期筛选;源接口同时兼容下划线参数名及对应的 camelCase 别名,handler 使用文档中的下划线参数名。
        
    • financial-calendar
      • scripts
        • handler.py 3.2 KB
          #!/usr/bin/env python3
          """查询百度财经日历。"""
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/finance/financial-calendar/baidu"
          
          
          def safe_urlopen(req_or_url):
              url = req_or_url.full_url if isinstance(req_or_url, urllib.request.Request) else str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询百度财经日历")
              parser.add_argument("--start-date", required=True, help="开始日期,格式 YYYY-MM-DD")
              parser.add_argument("--end-date", required=True, help="结束日期,格式 YYYY-MM-DD")
              parser.add_argument(
                  "--category",
                  choices=("economic", "ipo", "report_time", "trade_reminder"),
                  default=None,
                  help="可选事件分类",
              )
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始")
              parser.add_argument("--page-size", dest="page_size", type=int, default=50, help="每页条数,最大 200")
              args = parser.parse_args()
          
              params = {
                  "start_date": args.start_date,
                  "end_date": args.end_date,
                  "category": args.category,
                  "page": args.page,
                  "page_size": args.page_size,
              }
              query = urllib.parse.urlencode({key: value for key, value in params.items() if value is not None})
              request = urllib.request.Request(f"{BASE_URL}{ENDPOINT}?{query}", method="GET", headers=_REQUEST_HEADERS)
          
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  body = error.read().decode().strip()
                  message = f"HTTP {error.code}"
                  if body:
                      message += f": {body}"
                  print(message, file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2.2 KB
          from __future__ import annotations
          
          import importlib.util
          import io
          import json
          import sys
          import urllib.error
          from pathlib import Path
          from urllib.parse import parse_qs, urlparse
          
          import pytest
          
          
          HANDLER = Path(__file__).with_name("handler.py")
          
          
          def load_handler():
              spec = importlib.util.spec_from_file_location("financial_calendar_handler", HANDLER)
              module = importlib.util.module_from_spec(spec)
              assert spec.loader is not None
              spec.loader.exec_module(module)
              return module
          
          
          class Response:
              def __init__(self, payload):
                  self.payload = payload
          
              def __enter__(self):
                  return self
          
              def __exit__(self, *args):
                  return None
          
              def read(self):
                  return json.dumps(self.payload).encode()
          
          
          def test_main_forwards_documented_query(monkeypatch, capsys):
              module = load_handler()
              opened = []
              monkeypatch.setattr(
                  sys,
                  "argv",
                  [
                      "handler.py",
                      "--start-date",
                      "2026-07-16",
                      "--end-date",
                      "2026-07-17",
                      "--category",
                      "economic",
                      "--page",
                      "1",
                      "--page-size",
                      "5",
                  ],
              )
              monkeypatch.setattr(module, "safe_urlopen", lambda request: opened.append(request) or Response({"data": {"records": []}}))
          
              module.main()
          
              parsed = urlparse(opened[0].full_url)
              assert parsed.path == "/gateway/api/v1/market/data/finance/financial-calendar/baidu"
              assert parse_qs(parsed.query) == {
                  "start_date": ["2026-07-16"],
                  "end_date": ["2026-07-17"],
                  "category": ["economic"],
                  "page": ["1"],
                  "page_size": ["5"],
              }
              assert json.loads(capsys.readouterr().out) == {"data": {"records": []}}
          
          
          def test_empty_http_error_body_still_reports_status(monkeypatch, capsys):
              module = load_handler()
              monkeypatch.setattr(sys, "argv", ["handler.py", "--start-date", "2026-07-16", "--end-date", "2026-07-17"])
              error = urllib.error.HTTPError("https://example.invalid", 503, "unavailable", {}, io.BytesIO(b""))
              monkeypatch.setattr(module, "safe_urlopen", lambda request: (_ for _ in ()).throw(error))
          
              with pytest.raises(SystemExit):
                  module.main()
          
              assert capsys.readouterr().err.strip() == "HTTP 503"
          
      • SKILL.md 3 KB
        ---
        name: financial-calendar
        description: 按日期范围查询财经日历(market.ft.tech),含华尔街见闻与百度财经日历数据(宏观数据、IPO、财报时间、交易提醒等)。用户问财经日历、经济事件、IPO 日程、财报时间、交易提醒时使用。
        ---
        
        # 查询财经日历
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询财经日历 |
        | 外部接口 | `GET /api/v1/market/data/finance/financial-calendar` |
        | 请求方式 | GET |
        | 适用场景 | 按日期范围查询华尔街见闻与百度财经日历数据(宏观数据、IPO、财报时间、交易提醒等) |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | start_date | string | 是 | 开始日期 | 2026-05-01 | 格式 `YYYY-MM-DD` |
        | end_date | string | 是 | 结束日期 | 2026-05-07 | 格式 `YYYY-MM-DD` |
        
        ## 3. 响应说明
        
        响应为信封结构 `code` / `message` / `data`,`data` 包含两大来源:
        
        ### 3.1 华尔街见闻(wallstreetcn)
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | stat_date | string | 否 | 统计日期 |
        | tab_type | string | 否 | 标签类型 |
        | event_time | string | 否 | 事件时间 |
        | region | string | 否 | 地区 |
        | event | string | 否 | 事件名称 |
        | importance | string | 否 | 重要程度 |
        | actual_value | string | 是 | 公布值 |
        | forecast_value | string | 是 | 预测值 |
        | previous_value | string | 是 | 前值 |
        
        ### 3.2 百度财经日历(baidu)
        
        包含四类数据:
        
        - **economic**(经济数据):region、time、title、former_val、market_value、pub_val、indicate_val、star、negative、positive
        - **ipo**(IPO 数据):code、name、exchange、market、price、volume、amount、pe_ratio 等
        - **report_time**(财报时间):code、name、exchange、market、report_type、market_time 等
        - **trade_reminder**(交易提醒):code、name、exchange、market、meeting_type、reason、divi_cash、divi_date 等
        
        ```json
        {
          "code": 0,
          "message": "success",
          "data": {
            "wallstreetcn": { "items": [] },
            "baidu": {
              "economic": { "items": [] },
              "ipo": { "items": [] },
              "report_time": { "items": [] },
              "trade_reminder": { "items": [] }
            }
          }
        }
        ```
        
        ## 4. 用法
        
        通过主目录 `run.py` 调用(必填 `--start-date`、`--end-date`):
        
        ```bash
        python <RUN_PY> financial-calendar --start-date 2026-05-01 --end-date 2026-05-03
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。
        
        ## 5. 注意事项
        
        - 日期格式为 `YYYY-MM-DD`。
        - 各 `items` 列表可能为空数组,表示该日期范围内无对应事件。
        - 百度侧 IPO、财报时间、交易提醒等条目中,类型字段 JSON 名为 `type`。
        - **多日范围查询可能因响应过大导致服务端截断**(服务端 HTTP/2 限制)。建议每次仅查询单日(`start_date` 与 `end_date` 相同),如需多日数据请逐日调用后合并。
        
    • fund-asset-allocation-single-fund-paginated
      • scripts
        • handler.py 3.5 KB
          #!/usr/bin/env python3
          """基金资产配置"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/fund/fund-asset-allocation"
          
          def build_params(args):
              params = {}
              if args.fund_code is not None:
                  params["fund_code"] = args.fund_code
              if args.report_date is not None:
                  params["report_date"] = args.report_date
              if args.publish_date is not None:
                  params["publish_date"] = args.publish_date
              if args.start_date is not None:
                  params["start_date"] = args.start_date
              if args.end_date is not None:
                  params["end_date"] = args.end_date
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="基金资产配置")
              parser.add_argument("--fund_code", required=True, help="基金代码")
              parser.add_argument("--report_date", type=int, required=False, help="报告期 YYYYMMDD(与 start/end 互斥)")
              parser.add_argument("--publish_date", type=int, required=False, help="发布日期 YYYYMMDD")
              parser.add_argument("--start_date", type=int, required=False, help="报告期起始日期(需与 end_date 同传)")
              parser.add_argument("--end_date", type=int, required=False, help="报告期结束日期")
              parser.add_argument("--page", type=int, required=False, help="页码,默认 1")
              parser.add_argument("--page_size", type=int, required=False, help="每页记录数,默认 50,上限 200")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.8 KB
          #!/usr/bin/env python3
          """Tests for fund-asset-allocation-single-fund-paginated handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.fund_code = '000001'
                  args.report_date = None
                  args.publish_date = None
                  args.start_date = None
                  args.end_date = None
                  args.page = None
                  args.page_size = None
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/fund/fund-asset-allocation", url)
                  self.assertIn("fund_code=", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--fund_code', '000001']
                  argv += ['--report_date', '20260331']
                  argv += ['--page', '1']
                  argv += ['--page_size', '50']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.8 KB
        ---
        name: fund-asset-allocation-single-fund-paginated
        description: "按基金代码查报告期资产配置。当用户需要查询基金总资产/净资产及股票/债券/基金/货币资金/其他资产的市值与占比,通过 report_date 单期或 start_date+end_date 区间(互斥),publish_date 过滤时使用。"
        ---
        
        # 基金资产配置
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 基金资产配置                                                   |
        | 外部接口 | `/api/v1/market/data/fund/fund-asset-allocation`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--fund_code` | string | 是 | 基金代码 |
        | `--report_date` | int | 否 | 报告期 YYYYMMDD(与 start/end 互斥) |
        | `--publish_date` | int | 否 | 发布日期 YYYYMMDD |
        | `--start_date` | int | 否 | 报告期起始日期(需与 end_date 同传) |
        | `--end_date` | int | 否 | 报告期结束日期 |
        | `--page` | int | 否 | 页码,默认 1 |
        | `--page_size` | int | 否 | 每页记录数,默认 50,上限 200 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-asset-allocation-single-fund-paginated --fund_code 000001 --report_date 20260331 --page 1 --page_size 50
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-basicinfo-single-fund
      • scripts
        • handler.py 2.6 KB
          #!/usr/bin/env python3
          """查询基金基础信息(分页)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询基金基础信息(分页)")
              parser.add_argument("--fund-code", default=None, help="6 位数字基金代码,如 000001;不传时返回全市场默认分页")
              parser.add_argument("--page", type=int, default=None, help="页码,从 1 开始(默认 1)")
              parser.add_argument("--page-size", type=int, default=None, help="每页记录数,默认 200,最大 500")
              args = parser.parse_args()
          
              params = {"fund_code": args.fund_code, "page": args.page, "page_size": args.page_size}
              params = {key: value for key, value in params.items() if value is not None}
              url = f"{BASE_URL}/api/v1/market/data/fund/fund-basicinfo?" + urllib.parse.urlencode(params)
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(body, file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.5 KB
        ---
        name: fund-basicinfo-single-fund
        description: Get basic information of funds by fund code with pagination. Use when user asks about fund details, 基金基本信息, 基金管理人, 基金经理, 基金类型, 投资目标.
        ---
        
        # 查询基金基础信息(分页)
        
        ## 参数
        
        | 参数 | 类型 | 必填 | 说明 | 示例 |
        |------|------|------|------|------|
        | `--fund-code` | string | 否 | 6 位数字基金代码;不传时返回全市场数据的默认分页 | `000001` |
        | `--page` | int | 否 | 页码,从 1 开始(默认 1) | `1` |
        | `--page-size` | int | 否 | 每页记录数,默认 200,最大 500 | `200` |
        
        ## 用法
        
        通过主目录 `run.py` 调用(所有参数均可选):
        
        ```bash
        python <RUN_PY> fund-basicinfo-single-fund --fund-code 000001
        python <RUN_PY> fund-basicinfo-single-fund --page 1 --page-size 200
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。脚本输出分页 JSON(`items`/`page_num`/`page_size`/`total`/`pages`),`items` 中每项含 `fund_code`、`fund_name`、管理人、经理、类型、投资理念/目标/范围、业绩基准等完整字段,按用户关注点展示。
        
        ## 注意
        
        - `fund_code` 必须是 6 位数字;不传时查询全市场数据的默认分页
        - 响应为分页结构,基金档案在 `items` 数组中;需要全量数据时循环请求直到 `page > pages`
        - 若用户只给基金名称,建议先用 `fund-support-symbols-all-funds-paginated` 或 `fund-overview-all-funds-paginated` 查到对应 6 位 `fund-code` 再调用本接口。
        
    • fund-cal-return-single-fund-specific-period
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          """查询指定基金在指定区间的累计收益率时间序列"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          VALID_CAL_TYPES = ["1M", "3M", "6M", "1Y", "3Y", "5Y", "YTD"]
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询基金累计收益率")
              parser.add_argument("--fund-code", required=True, help="6 位数字基金代码,如 159619")
              parser.add_argument("--cal-type", required=True, choices=VALID_CAL_TYPES,
                                  help="区间类型:1M / 3M / 6M / 1Y / 3Y / 5Y / YTD")
              args = parser.parse_args()
          
              params = {"fund_code": args.fund_code, "cal-type": args.cal_type}
              url = f"{BASE_URL}/api/v1/market/data/fund/fund-cal-return?" + urllib.parse.urlencode(params)
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(body, file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.4 KB
        ---
        name: fund-cal-return-single-fund-specific-period
        description: Get cumulative return time series for a specific fund over a given period. Use when user asks about 基金累计收益率, 近1年/近3个月收益, YTD收益, 基金收益曲线.
        ---
        
        # 查询指定基金在指定区间的累计收益率
        
        ## 参数
        
        | 参数 | 类型 | 必填 | 说明 | 示例 |
        |------|------|------|------|------|
        | `--fund-code` | string | 是 | 6 位数字基金代码 | `159619` |
        | `--cal-type` | string | 是 | 区间类型:`1M` / `3M` / `6M` / `1Y` / `3Y` / `5Y` / `YTD` | `1Y` |
        
        ## 用法
        
        通过主目录 `run.py` 调用(必填 `--fund-code`、`--cal-type`):
        
        ```bash
        python <RUN_PY> fund-cal-return-single-fund-specific-period --fund-code 159619 --cal-type 1Y
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。脚本输出 JSON 数组,每项含 `date`(YYYYMMDD 整型)和 `return`(小数形式累计收益率),按时间序列展示。
        
        ## 注意
        
        - 响应为数组,不是分页结构
        - `return` 为小数(如 0.0234 表示 2.34%),展示时可乘以 100 转为百分比
        - 区间选项:`1M`(近1月)、`3M`(近3月)、`6M`(近6月)、`1Y`(近1年)、`3Y`(近3年)、`5Y`(近5年)、`YTD`(今年来)
        - 若用户只给基金名称,建议先用 `fund-support-symbols-all-funds-paginated` 或 `fund-overview-all-funds-paginated` 查到对应 6 位 `fund-code` 再查询收益。
        
    • fund-classification-single-fund
      • scripts
        • handler.py 2.7 KB
          #!/usr/bin/env python3
          """基金分类"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/fund/fund-classification"
          
          def build_params(args):
              params = {}
              if args.fund_code is not None:
                  params["fund_code"] = args.fund_code
              if args.classify_std is not None:
                  params["classify_std"] = args.classify_std
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="基金分类")
              parser.add_argument("--fund_code", required=True, help="基金代码(trade_code 形式,如 000001 或 000001.OF)")
              parser.add_argument("--classify_std", required=False, help="分类标准:证监会基金分类/晨星基金分类/银河证券分类2017版,缺省全部")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.6 KB
          #!/usr/bin/env python3
          """Tests for fund-classification-single-fund handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.fund_code = '000001'
                  args.classify_std = None
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/fund/fund-classification", url)
                  self.assertIn("fund_code=", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--fund_code', '000001']
                  argv += ['--classify_std', '晨星基金分类']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.5 KB
        ---
        name: fund-classification-single-fund
        description: "查询基金在多套分类标准下的分类。当用户需要查询基金按证监会/晨星/银河证券等标准的分类,可按 classify_std 过滤时使用。"
        ---
        
        # 基金分类
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 基金分类                                                   |
        | 外部接口 | `/api/v1/market/data/fund/fund-classification`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--fund_code` | string | 是 | 基金代码(trade_code 形式,如 000001 或 000001.OF) |
        | `--classify_std` | string | 否 | 分类标准:证监会基金分类/晨星基金分类/银河证券分类2017版,缺省全部 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-classification-single-fund --fund_code 000001 --classify_std 晨星基金分类
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-company-list-paginated
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          """基金公司"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/fund/fund-company"
          
          def build_params(args):
              params = {}
              if args.fund_company is not None:
                  params["fund_company"] = args.fund_company
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="基金公司")
              parser.add_argument("--fund_company", required=False, help="基金公司名称,精确匹配")
              parser.add_argument("--page", type=int, required=False, help="页码,默认 1")
              parser.add_argument("--page_size", type=int, required=False, help="每页记录数,默认 50,上限 200")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.6 KB
          #!/usr/bin/env python3
          """Tests for fund-company-list-paginated handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.fund_company = None
                  args.page = None
                  args.page_size = None
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/fund/fund-company", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--fund_company', '华夏基金']
                  argv += ['--page', '1']
                  argv += ['--page_size', '50']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.4 KB
        ---
        name: fund-company-list-paginated
        description: "分页查询基金公司列表及名下基金数量。当用户需要查询基金公司名称与名下基金数量,可按公司名精确过滤时使用。"
        ---
        
        # 基金公司
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 基金公司                                                   |
        | 外部接口 | `/api/v1/market/data/fund/fund-company`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--fund_company` | string | 否 | 基金公司名称,精确匹配 |
        | `--page` | int | 否 | 页码,默认 1 |
        | `--page_size` | int | 否 | 每页记录数,默认 50,上限 200 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-company-list-paginated --fund_company 华夏基金 --page 1 --page_size 50
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-fee-single-fund-paginated
      • scripts
        • handler.py 3.2 KB
          #!/usr/bin/env python3
          """基金费率"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/fund/fund-fee"
          
          def build_params(args):
              params = {}
              if args.fund_code is not None:
                  params["fund_code"] = args.fund_code
              if args.charge_type is not None:
                  params["charge_type"] = args.charge_type
              if args.client_type is not None:
                  params["client_type"] = args.client_type
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="基金费率")
              parser.add_argument("--fund_code", required=True, help="基金代码(如 000001 或 000001.OF)")
              parser.add_argument("--charge_type", required=False, help="费率类型:日常申购费/日常赎回费/认购费/管理费/托管费/销售服务费")
              parser.add_argument("--client_type", required=False, help="客户类型:一般/机构/养老金/REITs")
              parser.add_argument("--page", type=int, required=False, help="页码,默认 1")
              parser.add_argument("--page_size", type=int, required=False, help="每页记录数,默认 50,上限 200")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.7 KB
          #!/usr/bin/env python3
          """Tests for fund-fee-single-fund-paginated handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.fund_code = '000001'
                  args.charge_type = None
                  args.client_type = None
                  args.page = None
                  args.page_size = None
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/fund/fund-fee", url)
                  self.assertIn("fund_code=", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--fund_code', '000001']
                  argv += ['--charge_type', '日常申购费']
                  argv += ['--page', '1']
                  argv += ['--page_size', '50']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.7 KB
        ---
        name: fund-fee-single-fund-paginated
        description: "查询基金费率(当前有效,分页)。当用户需要查询基金日常申购费/赎回费/认购费/管理费/托管费/销售服务费,可按 charge_type 费率类型与 client_type 客户类型过滤时使用。"
        ---
        
        # 基金费率
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 基金费率                                                   |
        | 外部接口 | `/api/v1/market/data/fund/fund-fee`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--fund_code` | string | 是 | 基金代码(如 000001 或 000001.OF) |
        | `--charge_type` | string | 否 | 费率类型:日常申购费/日常赎回费/认购费/管理费/托管费/销售服务费 |
        | `--client_type` | string | 否 | 客户类型:一般/机构/养老金/REITs |
        | `--page` | int | 否 | 页码,默认 1 |
        | `--page_size` | int | 否 | 每页记录数,默认 50,上限 200 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-fee-single-fund-paginated --fund_code 000001 --charge_type 日常申购费 --page 1 --page_size 50
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-holder-structure-single-fund
      • scripts
        • handler.py 3 KB
          #!/usr/bin/env python3
          """基金持有人结构"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/fund/fund-holder-structure"
          
          def build_params(args):
              params = {}
              if args.fund_code is not None:
                  params["fund_code"] = args.fund_code
              if args.report_type is not None:
                  params["report_type"] = args.report_type
              if args.start_date is not None:
                  params["start_date"] = args.start_date
              if args.end_date is not None:
                  params["end_date"] = args.end_date
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="基金持有人结构")
              parser.add_argument("--fund_code", required=True, help="基金代码(如 000001 或 000001.OF)")
              parser.add_argument("--report_type", required=False, help="报告类型:年度报告/中期报告/上市公告书/基金合同生效公告,缺省全部")
              parser.add_argument("--start_date", type=int, required=False, help="报告期起始日期 YYYYMMDD")
              parser.add_argument("--end_date", type=int, required=False, help="报告期截止日期 YYYYMMDD")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.7 KB
          #!/usr/bin/env python3
          """Tests for fund-holder-structure-single-fund handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.fund_code = '000001'
                  args.report_type = None
                  args.start_date = None
                  args.end_date = None
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/fund/fund-holder-structure", url)
                  self.assertIn("fund_code=", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--fund_code', '000001']
                  argv += ['--report_type', '年度报告']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.7 KB
        ---
        name: fund-holder-structure-single-fund
        description: "查询基金持有人结构(按报告期)。当用户需要查询基金持有人户数、户均持有份额、机构/个人/前十大持有人份额与比例等,可按 report_type 与 start_date/end_date 过滤时使用。直接返回数组(非分页)。"
        ---
        
        # 基金持有人结构
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 基金持有人结构                                                   |
        | 外部接口 | `/api/v1/market/data/fund/fund-holder-structure`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--fund_code` | string | 是 | 基金代码(如 000001 或 000001.OF) |
        | `--report_type` | string | 否 | 报告类型:年度报告/中期报告/上市公告书/基金合同生效公告,缺省全部 |
        | `--start_date` | int | 否 | 报告期起始日期 YYYYMMDD |
        | `--end_date` | int | 否 | 报告期截止日期 YYYYMMDD |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-holder-structure-single-fund --fund_code 000001 --report_type 年度报告
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-index-tracking-funds
      • scripts
        • handler.py 2.6 KB
          #!/usr/bin/env python3
          """指数跟踪基金"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v2/market/data/fund/index-fund"
          
          def build_params(args):
              params = {}
              if args.index_code is not None:
                  params["index_code"] = args.index_code
              if args.scope is not None:
                  params["scope"] = args.scope
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="指数跟踪基金")
              parser.add_argument("--index_code", required=True, help="指数代码,支持裸码(如 000300)或带后缀(如 000300.SH)")
              parser.add_argument("--scope", required=False, help="all 全市场(默认)/ etf 仅场内 ETF")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.6 KB
          #!/usr/bin/env python3
          """Tests for fund-index-tracking-funds handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.index_code = '000300'
                  args.scope = None
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v2/market/data/fund/index-fund", url)
                  self.assertIn("index_code=", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--index_code', '000300']
                  argv += ['--scope', 'etf']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.5 KB
        ---
        name: fund-index-tracking-funds
        description: "按指数代码查询跟踪该指数的基金。当用户需要查询跟踪某只指数(如沪深 300)的全部基金(场内 ETF + 场外联接基金),或仅场内 ETF(scope=etf)时使用。直接返回数组(非分页)。"
        ---
        
        # 指数跟踪基金
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 指数跟踪基金                                                   |
        | 外部接口 | `/api/v1/market/data/fund/index-fund`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--index_code` | string | 是 | 指数代码,支持裸码(如 000300)或带后缀(如 000300.SH) |
        | `--scope` | string | 否 | all 全市场(默认)/ etf 仅场内 ETF |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-index-tracking-funds --index_code 000300 --scope etf
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-list-paginated
      • scripts
        • handler.py 3 KB
          #!/usr/bin/env python3
          """基金列表"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/fund/fund-list"
          
          def build_params(args):
              params = {}
              if args.fund_code is not None:
                  params["fund_code"] = args.fund_code
              if args.fund_type is not None:
                  params["fund_type"] = args.fund_type
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="基金列表")
              parser.add_argument("--fund_code", required=False, help="基金代码,精确查单只")
              parser.add_argument("--fund_type", required=False, help="基金类型,精确匹配(股票型/混合型/债券型/货币型/保本型/其他型/REITs)")
              parser.add_argument("--page", type=int, required=False, help="页码,默认 1")
              parser.add_argument("--page_size", type=int, required=False, help="每页记录数,默认 50,上限 200")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.6 KB
          #!/usr/bin/env python3
          """Tests for fund-list-paginated handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.fund_code = None
                  args.fund_type = None
                  args.page = None
                  args.page_size = None
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/fund/fund-list", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--fund_type', '股票型']
                  argv += ['--page', '1']
                  argv += ['--page_size', '50']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.6 KB
        ---
        name: fund-list-paginated
        description: "分页查询公募基金基础列表。当用户需要查询基金代码/名称/交易代码/基金公司/经理/托管人/运作方式/类型/成立日期/规模等基金基础档案,可按 fund_code 查单只或按 fund_type 过滤时使用。"
        ---
        
        # 基金列表
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 基金列表                                                   |
        | 外部接口 | `/api/v1/market/data/fund/fund-list`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--fund_code` | string | 否 | 基金代码,精确查单只 |
        | `--fund_type` | string | 否 | 基金类型,精确匹配(股票型/混合型/债券型/货币型/保本型/其他型/REITs) |
        | `--page` | int | 否 | 页码,默认 1 |
        | `--page_size` | int | 否 | 每页记录数,默认 50,上限 200 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-list-paginated --fund_type 股票型 --page 1 --page_size 50
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-manager-relationship
      • scripts
        • handler.py 3.1 KB
          #!/usr/bin/env python3
          """基金经理任职关系"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/fund/fund-manager"
          
          def build_params(args):
              params = {}
              if args.fund_code is not None:
                  params["fund_code"] = args.fund_code
              if args.fund_manager is not None:
                  params["fund_manager"] = args.fund_manager
              if args.is_inoffice is not None:
                  params["is_inoffice"] = args.is_inoffice
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="基金经理任职关系")
              parser.add_argument("--fund_code", required=False, help="基金代码(与 fund_manager 二选一)")
              parser.add_argument("--fund_manager", required=False, help="基金经理姓名(与 fund_code 二选一)")
              parser.add_argument("--is_inoffice", required=False, help="1 在任 / 0 离任")
              parser.add_argument("--page", type=int, required=False, help="页码,默认 1")
              parser.add_argument("--page_size", type=int, required=False, help="每页记录数,默认 50,上限 200")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.6 KB
          #!/usr/bin/env python3
          """Tests for fund-manager-relationship handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.fund_code = None
                  args.fund_manager = None
                  args.is_inoffice = None
                  args.page = None
                  args.page_size = None
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/fund/fund-manager", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--fund_code', '000001']
                  argv += ['--is_inoffice', '1']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.6 KB
        ---
        name: fund-manager-relationship
        description: "按基金代码查历任/现任基金经理,或按基金经理姓名查其管理过的任职关系。当用户需要查基金经理姓名、职务、任职/离任日期、是否在任等,按 fund_code 或 fund_manager 二选一查询时使用。"
        ---
        
        # 基金经理任职关系
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 基金经理任职关系                                                   |
        | 外部接口 | `/api/v1/market/data/fund/fund-manager`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--fund_code` | string | 否 | 基金代码(与 fund_manager 二选一) |
        | `--fund_manager` | string | 否 | 基金经理姓名(与 fund_code 二选一) |
        | `--is_inoffice` | string | 否 | 1 在任 / 0 离任 |
        | `--page` | int | 否 | 页码,默认 1 |
        | `--page_size` | int | 否 | 每页记录数,默认 50,上限 200 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-manager-relationship --fund_code 000001 --is_inoffice 1
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-net-value-detail-single-fund
      • scripts
        • handler.py 3.3 KB
          #!/usr/bin/env python3
          """基金净值明细"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/fund/fund-net-value"
          
          def build_params(args):
              params = {}
              if args.fund_code is not None:
                  params["fund_code"] = args.fund_code
              if args.nav_date is not None:
                  params["nav_date"] = args.nav_date
              if args.start_date is not None:
                  params["start_date"] = args.start_date
              if args.end_date is not None:
                  params["end_date"] = args.end_date
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="基金净值明细")
              parser.add_argument("--fund_code", required=True, help="基金代码")
              parser.add_argument("--nav_date", type=int, required=False, help="净值日期 YYYYMMDD(与 start/end 互斥)")
              parser.add_argument("--start_date", type=int, required=False, help="净值开始日期 YYYYMMDD(需与 end_date 同传)")
              parser.add_argument("--end_date", type=int, required=False, help="净值结束日期 YYYYMMDD")
              parser.add_argument("--page", type=int, required=False, help="页码,默认 1")
              parser.add_argument("--page_size", type=int, required=False, help="每页记录数,默认 50,上限 200")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.8 KB
          #!/usr/bin/env python3
          """Tests for fund-net-value-detail-single-fund handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.fund_code = '000001'
                  args.nav_date = None
                  args.start_date = None
                  args.end_date = None
                  args.page = None
                  args.page_size = None
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/fund/fund-net-value", url)
                  self.assertIn("fund_code=", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--fund_code', '000001']
                  argv += ['--start_date', '20260101']
                  argv += ['--end_date', '20260717']
                  argv += ['--page', '1']
                  argv += ['--page_size', '50']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.7 KB
        ---
        name: fund-net-value-detail-single-fund
        description: "按基金代码查询基金净值明细。当用户需要查询基金单位/累计/复权净值、净值增长率、每万份基金收益、7 日年化等明细,通过 nav_date 单日或 start_date+end_date 区间(互斥)查询时使用。"
        ---
        
        # 基金净值明细
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 基金净值明细                                                   |
        | 外部接口 | `/api/v1/market/data/fund/fund-net-value`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--fund_code` | string | 是 | 基金代码 |
        | `--nav_date` | int | 否 | 净值日期 YYYYMMDD(与 start/end 互斥) |
        | `--start_date` | int | 否 | 净值开始日期 YYYYMMDD(需与 end_date 同传) |
        | `--end_date` | int | 否 | 净值结束日期 YYYYMMDD |
        | `--page` | int | 否 | 页码,默认 1 |
        | `--page_size` | int | 否 | 每页记录数,默认 50,上限 200 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-net-value-detail-single-fund --fund_code 000001 --start_date 20260101 --end_date 20260717 --page 1 --page_size 50
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-net-value-performance-single-fund
      • scripts
        • handler.py 3.3 KB
          #!/usr/bin/env python3
          """基金净值收益表现"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/fund/fund-net-value-performance"
          
          def build_params(args):
              params = {}
              if args.fund_code is not None:
                  params["fund_code"] = args.fund_code
              if args.stat_date is not None:
                  params["stat_date"] = args.stat_date
              if args.start_date is not None:
                  params["start_date"] = args.start_date
              if args.end_date is not None:
                  params["end_date"] = args.end_date
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="基金净值收益表现")
              parser.add_argument("--fund_code", required=True, help="基金代码")
              parser.add_argument("--stat_date", type=int, required=False, help="统计日期 YYYYMMDD(与 start/end 互斥)")
              parser.add_argument("--start_date", type=int, required=False, help="统计开始日期(需与 end_date 同传)")
              parser.add_argument("--end_date", type=int, required=False, help="统计结束日期")
              parser.add_argument("--page", type=int, required=False, help="页码,默认 1")
              parser.add_argument("--page_size", type=int, required=False, help="每页记录数,默认 50,上限 200")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.8 KB
          #!/usr/bin/env python3
          """Tests for fund-net-value-performance-single-fund handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.fund_code = '000001'
                  args.stat_date = None
                  args.start_date = None
                  args.end_date = None
                  args.page = None
                  args.page_size = None
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/fund/fund-net-value-performance", url)
                  self.assertIn("fund_code=", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--fund_code', '000001']
                  argv += ['--start_date', '20260101']
                  argv += ['--end_date', '20260717']
                  argv += ['--page', '1']
                  argv += ['--page_size', '50']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.8 KB
        ---
        name: fund-net-value-performance-single-fund
        description: "按基金代码查净值口径收益表现。当用户需要查询基金多区间收益率(日/周/月/近三月/六月/今年/近一至十年/成立以来及对应年化收益率),通过 stat_date 单日或 start_date+end_date 区间(互斥)查询时使用。"
        ---
        
        # 基金净值收益表现
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 基金净值收益表现                                                   |
        | 外部接口 | `/api/v1/market/data/fund/fund-net-value-performance`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--fund_code` | string | 是 | 基金代码 |
        | `--stat_date` | int | 否 | 统计日期 YYYYMMDD(与 start/end 互斥) |
        | `--start_date` | int | 否 | 统计开始日期(需与 end_date 同传) |
        | `--end_date` | int | 否 | 统计结束日期 |
        | `--page` | int | 否 | 页码,默认 1 |
        | `--page_size` | int | 否 | 每页记录数,默认 50,上限 200 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-net-value-performance-single-fund --fund_code 000001 --start_date 20260101 --end_date 20260717 --page 1 --page_size 50
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-new-found-paginated
      • scripts
        • handler.py 3.2 KB
          #!/usr/bin/env python3
          """基金新发"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/fund/fund-new-found"
          
          def build_params(args):
              params = {}
              if args.start_date is not None:
                  params["start_date"] = args.start_date
              if args.end_date is not None:
                  params["end_date"] = args.end_date
              if args.fund_type is not None:
                  params["fund_type"] = args.fund_type
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="基金新发")
              parser.add_argument("--start_date", type=int, required=False, help="成立日起始日期 YYYYMMDD(不传默认近 1 年)")
              parser.add_argument("--end_date", type=int, required=False, help="成立日截止日期 YYYYMMDD(不传默认今天)")
              parser.add_argument("--fund_type", required=False, help="基金类型过滤:混合型/债券型/股票型/货币型/其他型/保本型/REITs")
              parser.add_argument("--page", type=int, required=False, help="页码,默认 1")
              parser.add_argument("--page_size", type=int, required=False, help="每页记录数,默认 50,上限 200")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.6 KB
          #!/usr/bin/env python3
          """Tests for fund-new-found-paginated handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.start_date = None
                  args.end_date = None
                  args.fund_type = None
                  args.page = None
                  args.page_size = None
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/fund/fund-new-found", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--fund_type', '混合型']
                  argv += ['--page', '1']
                  argv += ['--page_size', '50']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.7 KB
        ---
        name: fund-new-found-paginated
        description: "查询新发基金(按成立日范围 + 倒序,分页)。当用户需要查询新发基金代码/名称/管理人/托管人/运作方式/类型/成立日期/成立规模等,可按 fund_type 过滤、按 start_date/end_date 限定成立日范围时使用。"
        ---
        
        # 基金新发
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 基金新发                                                   |
        | 外部接口 | `/api/v1/market/data/fund/fund-new-found`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--start_date` | int | 否 | 成立日起始日期 YYYYMMDD(不传默认近 1 年) |
        | `--end_date` | int | 否 | 成立日截止日期 YYYYMMDD(不传默认今天) |
        | `--fund_type` | string | 否 | 基金类型过滤:混合型/债券型/股票型/货币型/其他型/保本型/REITs |
        | `--page` | int | 否 | 页码,默认 1 |
        | `--page_size` | int | 否 | 每页记录数,默认 50,上限 200 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-new-found-paginated --fund_type 混合型 --page 1 --page_size 50
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-overview-all-funds-paginated
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          """查询所有基金概览信息(分页)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询所有基金概览信息(分页)")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始(默认 1)")
              parser.add_argument("--page-size", type=int, default=20, help="每页记录数,最大 1000(默认 20)")
              args = parser.parse_args()
          
              params = {"page": args.page, "page_size": args.page_size}
              url = f"{BASE_URL}/api/v1/market/data/fund/fund-overview?" + urllib.parse.urlencode(params)
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(body, file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 995 B
        ---
        name: fund-overview-all-funds-paginated
        description: Get paginated overview information for all funds. Use when user asks about 基金概览, 所有基金信息, fund overview list, 基金列表概况.
        ---
        
        # 查询所有基金概览信息(分页)
        
        ## 参数
        
        | 参数 | 类型 | 必填 | 说明 | 示例 |
        |------|------|------|------|------|
        | `--page` | int | 否 | 页码,从 1 开始(默认 1) | `1` |
        | `--page-size` | int | 否 | 每页记录数,最大 1000(默认 20) | `20` |
        
        ## 用法
        
        通过主目录 `run.py` 调用(可选 `--page`、`--page-size`):
        
        ```bash
        python <RUN_PY> fund-overview-all-funds-paginated --page 1 --page-size 20
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。脚本输出带分页信息的 JSON,每项含基金代码、基金名称、基金类型、规模等概览字段,以表格展示给用户。
        
        ## 注意
        
        - 需要全量数据时,循环请求直到 `page > total_pages`
        - `page-size` 最大不超过 1000
        
    • fund-portfolio-single-fund-paginated
      • scripts
        • handler.py 3.5 KB
          #!/usr/bin/env python3
          """基金持仓明细"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/fund/fund-portfolio"
          
          def build_params(args):
              params = {}
              if args.fund_code is not None:
                  params["fund_code"] = args.fund_code
              if args.report_date is not None:
                  params["report_date"] = args.report_date
              if args.publish_date is not None:
                  params["publish_date"] = args.publish_date
              if args.start_date is not None:
                  params["start_date"] = args.start_date
              if args.end_date is not None:
                  params["end_date"] = args.end_date
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="基金持仓明细")
              parser.add_argument("--fund_code", required=True, help="基金代码")
              parser.add_argument("--report_date", type=int, required=False, help="报告期 YYYYMMDD(与 start/end 互斥)")
              parser.add_argument("--publish_date", type=int, required=False, help="发布日期 YYYYMMDD")
              parser.add_argument("--start_date", type=int, required=False, help="报告期起始日期(需与 end_date 同传)")
              parser.add_argument("--end_date", type=int, required=False, help="报告期结束日期")
              parser.add_argument("--page", type=int, required=False, help="页码,默认 1")
              parser.add_argument("--page_size", type=int, required=False, help="每页记录数,默认 50,上限 200")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.8 KB
          #!/usr/bin/env python3
          """Tests for fund-portfolio-single-fund-paginated handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.fund_code = '000001'
                  args.report_date = None
                  args.publish_date = None
                  args.start_date = None
                  args.end_date = None
                  args.page = None
                  args.page_size = None
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/fund/fund-portfolio", url)
                  self.assertIn("fund_code=", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--fund_code', '000001']
                  argv += ['--report_date', '20260331']
                  argv += ['--page', '1']
                  argv += ['--page_size', '50']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.8 KB
        ---
        name: fund-portfolio-single-fund-paginated
        description: "按基金代码查报告期持仓明细。当用户需要查询基金持仓股票/债券/转债的代码、名称、数量、市值、占净值比等,通过 report_date 单期或 start_date+end_date 区间(互斥),publish_date 按发布日期过滤时使用。"
        ---
        
        # 基金持仓明细
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 基金持仓明细                                                   |
        | 外部接口 | `/api/v1/market/data/fund/fund-portfolio`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--fund_code` | string | 是 | 基金代码 |
        | `--report_date` | int | 否 | 报告期 YYYYMMDD(与 start/end 互斥) |
        | `--publish_date` | int | 否 | 发布日期 YYYYMMDD |
        | `--start_date` | int | 否 | 报告期起始日期(需与 end_date 同传) |
        | `--end_date` | int | 否 | 报告期结束日期 |
        | `--page` | int | 否 | 页码,默认 1 |
        | `--page_size` | int | 否 | 每页记录数,默认 50,上限 200 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-portfolio-single-fund-paginated --fund_code 000001 --report_date 20260331 --page 1 --page_size 50
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-risk-level-single-fund
      • scripts
        • handler.py 2.6 KB
          #!/usr/bin/env python3
          """基金风险等级"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/fund/fund-risk-level"
          
          def build_params(args):
              params = {}
              if args.fund_code is not None:
                  params["fund_code"] = args.fund_code
              if args.history:
                  params["history"] = "true"
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="基金风险等级")
              parser.add_argument("--fund_code", required=True, help="基金代码(如 000001 或 000001.OF)")
              parser.add_argument("--history", action="store_true", help="返回全部变更历史(缺省仅当前有效)")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.6 KB
          #!/usr/bin/env python3
          """Tests for fund-risk-level-single-fund handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.fund_code = '000001'
                  args.history = False
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/fund/fund-risk-level", url)
                  self.assertIn("fund_code=", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--fund_code', '000001']
                  argv += ['--history']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.4 KB
        ---
        name: fund-risk-level-single-fund
        description: "查询基金风险等级。当用户需要查询基金风险等级(低/中低/中/中高/高,对应适当性 R1-R5)及其变更历史,可按 history=true 返回全部历史或仅当前有效时使用。直接返回数组(非分页)。"
        ---
        
        # 基金风险等级
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 基金风险等级                                                   |
        | 外部接口 | `/api/v1/market/data/fund/fund-risk-level`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--fund_code` | string | 是 | 基金代码(如 000001 或 000001.OF) |
        | `--history` | flag | 否 | 返回全部变更历史(缺省仅当前有效) |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-risk-level-single-fund --fund_code 000001 --history
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-share-single-fund-paginated
      • scripts
        • handler.py 3.3 KB
          #!/usr/bin/env python3
          """基金份额"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/fund/fund-share"
          
          def build_params(args):
              params = {}
              if args.fund_code is not None:
                  params["fund_code"] = args.fund_code
              if args.stati_perd is not None:
                  params["stati_perd"] = args.stati_perd
              if args.start_date is not None:
                  params["start_date"] = args.start_date
              if args.end_date is not None:
                  params["end_date"] = args.end_date
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
              return params
          
          def fetch(
          args):
              params = urllib.parse.urlencode(build_params(args))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="基金份额")
              parser.add_argument("--fund_code", required=True, help="基金代码")
              parser.add_argument("--stati_perd", required=False, help="统计周期:日/季度/年度/截止时点/半年/全部,默认日")
              parser.add_argument("--start_date", type=int, required=False, help="开始日期 YYYYMMDD(按 trade_date 过滤)")
              parser.add_argument("--end_date", type=int, required=False, help="结束日期 YYYYMMDD")
              parser.add_argument("--page", type=int, required=False, help="页码,默认 1")
              parser.add_argument("--page_size", type=int, required=False, help="每页记录数,默认 50,上限 200")
              args = parser.parse_args()
          
              result = fetch(args)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.8 KB
          #!/usr/bin/env python3
          """Tests for fund-share-single-fund-paginated handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_fetch_forwards_to_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  class Args: pass
                  args = Args()
                  args.fund_code = '000001'
                  args.stati_perd = None
                  args.start_date = None
                  args.end_date = None
                  args.page = None
                  args.page_size = None
                  handler.fetch(args)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/fund/fund-share", url)
                  self.assertIn("fund_code=", url)
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, 'safe_urlopen')
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'[]'
                  argv = ['handler.py']
                  argv += ['--fund_code', '000001']
                  argv += ['--stati_perd', '日']
                  argv += ['--start_date', '20260101']
                  argv += ['--end_date', '20260717']
                  argv += ['--page', '1']
                  argv += ['--page_size', '50']
                  with patch.object(sys, 'argv', argv):
                      with patch('sys.stdout', new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          if __name__ == '__main__':
              unittest.main()
          
      • SKILL.md 1.7 KB
        ---
        name: fund-share-single-fund-paginated
        description: "按基金代码分页查询基金份额变动。当用户需要查询基金份额变动(期末/期初份额、申购赎回、份额变动率),按 stati_perd 统计周期(日/季度/年度/截止时点/半年/全部)过滤时使用。"
        ---
        
        # 基金份额
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 基金份额                                                   |
        | 外部接口 | `/api/v1/market/data/fund/fund-share`                                  |
        | 请求方式 | GET                                                                |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 |
        |--------|------|----------|------|
        | `--fund_code` | string | 是 | 基金代码 |
        | `--stati_perd` | string | 否 | 统计周期:日/季度/年度/截止时点/半年/全部,默认日 |
        | `--start_date` | int | 否 | 开始日期 YYYYMMDD(按 trade_date 过滤) |
        | `--end_date` | int | 否 | 结束日期 YYYYMMDD |
        | `--page` | int | 否 | 页码,默认 1 |
        | `--page_size` | int | 否 | 每页记录数,默认 50,上限 200 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> fund-share-single-fund-paginated --fund_code 000001 --stati_perd 日 --start_date 20260101 --end_date 20260717 --page 1 --page_size 50
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        JSON 对象或数组,结构请参考 [ftshare-doc](../..) 接口文档。
        
        ## 注意事项
        
        - 必填参数必须提供。
        - 响应以 JSON 格式输出至 stdout,诊断信息输出至 stderr。
        
    • fund-support-symbols-all-funds-paginated
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          """查询所有支持基金的标的列表(分页)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询所有支持基金标的列表(分页)")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始(默认 1)")
              parser.add_argument("--page-size", type=int, default=20, help="每页记录数,最大 1000(默认 20)")
              args = parser.parse_args()
          
              params = {"page": args.page, "page_size": args.page_size}
              url = f"{BASE_URL}/api/v1/market/data/fund/fund-support-symbols?" + urllib.parse.urlencode(params)
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(body, file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1014 B
        ---
        name: fund-support-symbols-all-funds-paginated
        description: Get paginated list of all supported fund symbols with code and name. Use when user asks about 支持的基金列表, 基金代码清单, 所有基金标的.
        ---
        
        # 查询所有支持基金的标的列表(分页)
        
        ## 参数
        
        | 参数 | 类型 | 必填 | 说明 | 示例 |
        |------|------|------|------|------|
        | `--page` | int | 否 | 页码,从 1 开始(默认 1) | `1` |
        | `--page-size` | int | 否 | 每页记录数,最大 1000(默认 20) | `20` |
        
        ## 用法
        
        通过主目录 `run.py` 调用(可选 `--page`、`--page-size`):
        
        ```bash
        python <RUN_PY> fund-support-symbols-all-funds-paginated --page 1 --page-size 20
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。脚本输出带分页信息的 JSON,每项含 `fund_code`(基金代码)和 `fund_name`(基金名称),以表格展示。
        
        ## 注意
        
        - 需要全量数据时,循环请求直到 `page > total_pages`
        - `page-size` 最大不超过 1000
        
    • futures-base-data
      • scripts
        • handler.py 2.3 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/futures/futures-base-data'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key=os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url=request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed,base=urllib.parse.urlparse(url),urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request=urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key=_require_api_key(); parser=argparse.ArgumentParser(description='中国期货基础数据')
              parser.add_argument("--trade-date", dest="trade_date", type=int)
              parser.add_argument("--symbol", dest="symbol")
              ns=parser.parse_args(); params={}
              for name,value in vars(ns).items():
                  if value is not None: params[name.replace("_","-") if False else name]=value
              request=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params), headers={"FTSHARE_API_KEY":key,"X-Client-Name":"ft-claw","Content-Type":"application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload=json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 559 B
        ---
        name: futures-base-data
        description: 中国期货基础数据。调用 /api/v1/market/data/futures/futures-base-data,所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 中国期货基础数据
        
        外部接口:`GET /api/v1/market/data/futures/futures-base-data`。
        
        支持源接口文档列出的查询参数,handler 将参数作为 query 发送,并以 JSON 输出响应。所有请求必须设置环境变量 `FTSHARE_API_KEY`;缺失凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> futures-base-data --symbol 600519.SH
        ```
        
    • futures-contract-kline
      • scripts
        • handler.py 4 KB
          #!/usr/bin/env python3
          """期货行情:期货合约日/周/月/季/年 K 线(GET /api/v1/market/data/futures/kline)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/futures/kline"
          
          INTERVALS = (
              "daily", "1d",
              "weekly", "1w", "week",
              "monthly", "1mo", "month",
              "quarterly", "1q", "quarter",
              "yearly", "1y", "year",
          )
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def build_params(args):
              params = {
                  "symbol": args.symbol,
                  "interval": args.interval,
              }
              if args.start is not None:
                  params["start"] = args.start
              if args.end is not None:
                  params["end"] = args.end
              if args.limit is not None:
                  params["limit"] = args.limit
              return params
          
          
          def fetch(params):
              query = urllib.parse.urlencode(params)
              req = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}?{query}",
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询期货合约日/周/月/季/年 K 线")
              parser.add_argument("--symbol", required=True,
                                  help="期货合约代码,如 A2605.DCE;支持交易所短后缀")
              parser.add_argument("--interval", default="daily", choices=INTERVALS,
                                  help="K 线周期,默认 daily;周/月/季/年 K 基于日 K 按北京时间聚合")
              parser.add_argument("--start", type=int, default=None,
                                  help="起始时间戳(毫秒,闭区间);可省略时间范围")
              parser.add_argument("--end", type=int, default=None,
                                  help="结束时间戳(毫秒,闭区间);不能单独传入")
              parser.add_argument("--limit", type=int, default=None,
                                  help="返回条数,默认 500;最小值 1,传 0 时按 1 处理")
              args = parser.parse_args()
          
              if args.end is not None and args.start is None:
                  print("--end 不能单独传入,必须与 --start 同时使用", file=sys.stderr)
                  raise SystemExit(2)
          
              print(json.dumps(fetch(build_params(args)), ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2 KB
          #!/usr/bin/env python3
          """Tests for futures-contract-kline handler"""
          import os
          import sys
          import unittest
          from unittest.mock import patch
          import importlib.util
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          def _run(argv):
              with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                  with patch.object(sys, "argv", ["handler.py"] + argv):
                      handler.main()
          
          
          class TestParams(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_rejects_end_without_start(self):
                  with self.assertRaises(SystemExit):
                      _run(["--symbol", "A2605.DCE", "--end", "1756791000000"])
          
              @patch.object(handler, "safe_urlopen")
              def test_defaults(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"{}"
                  _run(["--symbol", "A2605.DCE"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/futures/kline", req.full_url)
                  self.assertIn("symbol=A2605.DCE", req.full_url)
                  self.assertIn("interval=daily", req.full_url)
                  self.assertNotIn("limit", req.full_url)
                  self.assertNotIn("start", req.full_url)
          
              @patch.object(handler, "safe_urlopen")
              def test_weekly_interval(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"{}"
                  _run(["--symbol", "A2605.DCE", "--interval", "weekly", "--limit", "5",
                        "--start", "1756431000000", "--end", "1756791000000"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("interval=weekly", req.full_url)
                  self.assertIn("limit=5", req.full_url)
                  self.assertIn("start=1756431000000", req.full_url)
                  self.assertIn("end=1756791000000", req.full_url)
          
              def test_rejects_unknown_interval(self):
                  with self.assertRaises(SystemExit):
                      _run(["--symbol", "A2605.DCE", "--interval", "minute"])
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 3.1 KB
        ---
        name: futures-contract-kline
        description: 查询期货行情(期货合约日/周/月/季/年 K 线,futures_contract_kline)。用户问期货合约 K 线、期货日K/周K/月K、合约开高低收、期货成交量持仓量、vwap、主力合约复权因子时使用。
        ---
        
        # 期货行情(期货合约 K 线)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 期货行情(futures_contract_kline) |
        | 外部接口 | `GET /api/v1/market/data/futures/kline` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 查询期货合约日、周、月、季、年 K 线,含开高低收、成交量、成交额、vwap、持仓量、主力合约与前/后复权因子 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 是 | 期货合约代码 | A2605.DCE | 支持交易所短后缀 |
        | interval | string | 否 | K 线周期 | daily | `daily`/`1d`、`weekly`/`1w`/`week`、`monthly`/`1mo`/`month`、`quarterly`/`1q`/`quarter`、`yearly`/`1y`/`year`;默认 `daily` |
        | start | int | 否 | 起始时间戳(毫秒) | 1756431000000 | 闭区间;可省略时间范围 |
        | end | int | 否 | 结束时间戳(毫秒) | 1756791000000 | 闭区间;不能单独传入 |
        | limit | int | 否 | 返回条数 | 5 | 默认 500,最小值 1,传 0 时按 1 处理 |
        
        ## 3. 响应说明
        
        外层固定为 `code` / `message` / `data`。`data` 为对象:`items`(K 线记录列表)和 `total`(符合条件记录总数)。
        
        items 元素:
        
        | 字段名 | 类型 | 说明 |
        |--------|------|------|
        | symbol | string | 规范化后的合约代码 |
        | datetime | int64 | K 线时间戳(毫秒) |
        | trade_date | int | 交易日 YYYYMMDD |
        | open / high / low / close | number | 开盘价 / 最高价 / 最低价 / 收盘价 |
        | volume | int64 | 成交量 |
        | amount | number | 成交额 |
        | vwap | number | 成交均价 |
        | open_interest | number | 持仓量 |
        | dominant_contract | string / null | 主力合约;合约 K 线不返回该字段值 |
        | forward_factor | number / null | 前复权因子;合约 K 线不返回该字段值 |
        | backward_factor | number / null | 后复权因子;合约 K 线不返回该字段值 |
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> futures-contract-kline --symbol A2605.DCE --interval daily --limit 5
        python <RUN_PY> futures-contract-kline --symbol A2605.DCE --interval weekly --limit 5
        python <RUN_PY> futures-contract-kline --symbol A2605.DCE --start 1756431000000 --end 1756791000000
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - 周、月、季、年 K 线基于日 K 线按北京时间聚合。
        - `end` 不能单独传入,必须与 `start` 同时使用(handler 会本地校验并拒绝);仅传 `start` 时查询 `start` 之后的数据。
        - 同时传入 `start` 和 `end` 时,跨度不得超过 12 个日历月。
        - 本接口查询的是具体合约 K 线;`dominant_contract` 和复权因子字段对合约 K 线不返回值。
        
    • futures-limit
      • scripts
        • handler.py 2.6 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/futures/ft-limit'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key=os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url=request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed,base=urllib.parse.urlparse(url),urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request=urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key=_require_api_key(); parser=argparse.ArgumentParser(description='期货合约涨跌停价')
              parser.add_argument("--ts-code", dest="ts_code")
              parser.add_argument("--trade-date", dest="trade_date", type=int)
              parser.add_argument("--start-date", dest="start_date", type=int)
              parser.add_argument("--end-date", dest="end_date", type=int)
              parser.add_argument("--cont", dest="cont")
              parser.add_argument("--exchange", dest="exchange")
              parser.add_argument("--page", dest="page", type=int)
              parser.add_argument("--page-size", dest="page_size", type=int)
              ns=parser.parse_args(); params={}
              for name,value in vars(ns).items():
                  if value is not None: params[name.replace("_","-") if False else name]=value
              request=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params), headers={"FTSHARE_API_KEY":key,"X-Client-Name":"ft-claw","Content-Type":"application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload=json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 594 B
        ---
        name: futures-limit
        description: 期货合约涨跌停价。调用 /api/v1/market/data/futures/ft-limit,所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 期货合约涨跌停价
        
        外部接口:`GET /api/v1/market/data/futures/ft-limit`。
        
        支持源接口文档列出的查询参数,handler 将参数作为 query 发送,并以 JSON 输出响应。所有请求必须设置环境变量 `FTSHARE_API_KEY`;缺失凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> futures-limit --ts-code A2609.DCE --trade-date 20260721 --exchange DCE --page 1 --page-size 5
        ```
        
    • futures-lists
      • scripts
        • handler.py 2.2 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/futures/futures-lists'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key=os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url=request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed,base=urllib.parse.urlparse(url),urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request=urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key=_require_api_key(); parser=argparse.ArgumentParser(description='中国期货列表')
              parser.add_argument("--trade-date", dest="trade_date", type=int)
              ns=parser.parse_args(); params={}
              for name,value in vars(ns).items():
                  if value is not None: params[name.replace("_","-") if False else name]=value
              request=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params), headers={"FTSHARE_API_KEY":key,"X-Client-Name":"ft-claw","Content-Type":"application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload=json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 512 B
        ---
        name: futures-lists
        description: 中国期货列表。调用 /api/v1/market/data/futures/futures-lists,所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 中国期货列表
        
        外部接口:`GET /api/v1/market/data/futures/futures-lists`。
        
        支持源接口文档列出的查询参数,handler 将参数作为 query 发送,并以 JSON 输出响应。所有请求必须设置环境变量 `FTSHARE_API_KEY`;缺失凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> futures-lists
        ```
        
    • futures-settle
      • scripts
        • handler.py 2.6 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/futures/fut-settle'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key=os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url=request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed,base=urllib.parse.urlparse(url),urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request=urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key=_require_api_key(); parser=argparse.ArgumentParser(description='期货每日结算参数')
              parser.add_argument("--ts-code", dest="ts_code")
              parser.add_argument("--trade-date", dest="trade_date", type=int)
              parser.add_argument("--start-date", dest="start_date", type=int)
              parser.add_argument("--end-date", dest="end_date", type=int)
              parser.add_argument("--exchange", dest="exchange")
              parser.add_argument("--page", dest="page", type=int)
              parser.add_argument("--page-size", dest="page_size", type=int)
              ns=parser.parse_args(); params={}
              for name,value in vars(ns).items():
                  if value is not None: params[name.replace("_","-") if False else name]=value
              request=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params), headers={"FTSHARE_API_KEY":key,"X-Client-Name":"ft-claw","Content-Type":"application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload=json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 758 B
        ---
        name: futures-settle
        description: 期货每日结算参数。调用 /api/v1/market/data/futures/fut-settle,所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 期货每日结算参数
        
        外部接口:`GET /api/v1/market/data/futures/fut-settle`。
        
        支持源接口文档列出的查询参数,handler 将参数作为 query 发送,并以 JSON 输出响应。`--exchange` 必填(`SHFE`/`INE`/`CZCE`/`CFFEX`/`DCE`/`GFEX`);`--trade-date` 与 `--start-date`/`--end-date` 二选一,区间跨度不超过 31 天。所有请求必须设置环境变量 `FTSHARE_API_KEY`;缺失凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> futures-settle --exchange DCE --ts-code A2609.DCE --trade-date 20260721 --page 1 --page-size 5
        ```
        
    • futures-warehouse-receipt
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/futures/fut-wsr'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key=os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url=request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed,base=urllib.parse.urlparse(url),urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request=urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key=_require_api_key(); parser=argparse.ArgumentParser(description='期货仓单日报')
              parser.add_argument("--trade-date", dest="trade_date", type=int)
              parser.add_argument("--start-date", dest="start_date", type=int)
              parser.add_argument("--end-date", dest="end_date", type=int)
              parser.add_argument("--symbol", dest="symbol")
              parser.add_argument("--page", dest="page", type=int)
              parser.add_argument("--page-size", dest="page_size", type=int)
              ns=parser.parse_args(); params={}
              for name,value in vars(ns).items():
                  if value is not None: params[name.replace("_","-") if False else name]=value
              request=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params), headers={"FTSHARE_API_KEY":key,"X-Client-Name":"ft-claw","Content-Type":"application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload=json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 581 B
        ---
        name: futures-warehouse-receipt
        description: 期货仓单日报。调用 /api/v1/market/data/futures/fut-wsr,所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 期货仓单日报
        
        外部接口:`GET /api/v1/market/data/futures/fut-wsr`。
        
        支持源接口文档列出的查询参数,handler 将参数作为 query 发送,并以 JSON 输出响应。所有请求必须设置环境变量 `FTSHARE_API_KEY`;缺失凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> futures-warehouse-receipt --symbol CU --trade-date 20260721 --page 1 --page-size 5
        ```
        
    • futures-weekly-detail
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/futures/fut-weekly-detail'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key=os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url=request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed,base=urllib.parse.urlparse(url),urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request=urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key=_require_api_key(); parser=argparse.ArgumentParser(description='期货主要品种交易周报')
              parser.add_argument("--week", dest="week")
              parser.add_argument("--prd", dest="prd")
              parser.add_argument("--start-week", dest="start_week")
              parser.add_argument("--end-week", dest="end_week")
              parser.add_argument("--exchange", dest="exchange")
              parser.add_argument("--page", dest="page", type=int)
              parser.add_argument("--page-size", dest="page_size", type=int)
              ns=parser.parse_args(); params={}
              for name,value in vars(ns).items():
                  if value is not None: params[name.replace("_","-") if False else name]=value
              request=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params), headers={"FTSHARE_API_KEY":key,"X-Client-Name":"ft-claw","Content-Type":"application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload=json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 609 B
        ---
        name: futures-weekly-detail
        description: 期货主要品种交易周报。调用 /api/v1/market/data/futures/fut-weekly-detail,所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 期货主要品种交易周报
        
        外部接口:`GET /api/v1/market/data/futures/fut-weekly-detail`。
        
        支持源接口文档列出的查询参数,handler 将参数作为 query 发送,并以 JSON 输出响应。所有请求必须设置环境变量 `FTSHARE_API_KEY`;缺失凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> futures-weekly-detail --week 202628 --prd CU,AL --page 1 --page-size 5
        ```
        
    • global-index-daily-kline
      • scripts
        • handler.py 4.2 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/global-index/daily-kline'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='全球指数日K线')
              parser.add_argument("--secid", required=True)
              parser.add_argument("--start_date")
              parser.add_argument("--end_date")
              parser.add_argument("--limit")
              parser.add_argument("--total", required=False)
              parser.add_argument("--items", required=False)
              parser.add_argument("--name", required=True)
              parser.add_argument("--trade_date", required=True)
              parser.add_argument("--open", required=False)
              parser.add_argument("--close", required=False)
              parser.add_argument("--high", required=False)
              parser.add_argument("--low", required=False)
              parser.add_argument("--volume", required=False)
              parser.add_argument("--amount", required=False)
              parser.add_argument("--amplitude", required=True)
              parser.add_argument("--change_pct", required=False)
              parser.add_argument("--change_amount", required=False)
              parser.add_argument("--turnover", required=False)
              args = parser.parse_args()
              params = {}
              if args.secid is not None: params["secid"] = args.secid
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.limit is not None: params["limit"] = args.limit
              if args.total is not None: params["total"] = args.total
              if args.items is not None: params["items"] = args.items
              if args.name is not None: params["name"] = args.name
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.open is not None: params["open"] = args.open
              if args.close is not None: params["close"] = args.close
              if args.high is not None: params["high"] = args.high
              if args.low is not None: params["low"] = args.low
              if args.volume is not None: params["volume"] = args.volume
              if args.amount is not None: params["amount"] = args.amount
              if args.amplitude is not None: params["amplitude"] = args.amplitude
              if args.change_pct is not None: params["change_pct"] = args.change_pct
              if args.change_amount is not None: params["change_amount"] = args.change_amount
              if args.turnover is not None: params["turnover"] = args.turnover
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 687 B
        ---
        name: global-index-daily-kline
        description: 查询全球指数日K线。接口:GET /api/v1/market/data/global-index/daily-kline。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 全球指数日K线
        
        接口:GET `/api/v1/market/data/global-index/daily-kline`。参数和响应以 `ftshare-doc/api-doc/指数专题/指数行情/全球指数日K线.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> global-index-daily-kline --secid 1 --limit 5 --name 1 --trade_date 20260828 --amplitude 1
        ```
        
    • hibor-daily
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/hibor-daily'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='HIBOR日度利率')
              parser.add_argument("--start_date", required=True)
              parser.add_argument("--end_date", required=True)
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--trade_date", required=True)
              parser.add_argument("--on_rate", required=False)
              args = parser.parse_args()
              params = {}
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.on_rate is not None: params["on_rate"] = args.on_rate
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 637 B
        ---
        name: hibor-daily
        description: 查询HIBOR日度利率。接口:GET /api/v1/market/data/hibor-daily。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # HIBOR日度利率
        
        接口:GET `/api/v1/market/data/hibor-daily`。参数和响应以 `ftshare-doc/api-doc/宏观经济/国际宏观/HIBOR日度利率.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> hibor-daily --start_date 20260828 --end_date 20260828 --page 1 --trade_date 20260828
        ```
        
    • hk-candlesticks
      • scripts
        • handler.py 5.1 KB
          #!/usr/bin/env python3
          """查询港股 K 线(market.ft.tech)"""
          import argparse
          import json
          import re
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          from datetime import datetime, timedelta, timezone
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          INTERVAL_UNITS = ("day", "month", "quarter", "year")
          ADJUST_KINDS = ("forward", "none")
          
          BEIJING_TZ = timezone(timedelta(hours=8))
          DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def _validate_date(value: str, name: str) -> str:
              """校验 YYYY-MM-DD 格式;若传入时带时间/时区则截取日期部分(东八区)。"""
              v = value.strip()
              if DATE_RE.match(v):
                  return v
              try:
                  raw = v
                  if raw.endswith("Z"):
                      raw = raw[:-1] + "+00:00"
                  dt = datetime.fromisoformat(raw)
                  if dt.tzinfo is None:
                      dt = dt.replace(tzinfo=BEIJING_TZ)
                  else:
                      dt = dt.astimezone(BEIJING_TZ)
                  return dt.strftime("%Y-%m-%d")
              except ValueError:
                  print(f"{name} 格式错误,需为 YYYY-MM-DD:{value}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询港股 K 线")
              parser.add_argument(
                  "--trade-code",
                  dest="trade_code",
                  required=True,
                  help="港股代码,如 00700.HK 或 700;响应统一为 5 位 + .HK",
              )
              parser.add_argument(
                  "--interval-unit",
                  dest="interval_unit",
                  required=True,
                  choices=INTERVAL_UNITS,
                  help="K 线间隔:day / month / quarter / year",
              )
              parser.add_argument(
                  "--until-date",
                  dest="until_date",
                  required=True,
                  help="结束日期,YYYY-MM-DD(东八区交易日)",
              )
              parser.add_argument(
                  "--since-date",
                  dest="since_date",
                  default=None,
                  help="开始日期,YYYY-MM-DD;不传则从库中最早数据起",
              )
              parser.add_argument(
                  "--adjust-kind",
                  dest="adjust_kind",
                  default="forward",
                  choices=ADJUST_KINDS,
                  help="复权类型:forward(前复权,默认)或 none(不复权)",
              )
              parser.add_argument(
                  "--interval-value",
                  dest="interval_value",
                  type=int,
                  default=1,
                  help="间隔数值,当前仅支持 1(默认 1)",
              )
              parser.add_argument(
                  "--limit",
                  type=int,
                  default=None,
                  help="返回条数上限;日 K 在 SQL 层下推,月/季/年聚合后截取最近 N 根",
              )
              args = parser.parse_args()
          
              until_date = _validate_date(args.until_date, "--until-date")
              since_date = None
              if args.since_date is not None and args.since_date.strip():
                  since_date = _validate_date(args.since_date, "--since-date")
          
              params = {
                  "trade_code": args.trade_code.strip(),
                  "interval_unit": args.interval_unit,
                  "until_date": until_date.replace("-", ""),
                  "adjust_kind": args.adjust_kind,
                  "interval_value": args.interval_value,
              }
              if since_date is not None:
                  params["since_date"] = since_date.replace("-", "")
              if args.limit is not None:
                  params["limit"] = args.limit
          
              path = "/api/v2/market/data/hk/hk-candlesticks"
              url = BASE_URL + path + "?" + urllib.parse.urlencode(params)
          
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
          
              try:
                  with safe_urlopen(req) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3.7 KB
        ---
        name: hk-candlesticks
        description: 按港股代码查询日/月/季/年 K 线。用户问港股 K 线、日 K / 月 K / 季 K / 年 K、00700 历史行情、港股开高低收、OHLC、成交量成交额、前复权不复权港股时使用。必填 trade_code、interval_unit、until_date;可选 since_date、adjust_kind、limit。
        ---
        
        # 查询港股 K 线(hk-candlesticks)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询港股 K 线 |
        | 外部接口 | `/api/v2/market/data/hk/hk-candlesticks` |
        | 请求方式 | GET |
        | 适用场景 | 按港股代码查询日/月/季/年 K 线;请求与响应中的代码均为 **5 位数字 + `.HK`** |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | trade_code | string | 是 | 港股代码 | 00700.HK | 支持 `700` 或 `00700.HK`,响应中统一为 5 位 + `.HK` |
        | interval_unit | string | 是 | K 线间隔单位 | day | 取值:`day`、`month`、`quarter`、`year`(kebab-case 序列化) |
        | until_date | string | 是 | 结束日期 | 2026-03-24 | 格式 `YYYY-MM-DD` |
        | since_date | string | 否 | 开始日期 | 2026-01-01 | 不传则从库中最早数据起至 `until_date` |
        | adjust_kind | string | 否 | 复权类型 | forward | 默认 `forward`(前复权);`none` 为不复权 |
        | interval_value | int | 否 | 间隔数值 | 1 | 当前仅支持 `1`,其它值会报错 |
        | limit | int | 否 | 返回条数上限 | 100 | 日 K 在 SQL 层下推;月/季/年在聚合后截取最近 N 根 |
        
        ## 3. 响应说明
        
        返回值为 **`HkCandlesticksResponse`**:`trade_code` + K 线数组 `items`。
        
        ### HkCandlesticksResponse 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | trade_code | String | 否 | 规范化后的港股代码(5 位 + `.HK`) | - |
        | items | Array | 否 | K 线列表,按日期升序 | - |
        
        ### HkCandlestick 结构(items 元素)
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | open | String | 否 | 开盘价 | 元 |
        | high | String | 否 | 最高价 | 元 |
        | low | String | 否 | 最低价 | 元 |
        | close | String | 否 | 收盘价 | 元 |
        | date | String | 否 | 交易日 | `YYYY-MM-DD` |
        | turnover | String | 否 | 成交额 | 元 |
        | volume | int64 | 否 | 成交量 | 股 |
        
        ### 时区说明
        
        `since_date` / `until_date` 及响应中的 `date` 均为 **港交所交易日历(UTC+8 / 东八区)** 日期。若 Agent 所在系统时区非东八区,计算「今天」等相对日期时应先转为东八区再传参。Handler 内置了东八区容错:若传入 ISO 8601 含时区的字符串,会自动转为东八区后截取日期部分。
        
        ## 4. 调用方式
        
        本 handler 与上级 `FTShare-hk-data/run.py` 配合使用:
        
        ```bash
        python <RUN_PY> hk-candlesticks --trade-code 00700.HK --interval-unit day --until-date 2026-03-24 --since-date 2026-03-01 --limit 20
        python <RUN_PY> hk-candlesticks --trade-code 00700.HK --interval-unit month --until-date 2026-03-24 --limit 12
        ```
        
        其中 `<RUN_PY>` 为 `FTShare-hk-data/run.py` 的绝对路径。
        
        CLI 统一使用 **kebab-case** 长选项名:`--trade-code`、`--interval-unit`、`--until-date`、`--since-date`、`--adjust-kind`、`--interval-value`;查询串参数名仍为接口文档中的 snake_case。
        
        ### 直接执行 handler(调试)
        
        ```bash
        python scripts/handler.py --trade-code 00700.HK --interval-unit day --until-date 2026-03-24
        ```
        
        (需在 `sub-skills/hk-candlesticks` 目录下执行,或传入脚本完整路径。)
        
        ## 5. 请求示例
        
        ```
        GET /api/v2/market/data/hk/hk-candlesticks?trade_code=00700.HK&interval_unit=day&since_date=2026-03-01&until_date=2026-03-24&limit=20
        ```
        
    • hsi-daily-weight
      • scripts
        • handler.py 4.1 KB
          #!/usr/bin/env python3
          """查询恒生系列指数成分股权重(HSI/HSCEI/HSAIT 等),按交易日/日期范围/指数/股票"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v1/market/data/hk/hsi-daily-weight"
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def fetch_page(page: int, page_size: int, **extra) -> dict:
              params = {"page": page, "page_size": page_size}
              params.update({k: v for k, v in extra.items() if v is not None})
              qs = urllib.parse.urlencode(params)
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询恒生系列指数成分股权重")
              parser.add_argument("--trade_date", type=int, default=None, help="交易日 YYYYMMDD;与 start_date/end_date 互斥")
              parser.add_argument("--start_date", type=int, default=None, help="交易日下界 YYYYMMDD;须与 end_date 同时提供")
              parser.add_argument("--end_date", type=int, default=None, help="交易日上界 YYYYMMDD;须与 start_date 同时提供")
              parser.add_argument("--index_slug", type=str, default=None,
                                  help="指数代码,大小写不敏感:HSI(恒生)/HSCEI(国企)/HSAIT(恒生科技)")
              parser.add_argument("--stock_code", type=str, default=None, help="4 位港股代码,如 0700(腾讯)")
              parser.add_argument("--page", type=int, default=1, help="页码(默认 1)")
              parser.add_argument("--page_size", type=int, default=50, help="每页条数(默认 50,最大 200)")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量")
              args = parser.parse_args()
          
              # trade_date 与 start/end 互斥
              if args.trade_date is not None and (args.start_date is not None or args.end_date is not None):
                  print("错误:--trade_date 不可与 --start_date/--end_date 同时使用", file=sys.stderr)
                  sys.exit(1)
              # start/end 必须成对
              if (args.start_date is None) != (args.end_date is None):
                  print("错误:--start_date 与 --end_date 必须同时提供", file=sys.stderr)
                  sys.exit(1)
              # 至少一个过滤条件
              if not (args.trade_date or args.start_date or args.index_slug or args.stock_code):
                  print("错误:至少需要一个过滤条件:--trade_date / --start_date+--end_date / --index_slug / --stock_code", file=sys.stderr)
                  sys.exit(1)
          
              extra = dict(trade_date=args.trade_date, start_date=args.start_date, end_date=args.end_date,
                           index_slug=args.index_slug, stock_code=args.stock_code)
          
              if args.fetch_all:
                  first = fetch_page(1, args.page_size, **extra)
                  data = first.get("data") or {}
                  records = list(data.get("records", []))
                  pages = data.get("pages", 1)
                  for p in range(2, pages + 1):
                      d = fetch_page(p, args.page_size, **extra).get("data") or {}
                      records.extend(d.get("records", []))
                  result = {
                      "code": first.get("code"), "message": first.get("message"),
                      "data": {"pageNum": 1, "pageSize": args.page_size,
                               "total": data.get("total", len(records)), "pages": pages, "records": records},
                  }
              else:
                  result = fetch_page(args.page, args.page_size, **extra)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 4.7 KB
        ---
        name: hsi-daily-weight
        description: 查询恒生系列指数成份股权重(HSI 恒生指数 / HSCEI 国企指数 / HSAIT 等)。按交易日/日期范围/指数/股票过滤。Use when user asks about 港股指数权重, 恒生指数成份股权重, 国企指数权重, 恒生科技权重, hsi daily weight, 港股权重. 注意:HSAIT 的"恒生科技"映射存疑(见注意事项),HSI/HSCEI 可用。
        ---
        
        # 查询恒生系列指数成份股权重
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 对外接口-查询恒生指数成分权重 |
        | 接口列表 | `hsi-daily-weight` |
        | 外部接口 | GET /api/v1/market/data/hk/hsi-daily-weight |
        | 请求方式 | GET |
        | 适用场景 | 查询恒生系列指数成份股的权重数据,支持按交易日、日期范围、指数代码、股票代码过滤。 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | trade_date | int | 否 | 交易日 | 20260529 | YYYYMMDD;**与 start_date/end_date 互斥** |
        | start_date | int | 否 | 交易日下界 | 20260501 | YYYYMMDD;须与 end_date 同时提供 |
        | end_date | int | 否 | 交易日上界 | 20260529 | YYYYMMDD;须与 start_date 同时提供 |
        | index_slug | string | 否 | 指数代码(大小写不敏感) | HSI | `HSI`(恒生)/`HSCEI`(国企)/`HSAIT`(见注意) |
        | stock_code | string | 否 | 4 位港股代码 | 0700 | 如 `0700`(腾讯),**4 位非 5 位** |
        | page / page_size | int | 否 | 分页 | 1 / 50 | 默认 1 / 50,page_size 最大 200 |
        
        > **至少需要一个过滤条件**:`trade_date`、`start_date`+`end_date`、`index_slug`、`stock_code`,否则 400。
        
        ## 执行方式
        
        ```bash
        # 恒生指数 HSI 某日成份权重(先按指数取,再按最新交易日筛)
        python scripts/handler.py --index_slug HSI --page 1 --page_size 200
        # 某日某指数成份权重
        python scripts/handler.py --trade_date 20260529 --index_slug HSCEI
        # 某股票在各指数的权重历史
        python scripts/handler.py --stock_code 0700
        # 日期范围 + 指数
        python scripts/handler.py --start_date 20260501 --end_date 20260529 --index_slug HSI
        ```
        
        ## 响应结构(信封:code/message/data.records)
        
        ```json
        {
          "code": 0, "message": "success",
          "data": {
            "pageNum": 1, "pageSize": 50, "total": 90, "pages": 2,
            "records": [
              { "trade_date": "2026-05-29", "index_slug": "hsi", "index_name": "hsi",
                "stock_code": "0700", "stock_name": "TENCENT 騰訊控股", "weight_pct": "8.4",
                "source_file": "con_29May26.pdf", "url_hash": "...", "processed_at": "2026-05-29 19:34:36" }
            ]
          }
        }
        ```
        
        > ⚠️ 数据在 `data.records`,**不是顶层数组**。
        
        ### records 字段
        
        | 字段 | 说明 |
        |---|---|
        | trade_date | 交易日 YYYY-MM-DD |
        | index_slug | 指数代码(小写,如 `hsi`/`hscei`/`hsait`) |
        | index_name | 当前与 index_slug 相同(小写代码,非中文名) |
        | stock_code | 4 位港股代码(`0700`,非 `00700`) |
        | stock_name | 股票名称(多为英文/繁体,如 `TENCENT 騰訊控股`) |
        | weight_pct | 权重百分比,**字符串**(如 `"8.4"`);未缩放,直接是百分数 |
        | source_file / url_hash / processed_at | 来源文件 / URL 哈希 / 处理时间 |
        
        ## 注意事项
        
        - **⚠️ `HSAIT` 的"恒生科技"映射存疑(实测发现,2026-05-29 验证)**:
          - 文档称 `HSAIT = 恒生科技指数`,但实测返回 **40 只成份股**,且**中国移动(0941)以 10.86% 排第一**。
          - 官方恒生科技指数(HSTECH)为 **30 只**、**8% 个股权重上限**、**不含中国移动**——三点均不符。
          - → **勿将 `HSAIT` 直接当作恒生科技指数权重**。其真实对应指数未确定(可能为恒生港股通科技/综合类)。需数据团队核对 `index_slug` 映射或上游 PDF 解析。
          - 另:kline 接口里恒生科技叫 `HSTECH`,与本接口的 `HSAIT` 命名不一致。
        - **`HSI` / `HSCEI` 权重可用**:HSI 实测返回 ~90 只成份股(恒指规模相符),Top 为汇丰/阿里/腾讯/友邦/建行,合理。
        - `weight_pct` 是字符串百分数(**未做 ×1e4 缩放**,与港股财报的金额缩放不同)。
        - `stock_code` 为 **4 位**(`0700`,非 `00700`/`00700.HK`)。
        - `index_slug` 大小写不敏感,响应中始终返回小写。
        - `trade_date` 与 `start_date`/`end_date` 互斥;`start_date`/`end_date` 必须成对且 `start_date ≤ end_date`。
        - 排序:`trade_date DESC, index_slug, stock_code`。
        - HTTP 恒为 200,业务错误通过 `code`/`message` 携带(非 0 为错误);高频调用建议用 `trade_date` 精确过滤。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> hsi-daily-weight --trade_date 20260821 --index_slug HSI --page 1
        ```
        
    • index-candlesticks
      • scripts
        • handler.py 4.7 KB
          #!/usr/bin/env python3
          """查询单只指数历史 K 线(GET /api/v1/market/data/index-candlesticks)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/index-candlesticks"
          
          INTERVAL_UNITS = ("Minute", "Day", "Week", "Month", "Year")
          ADJUST_KINDS = ("None", "Forward", "Backward")
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def build_body(symbol, interval_unit, interval_value, adjust_kind,
                         since_ts_millis, until_ts_millis, limit):
              body = {
                  "symbol": symbol,
                  "interval_unit": interval_unit,
                  "until_ts_millis": until_ts_millis,
              }
              if interval_value is not None and interval_value != 1:
                  body["interval_value"] = interval_value
              if adjust_kind and adjust_kind != "None":
                  body["adjust_kind"] = adjust_kind
              if since_ts_millis is not None:
                  body["since_ts_millis"] = since_ts_millis
              if limit is not None:
                  body["limit"] = limit
              return body
          
          
          def fetch(
          symbol, interval_unit, interval_value, adjust_kind,
                    since_ts_millis, until_ts_millis, limit):
              body = build_body(symbol, interval_unit, interval_value, adjust_kind,
                                since_ts_millis, until_ts_millis, limit)
              query = urllib.parse.urlencode(body, doseq=True)
              url = f"{BASE_URL}{ENDPOINT}?{query}"
              req = urllib.request.Request(
                  url,
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  msg = e.read().decode()
                  print(f"HTTP {e.code}: {msg}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询单只指数历史 K 线(GET 查询参数)")
              parser.add_argument("--symbol", required=True, help="指数代码,如 000300.XSHG、399001.XSHE")
              parser.add_argument("--interval-unit", dest="interval_unit", required=True,
                                  choices=INTERVAL_UNITS, help="K 线周期:Minute/Day/Week/Month/Year")
              parser.add_argument("--interval-value", dest="interval_value", type=int, default=1,
                                  help="间隔数值,默认 1(Minute+5 表示 5 分钟 K)")
              parser.add_argument("--adjust-kind", dest="adjust_kind", default="None",
                                  choices=ADJUST_KINDS, help="复权:None(默认)/Forward/Backward")
              parser.add_argument("--since-ts-millis", dest="since_ts_millis", type=int, default=None,
                                  help="开始时间戳(毫秒);分钟 K 与 until 跨度 ≤3 天")
              parser.add_argument("--until-ts-millis", dest="until_ts_millis", required=True, type=int,
                                  help="结束时间戳(毫秒)")
              parser.add_argument("--limit", type=int, default=None, help="返回条数上限")
              args = parser.parse_args()
          
              data = fetch(args.symbol, args.interval_unit, args.interval_value,
                           args.adjust_kind, args.since_ts_millis, args.until_ts_millis, args.limit)
              print(json.dumps(data, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2 KB
          #!/usr/bin/env python3
          """Tests for index-candlesticks handler"""
          import json
          import sys
          import unittest
          import urllib.error
          from io import BytesIO, StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_get_to_index_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("000300.XSHG", "Day", 1, "None", None, 1756791000000, 5)
                  req = mock_open.call_args[0][0]
                  self.assertEqual(req.get_method(), "GET")
                  self.assertIn("/api/v1/market/data/index-candlesticks", req.full_url)
                  self.assertIsNone(req.data)
                  self.assertEqual(req.headers.get("X-client-name"), "ft-claw")
          
              @patch.object(handler, "safe_urlopen")
              def test_http_error_exits(self, mock_open):
                  mock_open.side_effect = urllib.error.HTTPError(
                      "https://fake", 500, "Internal Error", {}, BytesIO(b"server error")
                  )
                  with self.assertRaises(SystemExit):
                      handler.fetch("000300.XSHG", "Day", 1, "None", None, 1756791000000, None)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  with patch.object(sys, "argv", [
                      "handler.py", "--symbol", "000300.XSHG", "--interval-unit", "Day",
                      "--until-ts-millis", "1756791000000"
                  ]):
                      with patch("sys.stdout", new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 3.8 KB
        ---
        name: index-candlesticks
        description: 单只指数历史 K 线 GET 接口(market.ft.tech,index-candlesticks)。用户问某只指数的分/日/周/月/年 K 线、开高低收点位、前/后复权、分钟级 K 线、沪深300/上证指数日 K 时使用。必填 --symbol、--interval-unit、--until-ts-millis;可选 --interval-value、--adjust-kind、--since-ts-millis、--limit。
        ---
        
        # 指数 K 线 - 查询单只指数 K 线(index-candlesticks)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询单只指数历史 K 线 |
        | 外部接口 | `GET /api/v1/market/data/index-candlesticks` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 获取指定指数的分/日/周/月/年 K 线,含开高低收点位、成交量、成交额;支持前复权 / 后复权 / 不复权。仅接受指数标的 |
        
        > 与 `index-ohlcs`(`GET daec/history/ohlcs`,YYYYMMDD 日期区间,仅日/周/月)区别:本接口走 GET query 参数,参数为毫秒时间戳,支持分钟级与年 K。
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 是 | 指数代码(带市场后缀) | 000300.XSHG、399001.XSHE | 也接受 `.SH`/`.SZ` 短后缀;非指数标的当前返回系统错误 |
        | interval_unit | string | 是 | 周期单位 | Day | Minute/Day/Week/Month/Year |
        | interval_value | int | 否 | 间隔数值 | 1 | 默认 1;Minute+5 表示 5 分钟 K 线 |
        | adjust_kind | string | 否 | 复权类型 | Forward | None(默认)/Forward(前复权)/Backward(后复权) |
        | since_ts_millis | int | 否 | 开始时间戳(毫秒) | 1756700000000 | 分钟 K 线与 until 跨度 ≤3 天,其余周期不受 3 天限制 |
        | until_ts_millis | int | 是 | 结束时间戳(毫秒) | 1756791000000 | - |
        | limit | int | 否 | 返回条数上限 | 5 | 未传 since 和 limit 时默认最多返回 50 根 |
        
        ## 3. 响应说明
        
        返回裸数组,每根 K 线包含:
        
        | 字段名 | 类型 | 说明 | 单位 |
        |--------|------|------|------|
        | open | string | 开盘点位 | 指数点 |
        | high | string | 最高点位 | 指数点 |
        | low | string | 最低点位 | 指数点 |
        | close | string | 收盘点位(或最新点位) | 指数点 |
        | ts_millis | int | 收盘时间戳 | 毫秒 |
        | ts_millis_open | int | 开盘时间戳 | 毫秒 |
        | turnover | string | 成交额(成分股合计) | 元 |
        | volume | int64 | 成交量(成分股合计) | - |
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> index-candlesticks --symbol 000300.XSHG --interval-unit Day --since-ts-millis 1756700000000 --until-ts-millis 1756791000000 --limit 5
        python <RUN_PY> index-candlesticks --symbol 399001.XSHE --interval-unit Minute --interval-value 5 --adjust-kind Forward --since-ts-millis 1756700000000 --until-ts-millis 1756791000000
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON,请求头已内置 `X-Client-Name: ft-claw`。
        
        ## 5. 注意事项
        
        - `symbol`、`interval_unit`、`until_ts_millis` 必填。
        - `symbol` 必须是指数代码,格式 `{代码}.{市场}`;非指数标的当前外部接口返回系统错误。
        - 分钟 K 线(`interval_unit=Minute`)的 `since/until` 跨度硬限制 ≤3 天,超过需分段调用。
        - `interval_value` 仅在 `interval_unit=Minute` 时生效:不传或传 1 为 1 分钟 K,传 5/15/30/60/120 为对应多分钟 K;其他周期忽略该字段。
        - 多分钟 K 按北京时间的每个交易日分别聚合,不跨交易日;以 5 分钟 K 为例,首根为 09:30—09:35,开高低收取区间首根开盘价、最高价、最低价、末根收盘价,成交量和成交额按区间求和。
        - 默认不复权(None),`Forward` 前复权、`Backward` 后复权。
        - 价格字段(点位)JSON 中为字符串以避免精度丢失。
        
    • index-candlesticks-batch
      • scripts
        • handler.py 4.9 KB
          #!/usr/bin/env python3
          """批量查询多个指数 K 线(GET /api/v2/market/data/index-candlesticks/batch)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v2/market/data/index-candlesticks/batch"
          
          INTERVAL_UNITS = ("Day", "Week", "Month", "Year")
          ADJUST_KINDS = ("None", "Forward", "Backward")
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def parse_symbols(raw):
              syms = [s.strip() for s in raw.split(",") if s.strip()]
              if not syms:
                  print("--symbols 不能为空", file=sys.stderr)
                  sys.exit(1)
              return syms
          
          
          def build_query(symbols, interval_unit, adjust_kind, since_ts_millis, until_ts_millis, limit):
              body = {
                  "symbols": symbols,
                  "interval_unit": interval_unit,
                  "until_ts_millis": until_ts_millis,
              }
              if adjust_kind and adjust_kind != "None":
                  body["adjust_kind"] = adjust_kind
              if since_ts_millis is not None:
                  body["since_ts_millis"] = since_ts_millis
              if limit is not None:
                  body["limit"] = limit
              return body
          
          
          def fetch(symbols, interval_unit, adjust_kind, since_ts_millis, until_ts_millis, limit):
              body = build_query(symbols, interval_unit, adjust_kind, since_ts_millis, until_ts_millis, limit)
              query = urllib.parse.urlencode(body, doseq=True)
              req = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}?{query}",
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="批量获取多个指数的历史 K 线(不支持分钟周期)")
              parser.add_argument("--symbols", required=True,
                                  help="指数代码列表,逗号分隔,如 000300.XSHG,399001.XSHE;也接受 .SH/.SZ 短后缀")
              parser.add_argument("--interval-unit", dest="interval_unit", required=True, type=str.capitalize,
                                  choices=INTERVAL_UNITS, help="K 线周期:Day/Week/Month/Year(大小写不敏感,不支持 Minute)")
              parser.add_argument("--adjust-kind", dest="adjust_kind", default="None",
                                  choices=ADJUST_KINDS, help="复权:None(默认)/Forward/Backward")
              parser.add_argument("--since-ts-millis", dest="since_ts_millis", required=True, type=int,
                                  help="开始时间戳(毫秒);与结束时间跨度不得超过 12 个日历月")
              parser.add_argument("--until-ts-millis", dest="until_ts_millis", required=True, type=int,
                                  help="结束时间戳(毫秒)")
              parser.add_argument("--limit", type=int, default=None,
                                  help="每个标的返回条数上限;不传时返回请求时间范围内的全部数据")
              args = parser.parse_args()
          
              if args.since_ts_millis > args.until_ts_millis:
                  print("--since-ts-millis 不能晚于 --until-ts-millis", file=sys.stderr)
                  raise SystemExit(2)
          
              symbols = parse_symbols(args.symbols)
              data = fetch(symbols, args.interval_unit, args.adjust_kind,
                           args.since_ts_millis, args.until_ts_millis, args.limit)
              print(json.dumps(data, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 4.6 KB
          #!/usr/bin/env python3
          """Tests for index-candlesticks-batch handler"""
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          SINCE = "1756431000000"
          UNTIL = "1756791000000"
          
          
          class TestBuildQuery(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_required_only(self):
                  body = handler.build_query(["000300.SH"], "Day", "None", 1756431000000, 1756791000000, None)
                  self.assertEqual(body["symbols"], ["000300.SH"])
                  self.assertEqual(body["interval_unit"], "Day")
                  self.assertEqual(body["since_ts_millis"], 1756431000000)
                  self.assertNotIn("adjust_kind", body)
                  self.assertNotIn("limit", body)
                  self.assertNotIn("interval_value", body)
          
              def test_minute_not_allowed(self):
                  with self.assertRaises(SystemExit):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "000300.SH",
                                                      "--interval-unit", "Minute",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          handler.main()
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_targets_index_route(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = (
                      b'{"code":200,"message":"success","data":[["000300.SH",[]]]}'
                  )
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "000300.SH,399001.SZ",
                                                      "--interval-unit", "Day",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          with patch("sys.stdout", new_callable=StringIO) as out:
                              handler.main()
                              data = json.loads(out.getvalue())
                              self.assertEqual(data["data"][0][0], "000300.SH")
                  req = mock_open.call_args[0][0]
                  self.assertIn("/api/v2/market/data/index-candlesticks/batch", req.full_url)
                  self.assertIn("since_ts_millis=1756431000000", req.full_url)
          
              def test_main_requires_since(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "000300.SH",
                                                      "--interval-unit", "Day",
                                                      "--until-ts-millis", UNTIL]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              def test_main_rejects_since_after_until(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "000300.SH",
                                                      "--interval-unit", "Day",
                                                      "--since-ts-millis", UNTIL,
                                                      "--until-ts-millis", SINCE]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              @patch.object(handler, "safe_urlopen")
              def test_interval_unit_case_insensitive(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"{}"
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "000300.SH",
                                                      "--interval-unit", "day",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          handler.main()
                  req = mock_open.call_args[0][0]
                  self.assertIn("interval_unit=Day", req.full_url)
          
              def test_main_rejects_empty_symbols(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", " , ",
                                                      "--interval-unit", "Day",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 3.8 KB
        ---
        name: index-candlesticks-batch
        description: 批量查询多个指数的历史 K 线(index_candlesticks_batch)。用户问多个指数日/周/月/年 K 线、批量指数开高低收、指数批量行情、多指数对比 K 线时使用。
        ---
        
        # 批量指数K线
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 批量指数K线(index_candlesticks_batch) |
        | 外部接口 | `GET /api/v2/market/data/index-candlesticks/batch` |
        | 请求方式 | GET(query 参数,`symbols` 可重复传入) |
        | 适用场景 | 一次批量获取多个指数的历史 K 线(开高低收、成交量、成交额、换手率),支持日/周/月/年周期与前复权/后复权 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbols | string[] | 是 | 指数代码列表,逗号分隔传给 CLI | 000300.SH,399001.SZ | 沪市支持 `.XSHG`/`.SH`,深市支持 `.XSHE`/`.SZ`;接口侧以重复 query 参数发送 |
        | interval_unit | string | 是 | 周期单位 | Day | Day/Week/Month/Year,大小写不敏感;**不支持 Minute** |
        | adjust_kind | string | 否 | 复权类型 | Forward | None(默认)/Forward(前复权)/Backward(后复权) |
        | since_ts_millis | int | 是 | 开始时间戳(毫秒) | 1756431000000 | 与 until 的跨度不得超过 12 个日历月;不得晚于 until |
        | until_ts_millis | int | 是 | 结束时间戳(毫秒) | 1756791000000 | - |
        | limit | int | 否 | 每个标的返回条数上限 | 2 | 不传时返回请求时间范围内的全部数据 |
        
        ## 3. 响应说明
        
        外层固定为 `code`(成功 200)/ `message`(成功 `success`)/ `data`(失败时为 `null`)。`data` 为非分页嵌套数组,外层每项为 `[symbol, K线数组]`,每根 K 线字段:
        
        | 字段名 | 类型 | 说明 | 单位 |
        |--------|------|------|------|
        | symbol | string | 指数代码,响应统一使用 `.SH`、`.SZ` 短后缀 | - |
        | open / high / low / close | number | 开/高/低/收盘点位 | 指数点 |
        | ts_millis | string | 收盘时间戳 | 毫秒 |
        | ts_millis_open | string | 开盘时间戳 | 毫秒 |
        | turnover | number | 成交额(指数成分股合计) | 元 |
        | volume | integer | 成交量(指数成分股合计) | - |
        | turnover_rate | number | 换手率;指数标的当前为 `null` | % |
        
        注:`open/high/low/close`、`turnover` 在 JSON 中实际以字符串返回(避免精度丢失);`ts_millis` 为数字。
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> index-candlesticks-batch --symbols 000300.SH,399001.SZ --interval-unit Day --since-ts-millis 1756431000000 --until-ts-millis 1756791000000 --limit 2
        python <RUN_PY> index-candlesticks-batch --symbols 000300.XSHG,399001.XSHE --interval-unit Week --adjust-kind Forward --since-ts-millis 1754092800000 --until-ts-millis 1756791000000
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - `symbols`、`interval_unit`、`since_ts_millis`、`until_ts_millis` 必填;所有 `symbols` 使用相同周期。
        - 接口仅支持 GET;`symbols` 在查询参数中以重复参数形式发送(`symbols=000300.SH&symbols=399001.SZ`)。
        - 时间跨度最多 12 个日历月;需要更长历史时按窗口分段多次调用。
        - 不支持分钟 K 线;分钟数据请使用 `index-minutes-batch` 子 skill。
        - `symbols` 中每项必须是指数标的:若混入非指数(如 ETF、股票),整个批量请求失败,不静默过滤(当前返回系统错误)。
        - 输入 `.XSHG`/`.XSHE` 长后缀时,响应中的 symbol 会规范化为 `.SH`、`.SZ` 短后缀。
        - 默认不复权(None);仅使用历史日 K 数据计算,不含实时行情,实际起始日期以行情数据源覆盖为准。
        
    • index-description-all
      • scripts
        • handler.py 1.9 KB
          #!/usr/bin/env python3
          """查询全部指数基础信息(market.ft.tech)"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          
          
          def safe_urlopen(req, timeout=30):
              url = req.full_url if isinstance(req, urllib.request.Request) else str(req)
              parsed = urllib.parse.urlparse(url)
              base = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req, urllib.request.Request):
                  req = urllib.request.Request(url, method="GET")
              for key, value in _REQUEST_HEADERS.items():
                  req.add_unredirected_header(key, value)
              return SAFE_URLOPENER.open(req, timeout=timeout)
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/index-description-all"
          
          
          def main():
              _require_api_key()
              url = BASE_URL + ENDPOINT
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
              req.add_header("X-Client-Name", "ft-claw")
              req.add_header("Content-Type", "application/json")
          
              try:
                  with safe_urlopen(req, timeout=30) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2 KB
        ---
        name: index-description-all
        description: 查询全部指数基础信息(market.ft.tech)。用户问全部指数列表、指数简称全称、指数 PB/PE TTM、支持的指数清单、有哪些指数时使用。
        ---
        
        # 指数 - 查询全部指数基础信息
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询全部指数基础信息 |
        | 外部接口 | `GET /api/v1/market/data/index-description-all` |
        | 请求方式 | GET |
        | 适用场景 | 获取当前服务支持的 A 股相关指数列表及简称、全称、市净率、市盈率(TTM)等基础信息 |
        
        ## 2. 请求参数
        
        说明:该接口无需请求参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | - | - | - | 无需参数 | - | - |
        
        ## 3. 响应说明
        
        返回值为指数基础信息数组。
        
        ```json
        [
          {
            "symbol": "000001.XSHG",
            "full_name": "上证综合指数",
            "name": "上证指数",
            "pb": 1.5234,
            "pe_ttm": 16.5903
          }
        ]
        ```
        
        ### IndexDescriptionItem 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | symbol | string | 否 | 指数代码,一般为六位代码加交易所后缀(如 `000001.XSHG`、`399001.XSHE`、`899050.BJSE`) | - |
        | full_name | string | 否 | 指数全称 | - |
        | name | string | 否 | 指数简称 | - |
        | pb | float | 是 | 市净率(LF),无数据时为 null | - |
        | pe_ttm | float | 是 | 市盈率(TTM),无数据时为 null | - |
        
        ## 4. 用法
        
        通过主目录 `run.py` 调用(无参数):
        
        ```bash
        python <RUN_PY> index-description-all
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。脚本输出 JSON;请求头已内置 `X-Client-Name: ft-claw`。
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/index-description-all
        ```
        
        ## 6. 注意事项
        
        - 响应为数组;字段 `pb`、`pe_ttm` 可能为 `null`。
        - 数据更新时间以服务端为准。
        
    • index-description-paginated
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          """分页查询指数描述列表(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          
          def safe_urlopen(req, timeout=30):
              url = req.full_url if isinstance(req, urllib.request.Request) else str(req)
              parsed = urllib.parse.urlparse(url)
              base = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req, urllib.request.Request):
                  req = urllib.request.Request(url, method="GET")
              for key, value in _REQUEST_HEADERS.items():
                  req.add_unredirected_header(key, value)
              return SAFE_URLOPENER.open(req, timeout=timeout)
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/index/index_description"
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="分页查询指数描述列表")
              parser.add_argument(
                  "--page",
                  type=int,
                  default=1,
                  help="页码,从 1 开始,默认 1",
              )
              parser.add_argument(
                  "--page-size",
                  dest="page_size",
                  type=int,
                  default=20,
                  help="每页条数,默认 20,最大 100",
              )
              args = parser.parse_args()
          
              if args.page < 1:
                  print("page 须 >= 1", file=sys.stderr)
                  sys.exit(1)
              if args.page_size < 1 or args.page_size > 100:
                  print("page_size 须在 1~100 之间", file=sys.stderr)
                  sys.exit(1)
          
              params = {"page": args.page, "page_size": args.page_size}
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params)
          
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
              req.add_header("X-Client-Name", "ft-claw")
              req.add_header("Content-Type", "application/json")
          
              try:
                  with safe_urlopen(req, timeout=30) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"请求失败: {e.reason}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.9 KB
        ---
        name: index-description-paginated
        description: 含指数代码(纯 6 位 `index_code`)、名称、简介及其他描述字段。用户问指数描述分页、指数简介列表、某只指数简介、有哪些指数描述时使用。也可作为名称→代码映射源:用户给出中文指数名称时,先调本接口按 `index_name` 匹配,取 `index_code` 供 `index-weight-list` 等接口使用。
        ---
        
        # 分页查询指数描述列表(index-description-paginated)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 分页查询指数描述列表 |
        | 外部接口 | `/api/v1/market/data/index/index_description` |
        | 请求方式 | GET |
        | 适用场景 | 分页获取 A 股相关指数描述信息,含指数代码、名称、简介及用于下载文件的 `url_hash` |
        
        ## 2. 请求参数
        
        说明:使用查询参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | page | int | 否 | 页码 | 1 | 默认 1 |
        | page_size | int | 否 | 每页条数 | 20 | 默认 20,最大 100 |
        
        ## 3. 响应说明
        
        返回值为分页列表对象 **`IndexDescriptionListResponse`**。
        
        ### IndexDescriptionListResponse 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | total | int | 否 | 总条数 | - |
        | page | int | 否 | 当前页码 | - |
        | page_size | int | 否 | 每页条数 | - |
        | index_descriptions | array | 否 | 指数描述条目列表 | - |
        
        ### IndexDescriptionInfo 结构(index_descriptions 元素)
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | index_code | String | 否 | 指数代码 | - |
        | index_orig | String | 否 | 指数原始名称/简称类字段(与上游一致) | - |
        | index_name | String | 否 | 指数名称 | - |
        | index_intro | String | 否 | 指数简介 | - |
        | url_hash | String | 否 | 对应描述文件的哈希,用于「下载指数描述文件」接口 | - |
        
        ## 4. 调用方式
        
        本 handler 与上级 `FTShare-index-data/run.py` 配合使用:
        
        ```bash
        python <RUN_PY> index-description-paginated --page 1 --page-size 20
        ```
        
        其中 `<RUN_PY>` 为 `FTShare-index-data/run.py` 的绝对路径。
        
        ### 直接执行 handler(调试)
        
        ```bash
        python scripts/handler.py --page 1 --page-size 20
        ```
        
        (需在 `sub-skills/index-description-paginated` 目录下执行,或传入脚本完整路径。)
        
        ## 5. 链路说明
        
        本接口返回的 `index_code`(纯 6 位,如 `000300`)与 `index_name` 可用于名称到代码映射。当其他接口(如 `index-weight-list`)需要 `index_code` 但用户只给了中文名称时:
        
        1. 调用本接口 `--page 1 --page-size 100`
        2. 在 `index_descriptions` 中按 `index_name` 模糊搜索用户给出的名称
        3. 取匹配项的 `index_code` 传给目标接口
        
        ## 6. 请求示例
        
        ```
        GET /api/v1/market/data/index/index_description?page=1&page_size=20
        ```
        
    • index-minutes
      • scripts
        • handler.py 3 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v2/market/data/index_minutes"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询指数历史分钟行情")
              parser.add_argument("--symbol", required=True)
              parser.add_argument("--interval-value", dest="interval_value", type=int, default=1)
              parser.add_argument("--adjust-kind", dest="adjust_kind")
              parser.add_argument("--since-ts-millis", dest="since_ts_millis", required=True, type=int)
              parser.add_argument("--until-ts-millis", dest="until_ts_millis", required=True, type=int)
              parser.add_argument("--limit", type=int, default=50)
              args = parser.parse_args()
              if args.interval_value <= 0 or not 1 <= args.limit <= 1000:
                  parser.error("interval-value 须大于 0,limit 须在 1~1000 之间")
              params = {"symbol": args.symbol, "interval_value": args.interval_value,
                        "since_ts_millis": args.since_ts_millis, "until_ts_millis": args.until_ts_millis,
                        "limit": args.limit}
              if args.adjust_kind is not None:
                  params["adjust_kind"] = args.adjust_kind
              request = urllib.request.Request(BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params),
                                               headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 704 B
        ---
        name: index-minutes
        description: 指数历史分钟行情。必填 --symbol;可选日期和分页参数。 用户询问指数历史分钟行情时使用。
        ---
        
        # 指数历史分钟行情
        
        外部接口:`GET /api/v2/market/data/index_minutes`。
        
        必填 --symbol;可选日期和分页参数。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> index-minutes --symbol 000300.SH --since-ts-millis 1787189400000 --until-ts-millis 1787191200000 --limit 5
        ```
        
        接口返回 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
    • index-minutes-batch
      • scripts
        • handler.py 1.7 KB
          #!/usr/bin/env python3
          import argparse,json,os,sys,urllib.parse,urllib.request
          BASE_URL=os.environ.get("FTSHARE_BASE_URL","https://market.ft.tech/gateway").rstrip("/");_REQUEST_HEADERS={"FTSHARE_API_KEY":os.environ.get("FTSHARE_API_KEY", ""),"Content-Type":"application/json"}; ENDPOINT="/api/v2/market/data/index_minutes/batch"
          def main():
           p=argparse.ArgumentParser(); p.add_argument("--symbols",required=True); p.add_argument("--since-ts-millis",required=True,type=int); p.add_argument("--until-ts-millis",required=True,type=int); p.add_argument("--interval-value",type=int,default=1); p.add_argument("--adjust-kind",choices=["None","Forward","Backward"],default="None"); p.add_argument("--limit",type=int,default=50); a=p.parse_args()
           if a.interval_value<1 or not 1<=a.limit<=1000:p.error("interval-value 须大于 0,limit 须在 1~1000 之间")
           key=os.environ.get("FTSHARE_API_KEY")
           if not key: print("FTSHARE_API_KEY environment variable is required",file=sys.stderr); raise SystemExit(2)
           params=[("symbols",s.strip()) for s in a.symbols.split(",") if s.strip()]+[("since_ts_millis",a.since_ts_millis),("until_ts_millis",a.until_ts_millis),("interval_value",a.interval_value),("limit",a.limit)]
           if a.adjust_kind!="None":params.append(("adjust_kind",a.adjust_kind))
           req=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params),headers={"FTSHARE_API_KEY":key,"Content-Type":"application/json"},method="GET")
           try:
            with urllib.request.urlopen(req,timeout=30) as r: print(json.dumps(json.loads(r.read().decode()),ensure_ascii=False,indent=2))
           except Exception as e: print(f"请求失败: {e}",file=sys.stderr); raise SystemExit(1)
          if __name__=="__main__":main()
          
      • SKILL.md 711 B
        ---
        name: index-minutes-batch
        description: 批量查询 指数历史分钟 K 线。用户询问多只指数的分钟行情时使用。
        ---
        
        # 批量指数历史分钟行情
        
        接口:`GET /api/v2/market/data/index_minutes/batch`。必填 `--symbols`、`--since-ts-millis`、`--until-ts-millis`;`--symbols` 使用逗号分隔,最多 20 只;可选 `--interval-value`、`--limit`,时间跨度不超过 3 天,`limit` 范围 1~1000。
        
        ```bash
        python <RUN_PY> index-minutes-batch --symbols 000300.SH,399001.SZ --since-ts-millis 1787189400000 --until-ts-millis 1787191200000 --limit 5
        ```
        
        返回 `code/message/data`,`data` 为每只标的的 `symbol`、`total` 和 `items` 列表;不是分页接口。
        
    • index-realtime-day-kline
      • scripts
        • handler.py 2.3 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v4/market/data/index-realtime-day-kline'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description='查询指数实时日 K 线')
              parser.add_argument("--symbols", nargs="+", required=True)
          
              args = parser.parse_args()
              params = {"symbols": json.dumps(args.symbols, ensure_ascii=False)}
          
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 664 B
        ---
        name: index-realtime-day-kline
        description: 查询指数当前交易日实时日 K 线。必填 --symbols,按空格分隔。接口:GET /api/v4/market/data/index-realtime-day-kline。
        ---
        
        # 指数实时日K线
        
        查询指数当前交易日实时日 K 线。必填 --symbols,按空格分隔。接口:GET /api/v4/market/data/index-realtime-day-kline。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> index-realtime-day-kline --symbols 000300.SH 399001.SZ --symbols 000300.SH 399001.SZ
        ```
        
    • index-realtime-minute-kline
      • scripts
        • handler.py 2.3 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v4/market/data/index-realtime-minute-kline'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description='查询指数实时分钟 K 线')
              parser.add_argument("--symbols", nargs="+", required=True)
          
              args = parser.parse_args()
              params = {"symbols": json.dumps(args.symbols, ensure_ascii=False)}
          
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 705 B
        ---
        name: index-realtime-minute-kline
        description: 查询指数当前交易日实时 1 分钟 K 线。必填 --symbols,按空格分隔,单次最多 20 个。接口:GET /api/v4/market/data/index-realtime-minute-kline。
        ---
        
        # 指数实时分钟K线
        
        查询指数当前交易日实时 1 分钟 K 线。必填 --symbols,按空格分隔,单次最多 20 个。接口:GET /api/v4/market/data/index-realtime-minute-kline。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> index-realtime-minute-kline --symbols 000300.SH 399001.SZ
        ```
        
    • index-weight-list
      • scripts
        • handler.py 3.4 KB
          #!/usr/bin/env python3
          """分页查询指数权重列表(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          
          def safe_urlopen(req, timeout=30):
              url = req.full_url if isinstance(req, urllib.request.Request) else str(req)
              parsed = urllib.parse.urlparse(url)
              base = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req, urllib.request.Request):
                  req = urllib.request.Request(url, method="GET")
              for key, value in _REQUEST_HEADERS.items():
                  req.add_unredirected_header(key, value)
              return SAFE_URLOPENER.open(req, timeout=timeout)
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/index/index_weight"
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="分页查询指数权重列表")
              parser.add_argument(
                  "--index-code",
                  dest="index_code",
                  required=True,
                  help="指数代码,如 000300,不能为空",
              )
              parser.add_argument(
                  "--date",
                  default=None,
                  help="查询日期,YYYYMMDD;不传则由服务端默认行为决定",
              )
              parser.add_argument(
                  "--page",
                  type=int,
                  default=1,
                  help="页码,从 1 开始,默认 1",
              )
              parser.add_argument(
                  "--page-size",
                  dest="page_size",
                  type=int,
                  default=20,
                  help="每页条数,默认 20,最大 100",
              )
              args = parser.parse_args()
          
              index_code = args.index_code.strip()
              if not index_code:
                  print("index_code 不能为空", file=sys.stderr)
                  sys.exit(1)
              if args.page < 1:
                  print("page 须 >= 1", file=sys.stderr)
                  sys.exit(1)
              if args.page_size < 1 or args.page_size > 100:
                  print("page_size 须在 1~100 之间", file=sys.stderr)
                  sys.exit(1)
          
              params = {"index_code": index_code, "page": args.page, "page_size": args.page_size}
              if args.date is not None and str(args.date).strip() != "":
                  params["date"] = args.date.strip()
          
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params)
          
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
              req.add_header("X-Client-Name", "ft-claw")
              req.add_header("Content-Type", "application/json")
          
              try:
                  with safe_urlopen(req, timeout=30) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"请求失败: {e.reason}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3.8 KB
        ---
        name: index-weight-list
        description: 分页查询指数成份权重明细(market.ft.tech)。必填 index_code;可选 date、分页。每条含成份代码、名称、权重及该期 url_hash。用户问指数成份权重、沪深300 权重列表、某期指数权重明细、指数成份股占比时使用。前置:可先用 index-weight-summary 确认有哪些期数/日期。下游:取到 url_hash 后可调 index-weight-download 下载 xlsx。
        ---
        
        # 分页查询指数权重列表(index-weight-list)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 分页查询指数权重列表 |
        | 外部接口 | `/api/v1/market/data/index/index_weight` |
        | 请求方式 | GET |
        | 适用场景 | 按指数代码(及可选日期)分页查询成份权重明细;每条记录含成份代码、名称、权重比例及该期文件 `url_hash` |
        
        ## 2. 请求参数
        
        说明:使用查询参数。服务端校验 `index_code` 非空,否则返回错误。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | index_code | string | 是 | 指数代码 | 000300 | 不能为空或仅空白 |
        | date | string | 否 | 查询日期,格式 `YYYYMMDD` | 20250320 | 不传则由上游默认行为决定 |
        | page | int | 否 | 页码 | 1 | 默认 1 |
        | page_size | int | 否 | 每页条数 | 20 | 默认 20,最大 100 |
        
        ## 3. 响应说明
        
        返回值为分页列表对象 **`IndexWeightListResponse`**。
        
        ### IndexWeightListResponse 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | total | int | 否 | 总条数 | - |
        | page | int | 否 | 当前页码 | - |
        | page_size | int | 否 | 每页条数 | - |
        | index_weights | array | 否 | 权重明细列表 | - |
        
        ### IndexWeightInfo 结构(index_weights 元素)
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | date | String | 否 | 权重日期,`YYYYMMDD`(线上也可能为其他日期字符串格式,以实际为准) | - |
        | index_code | String | 否 | 指数代码 | - |
        | component_code | String | 否 | 成份证券代码 | - |
        | component_name | String | 否 | 成份证券名称 | - |
        | weight | String | 否 | 权重(字符串形式,与上游一致) | - |
        | url_hash | String | 否 | 该期完整权重表文件的哈希,可用于下载 xlsx | - |
        
        ## 4. 调用方式
        
        本 handler 与上级 `FTShare-index-data/run.py` 配合使用:
        
        ```bash
        python <RUN_PY> index-weight-list --index-code 000300 --date 20250320 --page 1 --page-size 20
        python <RUN_PY> index-weight-list --index-code 000300 --page 1 --page-size 20
        ```
        
        其中 `<RUN_PY>` 为 `FTShare-index-data/run.py` 的绝对路径。
        
        ### 直接执行 handler(调试)
        
        ```bash
        python scripts/handler.py --index-code 000300 --date 20250320 --page 1 --page-size 20
        ```
        
        (需在 `sub-skills/index-weight-list` 目录下执行,或传入脚本完整路径。)
        
        ## 5. 链路说明
        
        本接口为**权重链第 2 步**。  
        - **前置**:`index-weight-summary` → 取得 `index_code` 与可用 `date`。  
        - **下游**:返回的 `url_hash` → `index-weight-download --url-hash <url_hash>` 下载整期 xlsx。
        
        ### 名称→代码映射
        
        `--index-code` 只接受**纯 6 位指数代码**(如 `000300`),不接受中文名称。  
        若用户给出中文名称(如"沪深300"),须先映射:
        
        1. 调用 `index-description-paginated --page 1 --page-size 100` → 在 `index_descriptions` 中按 `index_name` 模糊匹配 → 取得 `index_code`。  
        2. 或调用 `index-description-all` → 从 `symbol`(如 `000300.XSHG`)截取前 6 位。
        
        ## 6. 请求示例
        
        ```
        GET /api/v1/market/data/index/index_weight?index_code=000300&date=20250320&page=1&page_size=20
        ```
        
    • index-weight-summary
      • scripts
        • handler.py 3.2 KB
          #!/usr/bin/env python3
          """分页查询指数权重汇总(market.ft.tech)"""
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          DEFAULT_BASE_URL = "https://market.ft.tech/gateway/"
          ENDPOINT = "api/v1/market/data/index/index_weight_summary"
          
          
          def base_url() -> str:
              return os.environ.get("FTSHARE_BASE_URL", DEFAULT_BASE_URL).rstrip("/") + "/"
          
          
          def build_url(params: dict) -> str:
              return urllib.parse.urljoin(base_url(), ENDPOINT) + "?" + urllib.parse.urlencode(params)
          
          
          def safe_urlopen(req, timeout=30):
              url = req.full_url if isinstance(req, urllib.request.Request) else str(req)
              parsed = urllib.parse.urlparse(url)
              base = urllib.parse.urlparse(base_url())
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req, urllib.request.Request):
                  req = urllib.request.Request(url, method="GET")
              for key, value in _REQUEST_HEADERS.items():
                  req.add_unredirected_header(key, value)
              return SAFE_URLOPENER.open(req, timeout=timeout)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="分页查询指数权重汇总")
              parser.add_argument(
                  "--index-code",
                  dest="index_code",
                  default=None,
                  help="指数代码,如 000300;不传则分页返回全部指数权重汇总",
              )
              parser.add_argument(
                  "--page",
                  type=int,
                  default=1,
                  help="页码,从 1 开始,默认 1",
              )
              parser.add_argument(
                  "--page-size",
                  dest="page_size",
                  type=int,
                  default=20,
                  help="每页条数,默认 20,最大 100",
              )
              args = parser.parse_args()
          
              if args.page < 1:
                  print("page 须 >= 1", file=sys.stderr)
                  sys.exit(1)
              if args.page_size < 1 or args.page_size > 100:
                  print("page_size 须在 1~100 之间", file=sys.stderr)
                  sys.exit(1)
          
              params = {"page": args.page, "page_size": args.page_size}
              if args.index_code:
                  params["index_code"] = args.index_code
              url = build_url(params)
          
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
              req.add_header("X-Client-Name", "ft-claw")
              req.add_header("Content-Type", "application/json")
          
              try:
                  with safe_urlopen(req, timeout=30) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"请求失败: {e.reason}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3.3 KB
        ---
        name: index-weight-summary
        description: 分页查询指数权重汇总(market.ft.tech)。可按 index_code 查询指定指数各期权重的 date 与 url_hash。用户问指数权重汇总、成分股权重期数、权重文件 url_hash、沪深300 权重日期、有哪些指数权重数据时使用。下游:取到 url_hash 后可调 index-weight-download 下载 xlsx;取到 index_code/date 后可调 index-weight-list 查成份明细。
        ---
        
        # 分页查询指数权重汇总(index-weight-summary)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 分页查询指数权重汇总 |
        | 外部接口 | `/api/v1/market/data/index/index_weight_summary` |
        | 请求方式 | GET |
        | 适用场景 | 按指数汇总各期权重数据的 `date` 与 `url_hash`;可用 `index_code` 定位单个指数,便于再调用「下载指数权重文件」获取具体权重表 |
        
        ## 2. 请求参数
        
        说明:使用查询参数。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | index_code | string | 否 | 指数代码 | 000300 | 不传则分页返回全部指数 |
        | page | int | 否 | 页码 | 1 | 默认 1 |
        | page_size | int | 否 | 每页条数 | 20 | 默认 20,最大 100 |
        
        ## 3. 响应说明
        
        返回值为分页列表对象 **`IndexWeightSummaryListResponse`**。
        
        ### IndexWeightSummaryListResponse 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | total | int | 否 | 总条数(按指数汇总的记录数) | - |
        | page | int | 否 | 当前页码 | - |
        | page_size | int | 否 | 每页条数 | - |
        | index_weights | array | 否 | 单只指数的期数汇总列表 | - |
        
        ### IndexWeightSummary 结构(index_weights 元素)
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | index_code | String | 否 | 指数代码 | - |
        | periods | array | 否 | 该指数各期的日期与下载用 url_hash | - |
        
        ### IndexWeightPeriod 结构(periods 元素)
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | date | String | 否 | 权重日期;文档示例为 `YYYYMMDD`,线上也可能为 `YYYY-MM-DD`,以实际返回为准 | - |
        | url_hash | String | 否 | 该期权重文件的哈希 | - |
        
        ## 4. 调用方式
        
        本 handler 与上级 `FTShare-index-data/run.py` 配合使用:
        
        ```bash
        python <RUN_PY> index-weight-summary --index-code 000300 --page 1 --page-size 20
        python <RUN_PY> index-weight-summary --page 1 --page-size 20
        ```
        
        其中 `<RUN_PY>` 为 `FTShare-index-data/run.py` 的绝对路径。
        
        ### 直接执行 handler(调试)
        
        ```bash
        python scripts/handler.py --page 1 --page-size 20
        python scripts/handler.py --index-code 000300 --page 1 --page-size 20
        ```
        
        (需在 `sub-skills/index-weight-summary` 目录下执行,或传入脚本完整路径。)
        
        ## 5. 链路说明
        
        本接口为**权重链第 1 步**。  
        - 取到 `index_code` + `date` → 可调 `index-weight-list --index_code <code> --date <date>` 查成份明细。  
        - 取到 `url_hash` → 可调 `index-weight-download --url-hash <url_hash>` 直接下载整期 xlsx。
        
        ## 6. 请求示例
        
        ```
        GET /api/v1/market/data/index/index_weight_summary?index_code=000300&page=1&page_size=20
        ```
        
    • institution-share-holdings
      • scripts
        • handler.py 2.7 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/institution/institution-share-holdings'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='机构股本持股')
              parser.add_argument("--institution_id", required=False)
              parser.add_argument("--year", required=True)
              parser.add_argument("--report_type", required=True)
              parser.add_argument("--invest_type", required=True)
              args = parser.parse_args()
              params = {}
              if args.institution_id is not None: params["institution_id"] = args.institution_id
              if args.year is not None: params["year"] = args.year
              if args.report_type is not None: params["report_type"] = args.report_type
              if args.invest_type is not None: params["invest_type"] = args.invest_type
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 723 B
        ---
        name: institution-share-holdings
        description: 查询机构股本持股。接口:GET /api/v2/market/data/institution/institution-share-holdings。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 机构股本持股
        
        接口:GET `/api/v2/market/data/institution/institution-share-holdings`。参数和响应以 `ftshare-doc/api-doc/股票数据/基础数据/机构股本持股.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> institution-share-holdings --institution_id XXX --year 2025 --report_type annual --invest_type all
        ```
        
    • intraday-auction-volume
      • scripts
        • handler.py 2.2 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/intraday-auction-volume/symbol"
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询单标的连续竞价成交量")
              parser.add_argument("--symbol", required=True)
              parser.add_argument("--trade-date", dest="trade_date")
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", dest="page_size", type=int, default=50)
              args = parser.parse_args()
              if args.page < 1 or not 1 <= args.page_size <= 200:
                  parser.error("page 须大于等于 1,page-size 须在 1~200 之间")
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              params = {"symbol": args.symbol, "page": args.page, "page_size": args.page_size}
              if args.trade_date is not None:
                  params["trade_date"] = args.trade_date
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if (parsed.scheme, parsed.netloc) != (base.scheme, base.netloc):
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              request = urllib.request.Request(
                  url,
                  headers={"FTSHARE_API_KEY": key, "Content-Type": "application/json"},
                  method="GET",
              )
              try:
                  with urllib.request.urlopen(request, timeout=30) as response:
                      result = json.loads(response.read().decode())
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 643 B
        ---
        name: intraday-auction-volume
        description: 查询单只股票连续竞价成交量和成交额占比。用户询问个股分时成交占比时使用。
        ---
        
        # 单标的连续竞价成交量
        
        接口:`GET /api/v1/market/data/intraday-auction-volume/symbol`。必填 `--symbol`;可选 `--trade-date`(YYYYMMDD)、`--page` 和 `--page-size`,每页最多 200 条。
        
        ```bash
        python <RUN_PY> intraday-auction-volume --symbol 600000.SH --page 1 --page-size 50
        ```
        
        不传交易日查询当日实时数据,传入历史交易日查询历史分钟数据;响应为 `code/message/data` 分页信封,分钟记录位于 `data.records`。
        
    • kline-pattern-annotations
      • scripts
        • handler.py 2.7 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v3/market/data/kline-pattern-annotations'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='K线形态标注')
              parser.add_argument("--date")
              parser.add_argument("--symbol")
              parser.add_argument("--pattern")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              args = parser.parse_args()
              params = {}
              if args.date is not None: params["date"] = args.date
              if args.symbol is not None: params["symbol"] = args.symbol
              if args.pattern is not None: params["pattern"] = args.pattern
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 642 B
        ---
        name: kline-pattern-annotations
        description: 查询K线形态标注。接口:GET /api/v3/market/data/kline-pattern-annotations。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # K线形态标注
        
        接口:GET `/api/v3/market/data/kline-pattern-annotations`。参数和响应以 `ftshare-doc/api-doc/股票数据/特色数据/K线形态标注.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> kline-pattern-annotations --symbol 000002 --page 1
        ```
        
    • libor-daily
      • scripts
        • handler.py 3.2 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/libor-daily'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='国际基准利率日度')
              parser.add_argument("--start_date", required=True)
              parser.add_argument("--end_date", required=True)
              parser.add_argument("--currency")
              parser.add_argument("--tenor")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--trade_date", required=True)
              parser.add_argument("--rate", required=False)
              args = parser.parse_args()
              params = {}
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.currency is not None: params["currency"] = args.currency
              if args.tenor is not None: params["tenor"] = args.tenor
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.currency is not None: params["currency"] = args.currency
              if args.tenor is not None: params["tenor"] = args.tenor
              if args.rate is not None: params["rate"] = args.rate
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 658 B
        ---
        name: libor-daily
        description: 查询国际基准利率日度。接口:GET /api/v1/market/data/libor-daily。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 国际基准利率日度
        
        接口:GET `/api/v1/market/data/libor-daily`。参数和响应以 `ftshare-doc/api-doc/宏观经济/国际宏观/国际基准利率日度.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> libor-daily --start_date 20260828 --end_date 20260828 --page 1 --trade_date 20260828
        ```
        
    • limit-event-timeline
      • scripts
        • handler.py 4.5 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/limit-event-timeline-3s'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='涨跌停事件时间线')
              parser.add_argument("--symbol")
              parser.add_argument("--trade_date")
              parser.add_argument("--ready", required=False)
              parser.add_argument("--status", required=False)
              parser.add_argument("--limit_up_price", required=False)
              parser.add_argument("--limit_down_price", required=False)
              parser.add_argument("--limit_up_enter", required=False)
              parser.add_argument("--limit_up_break", required=False)
              parser.add_argument("--limit_down_enter", required=True)
              parser.add_argument("--limit_down_break", required=True)
              parser.add_argument("--first_limit_up_time", required=False)
              parser.add_argument("--limit_up_break_count", required=False)
              parser.add_argument("--last_limit_down_time", required=False)
              parser.add_argument("--limit_down_break_count", required=False)
              parser.add_argument("--limit_down_seal_value", required=False)
              args = parser.parse_args()
              params = {}
              if args.symbol is not None: params["symbol"] = args.symbol
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.symbol is not None: params["symbol"] = args.symbol
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.ready is not None: params["ready"] = args.ready
              if args.status is not None: params["status"] = args.status
              if args.limit_up_price is not None: params["limit_up_price"] = args.limit_up_price
              if args.limit_down_price is not None: params["limit_down_price"] = args.limit_down_price
              if args.limit_up_enter is not None: params["limit_up_enter"] = args.limit_up_enter
              if args.limit_up_break is not None: params["limit_up_break"] = args.limit_up_break
              if args.limit_down_enter is not None: params["limit_down_enter"] = args.limit_down_enter
              if args.limit_down_break is not None: params["limit_down_break"] = args.limit_down_break
              if args.first_limit_up_time is not None: params["first_limit_up_time"] = args.first_limit_up_time
              if args.limit_up_break_count is not None: params["limit_up_break_count"] = args.limit_up_break_count
              if args.last_limit_down_time is not None: params["last_limit_down_time"] = args.last_limit_down_time
              if args.limit_down_break_count is not None: params["limit_down_break_count"] = args.limit_down_break_count
              if args.limit_down_seal_value is not None: params["limit_down_seal_value"] = args.limit_down_seal_value
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 694 B
        ---
        name: limit-event-timeline
        description: 查询涨跌停事件时间线。接口:GET /api/v2/market/data/limit-event-timeline-3s。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 涨跌停事件时间线
        
        接口:GET `/api/v2/market/data/limit-event-timeline-3s`。参数和响应以 `ftshare-doc/api-doc/股票数据/打板专题数据/涨跌停事件时间线.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> limit-event-timeline --symbol 600519.SH --limit_down_enter 5 --limit_down_break 5
        ```
        
    • limit-list
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/limit-list'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='涨跌停池')
              parser.add_argument("--limit_type", required=True)
              parser.add_argument("--trade_date")
              args = parser.parse_args()
              params = {}
              if args.limit_type is not None: params["limit_type"] = args.limit_type
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 589 B
        ---
        name: limit-list
        description: 查询涨跌停池。接口:GET /api/v1/market/data/limit-list。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 涨跌停池
        
        接口:GET `/api/v1/market/data/limit-list`。参数和响应以 `ftshare-doc/api-doc/股票数据/打板专题数据/涨跌停池.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> limit-list --limit_type up --trade_date 20260828
        ```
        
    • limit-up-briefs
      • scripts
        • handler.py 2.2 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v3/market/data/limit-up-reports/briefs'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='涨停简报')
              parser.add_argument("--date", required=True)
              args = parser.parse_args()
              params = {"date": args.date}
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 603 B
        ---
        name: limit-up-briefs
        description: 查询涨停简报。接口:GET /api/v3/market/data/limit-up-reports/briefs。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 涨停简报
        
        接口:GET `/api/v3/market/data/limit-up-reports/briefs`。参数和响应以 `ftshare-doc/api-doc/股票数据/打板专题数据/涨停简报.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> limit-up-briefs --date 20260828
        ```
        
    • limit-up-public-report
      • scripts
        • handler.py 2.3 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v3/market/data/limit-up-reports/public-report'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='涨停对外归因报告')
              parser.add_argument("--date", required=True)
              parser.add_argument("--security_code", required=False)
              args = parser.parse_args()
              params = {"date": args.date, "security_code": args.security_code}
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 693 B
        ---
        name: limit-up-public-report
        description: 查询涨停对外归因报告。接口:GET /api/v3/market/data/limit-up-reports/public-report。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 涨停对外归因报告
        
        接口:GET `/api/v3/market/data/limit-up-reports/public-report`。参数和响应以 `ftshare-doc/api-doc/股票数据/打板专题数据/涨停对外归因报告.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> limit-up-public-report --date 20260724 --security_code 603976.SH
        ```
        
    • major-contract-by-date
      • scripts
        • handler.py 2.6 KB
          #!/usr/bin/env python3
          """按公告日期范围查询 A 股重大合同(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="按公告日期范围查询重大合同")
              parser.add_argument("--start_date", required=True, help="开始日期,格式 YYYYMMDD")
              parser.add_argument("--end_date", required=True, help="结束日期,格式 YYYYMMDD")
              args = parser.parse_args()
          
              params = {
                  "start_date": args.start_date,
                  "end_date": args.end_date,
              }
              path = "/api/v1/market/data/corporate/contract?" + urllib.parse.urlencode(params)
              url = BASE_URL + path
          
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
              req.add_header("Content-Type", "application/json")
          
              try:
                  with safe_urlopen(req) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.8 KB
        ---
        name: major-contract-by-date
        description: 按公告日期范围查询A股重大合同信息(market.ft.tech)。用户问重大合同、某日/某月重大合同、合同金额、签约方时使用。
        ---
        
        # 重大合同 - 按公告日期范围查询
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 按公告日期范围查询重大合同 |
        | 外部接口 | `GET /api/v1/market/data/corporate/contract` |
        | 请求方式 | GET |
        | 适用场景 | 按公告日期范围查询 A 股上市公司重大合同,包括合同方、金额、收入影响等 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | start_date | string | 是 | 开始日期 | `20260526` | 格式 YYYYMMDD |
        | end_date | string | 是 | 结束日期 | `20260526` | 格式 YYYYMMDD |
        
        ## 3. 响应说明
        
        返回 `ApiResponse` 包装的 JSON 对象。
        
        ```json
        {
          "code": 0,
          "message": "success",
          "data": {
            "total": 2,
            "items": [
              {
                "seq": 0,
                "security_code": "300209",
                "security_short_name": "行云科技",
                "signatory": "行云存算(深圳)科技有限公司",
                "signatory_rel_name": "控股子公司",
                "counter_party": "浙江甚湖科技有限公司",
                "counter_party_rel_name": "无关联关系",
                "contract_type_name": "销售合同",
                "contract_name": "设备销售框架协议",
                "amounts": "321750000.0000",
                "snd_yysr": null,
                "zsnd_yysr_bl": null,
                "operate_reve": null,
                "sign_date": null,
                "dim_rdate": "2026-05-26"
              }
            ]
          }
        }
        ```
        
        ### data.items 元素结构
        
        | 字段名 | 类型 | 说明 |
        |--------|------|------|
        | seq | int | 序号 |
        | security_code | string | 股票代码 |
        | security_short_name | string | 股票简称 |
        | signatory | string | 签约方 |
        | signatory_rel_name | string | 签约方关联名称 |
        | counter_party | string | 对方签约方 |
        | counter_party_rel_name | string | 对方签约方关联名称 |
        | contract_type_name | string | 合同类型名称 |
        | contract_name | string | 合同名称 |
        | amounts | string | 合同金额(元) |
        | snd_yysr | string | 上年度营业收入 |
        | zsnd_yysr_bl | string | 占上年度营业收入比例 |
        | operate_reve | string | 最近营业收入 |
        | sign_date | string | 签约日期 |
        | dim_rdate | string | 公告日期 |
        
        ## 4. 用法
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> major-contract-by-date --start_date 20260526 --end_date 20260526
        ```
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/corporate/contract?start_date=20260526&end_date=20260526
        ```
        
        ## 6. 注意事项
        
        - 金额字段为 Decimal 类型,以字符串返回保持精度。
        - 按 `dim_rdate` 降序排列。
        
    • major-contract-by-symbol
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          """按股票代码查询重大合同历史(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="按股票代码查询重大合同")
              parser.add_argument("--symbol", required=True, help="股票代码(6 位),如 300500")
              parser.add_argument("--page", type=int, default=None, help="页码,从 1 开始")
              parser.add_argument("--page_size", type=int, default=None, help="每页数量,默认 50,最大 200")
              args = parser.parse_args()
          
              params = {"symbol": args.symbol}
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
          
              path = "/api/v1/market/data/corporate/contract/by-symbol?" + urllib.parse.urlencode(params)
              url = BASE_URL + path
          
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
              req.add_header("Content-Type", "application/json")
          
              try:
                  with safe_urlopen(req) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.4 KB
        ---
        name: major-contract-by-symbol
        description: 按股票代码查询重大合同历史(market.ft.tech)。用户问某股票的重大合同、某公司签了哪些大单、合同金额时使用。
        ---
        
        # 重大合同 - 按股票代码查询
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 按股票代码查询重大合同 |
        | 外部接口 | `GET /api/v1/market/data/corporate/contract/by-symbol` |
        | 请求方式 | GET |
        | 适用场景 | 查询指定股票的所有重大合同信息,按公告日期降序排列,支持分页 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 是 | 股票代码 | `300500` | 6 位代码 |
        | page | int | 否 | 页码,从 1 开始 | `1` | 默认 1 |
        | page_size | int | 否 | 每页数量 | `50` | 默认 50,最大 200 |
        
        ## 3. 响应说明
        
        返回 `PaginatedApiResponse` 包装的 JSON 对象。`data.records` 元素结构与 [major-contract-by-date](major-contract-by-date) 的 `items` 一致。
        
        ## 4. 用法
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> major-contract-by-symbol --symbol 300500
        python <RUN_PY> major-contract-by-symbol --symbol 601668 --page 1 --page_size 50
        ```
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/corporate/contract/by-symbol?symbol=300500&page=1&page_size=50
        ```
        
    • major-contract-summary
      • scripts
        • handler.py 2.7 KB
          #!/usr/bin/env python3
          """近一年 A 股个股重大合同汇总排名(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="近一年个股重大合同汇总排名")
              parser.add_argument("--page", type=int, default=None, help="页码,从 1 开始")
              parser.add_argument("--page_size", type=int, default=None, help="每页数量,默认 50,最大 200")
              args = parser.parse_args()
          
              params = {}
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
          
              path = "/api/v1/market/data/corporate/contract/summary"
              if params:
                  path += "?" + urllib.parse.urlencode(params)
              url = BASE_URL + path
          
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
              req.add_header("Content-Type", "application/json")
          
              try:
                  with safe_urlopen(req) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.4 KB
        ---
        name: major-contract-summary
        description: 近一年A股个股重大合同汇总排名(market.ft.tech)。用户问重大合同排行榜、哪些公司签合同最多/金额最大、合同总额排名时使用。
        ---
        
        # 重大合同 - 近一年个股汇总
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 近一年个股重大合同汇总 |
        | 外部接口 | `GET /api/v1/market/data/corporate/contract/summary` |
        | 请求方式 | GET |
        | 适用场景 | 近一年各股票重大合同数量与金额汇总排名,按合同总额降序,支持分页 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | page | int | 否 | 页码,从 1 开始 | `1` | 默认 1 |
        | page_size | int | 否 | 每页数量 | `50` | 默认 50,最大 200 |
        
        ## 3. 响应说明
        
        返回 `PaginatedApiResponse` 包装的 JSON 对象。
        
        ```json
        {
          "code": 0,
          "message": "success",
          "data": {
            "pageNum": 1,
            "pageSize": 50,
            "total": 35,
            "pages": 1,
            "records": [
              {
                "seq": 0,
                "security_code": "002307",
                "security_short_name": "北新路桥",
                "contract_count": "1",
                "total_amount": "3249000000.0000",
                "prev_year_total": "3249000000.0000",
                "last_year_revenue": null,
                "revenue_ratio": null,
                "latest_revenue": null
              }
            ]
          }
        }
        ```
        
        ### data.records 元素结构
        
        | 字段名 | 类型 | 说明 |
        |--------|------|------|
        | seq | int | 序号 |
        | security_code | string | 股票代码 |
        | security_short_name | string | 股票简称 |
        | contract_count | string | 重大合同签署数量 |
        | total_amount | string | 合同金额总计(元) |
        | prev_year_total | string | 上年度合同金额总计(元) |
        | last_year_revenue | string | 上年度营业收入(元) |
        | revenue_ratio | string | 占上年营业收入比例(%) |
        | latest_revenue | string | 最新财务报表营业收入(元) |
        
        ## 4. 用法
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> major-contract-summary
        python <RUN_PY> major-contract-summary --page 1 --page_size 50
        ```
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/corporate/contract/summary?page=1&page_size=50
        ```
        
        ## 6. 注意事项
        
        - 近一年范围 = `dim_rdate >= DATE_SUB(NOW(), INTERVAL 1 YEAR)`。
        - 按合同总额降序排列。
        
    • margin-trading-details
      • scripts
        • handler.py 5.6 KB
          #!/usr/bin/env python3
          """获取 A 股融资融券明细,支持单日/区间查询、分页与全量拉取"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/margin-trading-details"
          
          
          def _check_date(value, flag):
              if len(value) != 8 or not value.isdigit():
                  print(f"{flag} 格式应为 YYYYMMDD:{value}", file=sys.stderr)
                  raise SystemExit(2)
              return value
          
          
          def build_params(page, page_size, date, start_date, end_date, stock):
              if page < 1:
                  print("--page 必须大于等于 1", file=sys.stderr)
                  raise SystemExit(2)
              if page_size < 1 or page_size > 1000:
                  print("--page_size 允许范围 1~1000", file=sys.stderr)
                  raise SystemExit(2)
              params = {"page": page, "page_size": page_size}
              if date:
                  params["date"] = _check_date(date, "--date")
              if start_date or end_date:
                  if date:
                      print("--date 不能与 --start-date/--end-date 同时使用", file=sys.stderr)
                      raise SystemExit(2)
                  if not (start_date and end_date and stock):
                      print("区间查询需要 --start-date、--end-date、--stock 同时提供", file=sys.stderr)
                      raise SystemExit(2)
                  _check_date(start_date, "--start-date")
                  _check_date(end_date, "--end-date")
                  if start_date >= end_date:
                      print("--start-date 必须早于 --end-date", file=sys.stderr)
                      raise SystemExit(2)
                  end_ymd = (int(end_date[:4]), int(end_date[4:6]), int(end_date[6:8]))
                  start_limit = (int(start_date[:4]) + 3, int(start_date[4:6]), int(start_date[6:8]))
                  if end_ymd > start_limit:
                      print("区间跨度不能超过 3 年", file=sys.stderr)
                      raise SystemExit(2)
                  params["start_date"] = start_date
                  params["end_date"] = end_date
                  params["stock"] = stock
              elif stock:
                  params["stock"] = stock
              return params
          
          
          def fetch_page(params: dict) -> dict:
              qs = urllib.parse.urlencode(params)
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="获取 A 股融资融券明细")
              parser.add_argument("--page", type=int, default=1, help="页码(从 1 开始)")
              parser.add_argument("--page_size", type=int, default=20, help="每页记录数(最大 1000)")
              parser.add_argument("--date", type=str, default=None,
                                  help="单日查询日期,格式 YYYYMMDD,必须为交易日;不传返回前一交易日快照")
              parser.add_argument("--start-date", dest="start_date", type=str, default=None,
                                  help="区间查询开始日期 YYYYMMDD;须与 --end-date、--stock 同时提供")
              parser.add_argument("--end-date", dest="end_date", type=str, default=None,
                                  help="区间查询结束日期 YYYYMMDD;须与 --start-date、--stock 同时提供,跨度不超过 3 年")
              parser.add_argument("--stock", type=str, default=None,
                                  help="标的代码过滤,如 600000.SH;区间查询时必填")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量数据")
              args = parser.parse_args()
          
              params = build_params(args.page, args.page_size, args.date,
                                    args.start_date, args.end_date, args.stock)
          
              if args.fetch_all:
                  first = fetch_page(params)
                  data = first.get("data") or {}
                  records = list(data.get("records", []))
                  pages = data.get("pages", 1)
                  for p in range(2, pages + 1):
                      page_params = dict(params, page=p)
                      page_data = fetch_page(page_params)
                      records.extend((page_data.get("data") or {}).get("records", []))
                  result = {"records": records, "pages": pages, "total": data.get("total", len(records))}
              else:
                  result = fetch_page(params)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 7.1 KB
          #!/usr/bin/env python3
          """Tests for margin-trading-details handler"""
          import json
          import os
          import sys
          import unittest
          import urllib.error
          from io import BytesIO, StringIO
          from unittest.mock import patch
          
          sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
          import importlib.util
          spec = importlib.util.spec_from_file_location("handler", os.path.join(os.path.dirname(os.path.abspath(__file__)), "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          ENVELOPE = b'{"code":200,"message":"success","data":{"pageNum":1,"pageSize":2,"total":16,"pages":8,"records":[{"symbol":"600000.SH"}]}}'
          
          
          class TestBuildParams(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_default_params(self):
                  params = handler.build_params(1, 20, None, None, None, None)
                  self.assertEqual(params, {"page": 1, "page_size": 20})
          
              def test_date_query(self):
                  params = handler.build_params(1, 20, "20260623", None, None, None)
                  self.assertEqual(params["date"], "20260623")
          
              def test_date_with_stock(self):
                  params = handler.build_params(1, 20, "20260623", None, None, "600000.SH")
                  self.assertEqual(params["date"], "20260623")
                  self.assertEqual(params["stock"], "600000.SH")
          
              def test_range_query(self):
                  params = handler.build_params(1, 20, None, "20260601", "20260623", "600000.SH")
                  self.assertEqual(params["start_date"], "20260601")
                  self.assertEqual(params["end_date"], "20260623")
                  self.assertEqual(params["stock"], "600000.SH")
                  self.assertNotIn("date", params)
          
              def test_rejects_date_with_range(self):
                  with self.assertRaises(SystemExit):
                      handler.build_params(1, 20, "20260623", "20260601", "20260623", "600000.SH")
          
              def test_rejects_incomplete_range(self):
                  with self.assertRaises(SystemExit):
                      handler.build_params(1, 20, None, "20260601", None, None)
                  with self.assertRaises(SystemExit):
                      handler.build_params(1, 20, None, "20260601", "20260623", None)
          
              def test_rejects_start_after_end(self):
                  with self.assertRaises(SystemExit):
                      handler.build_params(1, 20, None, "20260623", "20260601", "600000.SH")
          
              def test_rejects_span_over_three_years(self):
                  with self.assertRaises(SystemExit):
                      handler.build_params(1, 20, None, "20200101", "20260623", "600000.SH")
          
              def test_allows_exactly_three_years(self):
                  params = handler.build_params(1, 20, None, "20230623", "20260623", "600000.SH")
                  self.assertEqual(params["start_date"], "20230623")
          
              def test_rejects_bad_date_format(self):
                  with self.assertRaises(SystemExit):
                      handler.build_params(1, 20, "2026-06-23", None, None, None)
          
              def test_rejects_page_size_out_of_range(self):
                  with self.assertRaises(SystemExit):
                      handler.build_params(1, 1001, None, None, None, None)
          
          
          class TestFetchPage(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_url_contains_params(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = ENVELOPE
                  handler.fetch_page({"page": 2, "page_size": 200, "date": "20260623"})
                  called_url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/margin-trading-details", called_url)
                  self.assertIn("page=2", called_url)
                  self.assertIn("page_size=200", called_url)
                  self.assertIn("date=20260623", called_url)
          
              @patch.object(handler, "safe_urlopen")
              def test_returns_parsed_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = ENVELOPE
                  result = handler.fetch_page({"page": 1, "page_size": 2})
                  self.assertEqual(result["data"]["records"][0]["symbol"], "600000.SH")
                  self.assertEqual(result["data"]["pages"], 8)
          
              @patch.object(handler, "safe_urlopen")
              def test_http_error_exits(self, mock_open):
                  mock_open.side_effect = urllib.error.HTTPError(
                      "http://fake", 500, "Internal Error", {}, BytesIO(b"server error")
                  )
                  with self.assertRaises(SystemExit):
                      handler.fetch_page({"page": 1, "page_size": 20})
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_single_page(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = ENVELOPE
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--page", "1", "--page_size", "2"]):
                          with patch("sys.stdout", new_callable=StringIO) as fake_out:
                              handler.main()
                              result = json.loads(fake_out.getvalue())
                              self.assertEqual(result["code"], 200)
                              self.assertEqual(result["data"]["total"], 16)
          
              @patch.object(handler, "safe_urlopen")
              def test_range_query_flags(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = ENVELOPE
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--start-date", "20260601",
                                                      "--end-date", "20260623", "--stock", "600000.SH"]):
                          handler.main()
                  called_url = mock_open.call_args[0][0]
                  self.assertIn("start_date=20260601", called_url)
                  self.assertIn("end_date=20260623", called_url)
                  self.assertIn("stock=600000.SH", called_url)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_all_pagination(self, mock_open):
                  page1 = b'{"data":{"records":[{"symbol":"A"},{"symbol":"B"}],"pages":2,"total":3}}'
                  page2 = b'{"data":{"records":[{"symbol":"C"}],"pages":2,"total":3}}'
                  mock_open.return_value.__enter__.return_value.read.side_effect = [page1, page2]
          
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--page_size", "2", "--all"]):
                          with patch("sys.stdout", new_callable=StringIO) as fake_out:
                              handler.main()
                              result = json.loads(fake_out.getvalue())
                              self.assertEqual(len(result["records"]), 3)
                              self.assertEqual(result["pages"], 2)
                              self.assertEqual(result["total"], 3)
          
              def test_main_rejects_incomplete_range(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--start-date", "20260601"]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
          
          class TestSafeUrlopen(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_rejects_non_https(self):
                  with self.assertRaises(SystemExit):
                      handler.safe_urlopen("http://market.ft.tech/api")
          
              def test_rejects_wrong_host(self):
                  with self.assertRaises(SystemExit):
                      handler.safe_urlopen("https://evil.com/api")
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 4.4 KB
        ---
        name: margin-trading-details
        description: "获取融资融券明细。当用户需要获取 A 股融资融券明细列表(按交易日快照),支持单日、区间或标的过滤查询,或了解融资融券明细时使用。"
        ---
        
        # 获取融资融券明细
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 获取融资融券明细(margin_trading_details) |
        | 外部接口 | `/api/v1/market/data/margin-trading-details` |
        | 请求方式 | GET |
        | 适用场景 | 按交易日查询 A 股两融快照(每标的一行):单日查询、指定标的的交易日区间查询、默认前一交易日快照 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |---|---|---|---|---|---|
        | `--date` | str | 否 | 单日查询日期 | `20260623` | 格式 `YYYYMMDD`,必须为交易日;不传返回前一交易日快照 |
        | `--start-date` | str | 否 | 区间查询开始日期 | `20260601` | 须与 `--end-date`、`--stock` **同时提供**;跨度不超过 3 年 |
        | `--end-date` | str | 否 | 区间查询结束日期 | `20260623` | 须与 `--start-date`、`--stock` 同时提供;开始须早于结束 |
        | `--stock` | str | 否 | 标的代码过滤 | `600000.SH` | 单日/默认查询时可选;区间查询时必填 |
        | `--page` | int | 否 | 页码,从 1 开始 | `1` | 默认 1 |
        | `--page_size` | int | 否 | 每页记录数 | `20` | 默认 20,最大 1000 |
        | `--all` | - | 否 | 自动翻页获取全量数据 | - | 合并 `data.records` 输出 |
        
        ## 执行方式
        
        ```bash
        # 默认:前一交易日快照
        python <RUN_PY> margin-trading-details --page 1 --page_size 20
        
        # 指定交易日
        python <RUN_PY> margin-trading-details --date 20260623 --page 1 --page_size 20
        
        # 单日 + 标的过滤
        python <RUN_PY> margin-trading-details --date 20260623 --stock 600000.SH
        
        # 区间查询:start_date、end_date、stock 必须同时提供
        python <RUN_PY> margin-trading-details --start-date 20260601 --end-date 20260623 --stock 600000.SH
        
        # 自动翻页获取全量数据
        python <RUN_PY> margin-trading-details --date 20260623 --all
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        外层为 `code`(成功 200)/ `message` / `data`;分页信息与记录位于 `data`:
        
        ```json
        {
            "code": 200,
            "message": "success",
            "data": {
                "pageNum": 1,
                "pageSize": 20,
                "total": 4443,
                "pages": 223,
                "records": [
                    {
                        "date": "2026-06-16",
                        "margin_trading_balance": 13197538116,
                        "margin_trading_buying_amount": 3827217410,
                        "margin_trading_repayment_amount": 874371294,
                        "securities_lending_balance_volume": 143377,
                        "securities_lending_repayment_volume": 25800,
                        "securities_lending_selling_volume": 34500,
                        "total_balance": 13233247592,
                        "symbol": "002384.SZ",
                        "symbol_name": "合力泰"
                    }
                ]
            }
        }
        ```
        
        ### records 元素字段说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |---|---|---|---|---|
        | `date` | String | 否 | 交易日期,格式 `YYYY-MM-DD` | - |
        | `symbol` | String | 否 | 标的代码,短市场后缀(`600000.SH`、`000001.SZ`、`920178.BJ`) | - |
        | `symbol_name` | String | 是 | 标的名称 | - |
        | `margin_trading_balance` | int | 是 | 融资余额 | 元 |
        | `margin_trading_buying_amount` | int | 是 | 融资买入额 | 元 |
        | `margin_trading_repayment_amount` | int | 是 | 融资偿还额 | 元 |
        | `securities_lending_balance_volume` | int | 是 | 融券余量 | 股 |
        | `securities_lending_repayment_volume` | number | 是 | 融券偿还量 | 股 |
        | `securities_lending_selling_volume` | int | 是 | 融券卖出量 | 股 |
        | `total_balance` | int | 是 | 融资融券余额 | 元 |
        
        ## 注意事项
        
        - `--date` 不能与 `--start-date`/`--end-date` 同时使用;区间查询必须三参数(含 `--stock`)齐全,否则本地校验直接退出。
        - 区间包含首尾交易日,自动忽略非交易日;跨度不能超过 3 年。
        - `--stock` 支持与响应代码等价的格式(`600000.SH`、`600000.XSHG`、`600000`);响应 `symbol` 统一为短市场后缀。
        - 金额字段单位为**元**,量字段单位为**股**,无值时为 `null`。
        - 如需全量数据,使用 `--all` 自动翻页合并,或按 `--page` 递增循环至 `data.pages`。
        
    • market-distribution-intraday
      • scripts
        • handler.py 2.6 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/market-distribution-intraday'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='市场涨跌分布分时')
              parser.add_argument("--ts_millis", required=True)
              parser.add_argument("--up_limited", required=True)
              parser.add_argument("--down_limited", required=True)
              args = parser.parse_args()
              params = {}
              if args.ts_millis is not None: params["ts_millis"] = args.ts_millis
              if args.up_limited is not None: params["up_limited"] = args.up_limited
              if args.down_limited is not None: params["down_limited"] = args.down_limited
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 711 B
        ---
        name: market-distribution-intraday
        description: 查询市场涨跌分布分时。接口:GET /api/v2/market/data/market-distribution-intraday。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 市场涨跌分布分时
        
        接口:GET `/api/v2/market/data/market-distribution-intraday`。参数和响应以 `ftshare-doc/api-doc/股票数据/行情数据/市场涨跌分布分时.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> market-distribution-intraday --ts_millis 1787191200000 --up_limited 5 --down_limited 5
        ```
        
    • member-build-process
      • scripts
        • handler.py 3.5 KB
          #!/usr/bin/env python3
          """分页查询单个期货会员在指定合约/日期区间的持仓变化,并结合合约行情估算每日及累计盈亏"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v2/market/data/member-build-process"
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def fetch_page(page: int, page_size: int, **extra) -> dict:
              params = {"page": page, "page_size": page_size}
              params.update({k: v for k, v in extra.items() if v is not None})
              qs = urllib.parse.urlencode(params)
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询期货会员建仓过程")
              parser.add_argument("--exchange", required=True, help="交易所代码,如 SHFE/DCE/CZCE")
              parser.add_argument("--member_name", required=True, help="会员名称,如 永安期货")
              parser.add_argument("--instrument_id", required=True, help="合约代码,如 rb2601")
              parser.add_argument("--start_date", default=None, help="起始日期 YYYYMMDD 或 YYYY-MM-DD,默认 20260101")
              parser.add_argument("--end_date", default=None, help="结束日期 YYYYMMDD 或 YYYY-MM-DD,默认当天")
              parser.add_argument("--contract_multiplier", type=float, default=None,
                                  help="合约乘数;不传时按品种默认表推导")
              parser.add_argument("--page", type=int, default=1, help="页码(默认 1)")
              parser.add_argument("--page_size", type=int, default=50, help="每页条数(默认 50,最大 200)")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量")
              args = parser.parse_args()
          
              extra = dict(exchange=args.exchange, member_name=args.member_name, instrument_id=args.instrument_id,
                           start_date=args.start_date, end_date=args.end_date,
                           contract_multiplier=args.contract_multiplier)
          
              if args.fetch_all:
                  first = fetch_page(1, args.page_size, **extra)
                  data = first.get("data") or {}
                  records = list(data.get("records", []))
                  pages = data.get("pages", 1)
                  for p in range(2, pages + 1):
                      d = fetch_page(p, args.page_size, **extra).get("data") or {}
                      records.extend(d.get("records", []))
                  result = {
                      "code": first.get("code"), "message": first.get("message"),
                      "data": {"pageNum": 1, "pageSize": args.page_size,
                               "total": data.get("total", len(records)), "pages": pages, "records": records},
                  }
              else:
                  result = fetch_page(args.page, args.page_size, **extra)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 4.6 KB
        ---
        name: member-build-process
        description: 查询单个期货会员在指定合约/日期区间的持仓变化,并结合合约行情估算每日及累计盈亏。用户提到「会员建仓过程」「期货会员持仓盈亏」「会员净持仓变化」「member build process」时使用。按交易所/会员/合约过滤,可选日期区间和合约乘数,分页返回,page_size 最大 200,支持 --all 翻页。
        ---
        
        # 查询期货会员建仓过程
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询期货会员建仓过程 |
        | 外部接口 | GET /api/v2/market/data/member-build-process |
        | 请求方式 | GET |
        | 适用场景 | 查询单个期货会员在指定合约和日期区间内的持仓变化,并结合合约行情估算每日及累计盈亏 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | exchange | string | 是 | 交易所代码 | `DCE` | `SHFE`/`DCE`/`CZCE` 等 |
        | member_name | string | 是 | 会员名称 | `永安期货` | 中文,URL 编码后传入 |
        | instrument_id | string | 是 | 合约代码 | `a2609` | - |
        | start_date | string | 否 | 起始日期 | `20260701` | YYYYMMDD 或 YYYY-MM-DD,默认 `20260101` |
        | end_date | string | 否 | 结束日期 | `20260721` | YYYYMMDD 或 YYYY-MM-DD,默认当天 |
        | contract_multiplier | float | 否 | 合约乘数 | `10` | 不传时按品种默认表推导 |
        | page | int | 否 | 页码 | `1` | 默认 1 |
        | page_size | int | 否 | 每页条数 | `50` | 默认 50,最大 200 |
        
        ## 执行方式
        
        ```bash
        # 查某会员某合约某区间建仓过程
        python <RUN_PY> member-build-process --exchange DCE --member_name 永安期货 --instrument_id a2609 --start_date 20260701 --end_date 20260721
        # 指定合约乘数
        python <RUN_PY> member-build-process --exchange SHFE --member_name 永安期货 --instrument_id rb2601 --contract_multiplier 10 --all
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        返回 `code/message/data`,记录位于 `data.records`。
        
        ```json
        {
          "code": 200,
          "message": "success",
          "data": {
            "pageNum": 1, "pageSize": 50, "total": 15, "pages": 1,
            "records": [
              {
                "trade_date": "2026-07-21", "exchange": "DCE", "instrument_id": "a2609",
                "product_name": "豆一", "member_name": "永安期货",
                "volume_rank": 1, "value": 10000, "change": 100,
                "long_rank": 2, "long_value": 5000, "long_change": 200,
                "short_rank": null, "short_value": null, "short_change": null,
                "net_position": 5000, "observed_side": "long",
                "open_price": "4200", "high_price": "4220", "low_price": "4180", "close_price": "4210",
                "settlement_price": "4205", "pre_settlement_price": "4198",
                "volume": "12345", "open_interest": "45678", "open_interest_change": "-100",
                "contract_multiplier": 10,
                "estimated_net_change": 100,
                "estimated_position_cost": 42000000.0,
                "estimated_daily_pnl": 5000.0, "estimated_daily_pnl_wan": 0.5,
                "estimated_cumulative_pnl": 25000.0, "estimated_cumulative_pnl_wan": 2.5
              }
            ]
          }
        }
        ```
        
        ### records 字段
        
        | 字段 | 类型 | 说明 |
        |------|------|------|
        | trade_date | string | 交易日 |
        | exchange / instrument_id / product_name / member_name | string | 交易所 / 合约代码 / 品种名称 / 会员名称 |
        | volume_rank / value / change | int/null | 成交排名 / 成交量 / 变化 |
        | long_rank / long_value / long_change | int/null | 多头排名 / 持仓量 / 变化 |
        | short_rank / short_value / short_change | int/null | 空头排名 / 持仓量 / 变化 |
        | net_position | int/null | 净持仓 |
        | observed_side | string | 观察方向 |
        | open_price / high_price / low_price / close_price | string/null | 当日开高低收 |
        | settlement_price / pre_settlement_price | string/null | 结算价 / 前结算价 |
        | volume / open_interest / open_interest_change | string/null | 合约成交量 / 持仓量 / 持仓量变化 |
        | contract_multiplier | float/null | 合约乘数 |
        | estimated_net_change | int/null | 估算净持仓变化 |
        | estimated_position_cost | float/null | 估算持仓成本 |
        | estimated_daily_pnl / estimated_daily_pnl_wan | float/null | 估算当日盈亏 / 万元 |
        | estimated_cumulative_pnl / estimated_cumulative_pnl_wan | float/null | 估算累计盈亏 / 万元 |
        
        ## 注意事项
        
        - `exchange`/`member_name`/`instrument_id` 三者为必填。
        - `contract_multiplier` 不传时按品种默认表推导;不同品种乘数不同,影响盈亏估算结果。
        - 多数价格/数量字段以字符串返回,估算字段为 float。
        - `page_size` 最大 200。
        
    • member-position-ranking
      • scripts
        • handler.py 3.1 KB
          #!/usr/bin/env python3
          """分页查询指定交易日/交易所/合约/方向的期货会员持仓排名"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v2/market/data/member-position-ranking"
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def fetch_page(page: int, page_size: int, **extra) -> dict:
              params = {"page": page, "page_size": page_size}
              params.update({k: v for k, v in extra.items() if v is not None})
              qs = urllib.parse.urlencode(params)
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询期货会员持仓排名")
              parser.add_argument("--exchange", required=True, help="交易所代码,如 SHFE/DCE/CZCE")
              parser.add_argument("--instrument_id", required=True, help="合约代码,如 a2605")
              parser.add_argument("--trade_date", required=True, help="交易日 YYYYMMDD 或 YYYY-MM-DD")
              parser.add_argument("--direction", required=True, help="查询方向:long 或 short(也接受常见中文和缩写别名)")
              parser.add_argument("--page", type=int, default=1, help="页码(默认 1)")
              parser.add_argument("--page_size", type=int, default=50, help="每页条数(默认 50,最大 200)")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量")
              args = parser.parse_args()
          
              extra = dict(exchange=args.exchange, instrument_id=args.instrument_id,
                           trade_date=args.trade_date, direction=args.direction)
          
              if args.fetch_all:
                  first = fetch_page(1, args.page_size, **extra)
                  data = first.get("data") or {}
                  records = list(data.get("records", []))
                  pages = data.get("pages", 1)
                  for p in range(2, pages + 1):
                      d = fetch_page(p, args.page_size, **extra).get("data") or {}
                      records.extend(d.get("records", []))
                  result = {
                      "code": first.get("code"), "message": first.get("message"),
                      "data": {"pageNum": 1, "pageSize": args.page_size,
                               "total": data.get("total", len(records)), "pages": pages, "records": records},
                  }
              else:
                  result = fetch_page(args.page, args.page_size, **extra)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.7 KB
        ---
        name: member-position-ranking
        description: 查询指定交易日/交易所/合约/方向的期货会员持仓排名(品种/合约/方向/会员/持仓量/持仓量变化/净持仓)。用户提到「会员持仓排名」「期货会员持仓」「多头排名」「空头排名」「member position ranking」时使用。按交易所/合约/交易日/方向过滤,分页返回,page_size 最大 200,支持 --all 翻页。
        ---
        
        # 查询期货会员持仓排名
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询期货会员持仓排名 |
        | 外部接口 | GET /api/v2/market/data/member-position-ranking |
        | 请求方式 | GET |
        | 适用场景 | 查询指定交易日、交易所、合约及方向的期货会员持仓排名 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | exchange | string | 是 | 交易所代码 | `DCE` | `SHFE`/`DCE`/`CZCE` 等 |
        | instrument_id | string | 是 | 合约代码 | `a2605` | - |
        | trade_date | string | 是 | 交易日 | `20260721` | YYYYMMDD 或 YYYY-MM-DD |
        | direction | string | 是 | 查询方向 | `long` | `long` 或 `short`,也接受常见中文和缩写别名 |
        | page | int | 否 | 页码 | `1` | 默认 1 |
        | page_size | int | 否 | 每页条数 | `50` | 默认 50,最大 200 |
        
        ## 执行方式
        
        ```bash
        # 查某合约某日多头会员排名
        python <RUN_PY> member-position-ranking --exchange DCE --instrument_id a2605 --trade_date 20260721 --direction long
        # 查空头排名并翻全量
        python <RUN_PY> member-position-ranking --exchange SHFE --instrument_id rb2610 --trade_date 20260721 --direction short --all
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        返回 `code/message/data`,记录位于 `data.records`。
        
        ```json
        {
          "code": 200,
          "message": "success",
          "data": {
            "pageNum": 1, "pageSize": 50, "total": 100, "pages": 2,
            "records": [
              {
                "variety": "a", "code": "a2605", "date": "2026-07-21",
                "direction": "long", "broker": "永安期货",
                "oi": 10000, "oi_chg": 200, "net_position": 5000
              }
            ]
          }
        }
        ```
        
        ### records 字段
        
        | 字段 | 类型 | 说明 |
        |------|------|------|
        | variety | string | 品种名称或代码 |
        | code | string | 合约代码 |
        | date | string | 交易日 |
        | direction | string | 持仓方向 |
        | broker | string | 期货会员名称 |
        | oi | int64/null | 持仓量 |
        | oi_chg | int64/null | 持仓量变化 |
        | net_position | int64/null | 净持仓 |
        
        ## 注意事项
        
        - `exchange`/`instrument_id`/`trade_date`/`direction` 四者均为必填。
        - `direction` 接受 `long`/`short` 及常见中文和缩写别名。
        - `page_size` 最大 200。
        
    • namechange
      • scripts
        • handler.py 2.9 KB
          #!/usr/bin/env python3
          """查询 A 股股票曾用名(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          ENDPOINT = "/api/v1/market/data/namechange"
          
          
          def build_params(trade_code, start_date, end_date):
              params = {}
              if trade_code:
                  params["trade_code"] = trade_code
              if start_date:
                  params["start_date"] = start_date
              if end_date:
                  params["end_date"] = end_date
              return params
          
          
          def fetch(
          trade_code, start_date, end_date):
              params = urllib.parse.urlencode(build_params(trade_code, start_date, end_date))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询 A 股股票曾用名")
              parser.add_argument(
                  "--trade_code",
                  required=True,
                  help="股票代码(带 .SZ/.SH 后缀),支持逗号分隔多个,如 600848.SH",
              )
              parser.add_argument("--start_date", help="过滤区间起始日期,YYYYMMDD 格式")
              parser.add_argument("--end_date", help="过滤区间结束日期,YYYYMMDD 格式")
              args = parser.parse_args()
          
              result = fetch(args.trade_code, args.start_date, args.end_date)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.9 KB
          #!/usr/bin/env python3
          """Tests for namechange handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_by_trade_code(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("600848.SH", None, None)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/namechange", url)
                  self.assertIn("trade_code=600848.SH", url)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_with_date_range(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("600848.SH", "20200101", "20241231")
                  url = mock_open.call_args[0][0]
                  self.assertIn("start_date=20200101", url)
                  self.assertIn("end_date=20241231", url)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  with patch.object(sys, "argv", ["handler.py", "--trade_code", "600848.SH"]):
                      with patch("sys.stdout", new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
              def test_main_requires_trade_code(self):
                  with patch.object(sys, "argv", ["handler.py"]):
                      with self.assertRaises(SystemExit):
                          handler.main()
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 3.3 KB
        ---
        name: namechange
        description: "查询 A 股股票曾用名。用户问股票曾用名、历史名称、改名记录、之前叫什么、原名称、股票改名、600848 曾用名、自仪股份改名时使用。参数 trade_code 必填且需带 .SZ/.SH 后缀,支持逗号分隔多个代码。"
        ---
        
        # 查询 A 股股票曾用名
        
        ## 接口说明
        
        | 项目     | 说明                                       |
        |----------|--------------------------------------------|
        | 接口名称 | 查询 A 股股票曾用名                         |
        | 外部接口 | `/api/v1/market/data/namechange`           |
        | 请求方式 | GET                                        |
        | 适用场景 | 按股票代码查询该股票的历史曾用名及使用区间 |
        
        ## 请求参数
        
        | 参数名      | 类型   | 是否必填 | 描述                                                | 取值示例     | 备注                                                                  |
        |-------------|--------|----------|-----------------------------------------------------|--------------|-----------------------------------------------------------------------|
        | trade_code  | string | 是       | 股票代码                                            | 600848.SH    | 带 .SZ/.SH 后缀,支持逗号分隔多个,如 `600848.SH,000001.SZ`           |
        | start_date  | string | 否       | 过滤区间起始日期                                    | 20200101     | `YYYYMMDD` 格式;与 `end_date` 同时提供时须 `start_date` ≤ `end_date` |
        | end_date    | string | 否       | 过滤区间结束日期                                    | 20241231     | `YYYYMMDD` 格式;与 `start_date` 同时提供时须 `start_date` ≤ `end_date` |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> namechange --trade_code 600848.SH
        python <RUN_PY> namechange --trade_code 600848.SH --start_date 20200101 --end_date 20241231
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        ```json
        [
            {
                "trade_code": "600848.SH",
                "name": "上海临港",
                "start_date": "20151118",
                "end_date": null
            },
            {
                "trade_code": "600848.SH",
                "name": "自仪股份",
                "start_date": "20070514",
                "end_date": "20151117"
            }
        ]
        ```
        
        ### 字段说明
        
        | 字段名      | 类型   | 是否可为空 | 说明                                                                              |
        |-------------|--------|------------|-----------------------------------------------------------------------------------|
        | trade_code  | string | 否         | 股票代码(带 .SZ/.SH 后缀)                                                        |
        | name        | string | 否         | 该时段内使用的股票名称                                                            |
        | start_date  | string | 是         | 使用该名称的起始日期(`YYYYMMDD`;最早曾用名之前原名称可能为空字符串)             |
        | end_date    | string | 是         | 使用该名称的结束日期(`YYYYMMDD`);`null` 表示当前仍在使用                        |
        
        ## 注意事项
        
        - `trade_code` 必填,为空时接口返回 400。
        - 不分页,单只股票一次性返回全部历史名称记录;多 `trade_code`(逗号分隔)时合并返回。
        - 日期参数格式为 `YYYYMMDD`,非时间戳。
        
    • nanhua-index-kline
      • scripts
        • handler.py 2.7 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/futures/nanhua-index-kline'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='南华期货指数日K线')
              parser.add_argument("--trade_date")
              parser.add_argument("--start_date")
              parser.add_argument("--end_date")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              args = parser.parse_args()
              params = {}
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 664 B
        ---
        name: nanhua-index-kline
        description: 查询南华期货指数日K线。接口:GET /api/v1/market/data/futures/nanhua-index-kline。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 南华期货指数日K线
        
        接口:GET `/api/v1/market/data/futures/nanhua-index-kline`。参数和响应以 `ftshare-doc/api-doc/期货数据/南华期货指数日K线.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> nanhua-index-kline --trade_date 20260821 --page 1 --page_size 5
        ```
        
    • news-reaction-snapshot
      • scripts
        • handler.py 3.7 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v3/market/data/news-reaction-snapshot"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"Content-Type": "application/json", "X-Client-Name": "ft-claw"}
          LOOKBACK_HOURS = (24, 48)
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, headers=_REQUEST_HEADERS, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def fetch(params):
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params)
              request = urllib.request.Request(url, headers=_REQUEST_HEADERS, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      return json.loads(response.read().decode())
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="消息量价共振")
              parser.add_argument("--symbol", required=True, help="股票代码,带交易所后缀,如 600519.SH")
              parser.add_argument("--start-date", dest="start_date", required=True, help="起始交易日(含)")
              parser.add_argument("--end-date", dest="end_date", required=True, help="截止交易日(含),跨度不超过 31 天")
              parser.add_argument("--lookback-hours", dest="lookback_hours", type=int, choices=LOOKBACK_HOURS,
                                  help="新闻回看窗口(小时),仅 24 或 48;不传返回两套窗口")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始,默认 1,最大 1000")
              parser.add_argument("--page-size", dest="page_size", type=int, default=50, help="每页条数,默认 50,最大 200")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页返回全部记录")
              args = parser.parse_args()
              if not 1 <= args.page <= 1000 or not 1 <= args.page_size <= 200:
                  parser.error("page 须在 1~1000 之间,page-size 须在 1~200 之间")
          
              base = {"symbol": args.symbol, "start_date": args.start_date, "end_date": args.end_date}
              if args.lookback_hours is not None:
                  base["lookback_hours"] = args.lookback_hours
          
              result = fetch({**base, "page": 1 if args.fetch_all else args.page, "page_size": args.page_size})
              if args.fetch_all:
                  data = result.get("data") or {}
                  records = list(data.get("records", []))
                  for page in range(2, int(data.get("pages", 1)) + 1):
                      records.extend((fetch({**base, "page": page, "page_size": args.page_size}).get("data") or {}).get("records", []))
                  result["data"] = {**data, "records": records}
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 3.1 KB
          #!/usr/bin/env python3
          """Tests for news-reaction-snapshot handler."""
          import importlib.util
          import json
          import os
          import sys
          import unittest
          from unittest.mock import patch
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          def _run(argv):
              with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                  with patch.object(sys, "argv", ["handler.py"] + argv):
                      handler.main()
          
          
          class TestNewsReactionSnapshot(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_forwards_required_and_optional_params(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"{}"
                  _run(["--symbol", "600519.SH", "--start-date", "20260818", "--end-date", "20260828",
                        "--lookback-hours", "48", "--page", "2", "--page-size", "5"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("/api/v3/market/data/news-reaction-snapshot", req.full_url)
                  self.assertIn("symbol=600519.SH", req.full_url)
                  self.assertIn("start_date=20260818", req.full_url)
                  self.assertIn("end_date=20260828", req.full_url)
                  self.assertIn("lookback_hours=48", req.full_url)
                  self.assertIn("page=2", req.full_url)
                  self.assertIn("page_size=5", req.full_url)
          
              @patch.object(handler, "safe_urlopen")
              def test_lookback_hours_omitted_when_absent(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"{}"
                  _run(["--symbol", "600519.SH", "--start-date", "20260818", "--end-date", "20260828"])
                  req = mock_open.call_args[0][0]
                  self.assertNotIn("lookback_hours", req.full_url)
          
              def test_rejects_invalid_lookback_hours(self):
                  with self.assertRaises(SystemExit):
                      _run(["--symbol", "600519.SH", "--start-date", "20260818", "--end-date", "20260828",
                            "--lookback-hours", "12"])
          
              def test_rejects_out_of_range_pagination(self):
                  with self.assertRaises(SystemExit):
                      _run(["--symbol", "600519.SH", "--start-date", "20260818", "--end-date", "20260828",
                            "--page-size", "201"])
          
              def test_fetch_all_aggregates_records(self):
                  pages = {
                      1: {"data": {"pages": 2, "records": [{"trade_date": "20260819"}]}},
                      2: {"data": {"pages": 2, "records": [{"trade_date": "20260820"}]}},
                  }
                  captured = []
          
                  def fake_fetch(params):
                      captured.append(params["page"])
                      return pages[params["page"]]
          
                  with patch.object(handler, "fetch", side_effect=fake_fetch):
                      with patch("builtins.print") as mock_print:
                          _run(["--symbol", "600519.SH", "--start-date", "20260818", "--end-date", "20260828", "--all"])
                  self.assertEqual(captured, [1, 2])
                  output = json.loads(mock_print.call_args[0][0])
                  self.assertEqual([row["trade_date"] for row in output["data"]["records"]], ["20260819", "20260820"])
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 1.4 KB
        ---
        name: news-reaction-snapshot
        description: 查询指定 A 股的新闻市场反应快照(消息量价共振)。用户询问个股消息面与量价的共振、新闻反应方向或阶段时使用。
        ---
        
        # 消息量价共振
        
        接口:`GET /api/v3/market/data/news-reaction-snapshot`
        
        参数:`--symbol` 必填(带交易所后缀,如 `600519.SH`,兼容 `.XSHG/.XSHE/.BJSE`、`SH:600000` 等写法,响应统一为六位数字加 `.SH/.SZ/.BJ`);`--start-date`、`--end-date` 必填(`YYYYMMDD` 或 `YYYY-MM-DD`,与起始日相隔不超过 31 天);`--lookback-hours` 可选,仅 `24` 或 `48`,不传返回两套窗口;`--page` 默认 1(上限 1000)、`--page-size` 默认 50(上限 200);支持 `--all` 自动翻页。
        
        ```bash
        python <RUN_PY> news-reaction-snapshot --symbol 600519.SH --start-date 20260818 --end-date 20260828 --lookback-hours 48 --page 1 --page-size 5
        ```
        
        返回 `code/message/data` 分页信封,快照位于 `data.records`;每条包含 `trade_date`、`symbol`、`stock_name`、`lookback_hours`、`reaction_direction`、`reaction_stage`、`trend_interaction`、`price_volume_signature`、`relative_performance`、`participation_state`、`event_attention_state`、`observation_sufficiency`、`state_summary`、`confidence`、`data_insufficient`、`signal_ambiguous`。枚举字段保持英文码,中文解释只出现在 `state_summary`;接口只读历史成功快照,不重新调用模型,也不预测收益。
        
    • northbound
      • scripts
        • handler.py 2.3 KB
          #!/usr/bin/env python3
          """查询北向资金(沪股通、深股通)交易汇总数据"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/northbound"
          
          
          def fetch(
          date: str) -> dict:
              params = urllib.parse.urlencode({"date": date})
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询北向资金交易数据")
              parser.add_argument("--date", required=True, help="交易日期,格式 YYYYMMDD")
              args = parser.parse_args()
          
              result = fetch(args.date)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.3 KB
        ---
        name: northbound
        description: "查询北向资金交易数据。当用户需要查询指定交易日北向资金(沪股通、深股通)交易汇总数据,或了解北向资金交易数据时使用。"
        ---
        
        # 查询北向资金交易数据
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 查询北向资金交易数据 |
        | 外部接口 | `/api/v1/market/data/northbound` |
        | 请求方式 | GET |
        | 适用场景 | 查询指定交易日北向资金(沪股通、深股通)交易汇总数据 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |---|---|---|---|---|---|
        | `date` | string | 是 | 交易日期 | `20250101` | 格式 `YYYYMMDD` |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> northbound --date 20250101
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        ```json
        {
            "code": 0,
            "message": "success",
            "data": {
                "date": "20250101",
                "currency": "CNY",
                "total_amount": "100.50",
                "channels": {
                    "SH": { "amount": "60.00", "trade_count": 10 },
                    "SZ": { "amount": "40.50", "trade_count": 8 }
                }
            }
        }
        ```
        
        ### 顶层字段说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |---|---|---|---|
        | `code` | int | 否 | 业务状态码,0 表示成功 |
        | `message` | string | 否 | 状态说明 |
        | `data` | object | 否 | 北向资金数据 |
        
        ### data 字段说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |---|---|---|---|
        | `date` | string | 否 | 交易日期 |
        | `currency` | string | 否 | 币种(CNY) |
        | `total_amount` | string | 否 | 北向资金合计成交额 |
        | `channels` | object | 否 | 分市场通道数据 |
        
        ### channels 字段说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |---|---|---|---|
        | `SH` | object | 否 | 沪股通数据 |
        | `SH.amount` | string | 否 | 成交额 |
        | `SH.trade_count` | int | 否 | 成交笔数 |
        | `SZ` | object | 否 | 深股通数据 |
        | `SZ.amount` | string | 否 | 成交额 |
        | `SZ.trade_count` | int | 否 | 成交笔数 |
        
        ## 注意事项
        
        - `date` 为必填参数,格式 `YYYYMMDD`
        - 金额类字段以字符串格式返回
        - 响应为信封结构(`code` / `message` / `data`)
        
    • nth-trade-date
      • scripts
        • handler.py 1.8 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL=os.environ.get("FTSHARE_BASE_URL","https://market.ft.tech/gateway").rstrip("/"); ENDPOINT="/api/v1/market/data/time/get-nth-trade-date"
          _REQUEST_HEADERS={"Content-Type":"application/json"}
          def safe_urlopen(request):
           url=request.full_url if isinstance(request,urllib.request.Request) else str(request)
           parsed=urllib.parse.urlparse(url); base=urllib.parse.urlparse(BASE_URL)
           if (parsed.scheme,parsed.netloc)!=(base.scheme,base.netloc): print("Invalid URL for safe_urlopen",file=sys.stderr); raise SystemExit(1)
           if not isinstance(request,urllib.request.Request): request=urllib.request.Request(url,headers=_REQUEST_HEADERS,method="GET")
           return urllib.request.urlopen(request,timeout=30)
          def main():
           p=argparse.ArgumentParser(description="查询第 N 个交易日"); p.add_argument("--n",type=int,required=True); a=p.parse_args()
           if a.n<1: p.error("n 必须大于等于 1")
           key=os.environ.get("FTSHARE_API_KEY")
           if not key: print("FTSHARE_API_KEY environment variable is required",file=sys.stderr); raise SystemExit(2)
           url=BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode({"n":a.n}); b,purl=urllib.parse.urlparse(BASE_URL),urllib.parse.urlparse(url)
           if (b.scheme,b.netloc)!=(purl.scheme,purl.netloc): print("Invalid URL for safe_urlopen",file=sys.stderr); raise SystemExit(1)
           try:
            req=urllib.request.Request(url,headers={"FTSHARE_API_KEY":key,"Content-Type":"application/json"},method="GET")
            with safe_urlopen(req) as r: result=json.loads(r.read().decode())
           except urllib.error.HTTPError as e: print(f"HTTP {e.code}: {e.read().decode()}",file=sys.stderr); raise SystemExit(1)
           except urllib.error.URLError as e: print(f"请求失败: {e.reason}",file=sys.stderr); raise SystemExit(1)
           print(json.dumps(result,ensure_ascii=False,indent=2))
          if __name__=="__main__":main()
          
        • test_handler.py 1011 B
          import importlib.util
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          
          _DIR = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_DIR, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          spec.loader.exec_module(handler)
          
          class TestHandler(unittest.TestCase):
              @patch.object(handler, "safe_urlopen")
              def test_n_is_encoded_and_json_emitted(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'{"code": 200, "data": {"n": 5}}'
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}), patch.object(sys, "argv", ["handler.py", "--n", "5"]), patch("sys.stdout", new_callable=StringIO) as out:
                      handler.main()
                  request = mock_open.call_args.args[0]
                  self.assertIn("n=5", request.full_url)
                  self.assertEqual(json.loads(out.getvalue())["data"]["n"], 5)
          
          if __name__ == "__main__": unittest.main()
          
      • SKILL.md 545 B
        ---
        name: nth-trade-date
        description: 查询第 N 个交易日。Use when user asks for 前 N 个交易日、某日期对应的第 N 个交易日或交易日倒推。
        ---
        
        # 第 N 个交易日
        
        接口:`GET /api/v1/market/data/time/get-nth-trade-date`。按服务端当前日期向前计算第 N 个交易日。
        
        ```bash
        python <RUN_PY> nth-trade-date --n 5
        ```
        
        - `--n`:必填整数,必须大于等于 1。
        
        响应为 `code/message/data` 信封,`data` 包括 `current_date`、`nth_trade_date`(均为 `YYYY-MM-DD`)和请求的 `n`。
        
    • pledge-detail
      • scripts
        • handler.py 2.9 KB
          #!/usr/bin/env python3
          """查询单只 A 股股票所有报告期的股权质押详细信息"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/pledge/pledge-detail"
          
          
          def fetch(
          stock_code: str, page: int, page_size: int) -> dict:
              params = urllib.parse.urlencode({
                  "stock_code": stock_code,
                  "page": page,
                  "page_size": page_size,
              })
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询单只 A 股股票所有报告期的股权质押详细信息")
              parser.add_argument(
                  "--stock_code",
                  required=True,
                  help="股票代码,需携带市场后缀,如 603323.SH / 000001.SZ / 833171.BJ",
              )
              parser.add_argument(
                  "--page",
                  type=int,
                  default=1,
                  help="页码,从 1 开始,默认 1",
              )
              parser.add_argument(
                  "--page_size",
                  type=int,
                  default=50,
                  help="每页记录数,默认 50",
              )
              args = parser.parse_args()
          
              result = fetch(args.stock_code, args.page, args.page_size)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 4.5 KB
        ---
        name: pledge-detail
        description: "查询单票股权质押个股详细信息。当用户需要获取 A 股上市公司单只股票所有报告期的股权质押详细信息,支持沪深京股票,支持分页查询,或了解单票股权质押个股详细信息时使用。"
        ---
        
        # 查询单票股权质押个股详细信息
        
        ## 接口说明
        
        | 项目     | 说明                                                                              |
        |----------|-----------------------------------------------------------------------------------|
        | 接口名称 | 查询单票股权质押个股详细信息                                                      |
        | 外部接口 | `/api/v1/market/data/pledge/pledge-detail`                                   |
        | 请求方式 | GET                                                                               |
        | 适用场景 | 获取 A 股上市公司单只股票所有报告期的股权质押详细信息,支持沪深京股票,支持分页查询 |
        
        ## 请求参数
        
        | 参数名     | 类型   | 是否必填 | 描述            | 取值示例  | 备注                                                                                     |
        |------------|--------|----------|-----------------|-----------|------------------------------------------------------------------------------------------|
        | stock_code | string | 是       | 单个股票代码    | 603323.SH | 支持沪深京股票,A股需为6位数字+后缀(SH=上交所,SZ=深交所,BJ=北交所),单次仅支持一个代码 |
        | page       | int    | 否       | 页码,从 1 开始 | 1         | 默认值为 1,必须大于等于 1                                                               |
        | page_size  | int    | 否       | 每页记录数      | 50        | 默认值为 50,必须大于等于 1                                                              |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> pledge-detail --stock_code 603323.SH
        python <RUN_PY> pledge-detail --stock_code 603323.SH --page 2 --page_size 20
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        接口返回带分页信息的对象:
        
        ```json
        {
            "items": [
                {
                    "trade_code": "603323.SH",
                    "stock_name": "苏农银行",
                    "report_date": "2025-09-30",
                    "pledge_ratio": 12.34,
                    "pledge_n": 5,
                    "pledge_market_value": 98765432.0,
                    "pledge_unlimitn": 1234.56,
                    "pledge_limitn": 0.0,
                    "last_year_fluctuation": -5.67
                }
            ],
            "total_pages": 1,
            "total_items": 21
        }
        ```
        
        ### PledgeStockHolder 字段说明
        
        | 字段名                | 类型   | 是否可为空 | 说明                                                                   | 单位 |
        |-----------------------|--------|------------|------------------------------------------------------------------------|------|
        | trade_code            | String | 否         | 股票交易代码,固定携带 .SZ/.SH/.BJ 市场后缀                             | -    |
        | stock_name            | String | 否         | 上市公司对应股票名称                                                   | -    |
        | report_date           | String | 否         | 报告日期,固定格式为 YYYY-MM-DD                                         | -    |
        | pledge_ratio          | float  | 否         | 质押比例                                                               | %    |
        | pledge_n              | int    | 否         | 质押笔数                                                               | 笔   |
        | pledge_market_value   | float  | 否         | 质押市值(质押股数 × 报告日期收盘价)                                    | 元   |
        | pledge_unlimitn       | float  | 否         | 质押无限售股数                                                         | 万股 |
        | pledge_limitn         | float  | 否         | 质押限售股数                                                           | 万股 |
        | last_year_fluctuation | float  | 是         | 较上年变动(报告日期收盘价与一年前收盘价的涨跌幅),无上年数据时返回 null | %    |
        
        ## 注意事项
        
        - `stock_code` 为必填参数,单次请求只支持一个股票代码
        - 分页参数 `page` 和 `page_size` 均为可选,默认 `page=1`、`page_size=50`
        - 返回值包含 `items`、`total_pages`、`total_items` 分页包装
        - `last_year_fluctuation` 可为 null(无上年数据时)
        
    • pledge-summary
      • scripts
        • handler.py 3 KB
          #!/usr/bin/env python3
          """查询 A 股市场股权质押汇总。"""
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/pledge/pledge-summary"
          
          
          def safe_urlopen(req_or_url):
              url = req_or_url.full_url if isinstance(req_or_url, urllib.request.Request) else str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询 A 股市场股权质押汇总")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始")
              parser.add_argument("--page-size", dest="page_size", type=int, default=50, help="每页条数,最大 200")
              args = parser.parse_args()
          
              query = urllib.parse.urlencode({"page": args.page, "page_size": args.page_size})
              request = urllib.request.Request(f"{BASE_URL}{ENDPOINT}?{query}", method="GET", headers=_REQUEST_HEADERS)
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
              except urllib.error.HTTPError as error:
                  body = error.read().decode().strip()
                  message = f"HTTP {error.code}"
                  if body:
                      message += f": {body}"
                  print(message, file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  sys.exit(1)
          
              data = payload.get("data") if isinstance(payload, dict) else None
              records = data.get("records") if isinstance(data, dict) else None
              if not isinstance(records, list):
                  print(json.dumps({"error": "unexpected response shape"}, ensure_ascii=False), file=sys.stderr)
                  sys.exit(1)
              print(json.dumps(payload, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2.2 KB
          from __future__ import annotations
          
          import importlib.util
          import json
          import sys
          from pathlib import Path
          from urllib.parse import parse_qs, urlparse
          
          import pytest
          
          
          HANDLER = Path(__file__).with_name("handler.py")
          
          
          def load_handler():
              spec = importlib.util.spec_from_file_location("pledge_summary_handler", HANDLER)
              module = importlib.util.module_from_spec(spec)
              assert spec.loader is not None
              spec.loader.exec_module(module)
              return module
          
          
          class Response:
              def __init__(self, payload):
                  self.payload = payload
          
              def __enter__(self):
                  return self
          
              def __exit__(self, *args):
                  return None
          
              def read(self):
                  return json.dumps(self.payload).encode()
          
          
          def test_main_preserves_paginated_envelope(monkeypatch, capsys):
              module = load_handler()
              opened = []
              payload = {
                  "code": 0,
                  "message": "success",
                  "data": {"pageNum": 1, "pageSize": 5, "total": 1, "pages": 1, "records": [{"trade_date": "2026-07-17", "pledge_total_ratio": "0.1250"}]},
              }
              monkeypatch.setattr(sys, "argv", ["handler.py", "--page", "1", "--page-size", "5"])
              monkeypatch.setattr(module, "safe_urlopen", lambda request: opened.append(request) or Response(payload))
          
              module.main()
          
              parsed = urlparse(opened[0].full_url)
              assert parsed.path == "/gateway/api/v1/market/data/pledge/pledge-summary"
              assert parse_qs(parsed.query) == {"page": ["1"], "page_size": ["5"]}
              assert json.loads(capsys.readouterr().out) == payload
          
          
          def test_main_accepts_empty_records(monkeypatch, capsys):
              module = load_handler()
              payload = {"code": 0, "message": "success", "data": {"records": []}}
              monkeypatch.setattr(sys, "argv", ["handler.py"])
              monkeypatch.setattr(module, "safe_urlopen", lambda request: Response(payload))
          
              module.main()
          
              assert json.loads(capsys.readouterr().out) == payload
          
          
          def test_main_rejects_unexpected_response_shape(monkeypatch, capsys):
              module = load_handler()
              monkeypatch.setattr(sys, "argv", ["handler.py"])
              monkeypatch.setattr(module, "safe_urlopen", lambda request: Response(["unexpected"]))
          
              with pytest.raises(SystemExit):
                  module.main()
          
              assert json.loads(capsys.readouterr().err) == {"error": "unexpected response shape"}
          
      • SKILL.md 3.5 KB
        ---
        name: pledge-summary
        description: "查询所有期股权质押总揽数据。当用户需要获取 A 股市场所有报告期的股权质押总揽数据,包括质押公司数量、质押笔数、质押总股数、质押总市值、沪深300指数等信息,支持沪深京股票,或了解所有期股权质押总揽数据时使用。"
        ---
        
        # 查询所有期股权质押总揽数据
        
        ## 接口说明
        
        | 项目     | 说明                                                                                                                       |
        |----------|----------------------------------------------------------------------------------------------------------------------------|
        | 接口名称 | 查询所有期股权质押总揽数据                                                                                                 |
        | 外部接口 | `/api/v1/market/data/pledge/pledge-summary`                                                                           |
        | 请求方式 | GET                                                                                                                        |
        | 适用场景 | 获取 A 股市场所有报告期的股权质押总揽数据,包括质押公司数量、质押笔数、质押总股数、质押总市值、沪深300指数等信息,支持沪深京股票 |
        
        ## 请求参数
        
        本接口无需任何请求参数。
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> pledge-summary
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        接口直接返回数组,**不含分页信息**。
        
        ```json
        [
            {
                "trade_date": "2025-09-30",
                "pledge_total_ratio": 0,
                "pledge_company_count": 1234,
                "pledge_deal_count": 5678,
                "pledge_total_shares": 1234567890000.0,
                "pledge_total_market_value": 98765432100000.0,
                "hs300_index": 3456.78,
                "hs300_week_change_ratio": 2.34
            }
        ]
        ```
        
        ### PledgeMarketSummary 字段说明
        
        | 字段名                    | 类型   | 是否可为空 | 说明                                      | 单位 |
        |---------------------------|--------|------------|-------------------------------------------|------|
        | trade_date                | String | 否         | 报告日期,固定格式为 YYYY-MM-DD            | -    |
        | pledge_total_ratio        | float  | 否         | A 股质押总比例,当前返回 0                 | %    |
        | pledge_company_count      | int    | 否         | 质押公司数量(有质押的上市公司数量)        | 家   |
        | pledge_deal_count         | int    | 否         | 质押笔数(所有质押交易的总笔数)            | 笔   |
        | pledge_total_shares       | float  | 否         | 质押总股数(所有质押股票的总股数)          | 股   |
        | pledge_total_market_value | float  | 否         | 质押总市值(所有质押股票的总市值)          | 元   |
        | hs300_index               | float  | 否         | 沪深300指数(报告日期的沪深300指数收盘价)  | -    |
        | hs300_week_change_ratio   | float  | 否         | 沪深300周涨跌幅(与一周前对比的涨跌幅)     | %    |
        
        ## 注意事项
        
        - 本接口无需任何参数,直接请求即可
        - 返回值为**数组**,不含 `items` / `total_pages` / `total_items` 等分页包装
        - `pledge_total_ratio` 当前接口固定返回 `0`
        - `pledge_total_shares` 和 `pledge_total_market_value` 原始接口以字符串返回,handler 已转为数值输出
        
    • price-change
      • scripts
        • handler.py 3.7 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/price/get-price-change'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='价格变动')
              parser.add_argument("--stock_code", required=True)
              parser.add_argument("--base_date", required=True)
              parser.add_argument("--n", required=True)
              parser.add_argument("--direction", required=True)
              parser.add_argument("--stock_name", required=True)
              parser.add_argument("--start_date", required=True)
              parser.add_argument("--end_date", required=True)
              parser.add_argument("--actual_trading_days", required=True)
              parser.add_argument("--start_price", required=True)
              parser.add_argument("--end_price", required=True)
              parser.add_argument("--price_change", required=True)
              parser.add_argument("--change_pct", required=False)
              args = parser.parse_args()
              params = {}
              if args.stock_code is not None: params["stock_code"] = args.stock_code
              if args.base_date is not None: params["base_date"] = args.base_date
              if args.n is not None: params["n"] = args.n
              if args.direction is not None: params["direction"] = args.direction
              if args.stock_name is not None: params["stock_name"] = args.stock_name
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.actual_trading_days is not None: params["actual_trading_days"] = args.actual_trading_days
              if args.start_price is not None: params["start_price"] = args.start_price
              if args.end_price is not None: params["end_price"] = args.end_price
              if args.price_change is not None: params["price_change"] = args.price_change
              if args.change_pct is not None: params["change_pct"] = args.change_pct
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 768 B
        ---
        name: price-change
        description: 查询价格变动。接口:GET /api/v2/market/data/price/get-price-change。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 价格变动
        
        接口:GET `/api/v2/market/data/price/get-price-change`。参数和响应以 `ftshare-doc/api-doc/股票数据/行情数据/价格变动.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> price-change --stock_code 600519 --base_date 20260828 --n 5 --direction forward --stock_name 1 --start_date 20260828 --end_date 20260828 --actual_trading_days 1 --start_price 1 --end_price 1 --price_change 1
        ```
        
    • report-announcement-list
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          """分页查询报告公告列表,可选按证券代码过滤;公告 ID 可用于查询公告摘要"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v1/market/data/report-announcements/list"
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def fetch_page(page: int, page_size: int, **extra) -> dict:
              params = {"page": page, "page_size": page_size}
              params.update({k: v for k, v in extra.items() if v is not None})
              qs = urllib.parse.urlencode(params)
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询报告公告列表")
              parser.add_argument("--date", required=True, help="公告日期 YYYYMMDD 或 YYYY-MM-DD")
              parser.add_argument("--sec_code", default=None, help="证券代码;不传返回当天全部证券公告")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始,默认 1")
              parser.add_argument("--page_size", type=int, default=50, help="每页数量,默认 50,最大 200")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量")
              args = parser.parse_args()
          
              extra = dict(date=args.date, sec_code=args.sec_code)
          
              if args.fetch_all:
                  first = fetch_page(1, args.page_size, **extra)
                  data = first.get("data") or {}
                  records = list(data.get("records", []))
                  pages = data.get("pages", 1)
                  for p in range(2, pages + 1):
                      d = fetch_page(p, args.page_size, **extra).get("data") or {}
                      records.extend(d.get("records", []))
                  result = {
                      "code": first.get("code"), "message": first.get("message"),
                      "data": {"pageNum": 1, "pageSize": args.page_size,
                               "total": data.get("total", len(records)), "pages": pages, "records": records},
                  }
              else:
                  result = fetch_page(args.page, args.page_size, **extra)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3.6 KB
        ---
        name: report-announcement-list
        description: 按公告日期分页查询报告公告列表,可选按证券代码过滤。用户提到「报告公告列表」「公告列表」「某日公告」「report announcement list」时使用。返回公告标题/附件信息/处理状态和公告 ID;公告 ID 可用于查询公告摘要。分页返回,page_size 最大 200,支持 --all 翻页。
        ---
        
        # 查询报告公告列表
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询报告公告列表 |
        | 外部接口 | GET /api/v1/market/data/report-announcements/list |
        | 请求方式 | GET |
        | 适用场景 | 按公告日期分页查询报告公告,可选按证券代码过滤;公告 ID 可用于查询公告摘要 |
        
        > 同一处理逻辑的兼容入口还包括 `/api/v1/market/data/report-announcement/list`。
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | date | string | 是 | 公告日期 | `20260714` | YYYYMMDD 或 YYYY-MM-DD |
        | sec_code | string | 否 | 证券代码 | `600000` | 不传返回当天全部证券公告 |
        | page | int | 否 | 页码 | `1` | 从 1 开始,默认 1 |
        | page_size | int | 否 | 每页数量 | `50` | 默认 50,最大 200 |
        
        ## 执行方式
        
        ```bash
        # 某日某证券公告
        python <RUN_PY> report-announcement-list --date 20260714 --sec_code 600000 --page 1 --page_size 20
        # 某日全部公告并翻全量
        python <RUN_PY> report-announcement-list --date 20260714 --all
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        返回 `code/message/data`,分页数据位于 `data.records`。
        
        ```json
        {
          "code": 200,
          "message": "success",
          "data": {
            "pageNum": 1, "pageSize": 20, "total": 50, "pages": 3,
            "records": [
              {
                "id": 12345,
                "announcement_id": "AN202607140001",
                "url_hash": "...",
                "sec_code": "600000",
                "sec_name": "浦发银行",
                "announcement_title": "...",
                "announcement_time": "2026-07-14 09:30:00",
                "adjunct_type": "PDF", "adjunct_size": 102400,
                "column_type": "monthly", "plate": "sh",
                "status": "summarized", "retry_count": 0,
                "created_at": "2026-07-14 09:30:00",
                "updated_at": "2026-07-14 10:00:00",
                "processed_at": "2026-07-14 10:01:00"
              }
            ]
          }
        }
        ```
        
        ### data 字段
        
        | 字段 | 类型 | 说明 |
        |------|------|------|
        | pageNum | int | 当前页码 |
        | pageSize | int | 当前每页条数 |
        | total | int | 满足条件的总记录数 |
        | pages | int | 总页数;无记录时为 0 |
        | records | array | 当前页公告记录 |
        
        ### records 元素字段
        
        | 字段 | 类型 | 说明 |
        |------|------|------|
        | id | int64/null | 数据库记录 ID |
        | announcement_id | string/null | 公告 ID |
        | url_hash | string/null | 公告附件哈希 |
        | sec_code / sec_name | string/null | 证券代码 / 证券名称 |
        | announcement_title | string/null | 公告标题 |
        | announcement_time | string/null | 公告时间 |
        | adjunct_type | string/null | 附件类型 |
        | adjunct_size | int64/null | 附件大小 |
        | column_type | string/null | 公告栏目类型 |
        | plate | string/null | 所属板块 |
        | status | string/null | 处理状态 |
        | retry_count | int64/null | 重试次数 |
        | created_at / updated_at / processed_at | string/null | 创建 / 更新 / 处理时间 |
        
        ## 注意事项
        
        - `date` 为必填,支持 YYYYMMDD 或 YYYY-MM-DD。
        - `page_size` 最大 200。
        - 取得 `announcement_id` 后可调用 `report-announcement-summary` 查询公告摘要。
        - 两个路径(`/report-announcements/list` 与 `/report-announcement/list`)均为 v1 兼容入口。
        
    • report-announcement-summary
      • scripts
        • handler.py 1.6 KB
          #!/usr/bin/env python3
          """根据公告 ID 查询单条报告公告的摘要、标题、证券信息和处理状态"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v2/market/data/report-announcements/summary"
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询报告公告摘要")
              parser.add_argument("--announcement_id", required=True, help="公告 ID,由「报告公告列表」接口返回")
              args = parser.parse_args()
          
              qs = urllib.parse.urlencode({"announcement_id": args.announcement_id})
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.2 KB
        ---
        name: report-announcement-summary
        description: 根据公告 ID 查询单条报告公告的摘要、标题、证券信息和处理状态。用户提到「报告公告摘要」「公告摘要」「单条公告详情」「report announcement summary」时使用。未找到公告时 data 为 null、code 为 404。
        ---
        
        # 查询报告公告摘要
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询报告公告摘要 |
        | 外部接口 | GET /api/v2/market/data/report-announcements/summary |
        | 请求方式 | GET |
        | 适用场景 | 根据公告 ID 查询单条报告公告的摘要、标题、证券信息和处理状态 |
        
        > 同一处理逻辑的兼容入口还包括 `/api/v1/market/data/report-announcement/summary`。
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | announcement_id | string | 是 | 公告 ID | `AN202607140001` | 由「报告公告列表」接口返回 |
        
        ## 执行方式
        
        ```bash
        python <RUN_PY> report-announcement-summary --announcement_id AN202607140001
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        返回 `code/message/data`,未找到公告时 `code` 为 404、`data` 为 `null`。
        
        ```json
        {
          "code": 200,
          "message": "success",
          "data": {
            "announcement_id": "AN202607140001",
            "sec_code": "600000",
            "sec_name": "浦发银行",
            "announcement_title": "公告标题",
            "summary": "公告摘要",
            "status": "summarized",
            "announcement_time": "2026-07-14 09:30:00"
          }
        }
        ```
        
        ### data 字段
        
        | 字段 | 类型 | 说明 |
        |------|------|------|
        | announcement_id | string/null | 公告 ID |
        | sec_code / sec_name | string/null | 证券代码 / 证券名称 |
        | announcement_title | string/null | 公告标题 |
        | summary | string/null | 公告摘要 |
        | status | string/null | 摘要处理状态 |
        | announcement_time | string/null | 公告时间 |
        
        ## 注意事项
        
        - `announcement_id` 为必填,由「报告公告列表」接口返回。
        - 未找到公告时 `code` 为 404、`data` 为 `null`。
        - 两个路径(`/report-announcements/summary` 与 `/report-announcement/summary`)是同一处理逻辑的兼容入口。
        
    • risk-warning-stocks
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v1/market/data/risk-warning-stocks"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {
              "FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"],
              "Content-Type": "application/json",
          } if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              for key, value in _REQUEST_HEADERS.items():
                  request.add_unredirected_header(key, value)
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询风险警示股")
              parser.add_argument("--date", required=True, help="交易日,格式 YYYYMMDD")
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", dest="page_size", type=int, default=50)
              args = parser.parse_args()
          
              params = {"date": args.date, "page": args.page, "page_size": args.page_size}
              request = urllib.request.Request(
                  BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params),
                  headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw"},
                  method="GET",
              )
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 867 B
        ---
        name: risk-warning-stocks
        description: 按交易日查询风险警示股票列表。必填 --date,可选 --page、--page-size。接口:GET /api/v1/market/data/risk-warning-stocks。
        ---
        
        # 风险警示股
        
        按指定交易日查询风险警示股票列表。接口:GET `/api/v1/market/data/risk-warning-stocks`。
        
        ## 参数
        
        - `--date`:必填,交易日,格式 `YYYYMMDD`。
        - `--page`:可选,页码,默认 1。
        - `--page-size`:可选,每页条数,默认 50,最大 200。
        
        响应为统一 `code/message/data` JSON,分页记录位于 `data.records`。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送,并设置 `Content-Type: application/json`。缺失凭据时不会发起请求。
        
        ```bash
        python <RUN_PY> risk-warning-stocks --date 20260829 --page 1 --page-size 5
        ```
        
    • scripts
      • handler.py 2.9 KB
        #!/usr/bin/env python3
        import argparse
        import json
        import os
        import sys
        import urllib.error
        import urllib.parse
        import urllib.request
        
        BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
        ENDPOINT = '/api/v1/market/data/stock-list'
        SAFE_URLOPENER = urllib.request.build_opener()
        _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
        
        def _require_api_key():
            key = os.environ.get("FTSHARE_API_KEY")
            if not key:
                print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                raise SystemExit(2)
            return key
        
        def safe_urlopen(request, timeout=30):
            url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
            parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
            if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                raise SystemExit(1)
            if not isinstance(request, urllib.request.Request):
                request = urllib.request.Request(url, method="GET")
            request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
            request.add_unredirected_header("Content-Type", "application/json")
            return SAFE_URLOPENER.open(request, timeout=timeout)
        
        def main():
            key = _require_api_key()
            parser = argparse.ArgumentParser(description='股票列表')
            parser.add_argument("--page")
            parser.add_argument("--pageNum", required=True)
            parser.add_argument("--pageSize", required=True)
            parser.add_argument("--total", required=True)
            parser.add_argument("--stock-code", required=True)
            parser.add_argument("--stock-name", required=True)
            args = parser.parse_args()
            params = {}
            if args.page is not None: params["page"] = args.page
            if args.pageNum is not None: params["pageNum"] = args.pageNum
            if args.pageSize is not None: params["pageSize"] = args.pageSize
            if args.total is not None: params["total"] = args.total
            if args.stock_code is not None: params["stock_code"] = args.stock_code
            if args.stock_name is not None: params["stock_name"] = args.stock_name
            query = ("?" + urllib.parse.urlencode(params)) if params else ""
            request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
            try:
                with safe_urlopen(request) as response:
                    print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
            except urllib.error.HTTPError as error:
                print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                raise SystemExit(1)
            except urllib.error.URLError as error:
                print(f"请求失败: {error.reason}", file=sys.stderr)
                raise SystemExit(1)
        
        if __name__ == "__main__":
            main()
        
    • security-search
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/security/search/'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description='搜索股票标的')
              parser.add_argument("--query", required=True)
              parser.add_argument("--limit", type=int, default=1)
          
              args = parser.parse_args()
              params = {}
              if args.query is not None:
                  params['query'] = args.query
              if args.limit is not None:
                  params['limit'] = args.limit
          
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 572 B
        ---
        name: security-search
        description: 按名称、代码或拼音搜索标的。必填 --query,可选 --limit。接口:GET /api/v1/market/security/search/。
        ---
        
        # 标的搜索
        
        按名称、代码或拼音搜索标的。必填 --query,可选 --limit。接口:GET /api/v1/market/security/search/。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> security-search --query 贵州茅台 --limit 5
        ```
        
    • semantic-search-news
      • scripts
        • handler.py 4 KB
          #!/usr/bin/env python3
          """语义搜索新闻(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          from datetime import datetime, timedelta, timezone
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          BEIJING_TZ = timezone(timedelta(hours=8))
          
          
          def _normalize_to_beijing_iso(value: str) -> str:
              """
              将输入时间规范为东八区 ISO 8601 字符串。
              - 无时区:按东八区解释
              - 有时区:统一转换到东八区
              """
              raw = value.strip()
              if not raw:
                  return raw
              if raw.endswith("Z"):
                  raw = raw[:-1] + "+00:00"
              dt = datetime.fromisoformat(raw)
              if dt.tzinfo is None:
                  dt = dt.replace(tzinfo=BEIJING_TZ)
              else:
                  dt = dt.astimezone(BEIJING_TZ)
              return dt.isoformat()
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="语义搜索新闻")
              parser.add_argument(
                  "--query",
                  required=True,
                  help="搜索文字,如:人工智能",
              )
              parser.add_argument(
                  "--limit",
                  type=int,
                  default=10,
                  help="返回条数,默认 10",
              )
              parser.add_argument(
                  "--year",
                  type=int,
                  default=None,
                  help="年份,仅支持当年,用于限定搜索范围;不传则由服务端决定",
              )
              parser.add_argument(
                  "--start_time",
                  default=None,
                  help="开始时间(含),ISO 8601,如 2026-03-01T00:00:00+08:00",
              )
              parser.add_argument(
                  "--end_time",
                  default=None,
                  help="结束时间(含),ISO 8601,如 2026-03-15T23:59:59+08:00",
              )
              args = parser.parse_args()
          
              params = {"query": args.query, "limit": args.limit}
              if args.year is not None:
                  params["year"] = args.year
              try:
                  if args.start_time:
                      params["start_time"] = _normalize_to_beijing_iso(args.start_time)
                  if args.end_time:
                      params["end_time"] = _normalize_to_beijing_iso(args.end_time)
              except ValueError as e:
                  print(f"时间格式错误:{e}", file=sys.stderr)
                  sys.exit(1)
          
              url = BASE_URL + "/api/v3/market/data/semantic-search-news?" + urllib.parse.urlencode(params)
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
          
              try:
                  with safe_urlopen(req) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 4.1 KB
        ---
        name: semantic-search-news
        description: 语义搜索新闻(market.ft.tech)。用户问语义搜新闻、按关键词搜新闻、搜索相关新闻时使用。数据仅支持当年、最近半个月。
        ---
        
        # 语义搜索新闻
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 语义搜索新闻 |
        | 外部接口 | `GET /api/v3/market/data/semantic-search-news` |
        | 请求方式 | GET |
        | 适用场景 | 根据搜索文字进行语义搜索,返回相关新闻列表;数据仅支持查询**当年**、**最近半个月**内的新闻 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | query | string | 是 | 搜索文字 | 人工智能 | - |
        | limit | int | 否 | 返回条数 | 10 | 默认 10 |
        | year | int | 否 | 年份 | 2026 | 仅支持**当年**;数据仅保留最近半个月,用于限定搜索范围 |
        | start_time | string | 否 | 开始时间(含) | 2026-03-01T00:00:00+08:00 | ISO 8601 格式,用于限定搜索时间范围;无时区时按东八区解释 |
        | end_time | string | 否 | 结束时间(含) | 2026-03-15T23:59:59+08:00 | ISO 8601 格式,用于限定搜索时间范围;无时区时按东八区解释 |
        
        ## 3. 响应说明
        
        返回 `SearchResult` 数组,按相关度排序。
        
        ```json
        [
          {
            "news_id": 556106924001595392,
            "source_site": "21经济网_要闻",
            "article_url": "https://...",
            "publish_time": "2026-03-15T21:00:00",
            "fetch_time": "2026-03-15T21:19:49",
            "title": "越是AI,越需人文",
            "media_name": "21世纪经济",
            "summary": null,
            "content": "...",
            "is_truncated": 0,
            "is_reviewed": 1,
            "score": 0.6933832764625549
          }
        ]
        ```
        
        ### SearchResult 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | news_id | long | 否 | 新闻 ID |
        | source_site | string | 否 | 来源站点 |
        | article_url | string | 是 | 文章链接 |
        | publish_time | string | 是 | 发布时间,ISO 8601 格式 |
        | fetch_time | string | 否 | 抓取时间,ISO 8601 格式 |
        | title | string | 是 | 标题 |
        | media_name | string | 是 | 媒体名称 |
        | summary | string | 是 | 摘要 |
        | content | string | 否 | 正文内容 |
        | is_truncated | int | 否 | 是否被截断:0=否 1=是 |
        | is_reviewed | int | 否 | 是否已审核:0=否 1=是 |
        | score | float | 否 | 搜索匹配分数 |
        
        ## 4. 用法
        
        通过主目录 `run.py` 调用(必填 `--query`,可选 `--limit`、`--year`、`--start_time`、`--end_time`):
        
        ```bash
        python <RUN_PY> semantic-search-news --query 人工智能
        python <RUN_PY> semantic-search-news --query 人工智能 --limit 10 --year 2026
        python <RUN_PY> semantic-search-news --query 人工智能 --limit 10 --year 2026 --start_time 2026-03-01T00:00:00+08:00 --end_time 2026-03-15T23:59:59+08:00
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级的 `run.py` 绝对路径。脚本输出 JSON;本接口无需额外请求头。
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/semantic-search-news?query=人工智能&limit=10&year=2026&start_time=2026-03-01T00:00:00%2B08:00&end_time=2026-03-15T23:59:59%2B08:00
        ```
        
        ## 6. 数据范围与更新
        
        - 数据仅支持**当年**、**最近半个月**内的新闻;`year` 用于限定搜索范围,仅支持当年。
        - `start_time`、`end_time` 用于进一步限定检索时间窗口,推荐与 `year` 搭配使用;时间格式需为 ISO 8601。
        - 时区处理与 K 线脚本一致:无时区输入默认按东八区(UTC+8)解释;带时区输入会统一转换为东八区再请求。
        - 具体可查时间范围与更新频率以接口返回为准。
        
        ## 7. 展示与用户提示
        
        向用户展示结果时**必须**:
        
        1. **每条新闻展示原站与链接**:列出**来源**(`source_site`:来源站点)以及**文章链接**(`article_url`),便于用户跳转原文。
        2. **数据范围提示**:在结果前或结果后向用户明确提示:**「以下结果仅展示当年、最近半个月以内的新闻。」**
        3. **固定提示词(建议复用)**:**「仅支持查看最近半个月以内的新闻数据。」**
        
    • shareholder-meeting
      • scripts
        • handler.py 2.7 KB
          #!/usr/bin/env python3
          """查询 A 股股东大会信息(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询 A 股股东大会信息")
              parser.add_argument("--page", type=int, default=None, help="页码,从 1 开始,默认 1")
              parser.add_argument("--page_size", type=int, default=None, help="每页数量,默认 50,最大 200")
              args = parser.parse_args()
          
              params = {}
              if args.page is not None:
                  params["page"] = args.page
              if args.page_size is not None:
                  params["page_size"] = args.page_size
          
              path = "/api/v1/market/data/corporate/meeting"
              if params:
                  path += "?" + urllib.parse.urlencode(params)
              url = BASE_URL + path
          
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
              req.add_header("Content-Type", "application/json")
          
              try:
                  with safe_urlopen(req) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.7 KB
        ---
        name: shareholder-meeting
        description: 查询A股上市公司股东大会信息(market.ft.tech)。用户问股东大会、股东会议、股权登记日、投票日期、会议提案时使用。
        ---
        
        # 股东大会 - 查询股东大会信息
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询股东大会 |
        | 外部接口 | `GET /api/v1/market/data/corporate/meeting` |
        | 请求方式 | GET |
        | 适用场景 | 查询 A 股上市公司股东大会召开信息,包括会议日期、股权登记日、提案等,支持分页 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | page | int | 否 | 页码,从 1 开始 | `1` | 默认 1 |
        | page_size | int | 否 | 每页数量 | `50` | 默认 50,最大 200 |
        
        ## 3. 响应说明
        
        返回 `PaginatedApiResponse` 包装的 JSON 对象。
        
        ```json
        {
          "code": 0,
          "message": "success",
          "data": {
            "pageNum": 1,
            "pageSize": 50,
            "total": 189,
            "pages": 4,
            "records": [
              {
                "security_code": "920011",
                "security_name_abbr": "晨光电机",
                "meeting_title": "2026年第4次临时股东大会",
                "start_adjust_date": "2026-06-17",
                "equity_record_date": "2026-06-12",
                "onsite_record_date": null,
                "web_start_date": "2026-06-16",
                "web_end_date": "2026-06-17",
                "decision_notice_date": null,
                "notice_date": "2026-06-01",
                "serial_num": "238348",
                "proposal": "1、《关于公司董事2026年度薪酬方案的议案》"
              }
            ]
          }
        }
        ```
        
        ### data.records 元素结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | security_code | string | 是 | 股票代码 |
        | security_name_abbr | string | 是 | 股票简称 |
        | meeting_title | string | 是 | 会议标题 |
        | start_adjust_date | string | 是 | 开始调整日期(会议召开日期) |
        | equity_record_date | string | 是 | 股权登记日 |
        | onsite_record_date | string | 是 | 现场登记日 |
        | web_start_date | string | 是 | 网络投票开始日期 |
        | web_end_date | string | 是 | 网络投票结束日期 |
        | decision_notice_date | string | 是 | 决议公告日期 |
        | notice_date | string | 是 | 公告日期 |
        | serial_num | string | 是 | 序号 |
        | proposal | string | 是 | 提案内容 |
        
        ## 4. 用法
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> shareholder-meeting
        python <RUN_PY> shareholder-meeting --page 1 --page_size 50
        ```
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/corporate/meeting?page=1&page_size=50
        ```
        
        ## 6. 注意事项
        
        - 数据按 `notice_date` 降序排列。
        - 日期字段以 YYYY-MM-DD 格式返回,无数据时为 null。
        
    • shibor-daily
      • scripts
        • handler.py 3.5 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/shibor-daily'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='SHIBOR日度利率')
              parser.add_argument("--start_date", required=True)
              parser.add_argument("--end_date", required=True)
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--trade_date", required=True)
              parser.add_argument("--on_rate", required=False)
              parser.add_argument("--w1", required=True)
              parser.add_argument("--w2", required=True)
              parser.add_argument("--m1", required=True)
              parser.add_argument("--m3", required=True)
              parser.add_argument("--m6", required=True)
              parser.add_argument("--m9", required=True)
              parser.add_argument("--y1", required=True)
              args = parser.parse_args()
              params = {}
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.on_rate is not None: params["on_rate"] = args.on_rate
              if args.w1 is not None: params["w1"] = args.w1
              if args.w2 is not None: params["w2"] = args.w2
              if args.m1 is not None: params["m1"] = args.m1
              if args.m3 is not None: params["m3"] = args.m3
              if args.m6 is not None: params["m6"] = args.m6
              if args.m9 is not None: params["m9"] = args.m9
              if args.y1 is not None: params["y1"] = args.y1
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 693 B
        ---
        name: shibor-daily
        description: 查询SHIBOR日度利率。接口:GET /api/v1/market/data/shibor-daily。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # SHIBOR日度利率
        
        接口:GET `/api/v1/market/data/shibor-daily`。参数和响应以 `ftshare-doc/api-doc/宏观经济/国内宏观/SHIBOR日度利率.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> shibor-daily --start_date 20260828 --end_date 20260828 --page 1 --trade_date 20260828 --w1 1 --w2 1 --m1 1 --m3 1 --m6 1 --m9 1 --y1 1
        ```
        
    • shibor-quote-daily
      • scripts
        • handler.py 3.2 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/shibor-quote-daily'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='SHIBOR报价日度')
              parser.add_argument("--start_date", required=True)
              parser.add_argument("--end_date", required=True)
              parser.add_argument("--bank")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--trade_date", required=True)
              parser.add_argument("--bank_en", required=True)
              parser.add_argument("--on_rate", required=False)
              parser.add_argument("--y1", required=True)
              args = parser.parse_args()
              params = {}
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.bank is not None: params["bank"] = args.bank
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.bank is not None: params["bank"] = args.bank
              if args.bank_en is not None: params["bank_en"] = args.bank_en
              if args.on_rate is not None: params["on_rate"] = args.on_rate
              if args.y1 is not None: params["y1"] = args.y1
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 687 B
        ---
        name: shibor-quote-daily
        description: 查询SHIBOR报价日度。接口:GET /api/v1/market/data/shibor-quote-daily。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # SHIBOR报价日度
        
        接口:GET `/api/v1/market/data/shibor-quote-daily`。参数和响应以 `ftshare-doc/api-doc/宏观经济/国内宏观/SHIBOR报价日度.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> shibor-quote-daily --start_date 20260828 --end_date 20260828 --page 1 --trade_date 20260828 --bank_en 1 --y1 1
        ```
        
    • southbound
      • scripts
        • handler.py 2.3 KB
          #!/usr/bin/env python3
          """查询南向资金(港股通沪、港股通深)交易汇总数据"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/southbound"
          
          
          def fetch(
          date: str) -> dict:
              params = urllib.parse.urlencode({"date": date})
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询南向资金交易数据")
              parser.add_argument("--date", required=True, help="交易日期,格式 YYYYMMDD")
              args = parser.parse_args()
          
              result = fetch(args.date)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3.2 KB
        ---
        name: southbound
        description: "查询南向资金交易数据。当用户需要查询指定交易日南向资金(港股通沪、港股通深)交易汇总数据,或了解南向资金交易数据时使用。"
        ---
        
        # 查询南向资金交易数据
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 查询南向资金交易数据 |
        | 外部接口 | `/api/v1/market/data/southbound` |
        | 请求方式 | GET |
        | 适用场景 | 查询指定交易日南向资金(港股通沪、港股通深)交易汇总数据 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |---|---|---|---|---|---|
        | `date` | string | 是 | 交易日期 | `20250101` | 格式 `YYYYMMDD` |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> southbound --date 20250101
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        ```json
        {
            "code": 0,
            "message": "success",
            "data": {
                "date": "20250101",
                "currency": "HKD",
                "total": {
                    "buy_amount": "100.00",
                    "sell_amount": "80.00",
                    "net_buy_amount": "20.00",
                    "trade_count": 15
                },
                "channels": {
                    "SH_HK": {
                        "buy_amount": "60.00",
                        "sell_amount": "50.00",
                        "net_buy_amount": "10.00",
                        "trade_count": 8
                    },
                    "SZ_HK": {
                        "buy_amount": "40.00",
                        "sell_amount": "30.00",
                        "net_buy_amount": "10.00",
                        "trade_count": 7
                    }
                }
            }
        }
        ```
        
        ### 顶层字段说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |---|---|---|---|
        | `code` | int | 否 | 业务状态码,0 表示成功 |
        | `message` | string | 否 | 状态说明 |
        | `data` | object | 否 | 南向资金数据 |
        
        ### data 字段说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |---|---|---|---|
        | `date` | string | 否 | 交易日期 |
        | `currency` | string | 否 | 币种(HKD) |
        | `total` | object | 否 | 南向合计数据 |
        | `total.buy_amount` | string | 否 | 买入额 |
        | `total.sell_amount` | string | 否 | 卖出额 |
        | `total.net_buy_amount` | string | 否 | 净买入额 |
        | `total.trade_count` | int | 否 | 成交笔数 |
        | `channels` | object | 否 | 分市场通道数据 |
        
        ### channels 字段说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |---|---|---|---|
        | `SH_HK` | object | 否 | 港股通(沪)数据 |
        | `SH_HK.buy_amount` | string | 否 | 买入额 |
        | `SH_HK.sell_amount` | string | 否 | 卖出额 |
        | `SH_HK.net_buy_amount` | string | 否 | 净买入额 |
        | `SH_HK.trade_count` | int | 否 | 成交笔数 |
        | `SZ_HK` | object | 否 | 港股通(深)数据 |
        | `SZ_HK.buy_amount` | string | 否 | 买入额 |
        | `SZ_HK.sell_amount` | string | 否 | 卖出额 |
        | `SZ_HK.net_buy_amount` | string | 否 | 净买入额 |
        | `SZ_HK.trade_count` | int | 否 | 成交笔数 |
        
        ## 注意事项
        
        - `date` 为必填参数,格式 `YYYYMMDD`
        - 金额类字段以字符串格式返回
        - 响应为信封结构(`code` / `message` / `data`)
        - 南向资金币种为 HKD(港币)
        
    • stk-ah-comparison
      • scripts
        • handler.py 3.2 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/hk/stk-ah-comparison"
          SAFE_URLOPENER = urllib.request.build_opener()
          
          
          def require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def fetch(params):
              url = f"{BASE_URL}{ENDPOINT}?{urllib.parse.urlencode(params)}"
              request = urllib.request.Request(
                  url,
                  headers={"FTSHARE_API_KEY": require_api_key(), "Content-Type": "application/json"},
                  method="GET",
              )
              parsed = urllib.parse.urlparse(url)
              base = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              try:
                  with SAFE_URLOPENER.open(request, timeout=30) as response:
                      return json.loads(response.read().decode())
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询 AH 股比价")
              parser.add_argument("--hk-code", dest="hk_code")
              parser.add_argument("--ts-code", dest="ts_code")
              parser.add_argument("--trade-date", dest="trade_date", type=int)
              parser.add_argument("--start-date", dest="start_date", type=int)
              parser.add_argument("--end-date", dest="end_date", type=int)
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", dest="page_size", type=int, default=50)
              parser.add_argument("--all", action="store_true", dest="fetch_all")
              args = parser.parse_args()
              if args.page < 1 or args.page_size < 1 or args.page_size > 1000:
                  parser.error("page 须大于等于 1,page-size 须在 1~1000 之间")
              params = {k: v for k, v in vars(args).items() if k in {
                  "hk_code", "ts_code", "trade_date", "start_date", "end_date"
              } and v is not None}
              if args.fetch_all:
                  first = fetch({**params, "page": 1, "page_size": args.page_size})
                  data = first.get("data") or {}
                  records = list(data.get("records", []))
                  for page in range(2, int(data.get("pages", 1)) + 1):
                      payload = fetch({**params, "page": page, "page_size": args.page_size})
                      records.extend((payload.get("data") or {}).get("records", []))
                  first["data"] = {**data, "records": records}
                  result = first
              else:
                  result = fetch({**params, "page": args.page, "page_size": args.page_size})
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 751 B
        ---
        name: stk-ah-comparison
        description: 查询 A/H 股比价、涨跌幅和溢价率。用户询问港股与对应 A 股估值比较时使用。
        ---
        
        # AH 股对比
        
        接口:`GET /api/v1/market/data/hk/stk-ah-comparison`。可按 `--hk-code`、`--ts-code`、`--trade-date` 或日期区间 `--start-date`/`--end-date` 过滤;日期区间最多 30 个自然日。分页参数为 `--page`(默认 1)和 `--page-size`(默认 50,最大 1000),支持 `--all` 自动翻页。
        
        ```bash
        python <RUN_PY> stk-ah-comparison --hk-code 00700.HK --trade-date 20260623 --page 1 --page-size 20
        ```
        
        响应为 `code/message/data` 分页信封,记录位于 `data.records`,包含双方代码、名称、收盘价、涨跌幅、A/H 比价和溢价率。
        
    • stk-alert-broker
      • scripts
        • handler.py 3.2 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/stk-alert-broker'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='交易所重点提示证券')
              parser.add_argument("--ts_code")
              parser.add_argument("--start_date")
              parser.add_argument("--end_date")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--total", required=False)
              parser.add_argument("--name", required=True)
              parser.add_argument("--type", required=True)
              args = parser.parse_args()
              params = {}
              if args.ts_code is not None: params["ts_code"] = args.ts_code
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.ts_code is not None: params["ts_code"] = args.ts_code
              if args.name is not None: params["name"] = args.name
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.type is not None: params["type"] = args.type
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 651 B
        ---
        name: stk-alert-broker
        description: 查询交易所重点提示证券。接口:GET /api/v2/market/data/stk-alert-broker。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 交易所重点提示证券
        
        接口:GET `/api/v2/market/data/stk-alert-broker`。参数和响应以 `ftshare-doc/api-doc/股票数据/打板专题数据/交易所重点提示证券.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stk-alert-broker --page 1 --name 1 --type stock
        ```
        
    • stk-manager-hold
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          """查询上市公司管理层持股变动明细(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          ENDPOINT = "/api/v1/market/data/stk-manager-hold"
          
          
          def build_params(trade_code, end_date):
              params = {}
              if trade_code:
                  params["trade_code"] = trade_code
              if end_date:
                  params["end_date"] = end_date
              return params
          
          
          def fetch(
          trade_code, end_date):
              params = urllib.parse.urlencode(build_params(trade_code, end_date))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询上市公司管理层持股变动明细")
              parser.add_argument(
                  "--trade_code",
                  required=True,
                  help="股票代码(带 .SZ/.SH 后缀),支持逗号分隔多个,如 600848.SH",
              )
              parser.add_argument("--end_date", help="截止日期,精确匹配,YYYYMMDD 格式")
              args = parser.parse_args()
          
              result = fetch(args.trade_code, args.end_date)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.9 KB
          #!/usr/bin/env python3
          """Tests for stk-manager-hold handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_by_trade_code(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("600848.SH", None)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/stk-manager-hold", url)
                  self.assertIn("trade_code=600848.SH", url)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_with_end_date(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("600848.SH", "20241231")
                  url = mock_open.call_args[0][0]
                  self.assertIn("end_date=20241231", url)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  with patch.object(sys, "argv", ["handler.py", "--trade_code", "600848.SH"]):
                      with patch("sys.stdout", new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
              def test_main_requires_trade_code(self):
                  with patch.object(sys, "argv", ["handler.py"]):
                      with self.assertRaises(SystemExit):
                          handler.main()
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 3.1 KB
        ---
        name: stk-manager-hold
        description: "查询上市公司管理层人员持股变动明细。用户问管理层持股、高管持股、董监高持股、持股变动、变动原因时使用。参数 trade_code 必填且需带 .SZ/.SH 后缀,支持逗号分隔多个代码。"
        ---
        
        # 查询上市公司管理层持股变动明细
        
        ## 接口说明
        
        | 项目     | 说明                                                |
        |----------|-----------------------------------------------------|
        | 接口名称 | 查询上市公司管理层人员持股变动明细                    |
        | 外部接口 | `/api/v1/market/data/stk-manager-hold`              |
        | 请求方式 | GET                                                 |
        | 适用场景 | 查询上市公司管理层人员的持股变动明细,含持股数、变动原因 |
        
        ## 请求参数
        
        | 参数名      | 类型   | 是否必填 | 描述                          | 取值示例     | 备注                                                       |
        |-------------|--------|----------|-------------------------------|--------------|------------------------------------------------------------|
        | trade_code  | string | 是       | 股票代码                      | 600848.SH    | 带 .SZ/.SH 后缀,支持逗号分隔多个,如 `600848.SH,000001.SZ` |
        | end_date    | string | 否       | 截止日期,精确匹配            | 20241231     | `YYYYMMDD` 格式;不传则返回该股票全部历史记录               |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> stk-manager-hold --trade_code 600848.SH
        python <RUN_PY> stk-manager-hold --trade_code 600848.SH --end_date 20241231
        python <RUN_PY> stk-manager-hold --trade_code 600848.SH,000001.SZ
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        ```json
        [
            {
                "trade_code": "000001.SZ",
                "end_date": "20251231",
                "name": "杨志群",
                "shares_num": "50000",
                "change_reason": ""
            }
        ]
        ```
        
        ### 字段说明
        
        | 字段名          | 类型   | 是否可为空 | 说明                                                |
        |-----------------|--------|------------|-----------------------------------------------------|
        | trade_code      | string | 否         | 股票代码(带 .SZ/.SH 后缀)                          |
        | end_date        | string | 否         | 截止日期(`YYYYMMDD`)                              |
        | name            | string | 否         | 管理人员姓名                                        |
        | shares_num      | string | 否         | 持股数(股),高精度数值,序列化为字符串             |
        | change_reason   | string | 否         | 变动原因说明                                        |
        
        ## 注意事项
        
        - `trade_code` 必填,为空时接口返回 400。
        - 不分页,单只股票一次性返回全部持股记录;多 `trade_code`(逗号分隔)时合并返回。
        - 排序:`trade_code` 升序、`end_date` 降序、`name` 升序。
        - `shares_num` 为高精度数值,JSON 序列化为字符串以避免精度丢失。
        - 日期参数格式为 `YYYYMMDD`,非时间戳。
        
    • stk-manager-pay
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          """查询上市公司管理层年度薪酬(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          ENDPOINT = "/api/v1/market/data/stk-manager-pay"
          
          
          def build_params(trade_code, end_date):
              params = {}
              if trade_code:
                  params["trade_code"] = trade_code
              if end_date:
                  params["end_date"] = end_date
              return params
          
          
          def fetch(
          trade_code, end_date):
              params = urllib.parse.urlencode(build_params(trade_code, end_date))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询上市公司管理层年度薪酬")
              parser.add_argument(
                  "--trade_code",
                  required=True,
                  help="股票代码(带 .SZ/.SH 后缀),支持逗号分隔多个,如 600848.SH",
              )
              parser.add_argument("--end_date", help="截止日期,精确匹配,YYYYMMDD 格式")
              args = parser.parse_args()
          
              result = fetch(args.trade_code, args.end_date)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.9 KB
          #!/usr/bin/env python3
          """Tests for stk-manager-pay handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_by_trade_code(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("600848.SH", None)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/stk-manager-pay", url)
                  self.assertIn("trade_code=600848.SH", url)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_with_end_date(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("600848.SH", "20241231")
                  url = mock_open.call_args[0][0]
                  self.assertIn("end_date=20241231", url)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  with patch.object(sys, "argv", ["handler.py", "--trade_code", "600848.SH"]):
                      with patch("sys.stdout", new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
              def test_main_requires_trade_code(self):
                  with patch.object(sys, "argv", ["handler.py"]):
                      with self.assertRaises(SystemExit):
                          handler.main()
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 3 KB
        ---
        name: stk-manager-pay
        description: "查询上市公司管理层人员年度薪酬。用户问高管薪酬、董事薪酬、董监高薪酬、管理层年薪、变动原因时使用。参数 trade_code 必填且需带 .SZ/.SH 后缀,支持逗号分隔多个代码。"
        ---
        
        # 查询上市公司管理层年度薪酬
        
        ## 接口说明
        
        | 项目     | 说明                                                |
        |----------|-----------------------------------------------------|
        | 接口名称 | 查询上市公司管理层人员年度薪酬                        |
        | 外部接口 | `/api/v1/market/data/stk-manager-pay`              |
        | 请求方式 | GET                                                 |
        | 适用场景 | 查询上市公司管理层人员的年度薪酬,含截止日期、币种等 |
        
        ## 请求参数
        
        | 参数名      | 类型   | 是否必填 | 描述                          | 取值示例     | 备注                                                       |
        |-------------|--------|----------|-------------------------------|--------------|------------------------------------------------------------|
        | trade_code  | string | 是       | 股票代码                      | 600848.SH    | 带 .SZ/.SH 后缀,支持逗号分隔多个,如 `600848.SH,000001.SZ` |
        | end_date    | string | 否       | 截止日期,精确匹配            | 20241231     | `YYYYMMDD` 格式;不传则返回该股票全部历史记录               |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> stk-manager-pay --trade_code 600848.SH
        python <RUN_PY> stk-manager-pay --trade_code 600848.SH --end_date 20241231
        python <RUN_PY> stk-manager-pay --trade_code 600848.SH,000001.SZ
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        ```json
        [
            {
                "trade_code": "000001.SZ",
                "end_date": "20251231",
                "name": "冀光恒",
                "pay": "4000100",
                "currency": "人民币"
            }
        ]
        ```
        
        ### 字段说明
        
        | 字段名      | 类型   | 是否可为空 | 说明                                                |
        |-------------|--------|------------|-----------------------------------------------------|
        | trade_code  | string | 否         | 股票代码(带 .SZ/.SH 后缀)                          |
        | end_date    | string | 否         | 截止日期(`YYYYMMDD`)                              |
        | name        | string | 否         | 管理人员姓名                                        |
        | pay         | string | 否         | 年度薪酬,高精度数值,序列化为字符串                 |
        | currency    | string | 否         | 货币币种                                            |
        
        ## 注意事项
        
        - `trade_code` 必填,为空时接口返回 400。
        - 不分页,单只股票一次性返回全部薪酬记录;多 `trade_code`(逗号分隔)时合并返回。
        - 排序:`trade_code` 升序、`end_date` 降序、`name` 升序。
        - `pay` 为高精度数值,JSON 序列化为字符串以避免精度丢失。
        - 日期参数格式为 `YYYYMMDD`,非时间戳。
        
    • stk-managers
      • scripts
        • handler.py 3.1 KB
          #!/usr/bin/env python3
          """查询上市公司管理层信息(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          ENDPOINT = "/api/v1/market/data/stk-managers"
          
          
          def build_params(trade_code, candi_date, begin_date, end_date):
              params = {}
              if trade_code:
                  params["trade_code"] = trade_code
              if candi_date:
                  params["candi_date"] = candi_date
              if begin_date:
                  params["begin_date"] = begin_date
              if end_date:
                  params["end_date"] = end_date
              return params
          
          
          def fetch(
          trade_code, candi_date, begin_date, end_date):
              params = urllib.parse.urlencode(build_params(trade_code, candi_date, begin_date, end_date))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询上市公司管理层信息")
              parser.add_argument(
                  "--trade_code",
                  required=True,
                  help="股票代码(带 .SZ/.SH 后缀),支持逗号分隔多个,如 600848.SH",
              )
              parser.add_argument("--candi_date", help="候选日期,精确匹配,YYYYMMDD 格式")
              parser.add_argument("--begin_date", help="任职起始日过滤,YYYYMMDD 格式")
              parser.add_argument("--end_date", help="任职截止日过滤,YYYYMMDD 格式")
              args = parser.parse_args()
          
              result = fetch(args.trade_code, args.candi_date, args.begin_date, args.end_date)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2.3 KB
          #!/usr/bin/env python3
          """Tests for stk-managers handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_by_trade_code(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("600848.SH", None, None, None)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/stk-managers", url)
                  self.assertIn("trade_code=600848.SH", url)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_with_candi_date(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("600848.SH", "20251128", None, None)
                  url = mock_open.call_args[0][0]
                  self.assertIn("candi_date=20251128", url)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_with_begin_end(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("600848.SH", None, "20200101", "20241231")
                  url = mock_open.call_args[0][0]
                  self.assertIn("begin_date=20200101", url)
                  self.assertIn("end_date=20241231", url)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  with patch.object(sys, "argv", ["handler.py", "--trade_code", "600848.SH"]):
                      with patch("sys.stdout", new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
              def test_main_requires_trade_code(self):
                  with patch.object(sys, "argv", ["handler.py"]):
                      with self.assertRaises(SystemExit):
                          handler.main()
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 4.3 KB
        ---
        name: stk-managers
        description: "查询上市公司管理层人员信息。用户问上市公司管理层、高管、董事、独立董事、董事长、总经理、任职区间、候选日期、董监高、平安银行管理层、600848 高管时使用。参数 trade_code 必填且需带 .SZ/.SH 后缀,支持逗号分隔多个代码。"
        ---
        
        # 查询上市公司管理层信息
        
        ## 接口说明
        
        | 项目     | 说明                                                    |
        |----------|---------------------------------------------------------|
        | 接口名称 | 查询上市公司管理层人员信息                                |
        | 外部接口 | `/api/v1/market/data/stk-managers`                      |
        | 请求方式 | GET                                                     |
        | 适用场景 | 查询上市公司管理层人员,含姓名、岗位类别、职位、任职区间等 |
        
        ## 请求参数
        
        | 参数名      | 类型   | 是否必填 | 描述                                                | 取值示例     | 备注                                                                  |
        |-------------|--------|----------|-----------------------------------------------------|--------------|-----------------------------------------------------------------------|
        | trade_code  | string | 是       | 股票代码                                            | 600848.SH    | 带 .SZ/.SH 后缀,支持逗号分隔多个,如 `600848.SH,000001.SZ`            |
        | candi_date  | string | 否       | 候选日期,精确匹配                                  | 20251128     | `YYYYMMDD` 格式                                                       |
        | begin_date  | string | 否       | 任职起始日过滤                                      | 20200101     | `YYYYMMDD` 格式                                                       |
        | end_date    | string | 否       | 任职截止日过滤                                      | 20241231     | `YYYYMMDD` 格式;与 `begin_date` 同时提供时须 `begin_date` ≤ `end_date` |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> stk-managers --trade_code 600848.SH
        python <RUN_PY> stk-managers --trade_code 600848.SH --begin_date 20200101 --end_date 20241231
        python <RUN_PY> stk-managers --trade_code 600848.SH,000001.SZ
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        ```json
        [
            {
                "trade_code": "000001.SZ",
                "security_name": "平安银行",
                "name": "杨运杰",
                "s_type": "董事",
                "position": "独立董事",
                "birth": "19660101",
                "candi_date": "20251128",
                "begin_date": "20260326",
                "end_date": "20281215",
                "is_job": "在任",
                "change_reason": ""
            }
        ]
        ```
        
        ### 字段说明
        
        | 字段名          | 类型   | 是否可为空 | 说明                                            |
        |-----------------|--------|------------|-------------------------------------------------|
        | trade_code      | string | 否         | 股票代码(带 .SZ/.SH 后缀)                      |
        | security_name   | string | 否         | 股票名称                                        |
        | name            | string | 否         | 管理人员姓名                                    |
        | s_type          | string | 否         | 岗位类别(如「高管」「董事」等)                 |
        | position        | string | 否         | 职位                                            |
        | birth           | string | 否         | 出生年月(`YYYYMMDD`)                          |
        | candi_date      | string | 否         | 候选日期(`YYYYMMDD`)                          |
        | begin_date      | string | 否         | 任职起始日(`YYYYMMDD`;空字符串表示无确切起始日)|
        | end_date        | string | 否         | 任职截止日(`YYYYMMDD`;空字符串表示在职)       |
        | is_job          | string | 否         | 是否在职                                        |
        | change_reason   | string | 否         | 变动原因                                        |
        
        ## 注意事项
        
        - `trade_code` 必填,为空时接口返回 400。
        - 不分页,单只股票一次性返回全部管理层记录;多 `trade_code`(逗号分隔)时合并返回。
        - 日期参数格式为 `YYYYMMDD`,非时间戳。
        
    • stk-premarket
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/stk-premarket'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='盘前数据')
              parser.add_argument("--ts_code")
              parser.add_argument("--trade_date")
              parser.add_argument("--start_date")
              parser.add_argument("--end_date")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--pre_close")
              parser.add_argument("--up_limit")
              parser.add_argument("--down_limit")
              parser.add_argument("--price")
              parser.add_argument("--total_mv")
              parser.add_argument("--float_mv")
              args = parser.parse_args()
              params = {}
              for name in ("ts_code", "trade_date", "start_date", "end_date", "page", "page_size"):
                  value = getattr(args, name)
                  if value is not None:
                      params[name] = value
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 566 B
        ---
        name: stk-premarket
        description: 查询盘前数据。接口:GET /api/v2/market/data/stk-premarket。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 盘前数据
        
        接口:GET `/api/v2/market/data/stk-premarket`。参数和响应以 `ftshare-doc/api-doc/股票数据/特色数据/盘前数据.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stk-premarket --page 1
        ```
        
    • stk-shock
      • scripts
        • handler.py 3.9 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/stk-shock'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='个股异常波动')
              parser.add_argument("--ts_code")
              parser.add_argument("--trade_date")
              parser.add_argument("--start_date")
              parser.add_argument("--end_date")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--total", required=False)
              parser.add_argument("--order_book_id", required=True)
              parser.add_argument("--date", required=True)
              parser.add_argument("--side", required=False)
              parser.add_argument("--rank", required=True)
              parser.add_argument("--agency", required=False)
              parser.add_argument("--buy_value", required=False)
              parser.add_argument("--sell_value", required=False)
              parser.add_argument("--type", required=True)
              parser.add_argument("--reason", required=False)
              args = parser.parse_args()
              params = {}
              if args.ts_code is not None: params["ts_code"] = args.ts_code
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.order_book_id is not None: params["order_book_id"] = args.order_book_id
              if args.date is not None: params["date"] = args.date
              if args.side is not None: params["side"] = args.side
              if args.rank is not None: params["rank"] = args.rank
              if args.agency is not None: params["agency"] = args.agency
              if args.buy_value is not None: params["buy_value"] = args.buy_value
              if args.sell_value is not None: params["sell_value"] = args.sell_value
              if args.type is not None: params["type"] = args.type
              if args.reason is not None: params["reason"] = args.reason
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 630 B
        ---
        name: stk-shock
        description: 查询个股异常波动。接口:GET /api/v2/market/data/stk-shock。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 个股异常波动
        
        接口:GET `/api/v2/market/data/stk-shock`。参数和响应以 `ftshare-doc/api-doc/股票数据/打板专题数据/个股异常波动.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stk-shock --page 1 --order_book_id 1 --date 20260828 --rank 1 --type stock
        ```
        
    • stk-surv
      • scripts
        • handler.py 3.5 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/stk-surv'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='个股严重异常波动')
              parser.add_argument("--ts_code")
              parser.add_argument("--trade_date")
              parser.add_argument("--start_date")
              parser.add_argument("--end_date")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--total", required=False)
              parser.add_argument("--name", required=True)
              parser.add_argument("--trade_market", required=True)
              parser.add_argument("--reason", required=False)
              parser.add_argument("--period", required=False)
              args = parser.parse_args()
              params = {}
              if args.ts_code is not None: params["ts_code"] = args.ts_code
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.ts_code is not None: params["ts_code"] = args.ts_code
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.name is not None: params["name"] = args.name
              if args.trade_market is not None: params["trade_market"] = args.trade_market
              if args.reason is not None: params["reason"] = args.reason
              if args.period is not None: params["period"] = args.period
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 614 B
        ---
        name: stk-surv
        description: 查询个股严重异常波动。接口:GET /api/v2/market/data/stk-surv。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 个股严重异常波动
        
        接口:GET `/api/v2/market/data/stk-surv`。参数和响应以 `ftshare-doc/api-doc/股票数据/打板专题数据/个股严重异常波动.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stk-surv --page 1 --name 1 --trade_market A
        ```
        
    • stock-adjust-factor
      • scripts
        • handler.py 2.9 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/stock-adjust-factor'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description='查询股票复权因子')
              parser.add_argument("--symbol")
              parser.add_argument("--trade-date")
              parser.add_argument("--start-date")
              parser.add_argument("--end-date")
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", dest="page_size", type=int, default=50)
          
              args = parser.parse_args()
              params = {}
              if args.symbol is not None:
                  params['symbol'] = args.symbol
              if args.trade_date is not None:
                  params['trade_date'] = args.trade_date
              if args.start_date is not None:
                  params['start_date'] = args.start_date
              if args.end_date is not None:
                  params['end_date'] = args.end_date
              if args.page is not None:
                  params['page'] = args.page
              if args.page_size is not None:
                  params['page_size'] = args.page_size
          
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 641 B
        ---
        name: stock-adjust-factor
        description: 查询股票复权因子。支持 --symbol、--trade-date、--start-date、--end-date、--page、--page-size。接口:GET /api/v1/market/data/stock-adjust-factor。
        ---
        
        # 股票复权因子
        
        查询股票复权因子。支持 --symbol、--trade-date、--start-date、--end-date、--page、--page-size。接口:GET /api/v1/market/data/stock-adjust-factor。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> stock-adjust-factor
        ```
        
    • stock-announcements
      • scripts
        • handler.py 2.9 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v2/market/data/announcements/stock-announcements"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询 A 股公告列表")
              parser.add_argument("--stock-code", dest="stock_code")
              parser.add_argument("--start-date", dest="start_date")
              parser.add_argument("--end-date", dest="end_date")
              parser.add_argument("--type", default="stock")
              parser.add_argument("--page", type=int, required=True)
              parser.add_argument("--page-size", dest="page_size", type=int, required=True)
              args = parser.parse_args()
              if not args.stock_code and not args.start_date:
                  parser.error("stock-code 与 start-date 至少提供一个")
              if args.type != "stock":
                  parser.error("type 当前只支持 stock")
              params = {"type": args.type, "page": args.page, "page_size": args.page_size}
              for name in ("stock_code", "start_date", "end_date"):
                  value = getattr(args, name)
                  if value is not None:
                      params[name] = value
              request = urllib.request.Request(BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params),
                                               headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 614 B
        ---
        name: stock-announcements
        description: 查询 A 股公告列表。按 stock-code 或日期范围查询,page 和 page-size 必填。
        ---
        
        # 公告列表
        
        外部接口:`GET /api/v2/market/data/announcements/stock-announcements`。
        
        必须提供 `--stock-code` 或 `--start-date`,`--type` 当前固定为 `stock`,`--page` 与 `--page-size` 必填。按日期范围查询时日期跨度最多 3 天。所有请求必须设置环境变量 `FTSHARE_API_KEY`;缺失凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-announcements --start-date 20260908 --page 1 --page-size 5
        ```
        
    • stock-balance-all-stocks-specific-period
      • scripts
        • handler.py 2.9 KB
          #!/usr/bin/env python3
          """查询指定报告期全市场资产负债表(分页)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          VALID_REPORT_TYPES = ["q1", "q2", "q3", "annual"]
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询指定报告期全市场资产负债表(分页)")
              parser.add_argument("--year", type=int, required=True, help="报告所属年度,如 2025")
              parser.add_argument("--report-type", required=True, choices=VALID_REPORT_TYPES,
                                  help="报告期类型:q1(一季报)/ q2(半年报)/ q3(三季报)/ annual(年报)")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始(默认 1)")
              parser.add_argument("--page-size", type=int, default=20, help="每页记录数(默认 20)")
              args = parser.parse_args()
          
              params = {
                  "year": args.year,
                  "report_type": args.report_type,
                  "page": args.page,
                  "page_size": args.page_size,
              }
              url = f"{BASE_URL}/api/v1/market/data/finance/balance?" + urllib.parse.urlencode(params)
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3.6 KB
        ---
        name: stock-balance-all-stocks-specific-period
        description: Get balance sheet (资产负债表) for all stocks in a specific reporting period. Use when user asks about 指定报告期资产负债表, 全市场资产负债表, all stocks balance sheet for a specific year and period.
        ---
        
        # 查询单报告期全市场资产负债表(分页)
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询单报告期所有票资产负债表 |
        | 外部接口 | GET /api/v1/market/data/finance/balance |
        | 请求方式 | GET |
        | 适用场景 | 获取 A 股指定报告期所有股票的资产负债表,支持沪深京股票,支持分页 |
        
        ## 请求参数
        
        说明:year、report_type、page、page_size 为必填项。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | year | int | 是 | 报告所属年度 | 2025 | 必填 |
        | report_type | string | 是 | 报告期类型标识 | q2 | q1(一季报)、q2(半年报)、q3(三季报)、annual(全年报) |
        | page | int | 是 | 页码 | 1 | 从 1 开始 |
        | page_size | int | 是 | 每页记录数 | 20 | 建议 10–100 |
        
        ## 执行方式
        
        ```bash
        python scripts/handler.py --year 2025 --report-type q2 --page 1 --page-size 20
        ```
        
        ## 响应结构
        
        ```json
        {
          "items": [ { "stock_code", "stock_name", "publish_date", "year", "report_type", "report_type_cn", "report_form_type", "t_assets", "t_equity", "asset_liability_ratio", ... } ],
          "total_pages": 279,
          "total_items": 5565
        }
        ```
        
        ### 字段说明(BalanceItem)
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | stock_code | String | 否 | 股票交易代码,带 .SZ/.SH/.BJ 后缀 | - |
        | stock_name | String | 否 | 上市公司股票名称 | - |
        | publish_date | String | 否 | 报告发布日期,YYYY-MM-DD | - |
        | year | int | 否 | 报告所属年度 | - |
        | report_type | String | 否 | 报告期类型:q1/q2/q3/annual | - |
        | report_type_cn | String | 否 | 报告期类型中文:一季报/半年报/三季报/年报 | - |
        | report_form_type | String | 否 | 报表类型:合并未调整/合并调整 | - |
        | t_assets | float | 否 | 资产总计 | 元 |
        | total_assets_yoy | float | 是 | 总资产同比增长率,无上期时为 null | % |
        | t_fixed_assets | float | 否 | 固定资产合计 | 元 |
        | cash_equivalents | float | 否 | 货币资金 | 元 |
        | monetary_funds_yoy | float | 是 | 货币资金同比增长率 | % |
        | account_receivable | float | 否 | 应收账款 | 元 |
        | accounts_receivable_yoy | float | 是 | 应收账款同比增长率 | % |
        | inventory | float | 否 | 存货 | 元 |
        | inventory_yoy | float | 是 | 存货同比增长率 | % |
        | t_liability | float | 否 | 负债总计 | 元 |
        | total_liabilities_yoy | float | 是 | 总负债同比增长率 | % |
        | accounts_payable | float | 否 | 应付账款 | 元 |
        | accounts_payable_yoy | float | 是 | 应付账款同比增长率 | % |
        | advance_receipts | float | 是 | 预收账款 | 元 |
        | advance_receipts_yoy | float | 是 | 预收账款同比增长率 | % |
        | t_equity | float | 否 | 所有者权益合计 | 元 |
        | total_equity_yoy | float | 是 | 所有者权益合计同比增长率 | % |
        | asset_liability_ratio | float | 是 | 资产负债率(衍生) | % |
        
        ## 注意事项
        
        - report_type 取值:q1(一季报)、q2(半年报)、q3(三季报)、annual(年报)。
        - 需要全量数据时,按 page 递增请求直到 page > total_pages。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-balance-all-stocks-specific-period --year 2025 --report-type annual --page 1 --page-size 5
        ```
        
    • stock-candlesticks
      • scripts
        • handler.py 4.7 KB
          #!/usr/bin/env python3
          """查询单只标的 K 线(GET /api/v1/market/data/stock-candlesticks)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/stock-candlesticks"
          
          INTERVAL_UNITS = ("Minute", "Day", "Week", "Month", "Year")
          ADJUST_KINDS = ("None", "Forward", "Backward")
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def build_body(symbol, interval_unit, interval_value, adjust_kind,
                         since_ts_millis, until_ts_millis, limit):
              body = {
                  "symbol": symbol,
                  "interval_unit": interval_unit,
                  "until_ts_millis": until_ts_millis,
              }
              if interval_value is not None and interval_value != 1:
                  body["interval_value"] = interval_value
              if adjust_kind and adjust_kind != "None":
                  body["adjust_kind"] = adjust_kind
              if since_ts_millis is not None:
                  body["since_ts_millis"] = since_ts_millis
              if limit is not None:
                  body["limit"] = limit
              return body
          
          
          def fetch(
          symbol, interval_unit, interval_value, adjust_kind,
                    since_ts_millis, until_ts_millis, limit):
              body = build_body(symbol, interval_unit, interval_value, adjust_kind,
                                since_ts_millis, until_ts_millis, limit)
              query = urllib.parse.urlencode(body, doseq=True)
              url = f"{BASE_URL}{ENDPOINT}?{query}"
              req = urllib.request.Request(
                  url,
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  msg = e.read().decode()
                  print(f"HTTP {e.code}: {msg}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询单只标的 K 线(GET 查询参数)")
              parser.add_argument("--symbol", required=True, help="标的代码,如 600519.SH、000001.SZ、510300.XSHG")
              parser.add_argument("--interval-unit", dest="interval_unit", required=True,
                                  choices=INTERVAL_UNITS, help="K 线周期:Minute/Day/Week/Month/Year")
              parser.add_argument("--interval-value", dest="interval_value", type=int, default=1,
                                  help="间隔数值,默认 1(Minute+5 表示 5 分钟 K)")
              parser.add_argument("--adjust-kind", dest="adjust_kind", default="None",
                                  choices=ADJUST_KINDS, help="复权:None(默认)/Forward/Backward")
              parser.add_argument("--since-ts-millis", dest="since_ts_millis", type=int, default=None,
                                  help="开始时间戳(毫秒);分钟 K 与 until 跨度 ≤3 天")
              parser.add_argument("--until-ts-millis", dest="until_ts_millis", required=True, type=int,
                                  help="结束时间戳(毫秒)")
              parser.add_argument("--limit", type=int, default=None, help="返回条数上限")
              args = parser.parse_args()
          
              data = fetch(args.symbol, args.interval_unit, args.interval_value,
                           args.adjust_kind, args.since_ts_millis, args.until_ts_millis, args.limit)
              print(json.dumps(data, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 3.3 KB
          #!/usr/bin/env python3
          """Tests for stock-candlesticks handler"""
          import json
          import sys
          import unittest
          import urllib.error
          from io import BytesIO, StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestBuildBody(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_required_fields(self):
                  b = handler.build_body("600519.SH", "Day", 1, "None", None, 1756791000000, None)
                  self.assertEqual(b["symbol"], "600519.SH")
                  self.assertEqual(b["interval_unit"], "Day")
                  self.assertEqual(b["until_ts_millis"], 1756791000000)
                  self.assertNotIn("interval_value", b)
          
              def test_optional_fields(self):
                  b = handler.build_body("000001.SZ", "Minute", 5, "Forward",
                                         1756700000000, 1756791000000, 100)
                  self.assertEqual(b["interval_value"], 5)
                  self.assertEqual(b["adjust_kind"], "Forward")
                  self.assertEqual(b["limit"], 100)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_get_to_stock_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("600519.SH", "Day", 1, "None", None, 1756791000000, 5)
                  req = mock_open.call_args[0][0]
                  self.assertEqual(req.get_method(), "GET")
                  self.assertIn("/api/v1/market/data/stock-candlesticks", req.full_url)
                  self.assertIsNone(req.data)
                  self.assertIn("symbol=600519.SH", req.full_url)
                  self.assertEqual(req.headers.get("X-client-name"), "ft-claw")
          
              @patch.object(handler, "safe_urlopen")
              def test_http_error_exits(self, mock_open):
                  mock_open.side_effect = urllib.error.HTTPError(
                      "https://fake", 500, "Internal Error", {}, BytesIO(b"server error")
                  )
                  with self.assertRaises(SystemExit):
                      handler.fetch("600519.SH", "Day", 1, "None", None, 1756791000000, None)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = (
                      b'[{"open":"10.5","high":"10.6","low":"10.4","close":"10.55",'
                      b'"ts_millis":1756710000000,"ts_millis_open":1756690200000,'
                      b'"turnover":"100","volume":1000}]'
                  )
                  with patch.object(sys, "argv", [
                      "handler.py", "--symbol", "600519.SH", "--interval-unit", "Day",
                      "--until-ts-millis", "1756791000000"
                  ]):
                      with patch("sys.stdout", new_callable=StringIO) as fake_out:
                          handler.main()
                          data = json.loads(fake_out.getvalue())
                          self.assertEqual(data[0]["close"], "10.55")
          
          
          class TestSafeUrlopen(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_rejects_wrong_host(self):
                  with self.assertRaises(SystemExit):
                      handler.safe_urlopen("https://evil.com/api")
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 4 KB
        ---
        name: stock-candlesticks
        description: 单只股票/ETF/指数/可转债历史 K 线 GET 接口(market.ft.tech,stock-candlesticks)。用户问某只标的的分/日/周/月/年 K 线、开高低收、前/后复权、分钟级 K 线、5 分钟/日 K/周 K/月 K/年 K 时使用。必填 --symbol、--interval-unit、--until-ts-millis;可选 --interval-value、--adjust-kind、--since-ts-millis、--limit。
        ---
        
        # 股票 K 线 - 查询单只标的 K 线(stock-candlesticks)
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询单只标的 K 线(通用) |
        | 外部接口 | `GET /api/v1/market/data/stock-candlesticks` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 获取股票 / ETF / 指数 / 可转债等标的的分/日/周/月/年 K 线,含开高低收、成交量、成交额;支持前复权 / 后复权 / 不复权。通用语义,不做证券类别校验 |
        
        > 本接口统一使用毫秒时间戳和 GET query 参数,支持分钟级与年 K,并允许股票、ETF、指数、可转债等不同类别标的。
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 是 | 标的代码(带市场后缀) | 600519.SH、000001.SZ、510300.XSHG | 长短后缀均支持;`.XSHG`/`.SH`、`.XSHE`/`.SZ`、`.BJSE`/`.BJ` |
        | interval_unit | string | 是 | 周期单位 | Day | Minute/Day/Week/Month/Year |
        | interval_value | int | 否 | 间隔数值 | 1 | 默认 1;Minute+5 表示 5 分钟 K 线 |
        | adjust_kind | string | 否 | 复权类型 | Forward | None(默认)/Forward(前复权)/Backward(后复权) |
        | since_ts_millis | int | 否 | 开始时间戳(毫秒) | 1756700000000 | 分钟 K 线与 until 跨度 ≤3 天,其余周期不受 3 天限制 |
        | until_ts_millis | int | 是 | 结束时间戳(毫秒) | 1756791000000 | - |
        | limit | int | 否 | 返回条数上限 | 5 | 未传 since 和 limit 时默认最多返回 50 根 |
        
        ## 3. 响应说明
        
        返回裸数组,每根 K 线包含:
        
        | 字段名 | 类型 | 说明 | 单位 |
        |--------|------|------|------|
        | open | string | 开盘价 | 元 |
        | high | string | 最高价 | 元 |
        | low | string | 最低价 | 元 |
        | close | string | 收盘价(或最新价) | 元 |
        | ts_millis | int | 收盘时间戳 | 毫秒 |
        | ts_millis_open | int | 开盘时间戳 | 毫秒 |
        | turnover | string | 成交额 | 元 |
        | volume | int64 | 成交量 | 股/份 |
        | turnover_rate | number | 换手率 | % |
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> stock-candlesticks --symbol 600519.SH --interval-unit Day --since-ts-millis 1756431000000 --until-ts-millis 1756710000000 --limit 5
        python <RUN_PY> stock-candlesticks --symbol 000001.SZ --interval-unit Minute --interval-value 5 --adjust-kind Forward --since-ts-millis 1756700000000 --until-ts-millis 1756791000000
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON,请求头已内置 `X-Client-Name: ft-claw`。
        
        ## 5. 注意事项
        
        - `symbol`、`interval_unit`、`until_ts_millis` 必填。
        - `symbol` 必须带市场后缀;不带后缀或后缀无法识别时外部接口返回系统错误。
        - 分钟 K 线(`interval_unit=Minute`)的 `since/until` 跨度硬限制 ≤3 天,超过需分段调用。
        - `interval_value` 仅在 `interval_unit=Minute` 时生效:不传或传 1 为 1 分钟 K,传 5/15/30/60/120 为对应多分钟 K;其他周期忽略该字段。
        - 多分钟 K 按北京时间的每个交易日分别聚合,不跨交易日;以 5 分钟 K 为例,首根为 09:30—09:35,开高低收取区间首根开盘价、最高价、最低价、末根收盘价,成交量和成交额按区间求和。
        - 默认不复权(None),`Forward` 前复权、`Backward` 后复权。
        - 该接口不校验证券类别;查 ETF 推荐用 `etf-candlesticks`、查指数用 `index-candlesticks`、查可转债用 `convertible-bond-candlesticks`。
        - 价格字段 JSON 中为字符串以避免精度丢失。
        
    • stock-candlesticks-batch
      • scripts
        • handler.py 5 KB
          #!/usr/bin/env python3
          """批量查询多只标的 K 线(GET /api/v2/market/data/stock-candlesticks/batch)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v2/market/data/stock-candlesticks/batch"
          
          INTERVAL_UNITS = ("Day", "Week", "Month", "Year")
          ADJUST_KINDS = ("None", "Forward", "Backward")
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def parse_symbols(raw):
              syms = [s.strip() for s in raw.split(",") if s.strip()]
              if not syms:
                  print("--symbols 不能为空", file=sys.stderr)
                  sys.exit(1)
              return syms
          
          
          def build_body(symbols, interval_unit, adjust_kind,
                         since_ts_millis, until_ts_millis, limit):
              body = {
                  "symbols": symbols,
                  "interval_unit": interval_unit,
                  "until_ts_millis": until_ts_millis,
              }
              if adjust_kind and adjust_kind != "None":
                  body["adjust_kind"] = adjust_kind
              if since_ts_millis is not None:
                  body["since_ts_millis"] = since_ts_millis
              if limit is not None:
                  body["limit"] = limit
              return body
          
          
          def fetch(symbols, interval_unit, adjust_kind,
                    since_ts_millis, until_ts_millis, limit):
              body = build_body(symbols, interval_unit, adjust_kind,
                                since_ts_millis, until_ts_millis, limit)
              query = urllib.parse.urlencode(body, doseq=True)
              url = f"{BASE_URL}{ENDPOINT}?{query}"
              req = urllib.request.Request(
                  url,
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  msg = e.read().decode()
                  print(f"HTTP {e.code}: {msg}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="批量查询多只标的 K 线(GET 查询参数)")
              parser.add_argument("--symbols", required=True,
                                  help="标的代码列表,逗号分隔,如 600519.SH,510300.SH,113027.SH,000300.SH;支持长短后缀混用")
              parser.add_argument("--interval-unit", dest="interval_unit", required=True, type=str.capitalize,
                                  choices=INTERVAL_UNITS, help="K 线周期:Day/Week/Month/Year(大小写不敏感,不支持 Minute)")
              parser.add_argument("--adjust-kind", dest="adjust_kind", default="None",
                                  choices=ADJUST_KINDS, help="复权:None(默认)/Forward/Backward")
              parser.add_argument("--since-ts-millis", dest="since_ts_millis", required=True, type=int,
                                  help="开始时间戳(毫秒);与结束时间跨度不得超过 12 个日历月")
              parser.add_argument("--until-ts-millis", dest="until_ts_millis", required=True, type=int,
                                  help="结束时间戳(毫秒)")
              parser.add_argument("--limit", type=int, default=None,
                                  help="每个标的返回条数上限;不传时返回请求时间范围内的全部数据")
              args = parser.parse_args()
          
              if args.since_ts_millis > args.until_ts_millis:
                  print("--since-ts-millis 不能晚于 --until-ts-millis", file=sys.stderr)
                  raise SystemExit(2)
          
              symbols = parse_symbols(args.symbols)
              data = fetch(symbols, args.interval_unit,
                           args.adjust_kind, args.since_ts_millis, args.until_ts_millis, args.limit)
              print(json.dumps(data, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 4.9 KB
          #!/usr/bin/env python3
          """Tests for stock-candlesticks-batch handler"""
          import json
          import os
          import sys
          import unittest
          import urllib.error
          from io import BytesIO, StringIO
          from unittest.mock import patch
          import importlib.util
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          SINCE = "1756431000000"
          UNTIL = "1756791000000"
          
          
          class TestBuildBody(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_required_only(self):
                  body = handler.build_body(["600519.SH"], "Day", "None", 1756431000000, 1756791000000, None)
                  self.assertEqual(body["symbols"], ["600519.SH"])
                  self.assertEqual(body["interval_unit"], "Day")
                  self.assertEqual(body["since_ts_millis"], 1756431000000)
                  self.assertNotIn("adjust_kind", body)
                  self.assertNotIn("limit", body)
                  self.assertNotIn("interval_value", body)
          
              def test_minute_not_allowed(self):
                  with self.assertRaises(SystemExit):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "600519.SH",
                                                      "--interval-unit", "Minute",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          handler.main()
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_get_to_stock_batch_endpoint(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch(["600519.SH", "000001.SZ"], "Day", "None",
                                1756431000000, 1756791000000, 2)
                  req = mock_open.call_args[0][0]
                  self.assertEqual(req.get_method(), "GET")
                  self.assertIn("/api/v2/market/data/stock-candlesticks/batch", req.full_url)
                  self.assertIn("since_ts_millis=1756431000000", req.full_url)
                  self.assertIsNone(req.data)
                  self.assertEqual(req.headers.get("X-client-name"), "ft-claw")
          
              @patch.object(handler, "safe_urlopen")
              def test_http_error_exits(self, mock_open):
                  mock_open.side_effect = urllib.error.HTTPError(
                      "https://fake", 500, "Internal Error", {}, BytesIO(b"server error")
                  )
                  with self.assertRaises(SystemExit):
                      handler.fetch(["600519.SH"], "Day", "None", 1756431000000, 1756791000000, None)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", [
                          "handler.py", "--symbols", "600519.SH,000001.SZ",
                          "--interval-unit", "Day",
                          "--since-ts-millis", SINCE, "--until-ts-millis", UNTIL
                      ]):
                          with patch("sys.stdout", new_callable=StringIO) as fake_out:
                              handler.main()
                              self.assertEqual(json.loads(fake_out.getvalue()), [])
          
              def test_main_requires_since(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "600519.SH",
                                                      "--interval-unit", "Day",
                                                      "--until-ts-millis", UNTIL]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              def test_main_rejects_since_after_until(self):
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "600519.SH",
                                                      "--interval-unit", "Day",
                                                      "--since-ts-millis", UNTIL,
                                                      "--until-ts-millis", SINCE]):
                          with self.assertRaises(SystemExit):
                              handler.main()
          
              @patch.object(handler, "safe_urlopen")
              def test_interval_unit_case_insensitive(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"{}"
                  with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                      with patch.object(sys, "argv", ["handler.py", "--symbols", "600519.SH",
                                                      "--interval-unit", "day",
                                                      "--since-ts-millis", SINCE,
                                                      "--until-ts-millis", UNTIL]):
                          handler.main()
                  req = mock_open.call_args[0][0]
                  self.assertIn("interval_unit=Day", req.full_url)
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 4 KB
        ---
        name: stock-candlesticks-batch
        description: 批量获取多只股票/ETF/可转债/指数 K 线 GET 接口(market.ft.tech,stock-candlesticks/batch)。用户问多只标的的日/周/月/年 K 线、批量开高低收、混合多类证券的 K 线时使用。必填 --symbols、--interval-unit、--since-ts-millis、--until-ts-millis;可选 --adjust-kind、--limit。
        ---
        
        # 批量股票K线
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 批量股票K线(stock_candlesticks_batch) |
        | 外部接口 | `GET /api/v2/market/data/stock-candlesticks/batch` |
        | 请求方式 | GET(query 参数,`symbols` 可重复传入) |
        | 适用场景 | 一次批量查询股票 / ETF / 可转债 / 指数等多只标的的历史 K 线(开高低收、成交量、成交额、换手率),支持日/周/月/年周期与前复权/后复权。通用语义,允许不同证券类别混合查询,不做类别校验 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbols | string[] | 是 | 标的代码列表,逗号分隔传给 CLI | 600519.SH,510300.SH,113027.SH,000300.SH | 可混合股票/ETF/可转债/指数;沪市 `.XSHG`/`.SH`、深市 `.XSHE`/`.SZ`、北交所 `.BJSE`/`.BJ`;接口侧以重复 query 参数发送 |
        | interval_unit | string | 是 | 周期单位 | Day | Day/Week/Month/Year,大小写不敏感;**不支持 Minute** |
        | adjust_kind | string | 否 | 复权类型 | Forward | None(默认)/Forward(前复权)/Backward(后复权) |
        | since_ts_millis | int | 是 | 开始时间戳(毫秒) | 1756431000000 | 与 until 的跨度不得超过 12 个日历月;不得晚于 until |
        | until_ts_millis | int | 是 | 结束时间戳(毫秒) | 1756791000000 | - |
        | limit | int | 否 | 每个标的返回条数上限 | 3 | 不传时返回请求时间范围内的全部数据 |
        
        ## 3. 响应说明
        
        外层固定为 `code`(成功 200)/ `message`(成功 `success`)/ `data`(失败时为 `null`)。`data` 为非分页嵌套数组,外层每项为 `[symbol, K线数组]`,每根 K 线字段:
        
        | 字段名 | 类型 | 说明 | 单位 |
        |--------|------|------|------|
        | symbol | string | 标的代码,响应统一使用 `.SH`、`.SZ`、`.BJ` 短后缀 | - |
        | open / high / low / close | number | 开/高/低/收盘价 | 元 |
        | ts_millis | string | 收盘时间戳 | 毫秒 |
        | ts_millis_open | string | 开盘时间戳 | 毫秒 |
        | turnover | number | 成交额 | 元 |
        | volume | integer | 成交量 | - |
        | turnover_rate | number | 换手率 | % |
        
        注:`open/high/low/close`、`turnover` 在 JSON 中实际以字符串返回(避免精度丢失);`ts_millis` 为数字。
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> stock-candlesticks-batch --symbols 600519.SH,000001.SZ --interval-unit Day --since-ts-millis 1756431000000 --until-ts-millis 1756791000000 --limit 2
        python <RUN_PY> stock-candlesticks-batch --symbols 600519.SH,510300.SH,113027.SH --interval-unit Week --adjust-kind Forward --since-ts-millis 1756431000000 --until-ts-millis 1756791000000 --limit 3
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - `symbols`、`interval_unit`、`since_ts_millis`、`until_ts_millis` 必填;所有 `symbols` 使用相同周期。
        - 接口仅支持 GET;`symbols` 在查询参数中以重复参数形式发送(`symbols=600519.SH&symbols=000001.SZ`)。
        - 时间跨度最多 12 个日历月;需要更长历史时按窗口分段多次调用。
        - 不支持分钟 K 线;分钟数据请使用 `stock-minutes-batch` 子 skill。
        - 混合证券类别不做校验;换手率仅股票标的有值,ETF/可转债/指数标的当前为 `null`。
        - 输入 `.XSHG`/`.XSHE`/`.BJSE` 长后缀时,响应中的 symbol 会规范化为 `.SH`、`.SZ`、`.BJ` 短后缀。
        - 默认不复权(None);仅使用历史日 K 数据计算,不含实时行情,实际起始日期以行情数据源覆盖为准。
        
    • stock-capital-flows
      • scripts
        • handler.py 6.3 KB
          #!/usr/bin/env python3
          """查询 A 股股票资金流向(实时快照 / 历史 15 分钟切片),支持分页、全量拉取与单股定位"""
          import argparse
          import json
          import re
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/stock-capital-flows"
          
          # HHMM,分钟须为 00/15/30/45
          VALID_TIME_RE = re.compile(r"^\d{2}(00|15|30|45)$")
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def fetch_page(page: int, page_size: int, date: str = None, time_slice: str = None) -> dict:
              params = {"page": page, "page_size": page_size}
              if date:
                  params["date"] = date
                  params["time"] = time_slice or "1530"
              qs = urllib.parse.urlencode(params)
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def find_symbol(symbol: str, page_size: int, date: str = None, time_slice: str = None) -> dict:
              """定位单只标的。先扫第 1 页与最后 1 页(覆盖排名头部/尾部),命中即返回;
              否则从前向后补扫中间页。返回其在全市场中的排名与记录。"""
              target = symbol.strip().upper()
          
              def match(data, page_no):
                  for i, it in enumerate(data.get("items", [])):
                      if (it.get("symbol") or "").strip().upper() == target:
                          rank = (page_no - 1) * page_size + i + 1
                          return rank, it
                  return None, None
          
              first = fetch_page(1, page_size, date, time_slice)
              total_pages = first.get("total_pages", 1)
              total_items = first.get("total_items", 0)
          
              rank, rec = match(first, 1)
              if rec is not None:
                  return {"found": True, "symbol": symbol, "rank": rank,
                          "total_items": total_items, "record": rec}
          
              if total_pages > 1:
                  last = fetch_page(total_pages, page_size, date, time_slice)
                  rank, rec = match(last, total_pages)
                  if rec is not None:
                      return {"found": True, "symbol": symbol, "rank": rank,
                              "total_items": total_items, "record": rec}
          
              for p in range(2, total_pages):
                  rank, rec = match(fetch_page(p, page_size, date, time_slice), p)
                  if rec is not None:
                      return {"found": True, "symbol": symbol, "rank": rank,
                              "total_items": total_items, "record": rec}
          
              return {"found": False, "symbol": symbol, "rank": None, "total_items": total_items}
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询 A 股股票资金流向(实时快照 / 历史 15 分钟切片)")
              parser.add_argument("--date", type=str, default=None,
                                  help="查询日期,格式 YYYYMMDD;不传则返回当前实时快照")
              parser.add_argument("--time", type=str, default=None,
                                  help="15 分钟切片时刻,格式 HHMM,分钟须为 00/15/30/45;仅在传 --date 时有效,默认 1530(日终)")
              parser.add_argument("--symbol", type=str, default=None,
                                  help="只返回指定标的(如 601138.SH);逐页扫描定位,找到即停,返回其排名与记录")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始(默认 1)")
              parser.add_argument("--page-size", type=int, default=50, help="每页记录数(默认 50)")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量数据")
              args = parser.parse_args()
          
              # --time 仅在指定 --date 时有意义
              time_slice = None
              if args.date:
                  time_slice = args.time or "1530"
                  if not VALID_TIME_RE.match(time_slice):
                      print(f"错误:--time 必须为 HHMM 且分钟须为 00/15/30/45,当前为 [{time_slice}]", file=sys.stderr)
                      sys.exit(1)
              elif args.time:
                  print("错误:--time 仅在指定 --date 时有效。查询实时快照请去掉 --time。", file=sys.stderr)
                  sys.exit(1)
          
              # 单股定位(优先级最高;与 --all 互斥时以 --symbol 为准)
              if args.symbol:
                  result = find_symbol(args.symbol, args.page_size, args.date, time_slice)
              elif args.fetch_all:
                  first = fetch_page(1, args.page_size, args.date, time_slice)
                  all_items = list(first.get("items", []))
                  total_pages = first.get("total_pages", 1)
                  for p in range(2, total_pages + 1):
                      page_data = fetch_page(p, args.page_size, args.date, time_slice)
                      all_items.extend(page_data.get("items", []))
                  result = {
                      "items": all_items,
                      "total_pages": total_pages,
                      "total_items": first.get("total_items", len(all_items)),
                  }
              else:
                  result = fetch_page(args.page, args.page_size, args.date, time_slice)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 4.1 KB
        ---
        name: stock-capital-flows
        description: 查询 A 股股票资金流向(主力净流入)。支持当前实时快照,或指定日期的 15 分钟切片快照(默认日终 time=1530)。Use when user asks about 资金流向, 主力净流入, 超大单/大单/中单/小单净流入, 15分钟资金流, 个股资金流排名, capital flow, money flow, stock capital flows.
        ---
        
        # 查询 A 股股票资金流向(实时快照 / 15 分钟切片)
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 对外接口-查询股票资金流向 |
        | 外部接口 | GET /api/v1/market/data/stock-capital-flows |
        | 请求方式 | GET |
        | 适用场景 | 分页查询 A 股股票资金流向;不传 `date` 返回当前实时快照,传入 `date` 可指定 15 分钟切片(默认 `time=1530` 日终)。仅返回 A 股(不含 ETF/指数),按主力净流入降序排列。 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | date | string | 否 | 查询日期 | 20260616 | 格式 YYYYMMDD;不传=实时快照 |
        | time | string | 否 | 15 分钟切片时刻 | 1530 | 格式 HHMM,分钟须为 00/15/30/45;仅与 `date` 同时有效,默认 1530(日终) |
        | symbol | string | 否 | 只查指定标的 | 601138.SH | 外部格式(如 `601138.SH`);逐页扫描定位,找到即停,返回其全市场排名与单条记录。不传则按分页/全量返回 |
        | page | int | 否 | 页码 | 1 | 从 1 开始,默认 1 |
        | page_size | int | 否 | 每页条数 | 50 | 默认 50 |
        
        ## 执行方式
        
        ```bash
        # 当前实时快照(分页)
        python scripts/handler.py --page 1 --page-size 50
        # 历史 15 分钟切片(默认 time=1530 日终)
        python scripts/handler.py --date 20260616 --page 1 --page-size 50
        # 指定切片时刻(如 10:15)
        python scripts/handler.py --date 20260616 --time 1015 --page 1 --page-size 50
        # 全量自动翻页拉取某日日终资金流
        python scripts/handler.py --date 20260616 --all
        # 只查某只票某切片(逐页定位,返回排名+记录)
        python scripts/handler.py --date 20260616 --symbol 601138.SH
        # 只查某只票当前实时快照
        python scripts/handler.py --symbol 601138.SH
        ```
        
        ## 响应结构
        
        ```json
        {
          "items": [
            {
              "net_inflow_extra_large": "123456789.0000",
              "net_inflow_large": "98765432.1000",
              "net_inflow_main": "222222221.1000",
              "net_inflow_medium": "-50000000.0000",
              "net_inflow_small": "-172222221.1000",
              "symbol": "600000.SH",
              "symbol_name": "浦发银行",
              "ts_nanos": 1747037400000000000
            }
          ],
          "total_pages": 100,
          "total_items": 5000
        }
        ```
        
        ### 字段说明(CapitalFlowResponse)
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | net_inflow_extra_large | string | 否 | 超大单净流入,单位元 |
        | net_inflow_large | string | 否 | 大单净流入,单位元 |
        | net_inflow_main | string | 否 | 主力净流入,单位元;公式 = 超大单净流入 + 大单净流入 |
        | net_inflow_medium | string | 否 | 中单净流入,单位元 |
        | net_inflow_small | string | 否 | 小单净流入,单位元 |
        | symbol | string | 否 | 标的代码,外部格式如 `600000.SH`、`000001.SZ` |
        | symbol_name | string | 是 | 标的名称 |
        | ts_nanos | int | 否 | 快照时间戳(纳秒) |
        
        ## 注意事项
        
        - 不传 `date`:返回当前实时资金流向快照。
        - 传入 `date`:返回该日指定 15 分钟切片快照,默认 `time=1530`(日终);`time` 分钟须为 00/15/30/45。
        - 金额字段在 JSON 中以字符串形式返回(Decimal 序列化),单位元。
        - `symbol` 为外部代码格式(如 `600000.SH`)。
        - 仅返回 A 股股票(不含 ETF、指数等);列表按 `net_inflow_main` 降序排列。
        - 本接口响应带短期缓存(失败重试策略约 5 秒),相同 query 在缓存有效期内可能返回相同结果。
        - `--all` 自动翻页拉取全量;全市场约 5000+ 条,按需使用以免响应过大。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-capital-flows --symbol 600519.SH --page 1 --page-size 5
        ```
        
    • stock-cashflow-all-stocks-specific-period
      • scripts
        • handler.py 2.9 KB
          #!/usr/bin/env python3
          """查询指定报告期全市场现金流量表(分页)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          VALID_REPORT_TYPES = ["q1", "q2", "q3", "annual"]
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询指定报告期全市场现金流量表(分页)")
              parser.add_argument("--year", type=int, required=True, help="报告所属年度,如 2025")
              parser.add_argument("--report-type", required=True, choices=VALID_REPORT_TYPES,
                                  help="报告期类型:q1(一季报)/ q2(半年报)/ q3(三季报)/ annual(年报)")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始(默认 1)")
              parser.add_argument("--page-size", type=int, default=20, help="每页记录数(默认 20)")
              args = parser.parse_args()
          
              params = {
                  "year": args.year,
                  "report_type": args.report_type,
                  "page": args.page,
                  "page_size": args.page_size,
              }
              url = f"{BASE_URL}/api/v1/market/data/finance/cashflow?" + urllib.parse.urlencode(params)
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3.8 KB
        ---
        name: stock-cashflow-all-stocks-specific-period
        description: Get cash flow statement (现金流量表) for all stocks in a specific reporting period. Use when user asks about 指定报告期现金流量表, 全市场现金流量表, all stocks cashflow for a specific year and period.
        ---
        
        # 查询单报告期全市场现金流量表(分页)
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询单报告期所有票现金流量表 |
        | 外部接口 | GET /api/v1/market/data/finance/cashflow |
        | 请求方式 | GET |
        | 适用场景 | 获取 A 股指定报告期所有股票的现金流量表,支持沪深京股票,支持分页 |
        
        ## 请求参数
        
        说明:year、report_type、page、page_size 为必填项。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | year | int | 是 | 报告所属年度 | 2025 | 必填 |
        | report_type | string | 是 | 报告期类型标识 | q2 | q1(一季报)、q2(半年报)、q3(三季报)、annual(全年报) |
        | page | int | 是 | 页码 | 1 | 从 1 开始 |
        | page_size | int | 是 | 每页记录数 | 20 | 建议 10–100 |
        
        ## 执行方式
        
        ```bash
        python scripts/handler.py --year 2025 --report-type q2 --page 1 --page-size 20
        ```
        
        ## 响应结构
        
        ```json
        {
          "items": [ { "stock_code", "stock_name", "year", "report_type", "report_type_cn", "publish_date", "detail_report_type", "cash_equ_inc", ... } ],
          "total_pages": 279,
          "total_items": 5565
        }
        ```
        
        ### 字段说明(CashflowItem)
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | stock_code | String | 否 | 股票交易代码,带 .SZ/.SH/.BJ 后缀 | - |
        | stock_name | String | 否 | 上市公司股票名称 | - |
        | year | int | 否 | 报告所属年度 | - |
        | report_type | String | 否 | 报告期类型:q1/q2/q3/annual | - |
        | report_type_cn | String | 否 | 报告期类型中文:一季报/半年报/三季报/年报 | - |
        | publish_date | String | 否 | 报告发布日期,YYYY-MM-DD | - |
        | detail_report_type | String | 否 | 报告明细类型:合并未调整/合并调整 | - |
        | cash_equ_inc | float | 否 | 现金及现金等价物净增加额(核心净现金流) | 元 |
        | cash_equ_inc_yoy | float | 是 | 净现金流同比增长率,无上期时为 null | % |
        | net_oper_cash_flow | float | 否 | 经营活动产生的现金流量净额 | 元 |
        | net_oper_cash_flow_ratio | float | 是 | 经营性现金流量净额占净现金流比例 | % |
        | goods_sale_render_service_cash | float | 否 | 销售商品、提供劳务收到的现金 | 元 |
        | goods_sale_render_service_cash_ratio | float | 是 | 销售商品、提供劳务收到现金占净现金流比例 | % |
        | net_invest_cash_flow | float | 否 | 投资活动产生的现金流量净额 | 元 |
        | net_invest_cash_flow_ratio | float | 是 | 投资性现金流量净额占净现金流比例 | % |
        | invest_proceeds | float | 否 | 取得投资收益收到的现金 | 元 |
        | invest_proceeds_ratio | float | 是 | 取得投资收益收到现金占净现金流比例 | % |
        | fix_intan_long_pay_cash | float | 否 | 购建固定资产、无形资产等长期资产支付的现金 | 元 |
        | fix_intan_long_pay_cash_ratio | float | 是 | 购建长期资产支付现金占净现金流比例 | % |
        | net_fin_cash_flow | float | 否 | 筹资活动产生的现金流量净额 | 元 |
        | net_fin_cash_flow_ratio | float | 是 | 融资性现金流量净额占净现金流比例 | % |
        
        ## 注意事项
        
        - report_type 取值:q1(一季报)、q2(半年报)、q3(三季报)、annual(年报)。
        - 需要全量数据时,按 page 递增请求直到 page > total_pages。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-cashflow-all-stocks-specific-period --year 2025 --report-type annual --page 1 --page-size 5
        ```
        
    • stock-comment-desire
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          """查询指定股票的千股千评市场参与意愿(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询千股千评市场参与意愿")
              parser.add_argument("--symbol", type=str, required=True, help="6位股票代码")
              args = parser.parse_args()
          
              url = BASE_URL + "/api/v1/market/data/stock-comment/desire?" + urllib.parse.urlencode({"symbol": args.symbol})
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
          
              try:
                  with safe_urlopen(req) as resp:
                      chunks = []
                      while True:
                          chunk = resp.read(65536)
                          if not chunk:
                              break
                          chunks.append(chunk)
                      raw = b"".join(chunks).decode()
                  data = json.loads(raw)
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(body, file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.6 KB
        ---
        name: stock-comment-desire
        description: 查询指定股票的千股千评市场参与意愿及5日均值变化(market.ft.tech)。用户问市场参与意愿、散户参与度时使用。
        ---
        
        # 千股千评 — 市场参与意愿
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 市场参与意愿 |
        | 外部接口 | `GET /api/v1/market/data/stock-comment/desire` |
        | 请求方式 | GET |
        | 适用场景 | 查询指定股票的市场参与意愿及 5 日均值变化 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 是 | 股票代码 | `000001` | 6 位数字代码,不带交易所后缀 |
        
        ## 3. 响应说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | data.symbol | string | 否 | 股票代码 |
        | data.total | int | 否 | 总记录数 |
        | data.items[].trade_date | string | 是 | 交易日期 |
        | data.items[].security_code | string | 是 | 股票代码 |
        | data.items[].participation_wish | string | 是 | 参与意愿 |
        | data.items[].participation_wish_5days | string | 是 | 5 日平均参与意愿 |
        | data.items[].participation_wish_change | string | 是 | 参与意愿变化 |
        | data.items[].participation_wish_5days_change | string | 是 | 5 日平均参与意愿变化 |
        
        ## 4. 用法
        
        ```bash
        python <RUN_PY> stock-comment-desire --symbol 000001
        ```
        
        ## 5. 注意事项
        
        - `symbol` 为 6 位数字股票代码,不带交易所后缀。
        - 意愿指数字段以字符串形式返回以保持精度。
        
    • stock-comment-focus
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          """查询指定股票的千股千评市场热度用户关注指数(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询千股千评市场热度用户关注指数")
              parser.add_argument("--symbol", type=str, required=True, help="6位股票代码")
              args = parser.parse_args()
          
              url = BASE_URL + "/api/v1/market/data/stock-comment/focus?" + urllib.parse.urlencode({"symbol": args.symbol})
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
          
              try:
                  with safe_urlopen(req) as resp:
                      chunks = []
                      while True:
                          chunk = resp.read(65536)
                          if not chunk:
                              break
                          chunks.append(chunk)
                      raw = b"".join(chunks).decode()
                  data = json.loads(raw)
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(body, file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.6 KB
        ---
        name: stock-comment-focus
        description: 查询指定股票的千股千评市场热度用户关注指数及排名变动(market.ft.tech)。用户问用户关注指数、市场关注度排名时使用。
        ---
        
        # 千股千评 — 市场热度用户关注指数
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 市场热度用户关注指数 |
        | 外部接口 | `GET /api/v1/market/data/stock-comment/focus` |
        | 请求方式 | GET |
        | 适用场景 | 查询指定股票的用户关注指数、排名及变动情况 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 是 | 股票代码 | `000001` | 6 位数字代码,不带交易所后缀 |
        
        ## 3. 响应说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | data.symbol | string | 否 | 股票代码 |
        | data.total | int | 否 | 总记录数 |
        | data.items[].trade_date | string | 是 | 交易日期 |
        | data.items[].market_focus | string | 是 | 市场关注度 |
        | data.items[].market_focus_rank | string | 是 | 关注度排名 |
        | data.items[].total_market | string | 是 | 全市场关注度 |
        | data.items[].market_focus_change | string | 是 | 关注度变动 |
        | data.items[].close_price | string | 是 | 收盘价 |
        
        ## 4. 用法
        
        ```bash
        python <RUN_PY> stock-comment-focus --symbol 000001
        ```
        
        ## 5. 注意事项
        
        - `symbol` 为 6 位数字股票代码,不带交易所后缀。
        - 指数及价格字段以字符串形式返回以保持精度。
        
    • stock-comment-index
      • scripts
        • handler.py 2.3 KB
          #!/usr/bin/env python3
          """查询东方财富千股千评综合诊断主表(market.ft.tech)"""
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              _require_api_key()
              url = BASE_URL + "/api/v1/market/data/stock-comment/index"
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
          
              try:
                  with safe_urlopen(req) as resp:
                      chunks = []
                      while True:
                          chunk = resp.read(65536)
                          if not chunk:
                              break
                          chunks.append(chunk)
                      raw = b"".join(chunks).decode()
                  data = json.loads(raw, strict=False)
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(body, file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2 KB
        ---
        name: stock-comment-index
        description: 查询东方财富千股千评综合诊断主表(market.ft.tech),含收盘价、涨跌幅、换手率、市盈率、主力成本、机构参与度、综合评分、排名、关注指数。用户问千股千评、股票综合诊断、股票评分排名时使用。
        ---
        
        # 千股千评主表
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 千股千评综合诊断主表 |
        | 外部接口 | `GET /api/v1/market/data/stock-comment/index` |
        | 请求方式 | GET |
        | 适用场景 | 查询东方财富千股千评综合诊断主表,获取全市场股票的综合评分与排名 |
        
        ## 2. 请求参数
        
        无参数,返回全量数据。
        
        ## 3. 响应说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | data.total | int | 否 | 总记录数 |
        | data.items[].seq | int | 否 | 序号 |
        | data.items[].security_code | string | 否 | 股票代码 |
        | data.items[].security_name_abbr | string | 是 | 股票简称 |
        | data.items[].trade_date | string | 是 | 交易日期 |
        | data.items[].close_price | string | 是 | 收盘价 |
        | data.items[].change_rate | string | 是 | 涨跌幅 |
        | data.items[].turnover_rate | string | 是 | 换手率 |
        | data.items[].pe_dynamic | string | 是 | 动态市盈率 |
        | data.items[].prime_cost | string | 是 | 主力成本 |
        | data.items[].org_participate | string | 是 | 机构参与度 |
        | data.items[].total_score | string | 是 | 综合评分 |
        | data.items[].rank | string | 是 | 当前排名 |
        | data.items[].focus | string | 是 | 关注指数 |
        
        ## 4. 用法
        
        ```bash
        python <RUN_PY> stock-comment-index
        ```
        
        ## 5. 注意事项
        
        - 返回全量数据(约 5000+ 条),数据量较大,**可能因响应过大导致服务端截断**(HTTP/2 限制)。如遇到 JSON 解析失败,说明数据被截断,需等服务端修复或通过单股接口(`stock-comment-score` / `stock-comment-focus` 等)按个股查询。
        - 数值类字段以字符串形式返回以保持精度。
        
    • stock-comment-org-participate
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          """查询指定股票的千股千评机构参与度(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询千股千评机构参与度")
              parser.add_argument("--symbol", type=str, required=True, help="6位股票代码")
              args = parser.parse_args()
          
              url = BASE_URL + "/api/v1/market/data/stock-comment/org-participate?" + urllib.parse.urlencode({"symbol": args.symbol})
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
          
              try:
                  with safe_urlopen(req) as resp:
                      chunks = []
                      while True:
                          chunk = resp.read(65536)
                          if not chunk:
                              break
                          chunks.append(chunk)
                      raw = b"".join(chunks).decode()
                  data = json.loads(raw)
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(body, file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.2 KB
        ---
        name: stock-comment-org-participate
        description: 查询指定股票的千股千评机构参与度走势(market.ft.tech)。用户问机构参与度、机构关注度时使用。
        ---
        
        # 千股千评 — 机构参与度
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 机构参与度 |
        | 外部接口 | `GET /api/v1/market/data/stock-comment/org-participate` |
        | 请求方式 | GET |
        | 适用场景 | 查询指定股票的历史机构参与度走势 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 是 | 股票代码 | `000001` | 6 位数字代码,不带交易所后缀 |
        
        ## 3. 响应说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | data.symbol | string | 否 | 股票代码 |
        | data.total | int | 否 | 总记录数 |
        | data.items[].trade_date | string | 是 | 交易日期 |
        | data.items[].org_participate | string | 是 | 机构参与度 |
        
        ## 4. 用法
        
        ```bash
        python <RUN_PY> stock-comment-org-participate --symbol 000001
        ```
        
        ## 5. 注意事项
        
        - `symbol` 为 6 位数字股票代码,不带交易所后缀。
        - 参与度以字符串形式返回以保持精度。
        
    • stock-comment-score
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          """查询指定股票的千股千评历史综合评分(market.ft.tech)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询千股千评历史综合评分")
              parser.add_argument("--symbol", type=str, required=True, help="6位股票代码")
              args = parser.parse_args()
          
              url = BASE_URL + "/api/v1/market/data/stock-comment/score?" + urllib.parse.urlencode({"symbol": args.symbol})
              req = urllib.request.Request(url, method="GET", headers=_REQUEST_HEADERS)
          
              try:
                  with safe_urlopen(req) as resp:
                      chunks = []
                      while True:
                          chunk = resp.read(65536)
                          if not chunk:
                              break
                          chunks.append(chunk)
                      raw = b"".join(chunks).decode()
                  data = json.loads(raw)
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(body, file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.2 KB
        ---
        name: stock-comment-score
        description: 查询指定股票的千股千评历史综合评分走势(market.ft.tech)。用户问某股票历史评分、综合评分走势时使用。
        ---
        
        # 千股千评 — 综合评价历史评分
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 综合评价历史评分 |
        | 外部接口 | `GET /api/v1/market/data/stock-comment/score` |
        | 请求方式 | GET |
        | 适用场景 | 查询指定股票的历史综合评分走势 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 是 | 股票代码 | `000001` | 6 位数字代码,不带交易所后缀 |
        
        ## 3. 响应说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | data.symbol | string | 否 | 股票代码 |
        | data.total | int | 否 | 总记录数 |
        | data.items[].diagnose_date | string | 是 | 诊断日期 |
        | data.items[].total_score | string | 是 | 综合评分 |
        
        ## 4. 用法
        
        ```bash
        python <RUN_PY> stock-comment-score --symbol 000001
        ```
        
        ## 5. 注意事项
        
        - `symbol` 为 6 位数字股票代码,不带交易所后缀。
        - 评分字段以字符串形式返回以保持精度。
        
    • stock-connect-members
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/stock-connect-members'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='互联互通成份')
              parser.add_argument("--direction", required=True)
              parser.add_argument("--channel", required=True)
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--total")
              parser.add_argument("--symbol")
              args = parser.parse_args()
              params = {}
              if args.direction is not None: params["direction"] = args.direction
              if args.channel is not None: params["channel"] = args.channel
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.symbol is not None: params["symbol"] = args.symbol
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 647 B
        ---
        name: stock-connect-members
        description: 查询互联互通成份。接口:GET /api/v1/market/data/stock-connect-members。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 互联互通成份
        
        接口:GET `/api/v1/market/data/stock-connect-members`。参数和响应以 `ftshare-doc/api-doc/股票数据/基础数据/互联互通成份.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-connect-members --direction north --channel SH --page 1
        ```
        
    • stock-description-all
      • scripts
        • handler.py 2.6 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v1/market/data/stock-description"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {
              "FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"],
              "Content-Type": "application/json",
          } if os.environ.get("FTSHARE_API_KEY") else {"Content-Type": "application/json"}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed = urllib.parse.urlparse(url)
              base = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              _require_api_key()
              for key, value in _REQUEST_HEADERS.items():
                  request.add_unredirected_header(key, value)
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询股票基础信息")
              parser.add_argument("--symbol-id")
              parser.add_argument("--page", type=int)
              parser.add_argument("--page-size", type=int)
              args = parser.parse_args()
              params = {
                  "symbol_id": args.symbol_id,
                  "page": args.page,
                  "page_size": args.page_size,
              }
              query = urllib.parse.urlencode({key: value for key, value in params.items() if value is not None})
              request = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}?{query}" if query else f"{BASE_URL}{ENDPOINT}",
                  headers={"X-Client-Name": "ft-claw", "Content-Type": "application/json"},
                  method="GET",
              )
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.6 KB
        ---
        name: stock-description-all
        description: 查询 A 股股票基础信息,支持按 symbol_id 筛选和分页。用户询问股票基础资料、股票列表详情或股票代码对应公司信息时使用。接口:GET /api/v1/market/data/stock-description。
        ---
        
        # 股票基础信息
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 股票基础信息 |
        | 外部接口 | `/api/v1/market/data/stock-description` |
        | 请求方式 | GET |
        | 数据范围 | 最新股票基础信息快照,无时间维度 |
        
        ## 请求参数
        
        | 参数 | 必选 | 说明 |
        |---|---|---|
        | `--symbol-id` | 否 | 股票代码,支持 `600000.SH`、`000001.SZ`、`920001.BJ`,兼容 `.XSHG`、`.XSHE`、`.BJSE` 和不带后缀的数字代码 |
        | `--page` | 否 | 页码,从 1 开始,默认由服务使用 1 |
        | `--page-size` | 否 | 每页条数,范围 1–200,默认 50 |
        
        通过根目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> stock-description-all --symbol-id 600000.SH --page 1 --page-size 1
        python <RUN_PY> stock-description-all --page 1 --page-size 50
        ```
        
        ## 响应结构
        
        成功响应固定为 `code`、`message`、`data`,其中 `data` 是分页对象:
        
        ```json
        {
          "code": 200,
          "message": "success",
          "data": {
            "pageNum": 1,
            "pageSize": 1,
            "total": 1,
            "pages": 1,
            "records": [{
              "symbol": "600000.SH",
              "name": "浦发银行",
              "company_name_zh": "上海浦东发展银行股份有限公司",
              "company_name_en": "Shanghai Pudong Development Bank Co., Ltd.",
              "pinyin": "pufayinhang",
              "listing_date": "1999-11-10",
              "shares": 29352080397,
              "float_a_shares": 29352080397,
              "non_float_a_shares": 0,
              "marginable": true,
              "st": false,
              "unprofitable": false,
              "status": "normal",
              "sectors": {"concept": [], "industry": null, "region": {}}
            }]
          }
        }
        ```
        
        `records` 还可能包含 `base_name`、`board`、`delisting_date`、`issue_price`、`bvps`、`eps_ttm`、`revenue_ttm`、`pe_ttm`、`roe_ttm`、`cum_adjust_factor`、`introduction`、`net_inflow_main` 和 `net_inflow_small`;这些字段可能为 `null`。`sectors` 包含 `concept`、`industry` 和 `region`,板块元素使用 `name_en`、`name_zh`。
        
        ## 注意事项
        
        - 返回股票代码统一为 `代码.SH`、`代码.SZ` 或 `代码.BJ`。
        - 不带后缀的数字代码按 6 位代码匹配;不同市场存在重号时建议携带交易所后缀。
        - 显式传入空字符串、无法识别的后缀、`page < 1` 或 `page-size` 不在 1–200 范围内时返回 HTTP 400。
        - 合法但没有匹配记录时仍返回成功响应,`records` 为空数组,`total` 和 `pages` 为 0。
        
    • stock-dividends
      • scripts
        • handler.py 4 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/stock-dividends'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='股票分红记录')
              parser.add_argument("--symbol")
              parser.add_argument("--since_date")
              parser.add_argument("--until_date")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--total", required=False)
              parser.add_argument("--ann_date", required=False)
              parser.add_argument("--reporting_period", required=False)
              parser.add_argument("--cash_dividend_ratio", required=False)
              parser.add_argument("--bonus_issue_ratio", required=False)
              parser.add_argument("--bonus_issue_from_capital_reserves_ratio", required=False)
              parser.add_argument("--ex_dividend_date", required=False)
              parser.add_argument("--ann_url", required=False)
              args = parser.parse_args()
              params = {}
              if args.symbol is not None: params["symbol"] = args.symbol
              if args.since_date is not None: params["since_date"] = args.since_date
              if args.until_date is not None: params["until_date"] = args.until_date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.symbol is not None: params["symbol"] = args.symbol
              if args.ann_date is not None: params["ann_date"] = args.ann_date
              if args.reporting_period is not None: params["reporting_period"] = args.reporting_period
              if args.cash_dividend_ratio is not None: params["cash_dividend_ratio"] = args.cash_dividend_ratio
              if args.bonus_issue_ratio is not None: params["bonus_issue_ratio"] = args.bonus_issue_ratio
              if args.bonus_issue_from_capital_reserves_ratio is not None: params["bonus_issue_from_capital_reserves_ratio"] = args.bonus_issue_from_capital_reserves_ratio
              if args.ex_dividend_date is not None: params["ex_dividend_date"] = args.ex_dividend_date
              if args.ann_url is not None: params["ann_url"] = args.ann_url
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 611 B
        ---
        name: stock-dividends
        description: 查询股票分红记录。接口:GET /api/v1/market/data/stock-dividends。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 股票分红记录
        
        接口:GET `/api/v1/market/data/stock-dividends`。参数和响应以 `ftshare-doc/api-doc/股票数据/参考数据/股票分红记录.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-dividends --symbol 600519.SH --page 1
        ```
        
    • stock-dividends-effective
      • scripts
        • handler.py 4.5 KB
          #!/usr/bin/env python3
          """股票有效分红记录(GET /api/v2/market/data/stock-dividends-effective)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v2/market/data/stock-dividends-effective"
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def build_params(args):
              params = {}
              if args.symbol is not None:
                  params["symbol"] = args.symbol
              if args.since_date is not None:
                  params["since_date"] = args.since_date
              if args.until_date is not None:
                  params["until_date"] = args.until_date
              params["page"] = args.page
              params["page_size"] = args.page_size
              return params
          
          
          def fetch_page(params):
              query = urllib.parse.urlencode(params)
              req = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}?{query}",
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="A 股已实施分红记录(派息、送股、转增)分页查询")
              parser.add_argument("--symbol", default=None,
                                  help="标的代码,支持带交易所后缀,如 600519.XSHG、000028.SZ;不传返回全市场")
              parser.add_argument("--since-date", dest="since_date", default=None,
                                  help="开始公告日期 YYYY-MM-DD;与 --until-date 成对传入")
              parser.add_argument("--until-date", dest="until_date", default=None,
                                  help="结束公告日期 YYYY-MM-DD;与 --since-date 成对传入")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始,默认 1")
              parser.add_argument("--page-size", dest="page_size", type=int, default=50,
                                  help="每页条数,默认 50,最大 200")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量数据")
              args = parser.parse_args()
          
              if (args.since_date is None) != (args.until_date is None):
                  print("--since-date 与 --until-date 必须成对传入", file=sys.stderr)
                  raise SystemExit(2)
          
              params = build_params(args)
              if args.fetch_all:
                  first = fetch_page(params)
                  data = first.get("data") or {}
                  records = list(data.get("records", []))
                  total_pages = data.get("pages") or 1
                  for page in range(2, total_pages + 1):
                      page_data = fetch_page({**params, "page": page})
                      records.extend((page_data.get("data") or {}).get("records", []))
                  result = {
                      "records": records,
                      "pages": total_pages,
                      "total": data.get("total", len(records)),
                  }
              else:
                  result = fetch_page(params)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2.5 KB
          #!/usr/bin/env python3
          """Tests for stock-dividends-effective handler"""
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          def _run(argv):
              with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                  with patch.object(sys, "argv", ["handler.py"] + argv):
                      with patch("sys.stdout", new_callable=StringIO) as out:
                          handler.main()
                          return json.loads(out.getvalue())
          
          
          class TestParams(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_rejects_unpaired_dates(self):
                  with self.assertRaises(SystemExit):
                      _run(["--since-date", "2026-05-26", "--page", "1", "--page-size", "5"])
                  with self.assertRaises(SystemExit):
                      _run(["--until-date", "2026-05-26", "--page", "1", "--page-size", "5"])
          
              @patch.object(handler, "safe_urlopen")
              def test_query_mapping(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = (
                      b'{"code":200,"data":{"records":[],"pages":1,"total":0}}'
                  )
                  _run(["--symbol", "002043.SZ", "--since-date", "2026-05-26",
                        "--until-date", "2026-05-26", "--page", "1", "--page-size", "5"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("/api/v2/market/data/stock-dividends-effective", req.full_url)
                  self.assertIn("symbol=002043.SZ", req.full_url)
                  self.assertIn("since_date=2026-05-26", req.full_url)
                  self.assertIn("until_date=2026-05-26", req.full_url)
          
          
          class TestFetchAll(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_all_aggregates_pages(self, mock_open):
                  page1 = {"code": 200, "data": {"records": [{"symbol": "600519.SH"}], "pages": 2, "total": 2}}
                  page2 = {"code": 200, "data": {"records": [{"symbol": "000028.SZ"}], "pages": 2, "total": 2}}
                  mock_open.return_value.__enter__.return_value.read.side_effect = [
                      json.dumps(page1).encode(), json.dumps(page2).encode(),
                  ]
                  result = _run(["--page", "1", "--page-size", "1", "--all"])
                  self.assertEqual(len(result["records"]), 2)
                  self.assertEqual(result["total"], 2)
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 3.5 KB
        ---
        name: stock-dividends-effective
        description: 查询 A 股已实施(有效)分红记录(stock_dividends_effective)。用户问已实施分红、每股派息、送股、转增、除权除息日、股权登记日、现金到账日、分红公告链接时使用。
        ---
        
        # 股票有效分红记录
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 股票有效分红记录(stock_dividends_effective) |
        | 外部接口 | `GET /api/v2/market/data/stock-dividends-effective` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 查询 A 股已实施分红记录(派息、送股、转增)分页视图,可按标的和公告日期范围筛选 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 否 | 标的代码 | 600519.XSHG | 支持带交易所后缀;不传返回全市场 |
        | since_date | string | 成对 | 开始公告日期 | 2026-05-26 | 格式 `YYYY-MM-DD`,按 `ann_date` 筛选 |
        | until_date | string | 成对 | 结束公告日期 | 2026-05-26 | 与 `since_date` 成对且不早于开始日期 |
        | page | int | 否 | 页码 | 1 | 从 1 开始,默认 1 |
        | page_size | int | 否 | 每页条数 | 50 | 默认 50,最大 200 |
        | --all | - | 否 | 自动翻页拉全量 | - | 仅本子 skill 扩展参数 |
        
        ## 3. 响应说明
        
        外层固定为 `code` / `message` / `data`。`data` 为分页对象:`pageNum` / `pageSize` / `total` / `pages` / `records`。
        
        records 元素:
        
        | 字段名 | 类型 | 说明 |
        |--------|------|------|
        | symbol | string | 标的代码(规范化为短后缀,如 `600519.SH`) |
        | ann_date | string | 公告日期 |
        | reporting_period | string | 分红所属报告期 |
        | cash_dividend_ratio | string | 每股税前现金分红 |
        | bonus_issue_ratio | string | 每股送股比例 |
        | bonus_issue_from_capital_reserves_ratio | string | 每股转增比例 |
        | ex_dividend_date | string / null | 除权除息日 |
        | record_date | string / null | 股权登记日 |
        | payout_date | string / null | 现金到账日 |
        | share_listing_date | string / null | 送转股上市流通日 |
        | ann_url | string / null | 公告链接 |
        | total_cash_dividend_ratio | string / null | 同一股票同一除权日综合每股税前现金分红 |
        | total_bonus_issue_ratio | string / null | 同一股票同一除权日综合每股送股比例 |
        | total_bonus_issue_from_capital_reserves_ratio | string / null | 同一股票同一除权日综合每股转增比例 |
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> stock-dividends-effective --symbol 600519.XSHG --page 1 --page-size 1
        python <RUN_PY> stock-dividends-effective --symbol 002043.SZ --since-date 2026-05-26 --until-date 2026-05-26
        python <RUN_PY> stock-dividends-effective --all
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - 只返回已实施的分红记录;未实施或已取消的不返回。
        - 同一股票同一除权日可能返回多条记录;三个 `total_*` 字段为该组合计,组内各条记录取值相同。
        - 输入代码会规范化为带交易所后缀的 `symbol`,例如输入 `600519.XSHG` 返回 `600519.SH`。
        - 日期按公告日期 `ann_date` 筛选,不限制日期跨度;`since_date` / `until_date` 必须成对传入(handler 会本地校验并拒绝)。
        - `--all` 会按 `pages` 自动翻页,把所有 `records` 合并为一个数组返回。
        
    • stock-filter-v2
      • scripts
        • handler.py 4.9 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/stock-list/filter'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='股票筛选')
              parser.add_argument("--symbol")
              parser.add_argument("--board")
              parser.add_argument("--listing_date_since")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--type", required=True)
              parser.add_argument("--symbol_id", required=True)
              parser.add_argument("--symbol_name", required=True)
              parser.add_argument("--close")
              parser.add_argument("--open")
              parser.add_argument("--high")
              parser.add_argument("--low")
              parser.add_argument("--prev_close")
              parser.add_argument("--change")
              parser.add_argument("--change_rate")
              parser.add_argument("--amplitude")
              parser.add_argument("--volume")
              parser.add_argument("--turnover")
              parser.add_argument("--change_rate_day5")
              parser.add_argument("--change_rate_day10")
              parser.add_argument("--change_rate_day20")
              parser.add_argument("--change_rate_day60")
              parser.add_argument("--change_rate_ytd")
              parser.add_argument("--ts_nanos")
              args = parser.parse_args()
              params = {}
              if args.symbol is not None: params["symbol"] = args.symbol
              if args.board is not None: params["board"] = args.board
              if args.listing_date_since is not None: params["listing_date_since"] = args.listing_date_since
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.type is not None: params["type"] = args.type
              if args.symbol is not None: params["symbol"] = args.symbol
              if args.symbol_id is not None: params["symbol_id"] = args.symbol_id
              if args.symbol_name is not None: params["symbol_name"] = args.symbol_name
              if args.close is not None: params["close"] = args.close
              if args.open is not None: params["open"] = args.open
              if args.high is not None: params["high"] = args.high
              if args.low is not None: params["low"] = args.low
              if args.prev_close is not None: params["prev_close"] = args.prev_close
              if args.change is not None: params["change"] = args.change
              if args.change_rate is not None: params["change_rate"] = args.change_rate
              if args.amplitude is not None: params["amplitude"] = args.amplitude
              if args.volume is not None: params["volume"] = args.volume
              if args.turnover is not None: params["turnover"] = args.turnover
              if args.change_rate_day5 is not None: params["change_rate_day5"] = args.change_rate_day5
              if args.change_rate_day10 is not None: params["change_rate_day10"] = args.change_rate_day10
              if args.change_rate_day20 is not None: params["change_rate_day20"] = args.change_rate_day20
              if args.change_rate_day60 is not None: params["change_rate_day60"] = args.change_rate_day60
              if args.change_rate_ytd is not None: params["change_rate_ytd"] = args.change_rate_ytd
              if args.ts_nanos is not None: params["ts_nanos"] = args.ts_nanos
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 640 B
        ---
        name: stock-filter-v2
        description: 查询股票筛选。接口:GET /api/v2/market/data/stock-list/filter。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 股票筛选
        
        接口:GET `/api/v2/market/data/stock-list/filter`。参数和响应以 `ftshare-doc/api-doc/股票数据/行情数据/股票筛选.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-filter-v2 --symbol 600519.SH --page 1 --type stock --symbol_id 1 --symbol_name 1
        ```
        
    • stock-ggmx-buy-ranking
      • scripts
        • handler.py 2.6 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/holder/stock-ggmx-buy-ranking'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description='查询董监高增持排名')
              parser.add_argument("--time-range", dest="time_range", default="1m")
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", dest="page_size", type=int, default=50)
          
              args = parser.parse_args()
              params = {}
              if args.time_range is not None:
                  params['time_range'] = args.time_range
              if args.page is not None:
                  params['page'] = args.page
              if args.page_size is not None:
                  params['page_size'] = args.page_size
          
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 638 B
        ---
        name: stock-ggmx-buy-ranking
        description: 查询董监高增持排行。可选 --time-range(1m/3m/6m/1y/2y)、--page、--page-size。接口:GET /api/v2/market/data/holder/stock-ggmx-buy-ranking。
        ---
        
        # 董监高增持排名
        
        查询董监高增持排行。可选 --time-range(1m/3m/6m/1y/2y)、--page、--page-size。接口:GET /api/v2/market/data/holder/stock-ggmx-buy-ranking。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> stock-ggmx-buy-ranking
        ```
        
    • stock-ggmx-sell-ranking
      • scripts
        • handler.py 2.6 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/holder/stock-ggmx-sell-ranking'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description='查询董监高减持排名')
              parser.add_argument("--time-range", dest="time_range", default="1m")
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", dest="page_size", type=int, default=50)
          
              args = parser.parse_args()
              params = {}
              if args.time_range is not None:
                  params['time_range'] = args.time_range
              if args.page is not None:
                  params['page'] = args.page
              if args.page_size is not None:
                  params['page_size'] = args.page_size
          
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 642 B
        ---
        name: stock-ggmx-sell-ranking
        description: 查询董监高减持排行。可选 --time-range(1m/3m/6m/1y/2y)、--page、--page-size。接口:GET /api/v2/market/data/holder/stock-ggmx-sell-ranking。
        ---
        
        # 董监高减持排名
        
        查询董监高减持排行。可选 --time-range(1m/3m/6m/1y/2y)、--page、--page-size。接口:GET /api/v2/market/data/holder/stock-ggmx-sell-ranking。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> stock-ggmx-sell-ranking
        ```
        
    • stock-goodwill-detail
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          """查询个股商誉明细"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/goodwill/stock-detail"
          
          
          def fetch(
          date: str) -> dict:
              params = urllib.parse.urlencode({"date": date})
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询个股商誉明细")
              parser.add_argument(
                  "--date",
                  required=True,
                  help="报告期日期,格式 YYYYMMDD,如 20251231 查询 2025 全年",
              )
              args = parser.parse_args()
          
              result = fetch(args.date)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.7 KB
        ---
        name: stock-goodwill-detail
        description: Get goodwill detail (商誉明细) for all stocks in a specific year. Use when user asks about 商誉, 商誉明细, 商誉规模, 商誉占净资产比例, goodwill.
        ---
        
        # 查询个股商誉明细
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询个股商誉明细 |
        | 外部接口 | GET /api/v1/market/data/goodwill/stock-detail |
        | 请求方式 | GET |
        | 适用场景 | 查询个股的商誉规模、商誉占净资产比例、净利润同比变化等明细数据 |
        
        ## 请求参数
        
        说明:date 为必填项。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | date | string | 是 | 报告期日期 | 20251231 | 格式 YYYYMMDD,按年份范围过滤,如 20251231 查询 2025 全年 |
        
        ## 执行方式
        
        ```bash
        python scripts/handler.py --date 20251231
        ```
        
        ## 响应结构
        
        ```json
        {
          "code": 0,
          "message": "success",
          "data": {
            "pageNum": 1,
            "pageSize": 50,
            "total": 2676,
            "pages": 54,
            "records": [
              {
                "seq": 0,
                "security_code": "300860",
                "security_name": "锋尚文化",
                "goodwill_scale": "28235811.5900",
                "goodwill_to_net_assets_ratio": "0.00898123",
                "net_profit_scale": "-16592410.5300",
                "net_profit_yoy_ratio": "-1.39720928",
                "goodwill_previous": "28235811.5900",
                "notice_date": "2026-05-13 00:00:00",
                "trade_board": "cyb"
              }
            ]
          }
        }
        ```
        
        ### 字段说明(GoodwillStockDetailItem)
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | seq | int | 否 | 序号 |
        | security_code | string | 否 | 证券代码 |
        | security_name | string | 是 | 证券简称 |
        | goodwill_scale | string | 是 | 商誉规模(元) |
        | goodwill_to_net_assets_ratio | string | 是 | 商誉占净资产比例 |
        | net_profit_scale | string | 是 | 净利润规模(元) |
        | net_profit_yoy_ratio | string | 是 | 净利润同比变化率 |
        | goodwill_previous | string | 是 | 上期商誉(元) |
        | notice_date | string | 是 | 公告日期,格式 YYYY-MM-DD HH:MM:SS |
        | trade_board | string | 是 | 交易板块(sh/sz/cyb/star/bj/hk) |
        
        ## 注意事项
        
        - date 格式为 YYYYMMDD,按年份范围过滤(如 20251231 查询 2025 全年数据)
        - 金额字段均为 Decimal 类型,以字符串形式返回以保持精度
        - notice_date 大部分常规商誉记录为 null,仅减值相关记录有公告日期
        - trade_board 取值:sh(上交所主板)、sz(深交所主板)、cyb(创业板)、star(科创板)、bj(北交所)、hk(港股)
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-goodwill-detail --date 20260828
        ```
        
    • stock-goodwill-impairment
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          """查询个股商誉减值明细"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/goodwill/stock-impairment"
          
          
          def fetch(
          date: str) -> dict:
              params = urllib.parse.urlencode({"date": date})
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询个股商誉减值明细")
              parser.add_argument(
                  "--date",
                  required=True,
                  help="报告期日期,格式 YYYYMMDD,如 20251231 查询 2025 全年",
              )
              args = parser.parse_args()
          
              result = fetch(args.date)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.9 KB
        ---
        name: stock-goodwill-impairment
        description: Get goodwill impairment detail (商誉减值明细) for all stocks in a specific year. Use when user asks about 商誉减值, 商誉减值损失, 商誉减值占净利润比例, goodwill impairment.
        ---
        
        # 查询个股商誉减值明细
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询个股商誉减值明细 |
        | 外部接口 | GET /api/v1/market/data/goodwill/stock-impairment |
        | 请求方式 | GET |
        | 适用场景 | 查询存在商誉减值的个股数据,包含减值金额、减值占净利润比例等字段 |
        
        ## 请求参数
        
        说明:date 为必填项。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | date | string | 是 | 报告期日期 | 20251231 | 格式 YYYYMMDD,按年份范围过滤,如 20251231 查询 2025 全年 |
        
        ## 执行方式
        
        ```bash
        python scripts/handler.py --date 20251231
        ```
        
        ## 响应结构
        
        ```json
        {
          "code": 0,
          "message": "success",
          "data": {
            "pageNum": 1,
            "pageSize": 50,
            "total": 2676,
            "pages": 54,
            "records": [
              {
                "seq": 0,
                "security_code": "300860",
                "security_name": "锋尚文化",
                "goodwill_scale": "28235811.5900",
                "goodwill_change": null,
                "goodwill_to_net_assets_ratio": "0.00898123",
                "net_profit_scale": "-16592410.5300",
                "goodwill_impairment_to_net_profit": null,
                "notice_date": "2026-05-13 00:00:00",
                "trade_board": "cyb"
              }
            ]
          }
        }
        ```
        
        ### 字段说明(GoodwillStockImpairmentItem)
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | seq | int | 否 | 序号 |
        | security_code | string | 否 | 证券代码 |
        | security_name | string | 是 | 证券简称 |
        | goodwill_scale | string | 是 | 商誉规模(元) |
        | goodwill_change | string | 是 | 商誉变动额(元) |
        | goodwill_to_net_assets_ratio | string | 是 | 商誉占净资产比例 |
        | net_profit_scale | string | 是 | 净利润规模(元) |
        | goodwill_impairment_to_net_profit | string | 是 | 商誉减值占净利润比例,公式 = goodwill_change / net_profit |
        | notice_date | string | 是 | 公告日期,格式 YYYY-MM-DD HH:MM:SS |
        | trade_board | string | 是 | 交易板块(sh/sz/cyb/star/bj/hk) |
        
        ## 注意事项
        
        - 与 stock-detail 共用数据表,多返回 goodwill_change 和 goodwill_impairment_to_net_profit 字段
        - goodwill_impairment_to_net_profit = goodwill_change / net_profit_scale,当分母为 0 或任一项为 null 时返回 null
        - 金额字段均为 Decimal 类型,以字符串形式返回以保持精度
        - trade_board 取值:sh(上交所主板)、sz(深交所主板)、cyb(创业板)、star(科创板)、bj(北交所)、hk(港股)
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-goodwill-impairment --date 20260828
        ```
        
    • stock-goodwill-industry
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          """查询行业商誉"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/goodwill/industry"
          
          
          def fetch(
          date: str) -> dict:
              params = urllib.parse.urlencode({"date": date})
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询行业商誉")
              parser.add_argument(
                  "--date",
                  required=True,
                  help="报告期日期,格式 YYYYMMDD,如 20251231 查询 2025 全年",
              )
              args = parser.parse_args()
          
              result = fetch(args.date)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2 KB
        ---
        name: stock-goodwill-industry
        description: Get goodwill data by industry (行业商誉). Use when user asks about 行业商誉, 各行业商誉, 行业商誉占比, goodwill by industry, industry goodwill.
        ---
        
        # 查询行业商誉
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询行业商誉 |
        | 外部接口 | GET /api/v1/market/data/goodwill/industry |
        | 请求方式 | GET |
        | 适用场景 | 查询各行业的商誉规模、商誉占净资产比例、净利润规模等汇总数据 |
        
        ## 请求参数
        
        说明:date 为必填项。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | date | string | 是 | 报告期日期 | 20251231 | 格式 YYYYMMDD,按年份范围过滤,如 20251231 查询 2025 全年 |
        
        ## 执行方式
        
        ```bash
        python scripts/handler.py --date 20251231
        ```
        
        ## 响应结构
        
        ```json
        {
          "code": 0,
          "message": "success",
          "data": {
            "date": "20251231",
            "total": 127,
            "items": [
              {
                "industry_name": "IT服务Ⅱ",
                "company_count": 67,
                "goodwill_scale": "32954873172.3000",
                "net_assets": "265068390706.9200",
                "goodwill_to_net_assets_ratio": "0.12432593",
                "net_profit_scale": "-1094685130.1200"
              }
            ]
          }
        }
        ```
        
        ### 字段说明(GoodwillIndustryItem)
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | industry_name | string | 否 | 行业名称 |
        | company_count | int | 是 | 公司数量 |
        | goodwill_scale | string | 是 | 商誉规模(元) |
        | net_assets | string | 是 | 净资产(元) |
        | goodwill_to_net_assets_ratio | string | 是 | 商誉占净资产比例 |
        | net_profit_scale | string | 是 | 净利润规模(元) |
        
        ## 注意事项
        
        - date 格式为 YYYYMMDD,按年份范围过滤
        - 金额字段均为 Decimal 类型,以字符串形式返回以保持精度
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-goodwill-industry --date 20260828
        ```
        
    • stock-goodwill-market-overview
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """查询A股商誉市场概况"""
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/goodwill/market-overview"
          
          
          def fetch() -> dict:
              url = f"{BASE_URL}{ENDPOINT}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              result = fetch()
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.3 KB
        ---
        name: stock-goodwill-market-overview
        description: Get A-share market-wide goodwill overview (A股商誉市场概况) with historical trends. Use when user asks about 商誉市场概况, 全市场商誉, 商誉历史趋势, goodwill market overview.
        ---
        
        # 查询A股商誉市场概况
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询A股商誉市场概况 |
        | 外部接口 | GET /api/v1/market/data/goodwill/market-overview |
        | 请求方式 | GET |
        | 适用场景 | 查询 A 股全市场商誉历史概况,包含商誉规模、减值金额、占净资产/净利润比例等汇总指标 |
        
        ## 请求参数
        
        无需参数,返回全部历史数据,按报告期倒序排列。
        
        ## 执行方式
        
        ```bash
        python scripts/handler.py
        ```
        
        ## 响应结构
        
        ```json
        {
          "code": 0,
          "message": "success",
          "data": {
            "total": 17,
            "items": [
              {
                "report_date": "2026-03-31 00:00:00",
                "goodwill_scale": "1224218430250.2300",
                "goodwill_impairment": null,
                "net_assets": "56278253157394.5000",
                "goodwill_to_net_assets_ratio": "0.02175296",
                "impairment_to_net_assets_ratio": null,
                "net_profit_scale": "1133013378865.3600",
                "impairment_to_net_profit_ratio": null
              }
            ]
          }
        }
        ```
        
        ### 字段说明(GoodwillMarketOverviewItem)
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | report_date | string | 是 | 报告期,格式 YYYY-MM-DD HH:MM:SS |
        | goodwill_scale | string | 是 | 全市场商誉规模(元) |
        | goodwill_impairment | string | 是 | 全市场商誉减值金额(元) |
        | net_assets | string | 是 | 全市场净资产(元) |
        | goodwill_to_net_assets_ratio | string | 是 | 商誉占净资产比例 |
        | impairment_to_net_assets_ratio | string | 是 | 减值占净资产比例 |
        | net_profit_scale | string | 是 | 全市场净利润规模(元) |
        | impairment_to_net_profit_ratio | string | 是 | 减值占净利润比例 |
        
        ## 注意事项
        
        - 无需参数,返回全量历史数据,按 report_date 倒序
        - 金额字段均为 Decimal 类型,以字符串形式返回以保持精度
        - goodwill_impairment 对应数据库 goodwill_change 列(response 层做了语义化重命名)
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-goodwill-market-overview
        ```
        
    • stock-goodwill-predict
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          """查询商誉减值预期明细"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/goodwill/predict"
          
          
          def fetch(
          date: str) -> dict:
              params = urllib.parse.urlencode({"date": date})
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询商誉减值预期明细")
              parser.add_argument(
                  "--date",
                  required=True,
                  help="报告期日期,格式 YYYYMMDD,如 20251231 查询 2025 全年",
              )
              args = parser.parse_args()
          
              result = fetch(args.date)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3 KB
        ---
        name: stock-goodwill-predict
        description: Get goodwill impairment prediction data (商誉减值预期) for stocks. Use when user asks about 商誉减值预期, 商誉减值预告, 业绩预告, goodwill impairment prediction, goodwill forecast.
        ---
        
        # 查询商誉减值预期明细
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询商誉减值预期明细 |
        | 外部接口 | GET /api/v1/market/data/goodwill/predict |
        | 请求方式 | GET |
        | 适用场景 | 查询商誉减值业绩预告数据,包含预测净利润上下限、业绩变动幅度、上年同期净利润等字段 |
        
        ## 请求参数
        
        说明:date 为必填项。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | date | string | 是 | 报告期日期 | 20251231 | 格式 YYYYMMDD,按年份范围过滤,如 20251231 查询 2025 全年 |
        
        ## 执行方式
        
        ```bash
        python scripts/handler.py --date 20251231
        ```
        
        ## 响应结构
        
        ```json
        {
          "code": 0,
          "message": "success",
          "data": {
            "pageNum": 1,
            "pageSize": 50,
            "total": 820,
            "pages": 17,
            "records": [
              {
                "seq": 0,
                "security_code": "601211",
                "security_name": "国泰海通",
                "perform_change_explain": "业绩变动原因说明...",
                "predict_period": "20251231",
                "newest_goodwill": "10000000000.0000",
                "goodwill_previous": "9500000000.0000",
                "predict_netprofit_lower": "50000000000.0000",
                "predict_netprofit_upper": "60000000000.0000",
                "perform_change_lower": "10.50000000",
                "perform_change_upper": "20.50000000",
                "pe_samereport_netprofit": "45000000000.0000",
                "notice_date": "2025-04-01 00:00:00",
                "trade_market": "主板"
              }
            ]
          }
        }
        ```
        
        ### 字段说明(GoodwillPredictItem)
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |--------|------|------------|------|
        | seq | int | 否 | 序号 |
        | security_code | string | 否 | 证券代码 |
        | security_name | string | 是 | 证券简称 |
        | perform_change_explain | string | 是 | 业绩变动原因 |
        | predict_period | string | 是 | 预告周期 |
        | newest_goodwill | string | 是 | 最新商誉(元) |
        | goodwill_previous | string | 是 | 上期商誉(元) |
        | predict_netprofit_lower | string | 是 | 预测净利润下限(元) |
        | predict_netprofit_upper | string | 是 | 预测净利润上限(元) |
        | perform_change_lower | string | 是 | 业绩变动下限(%) |
        | perform_change_upper | string | 是 | 业绩变动上限(%) |
        | pe_samereport_netprofit | string | 是 | 上年同期净利润(元) |
        | notice_date | string | 是 | 公告日期,格式 YYYY-MM-DD HH:MM:SS |
        | trade_market | string | 是 | 交易市场 |
        
        ## 注意事项
        
        - date 格式为 YYYYMMDD,按年份范围过滤
        - 金额字段均为 Decimal 类型,以字符串形式返回以保持精度
        - 业绩变动上/下限为百分比值(如 10.5 表示 10.5%)
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-goodwill-predict --date 20260828
        ```
        
    • stock-history-list
      • scripts
        • handler.py 2.7 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v1/market/data/stock-history-list"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {
              "FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"],
              "Content-Type": "application/json",
          } if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              for key, value in _REQUEST_HEADERS.items():
                  request.add_unredirected_header(key, value)
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询股票历史列表")
              parser.add_argument("--trade-date", dest="trade_date", required=True, help="交易日,格式 YYYYMMDD")
              parser.add_argument("--code", help="股票代码,可为 600000.SH、600000.XSHG 或纯六位数字")
              parser.add_argument("--page", type=int, default=1)
              parser.add_argument("--page-size", dest="page_size", type=int, default=50)
              args = parser.parse_args()
          
              params = {
                  "trade_date": args.trade_date,
                  "page": args.page,
                  "page_size": args.page_size,
              }
              if args.code is not None:
                  params["code"] = args.code
              request = urllib.request.Request(
                  BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params),
                  headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw"},
                  method="GET",
              )
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.2 KB
        ---
        name: stock-history-list
        description: 按交易日查询股票历史行情截面列表。必填 --trade-date,可选 --code、--page、--page-size。接口:GET /api/v1/market/data/stock-history-list。
        ---
        
        # 股票历史列表
        
        按交易日查询 A 股主板股票历史行情截面。接口:GET `/api/v1/market/data/stock-history-list`。
        
        注意:`region_sector`、`cum_adjust_factor` 不可用;`pe_ttm`、`concept_sectors` 仅 2026 年起有数据。
        
        ## 参数
        
        - `--trade-date`:必填,交易日,格式 `YYYYMMDD`。
        - `--code`:可选,股票代码,支持 `600000.SH`、`600000.XSHG` 或纯六位数字。
        - `--page`:可选,页码,默认 1。
        - `--page-size`:可选,每页条数,默认 50,最大 200。
        
        响应为统一 `code/message/data` JSON,分页记录位于 `data.records`。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送,并设置 `Content-Type: application/json`。缺失凭据时不会发起请求。
        
        ```bash
        python <RUN_PY> stock-history-list --trade-date 20260909 --page 1 --page-size 5
        python <RUN_PY> stock-history-list --trade-date 20260909 --code 600000.SH --page 1 --page-size 5
        ```
        
    • stock-holder-ften
      • scripts
        • handler.py 3.5 KB
          #!/usr/bin/env python3
          """查询 A 股十大流通股东。
          
          两种模式:
          1) 指定 stock_code:返回该标的全部历史十大流通股东(不分页)。
          2) is_last=true:返回所有标的最新一期十大流通股东(分页)。
          """
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/holder/stock-holder-ften"
          
          
          def build_params(stock_code, is_last, page, page_size):
              params = {}
              if stock_code:
                  params["stock_code"] = stock_code
              if is_last:
                  params["is_last"] = "true"
              if page is not None:
                  params["page"] = page
              if page_size is not None:
                  params["page_size"] = page_size
              return params
          
          
          def fetch(
          stock_code, is_last, page, page_size):
              params = urllib.parse.urlencode(build_params(stock_code, is_last, page, page_size))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询 A 股十大流通股东")
              parser.add_argument(
                  "--stock_code",
                  help="股票代码,需携带市场后缀,如 603323.SH / 000001.SZ / 833171.BJ;不传时需配合 --is_last",
              )
              parser.add_argument(
                  "--is_last",
                  action="store_true",
                  help="返回所有标的最新一期十大流通股东(分页)",
              )
              parser.add_argument("--page", type=int, help="页码,从 1 开始(默认 1)")
              parser.add_argument("--page_size", type=int, help="每页记录数(默认 50)")
              args = parser.parse_args()
          
              if not args.stock_code and not args.is_last:
                  parser.error("需指定 --stock_code 或 --is_last 至少其一")
          
              result = fetch(args.stock_code, args.is_last, args.page, args.page_size)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2 KB
          #!/usr/bin/env python3
          """Tests for stock-holder-ften handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_by_stock_code(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("000001.SZ", False, None, None)
                  url = mock_open.call_args[0][0]
                  self.assertIn("stock_code=000001.SZ", url)
                  self.assertNotIn("is_last", url)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_is_last_paging(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch(None, True, 1, 50)
                  url = mock_open.call_args[0][0]
                  self.assertIn("is_last=true", url)
                  self.assertIn("page=1", url)
                  self.assertIn("page_size=50", url)
                  self.assertNotIn("stock_code=", url)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json_for_stock_code(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  with patch.object(sys, "argv", ["handler.py", "--stock_code", "000001.SZ"]):
                      with patch("sys.stdout", new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
              def test_main_requires_stock_code_or_is_last(self):
                  with patch.object(sys, "argv", ["handler.py"]):
                      with self.assertRaises(SystemExit):
                          handler.main()
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 5.3 KB
        ---
        name: stock-holder-ften
        description: "查询 A 股十大流通股东。当用户需要获取 A 股十大流通股东信息(按 stock_code 查全部历史,或以 is_last=true 查全市场最新一期),支持沪深京股票,或了解单票/全市场十大流通股东时使用。"
        ---
        
        # 查询 A 股十大流通股东
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 查询 A 股十大流通股东                                              |
        | 外部接口 | `/api/v1/market/data/holder/stock-holder-ften`                |
        | 请求方式 | GET                                                                |
        | 适用场景 | 指定 stock_code 查全部历史十大流通股东,或 is_last=true 查全市场最新一期 |
        
        ## 请求参数
        
        | 参数名     | 类型   | 是否必填 | 描述            | 取值示例  | 备注                                                                                       |
        |------------|--------|----------|-----------------|-----------|--------------------------------------------------------------------------------------------|
        | stock_code | string | 否       | 单个股票代码    | 603323.SH | 不传时需配合 `--is_last`;支持沪深京 A 股,需 6 位数字+后缀(SH/SZ/BJ),单次仅支持一个代码 |
        | is_last    | flag   | 否       | 取所有标的最新一期 | (flag)  | 传字符串 `"true"`;与 `stock_code` 二选一                                                    |
        | page       | int    | 否       | 页码            | 1         | 默认 1,仅 `is_last=true` 模式下生效                                                        |
        | page_size  | int    | 否       | 每页记录数      | 50        | 默认 50                                                                                     |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        # 查询 603323.SH 所有公告期十大流通股东
        python <RUN_PY> stock-holder-ften --stock_code 603323.SH
        
        # 查询全市场最新一期(分页)
        python <RUN_PY> stock-holder-ften --is_last --page 1 --page_size 50
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        ```json
        {
            "items": [
                {
                    "stock_code": "603323.SH",
                    "stock_name": "苏农银行",
                    "publish_date": "2026-03-31",
                    "share_holding": 23.15,
                    "fen_holders": [
                        {
                            "rank": 1,
                            "shareholder_name": "香港中央结算有限公司",
                            "shareholder_type": "境外法人",
                            "share_type": "A股",
                            "shareholding": 55000000.0,
                            "share_ratio": 3.90,
                            "limit_num": null,
                            "unlimit_num": null,
                            "change_shares": 1200000.0,
                            "change_type": "增持",
                            "change_percentage": 2.23
                        }
                    ]
                }
            ],
            "total_pages": 1,
            "total_items": 21
        }
        ```
        
        ### 顶层字段说明
        
        | 字段名      | 类型  | 是否可为空 | 说明                             |
        |-------------|-------|------------|----------------------------------|
        | items       | Array | 否         | 公告期列表(每个元素对应一个公告期) |
        | total_pages | int   | 否         | 总页数                           |
        | total_items | int   | 否         | 总记录数                         |
        
        ### fen_holders 数组元素字段
        
        | 字段名            | 类型   | 是否可为空 | 说明                                       |
        |-------------------|--------|------------|--------------------------------------------|
        | rank              | int    | 否         | 股东名次                                   |
        | shareholder_name  | String | 否         | 股东名称                                   |
        | shareholder_type  | String | 否         | 股东性质                                   |
        | share_type        | String | 否         | 股份类型,固定为"A股"                       |
        | shareholding      | string | 否         | 持股数(股)                                |
        | share_ratio       | string | 否         | 占股本持股比例(%)                          |
        | limit_num         | string | 是         | 流通限售数量                                |
        | unlimit_num       | string | 是         | 流通无限售数量                              |
        | change_shares     | string | 否         | 增减(股)                                  |
        | change_type       | String | 是         | 变动类型                                    |
        | change_percentage | string | 是         | 变动比例(%)                                |
        
        ## 注意事项
        
        - 必须至少指定 `--stock_code` 或 `--is_last` 其一,否则报错。
        - 指定 `--stock_code` 时返回该标的全部历史,通常 `total_pages` 为 1。
        - `--is_last` 模式按 `page`/`page_size` 分页返回全市场最新一期。
        - `is_last` 在 query string 中传字符串 `"true"`,否则部分客户端反序列化会 400。
        - `fen_holders` 中 `limit_num`/`unlimit_num` 等字段可能为空,展示时需做空值处理。
        
    • stock-holder-nums
      • scripts
        • handler.py 3.4 KB
          #!/usr/bin/env python3
          """查询 A 股股东人数。
          
          两种模式:
          1) 指定 stock_code:返回该标的全部历史股东人数记录(不分页)。
          2) is_last=true:返回所有标的最新一期股东人数(分页)。
          """
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/holder/stock-holder-nums"
          
          
          def build_params(stock_code, is_last, page, page_size):
              params = {}
              if stock_code:
                  params["stock_code"] = stock_code
              if is_last:
                  params["is_last"] = "true"
              if page is not None:
                  params["page"] = page
              if page_size is not None:
                  params["page_size"] = page_size
              return params
          
          
          def fetch(
          stock_code, is_last, page, page_size):
              params = urllib.parse.urlencode(build_params(stock_code, is_last, page, page_size))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询 A 股股东人数")
              parser.add_argument(
                  "--stock_code",
                  help="股票代码,需携带市场后缀,如 603323.SH / 000001.SZ / 833171.BJ;不传时需配合 --is_last",
              )
              parser.add_argument(
                  "--is_last",
                  action="store_true",
                  help="返回所有标的最新一期股东人数(分页)",
              )
              parser.add_argument("--page", type=int, help="页码,从 1 开始(默认 1)")
              parser.add_argument("--page_size", type=int, help="每页记录数(默认 50)")
              args = parser.parse_args()
          
              if not args.stock_code and not args.is_last:
                  parser.error("需指定 --stock_code 或 --is_last 至少其一")
          
              result = fetch(args.stock_code, args.is_last, args.page, args.page_size)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2 KB
          #!/usr/bin/env python3
          """Tests for stock-holder-nums handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_by_stock_code(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("000001.SZ", False, None, None)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/holder/stock-holder-nums", url)
                  self.assertIn("stock_code=000001.SZ", url)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_is_last_paging(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch(None, True, 1, 50)
                  url = mock_open.call_args[0][0]
                  self.assertIn("is_last=true", url)
                  self.assertIn("page=1", url)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json_for_stock_code(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  with patch.object(sys, "argv", ["handler.py", "--stock_code", "000001.SZ"]):
                      with patch("sys.stdout", new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
              def test_main_requires_stock_code_or_is_last(self):
                  with patch.object(sys, "argv", ["handler.py"]):
                      with self.assertRaises(SystemExit):
                          handler.main()
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 4.8 KB
        ---
        name: stock-holder-nums
        description: "查询 A 股股东人数。当用户需要获取 A 股股东人数信息(按 stock_code 查全部历史,或以 is_last=true 查全市场最新一期),含人均流通股、筹码集中度、十大股东持股比例等衍生指标时使用。"
        ---
        
        # 查询 A 股股东人数
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 查询 A 股股东人数                                                  |
        | 外部接口 | `/api/v1/market/data/holder/stock-holder-nums`                |
        | 请求方式 | GET                                                                |
        | 适用场景 | 指定 stock_code 查全部历史股东人数,或 is_last=true 查全市场最新一期 |
        
        ## 请求参数
        
        | 参数名     | 类型   | 是否必填 | 描述            | 取值示例  | 备注                                                                                       |
        |------------|--------|----------|-----------------|-----------|--------------------------------------------------------------------------------------------|
        | stock_code | string | 否       | 单个股票代码    | 603323.SH | 不传时需配合 `--is_last`;支持沪深京 A 股,需 6 位数字+后缀(SH/SZ/BJ),单次仅支持一个代码 |
        | is_last    | flag   | 否       | 取所有标的最新一期 | (flag)  | 传字符串 `"true"`;与 `stock_code` 二选一                                                    |
        | page       | int    | 否       | 页码            | 1         | 默认 1,仅 `is_last=true` 模式下生效                                                        |
        | page_size  | int    | 否       | 每页记录数      | 50        | 默认 50                                                                                     |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> stock-holder-nums --stock_code 603323.SH
        python <RUN_PY> stock-holder-nums --is_last --page 1 --page_size 50
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        ```json
        {
            "items": [
                {
                    "stock_code": "000001.SZ",
                    "stock_name": "平安银行",
                    "publish_date": "2026-04-29",
                    "report_date": "2026-03-31",
                    "holder_num": 11215,
                    "holder_num_change_ratio": -5.5022,
                    "per_capita_circ_share": 4895.6833,
                    "per_capita_share_change_ratio": null,
                    "chip_concentration": null,
                    "close_price": "0",
                    "per_capita_hold_amount": "0",
                    "ten_holder_ratio": null,
                    "ften_holder_ratio": null
                }
            ],
            "total_pages": 1,
            "total_items": 5784
        }
        ```
        
        ### 顶层字段说明
        
        | 字段名      | 类型  | 是否可为空 | 说明           |
        |-------------|-------|------------|-----------------|
        | items       | Array | 否         | 股东人数列表    |
        | total_pages | int   | 否         | 总页数          |
        | total_items | int   | 否         | 总记录数        |
        
        ### items 元素字段
        
        | 字段名                       | 类型   | 是否可为空 | 说明                              |
        |------------------------------|--------|------------|-----------------------------------|
        | stock_code                   | string | 否         | 股票代码                          |
        | stock_name                   | string | 否         | 股票名称                          |
        | publish_date                 | string | 否         | 发布日期                          |
        | report_date                  | string | 否         | 报告期                            |
        | holder_num                   | int    | 否         | 股东人数                          |
        | holder_num_change_ratio      | string | 是         | 股东人数较上期变化比例             |
        | per_capita_circ_share        | string | 是         | 人均流通股数                       |
        | per_capita_share_change_ratio| string | 是         | 人均流通股变化比例                 |
        | chip_concentration           | string | 是         | 筹码集中度                         |
        | close_price                  | string | 否         | 收盘价                            |
        | per_capita_hold_amount       | string | 否         | 人均持股金额                       |
        | ten_holder_ratio             | string | 是         | 十大股东持股比例                   |
        | ften_holder_ratio            | string | 是         | 十大流通股东持股比例               |
        
        ## 注意事项
        
        - 必须至少指定 `--stock_code` 或 `--is_last` 其一。
        - `chip_concentration`/`ten_holder_ratio`/`ften_holder_ratio` 等字段部分标的暂为空。
        - `is_last` 在 query string 中传字符串 `"true"`。
        
    • stock-holder-ten
      • scripts
        • handler.py 3.4 KB
          #!/usr/bin/env python3
          """查询 A 股十大股东。
          
          两种模式:
          1) 指定 stock_code:返回该标的全部历史十大股东(不分页)。
          2) is_last=true:返回所有标的最新一期十大股东(分页)。
          """
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/holder/stock-holder-ten"
          
          
          def build_params(stock_code, is_last, page, page_size):
              params = {}
              if stock_code:
                  params["stock_code"] = stock_code
              if is_last:
                  params["is_last"] = "true"
              if page is not None:
                  params["page"] = page
              if page_size is not None:
                  params["page_size"] = page_size
              return params
          
          
          def fetch(
          stock_code, is_last, page, page_size):
              params = urllib.parse.urlencode(build_params(stock_code, is_last, page, page_size))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询 A 股十大股东")
              parser.add_argument(
                  "--stock_code",
                  help="股票代码,需携带市场后缀,如 603323.SH / 000001.SZ / 833171.BJ;不传时需配合 --is_last",
              )
              parser.add_argument(
                  "--is_last",
                  action="store_true",
                  help="返回所有标的最新一期十大股东(分页)",
              )
              parser.add_argument("--page", type=int, help="页码,从 1 开始(默认 1)")
              parser.add_argument("--page_size", type=int, help="每页记录数(默认 50)")
              args = parser.parse_args()
          
              if not args.stock_code and not args.is_last:
                  parser.error("需指定 --stock_code 或 --is_last 至少其一")
          
              result = fetch(args.stock_code, args.is_last, args.page, args.page_size)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2 KB
          #!/usr/bin/env python3
          """Tests for stock-holder-ten handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_by_stock_code(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("000001.SZ", False, None, None)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/holder/stock-holder-ten", url)
                  self.assertIn("stock_code=000001.SZ", url)
                  self.assertNotIn("is_last", url)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_is_last_paging(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch(None, True, 1, 50)
                  url = mock_open.call_args[0][0]
                  self.assertIn("is_last=true", url)
                  self.assertIn("page=1", url)
                  self.assertIn("page_size=50", url)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json_for_stock_code(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  with patch.object(sys, "argv", ["handler.py", "--stock_code", "000001.SZ"]):
                      with patch("sys.stdout", new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
              def test_main_requires_stock_code_or_is_last(self):
                  with patch.object(sys, "argv", ["handler.py"]):
                      with self.assertRaises(SystemExit):
                          handler.main()
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 5 KB
        ---
        name: stock-holder-ten
        description: "查询 A 股十大股东。当用户需要获取 A 股十大股东信息(按 stock_code 查全部历史,或以 is_last=true 查全市场最新一期),支持沪深京股票,或了解单票/全市场十大股东时使用。"
        ---
        
        # 查询 A 股十大股东
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 查询 A 股十大股东                                                  |
        | 外部接口 | `/api/v1/market/data/holder/stock-holder-ten`                  |
        | 请求方式 | GET                                                                |
        | 适用场景 | 指定 stock_code 查全部历史十大股东,或 is_last=true 查全市场最新一期 |
        
        ## 请求参数
        
        | 参数名     | 类型   | 是否必填 | 描述            | 取值示例  | 备注                                                                                       |
        |------------|--------|----------|-----------------|-----------|--------------------------------------------------------------------------------------------|
        | stock_code | string | 否       | 单个股票代码    | 603323.SH | 不传时需配合 `--is_last`;支持沪深京 A 股,需 6 位数字+后缀(SH/SZ/BJ),单次仅支持一个代码 |
        | is_last    | flag   | 否       | 取所有标的最新一期 | (flag)  | 传字符串 `"true"`;与 `stock_code` 二选一                                                    |
        | page       | int    | 否       | 页码            | 1         | 默认 1,仅 `is_last=true` 模式下生效                                                        |
        | page_size  | int    | 否       | 每页记录数      | 50        | 默认 50                                                                                     |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> stock-holder-ten --stock_code 603323.SH
        python <RUN_PY> stock-holder-ten --is_last --page 1 --page_size 50
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        ```json
        {
            "items": [
                {
                    "stock_code": "603323.SH",
                    "stock_name": "苏农银行",
                    "publish_date": "2026-03-31",
                    "share_holding": 50.96,
                    "fen_holders": [
                        {
                            "rank": 1,
                            "shareholder_name": "苏州国际发展集团有限公司",
                            "shareholder_type": "国有法人",
                            "share_type": "A股",
                            "shareholding": 200000000,
                            "share_ratio": 10.55,
                            "limit_num": 0,
                            "unlimit_num": 200000000,
                            "change_shares": 0,
                            "change_type": "不变",
                            "change_percentage": 0
                        }
                    ]
                }
            ],
            "total_pages": 1,
            "total_items": 21
        }
        ```
        
        ### 顶层字段说明
        
        | 字段名      | 类型  | 是否可为空 | 说明                             |
        |-------------|-------|------------|----------------------------------|
        | items       | Array | 否         | 公告期列表                       |
        | total_pages | int   | 否         | 总页数                           |
        | total_items | int   | 否         | 总记录数                         |
        
        ### fen_holders 数组元素字段
        
        | 字段名            | 类型   | 是否可为空 | 说明                                       |
        |-------------------|--------|------------|--------------------------------------------|
        | rank              | int    | 否         | 股东名次                                   |
        | shareholder_name  | String | 否         | 股东名称                                   |
        | shareholder_type  | String | 否         | 股东性质                                   |
        | share_type        | String | 否         | 股份类型                                   |
        | shareholding      | string | 否         | 持股数(股)                                |
        | share_ratio       | string | 否         | 占股本持股比例(%)                          |
        | limit_num         | string | 是         | 限售数量                                    |
        | unlimit_num       | string | 是         | 无限售数量                                  |
        | change_shares     | string | 否         | 增减(股)                                  |
        | change_type       | String | 是         | 变动类型                                    |
        | change_percentage | string | 是         | 变动比例(%)                                |
        
        ## 注意事项
        
        - 必须至少指定 `--stock_code` 或 `--is_last` 其一。
        - 指定 `--stock_code` 时返回该标的全部历史,通常 `total_pages` 为 1。
        - `--is_last` 模式按 `page`/`page_size` 分页返回全市场最新一期。
        - `is_last` 在 query string 中传字符串 `"true"`。
        - `fen_holders` 中 `limit_num`/`unlimit_num`/`change_type`/`change_percentage` 可能为空。
        
    • stock-income-all-stocks-specific-period
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          """查询指定报告期全市场利润表(分页)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          VALID_REPORT_TYPES = ["q1", "q2", "q3", "annual"]
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询指定报告期全市场利润表(分页)")
              parser.add_argument("--year", type=int, required=True, help="报告所属年度,如 2025")
              parser.add_argument("--report-type", required=True, choices=VALID_REPORT_TYPES,
                                  help="报告期类型:q1(一季报)/ q2(半年报)/ q3(三季报)/ annual(年报)")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始(默认 1)")
              parser.add_argument("--page-size", type=int, default=20, help="每页记录数(默认 20)")
              args = parser.parse_args()
          
              params = {
                  "year": args.year,
                  "report_type": args.report_type,
                  "page": args.page,
                  "page_size": args.page_size,
              }
              url = f"{BASE_URL}/api/v1/market/data/finance/income?" + urllib.parse.urlencode(params)
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3.6 KB
        ---
        name: stock-income-all-stocks-specific-period
        description: Get income statement (利润表) for all stocks in a specific reporting period. Use when user asks about 指定报告期利润表, 全市场利润表, all stocks income for a specific year and period.
        ---
        
        # 查询单报告期全市场利润表(分页)
        
        ## 接口说明
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询单报告期所有票利润表 |
        | 外部接口 | GET /api/v1/market/data/finance/income |
        | 请求方式 | GET |
        | 适用场景 | 获取 A 股指定报告期所有股票的利润表,支持沪深京股票,支持分页 |
        
        ## 请求参数
        
        说明:year、report_type、page、page_size 为必填项。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | year | int | 是 | 报告所属年度 | 2025 | 必填 |
        | report_type | string | 是 | 报告期类型标识 | q2 | q1(一季报)、q2(半年报)、q3(三季报)、annual(全年报) |
        | page | int | 是 | 页码 | 1 | 从 1 开始 |
        | page_size | int | 是 | 每页记录数 | 20 | 建议 10–100 |
        
        ## 执行方式
        
        ```bash
        python scripts/handler.py --year 2025 --report-type q2 --page 1 --page-size 20
        ```
        
        ## 响应结构
        
        ```json
        {
          "items": [ { "stock_code", "stock_name", "year", "report_type", "report_type_cn", "publish_date", "report_form_type", "t_revenue", "n_profit", ... } ],
          "total_pages": 283,
          "total_items": 5652
        }
        ```
        
        ### 字段说明(IncomeItem)
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |--------|------|------------|------|------|
        | stock_code | String | 否 | 股票交易代码,带 .SZ/.SH/.BJ 后缀 | - |
        | stock_name | String | 否 | 上市公司股票名称 | - |
        | year | int | 否 | 报告所属年度 | - |
        | report_type | String | 否 | 报告期类型:q1/q2/q3/annual | - |
        | report_type_cn | String | 否 | 报告期类型中文:一季报/半年报/三季报/年报 | - |
        | publish_date | String | 否 | 报告发布日期,YYYY-MM-DD | - |
        | report_form_type | String | 否 | 报表类型:合并未调整/合并调整 | - |
        | t_revenue | float | 否 | 营业总收入 | 元 |
        | t_revenue_yoy | float | 是 | 营业总收入同比增长率,无上期时为 null | % |
        | cost | float | 否 | 营业支出(主营业务成本) | 元 |
        | cost_yoy | float | 是 | 主营业务成本同比增长率 | % |
        | t_cost | float | 否 | 营业总支出 | 元 |
        | t_cost_yoy | float | 是 | 营业总支出同比增长率 | % |
        | sale_expense | float | 否 | 销售费用 | 元 |
        | sale_expense_yoy | float | 是 | 销售费用同比增长率 | % |
        | manag_expense | float | 否 | 管理费用 | 元 |
        | manag_expense_yoy | float | 是 | 管理费用同比增长率 | % |
        | financial_cost | float | 否 | 财务费用 | 元 |
        | financial_cost_yoy | float | 是 | 财务费用同比增长率 | % |
        | profit | float | 否 | 营业利润 | 元 |
        | profit_yoy | float | 是 | 营业利润同比增长率 | % |
        | t_profit | float | 否 | 利润总额 | 元 |
        | t_profit_yoy | float | 是 | 利润总额同比增长率 | % |
        | n_profit | float | 否 | 净利润 | 元 |
        | n_profit_yoy | float | 是 | 净利润同比增长率 | % |
        | parcomp_n_profit | float | 否 | 扣非归母净利润 | 元 |
        | parcomp_n_profit_yoy | float | 是 | 扣非归母净利润同比增长率 | % |
        
        ## 注意事项
        
        - report_type 取值:q1(一季报)、q2(半年报)、q3(三季报)、annual(年报)。
        - 需要全量数据时,按 page 递增请求直到 page > total_pages。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-income-all-stocks-specific-period --year 2025 --report-type annual --page 1 --page-size 5
        ```
        
    • stock-institution-holdings
      • scripts
        • handler.py 2.8 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/share/stock-institution-holdings'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='机构持股')
              parser.add_argument("--year", required=True)
              parser.add_argument("--report_type", required=True)
              parser.add_argument("--institution_type", required=True)
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              args = parser.parse_args()
              params = {}
              if args.year is not None: params["year"] = args.year
              if args.report_type is not None: params["report_type"] = args.report_type
              if args.institution_type is not None: params["institution_type"] = args.institution_type
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 705 B
        ---
        name: stock-institution-holdings
        description: 查询机构持股。接口:GET /api/v2/market/data/share/stock-institution-holdings。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 机构持股
        
        接口:GET `/api/v2/market/data/share/stock-institution-holdings`。参数和响应以 `ftshare-doc/api-doc/股票数据/基础数据/机构持股.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-institution-holdings --year 2025 --report_type annual --institution_type all_inst --page 1 --page_size 5
        ```
        
    • stock-institution-holdings-detail
      • scripts
        • handler.py 2.9 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v2/market/data/share/stock-institution-holdings-detail'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='机构持股明细')
              parser.add_argument("--stock_code", required=True)
              parser.add_argument("--year", required=True)
              parser.add_argument("--report_type", required=True)
              parser.add_argument("--institution_type", required=True)
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              args = parser.parse_args()
              params = {}
              if args.stock_code is not None: params["stock_code"] = args.stock_code
              if args.year is not None: params["year"] = args.year
              if args.report_type is not None: params["report_type"] = args.report_type
              if args.institution_type is not None: params["institution_type"] = args.institution_type
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 771 B
        ---
        name: stock-institution-holdings-detail
        description: 查询机构持股明细。接口:GET /api/v2/market/data/share/stock-institution-holdings-detail。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 机构持股明细
        
        接口:GET `/api/v2/market/data/share/stock-institution-holdings-detail`。参数和响应以 `ftshare-doc/api-doc/股票数据/基础数据/机构持股明细.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-institution-holdings-detail --stock_code 600519 --year 2025 --report_type annual --institution_type all_inst --page 1 --page_size 5
        ```
        
    • stock-intraday
      • scripts
        • handler.py 3.6 KB
          #!/usr/bin/env python3
          """查询股票跨日分时行情(GET /api/v4/market/data/stock-intraday)"""
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v4/market/data/stock-intraday"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          RANGES = ("Today", "FiveDays")
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              for key, value in _REQUEST_HEADERS.items():
                  request.add_unredirected_header(key, value)
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def build_query(symbol, range_value, days, ts_ms):
              params = {"symbol": symbol}
              if range_value is not None:
                  params["range"] = range_value
              if days is not None:
                  params["days"] = days
              if ts_ms is not None:
                  params["ts_ms"] = ts_ms
              return params
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询股票跨日分时行情(逐分钟价格、均价、日累计量额)")
              parser.add_argument("--symbol", required=True, help="股票代码,需带市场后缀,如 600000.SH、000001.SZ")
              parser.add_argument("--range", dest="range_value", choices=RANGES, default=None,
                                  help="预置区间:Today(当日)/FiveDays(当日及此前 4 个交易日,默认)")
              parser.add_argument("--days", type=int, default=None,
                                  help="查询当日及此前 N-1 个交易日,范围 1~5;与 --range 同时传入时以 --days 为准")
              parser.add_argument("--ts-ms", dest="ts_ms", type=int, default=None,
                                  help="当日过滤起点(毫秒,包含起点);不能用于指定历史日期")
              args = parser.parse_args()
          
              if args.days is not None and not 1 <= args.days <= 5:
                  print("--days 须在 1~5 之间", file=sys.stderr)
                  raise SystemExit(2)
          
              params = build_query(args.symbol, args.range_value, args.days, args.ts_ms)
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params)
              request = urllib.request.Request(
                  url,
                  headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"},
                  method="GET",
              )
              try:
                  with safe_urlopen(request) as response:
                      print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
      • SKILL.md 4.1 KB
        ---
        name: stock-intraday
        description: 股票跨日分时行情,含均价与日累计量额(stock_intraday,GET /api/v4/market/data/stock-intraday)。用户问某只股票的分时数据、当日/近五日逐分钟价格与均价、日累计成交量额时使用。必填 --symbol;可选 --range、--days、--ts-ms,三者按 ts_ms > days > range 优先级生效。
        ---
        
        # 股票跨日分时行情
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 股票跨日分时行情(stock_intraday) |
        | 外部接口 | `GET /api/v4/market/data/stock-intraday` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 查询单只股票的分钟分时价格、均价及日累计成交量额;支持当日实时分时与近期历史分时合并返回 |
        | 数据范围 | 当日及近期历史交易日,时间范围按北京时间(Asia/Shanghai)计算 |
        | 单次限量 | 单只股票;`days` 为 1~5;无分页,返回所选范围内的可用数据点 |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | symbol | string | 是 | 股票代码(需带市场后缀) | 600000.SH | 不接受裸代码;也接受 `600000.XSHG` 等 MIC 后缀 |
        | range | string | 否 | 预置区间 | Today | `Today`(当日)/`FiveDays`(当日及此前 4 个交易日,默认);大小写敏感 |
        | days | int | 否 | 查询当日及此前 N−1 个交易日 | 3 | 范围 1~5,`1` 表示当日 |
        | ts_ms | int | 否 | 当日过滤起点(毫秒,包含起点) | 1789522140000 | 只过滤当日数据,不能通过它指定历史日期;早于当日零点时按当日零点处理 |
        
        三个时间参数可以同时传入,按 `ts_ms` > `days` > `range` 选择生效参数,低优先级参数不参与范围计算;三者均不传时默认 `FiveDays`。
        
        ## 3. 响应说明
        
        外层固定为 `code`(成功 200)/ `message`(成功 `success`)/ `data`。`data` 为分时点数组,按时间升序排列,同一分钟只返回一个数据点,无分页:
        
        | 字段名 | 类型 | 说明 | 单位 |
        |--------|------|------|------|
        | ts_ms | int | 分钟起点的 Unix 毫秒时间戳 | 毫秒 |
        | price | number | 成交价格;历史分钟取该分钟收盘价,不复权 | 元 |
        | avg_price | number / null | 当日累计成交均价;不可用时为 `null`,字段仍返回 | 元 |
        | volume | int | 当日累计成交量 | 股 |
        | turnover | number | 当日累计成交额 | 元 |
        
        注:`price` / `avg_price` / `turnover` 是 JSON 数字,不是字符串。历史均价按当日累计成交额除以累计成交量计算,累计成交量为零时均价为 `null`。
        
        ## 4. 调用方式
        
        ```bash
        # 当日分时
        python <RUN_PY> stock-intraday --symbol 600000.SH --range Today
        
        # 默认五日范围(不传任何时间参数)
        python <RUN_PY> stock-intraday --symbol 600000.SH
        
        # 当日及此前两个交易日
        python <RUN_PY> stock-intraday --symbol 600000.SH --days 3
        
        # 显式指定五日范围
        python <RUN_PY> stock-intraday --symbol 600000.SH --range FiveDays
        
        # 同时传入 days 与 range 时 days 优先,此请求按三日范围查询
        python <RUN_PY> stock-intraday --symbol 600000.SH --days 3 --range Today
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - `symbol` 必填且需带市场后缀,不接受裸代码。
        - `days=3` 表示当日及此前 2 个交易日,历史日期跳过休市日;例如在周二且周一、上周五均为交易日时,范围覆盖上周五、周一和周二。
        - 当日按自然日计算:非交易日不会自动用上一个交易日替换当日,因此实际有数据的日期数可能少于 `days`。
        - `ts_ms` 只过滤当日数据;传入过去日期的时间戳不会返回该历史日期。跨日查询使用 `days` 或 `range`。
        - 历史分钟价格不复权。
        - 行情查询所需数据不可用时接口返回错误,不会以缺少部分日期的数据冒充完整成功结果;HTTP 400 表示参数问题,405 不支持的方法,502 行情查询失败,503 行情服务暂不可用。
    • stock-ipos
      • scripts
        • handler.py 3.1 KB
          #!/usr/bin/env python3
          """获取 A 股 IPO 列表,支持分页与全量拉取"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/stock-ipos"
          
          
          def fetch_page(page: int, page_size: int) -> dict:
              params = urllib.parse.urlencode({"page": page, "page_size": page_size})
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="获取 A 股 IPO 列表")
              parser.add_argument("--page", type=int, default=1, help="页码(从 1 开始)")
              parser.add_argument("--page_size", type=int, default=20, help="每页记录数")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量数据")
              args = parser.parse_args()
          
              if args.fetch_all:
                  first = fetch_page(1, args.page_size)
                  all_items = list(first.get("items", []))
                  total_pages = first.get("total_pages", 1)
                  for p in range(2, total_pages + 1):
                      page_data = fetch_page(p, args.page_size)
                      all_items.extend(page_data.get("items", []))
                  result = {
                      "items": all_items,
                      "total_pages": total_pages,
                      "total_items": first.get("total_items", len(all_items)),
                  }
              else:
                  result = fetch_page(args.page, args.page_size)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 3 KB
        ---
        name: stock-ipos
        description: "获取股票 IPO 列表。当用户需要获取 A 股 IPO 列表,含发行价格、发行数量、申购日期、上市日期等,支持分页查询,或了解股票 IPO 列表时使用。"
        ---
        
        # 获取股票 IPO 列表
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 获取股票 IPO 列表 |
        | 外部接口 | `/api/v1/market/data/stock-ipos` |
        | 请求方式 | GET |
        | 适用场景 | 获取 A 股 IPO 列表,含发行价格、发行数量、申购日期、上市日期等,支持分页查询 |
        
        ## 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |---|---|---|---|---|---|
        | `page` | int | 是 | 页码,从 1 开始 | `1` | 必须大于等于 1 |
        | `page_size` | int | 是 | 每页记录数 | `20` | 必须大于等于 1,建议不超过 100 |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        # 获取第 1 页,每页 20 条
        python <RUN_PY> stock-ipos --page 1 --page_size 20
        
        # 获取第 2 页,每页 50 条
        python <RUN_PY> stock-ipos --page 2 --page_size 50
        
        # 获取全量数据(自动翻页合并)
        python <RUN_PY> stock-ipos --all
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        ```json
        {
            "items": [
                {
                    "symbol": "600989.SH",
                    "industry_pe": 15.8,
                    "listing_date": "2019-05-16",
                    "max_subscription_shares": 120000,
                    "online_shares": 600000000,
                    "pe": 22.5,
                    "price": "11.25",
                    "shares": 734000000,
                    "subscription_date": "2019-05-10",
                    "subscription_symbol_id": "730989"
                }
            ],
            "total_pages": 10,
            "total_items": 200
        }
        ```
        
        ### 顶层字段说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |---|---|---|---|
        | `items` | Array | 否 | 当前页 IPO 列表 |
        | `total_pages` | int | 否 | 总页数 |
        | `total_items` | int | 否 | 总记录数(未分页前) |
        
        ### items 元素字段说明(StockIpo)
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |---|---|---|---|---|
        | `symbol` | String | 否 | 标的代码,带 `.SZ`/`.SH`/`.BJ` 市场后缀 | - |
        | `industry_pe` | float | 是 | 行业市盈率 | - |
        | `listing_date` | String | 是 | 上市日期,格式 `YYYY-MM-DD` | - |
        | `max_subscription_shares` | int | 是 | 申购上限 | 股 |
        | `online_shares` | int | 否 | 网上发行数量 | 股 |
        | `pe` | float | 是 | 发行市盈率 | - |
        | `price` | String | 是 | 发行价格 | 元 |
        | `shares` | int | 否 | 发行数量 | 股 |
        | `subscription_date` | String | 是 | 申购日期,格式 `YYYY-MM-DD` | - |
        | `subscription_symbol_id` | String | 是 | 申购代码 | - |
        
        ## 注意事项
        
        - `page` 和 `page_size` 为必填项
        - 如需全量数据,按 `page` 递增循环请求至 `total_pages`,合并所有 `items`
        - 可为空字段(如 `industry_pe`、`listing_date` 等)可能返回 `null`,展示时需做空值处理
        - `price` 为字符串类型,比较或计算前需转为数值
        
    • stock-limit
      • scripts
        • handler.py 2.6 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/stk-limit'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='涨跌停价')
              parser.add_argument("--instrument_type")
              parser.add_argument("--symbol", required=True)
              parser.add_argument("--trade_date")
              parser.add_argument("--start_date")
              parser.add_argument("--end_date")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              args = parser.parse_args()
              params = {}
              for name in ("instrument_type", "symbol", "trade_date", "start_date", "end_date", "page", "page_size"):
                  value = getattr(args, name)
                  if value is not None:
                      params[name] = value
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 573 B
        ---
        name: stock-limit
        description: 查询涨跌停价。接口:GET /api/v1/market/data/stk-limit。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 涨跌停价
        
        接口:GET `/api/v1/market/data/stk-limit`。参数和响应以 `ftshare-doc/api-doc/股票数据/行情数据/涨跌停价.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-limit --symbol 600519.SH --page 1
        ```
        
    • stock-list-all-stocks
      • scripts
        • handler.py 2 KB
          #!/usr/bin/env python3
          """获取所有 A 股股票代码列表"""
          import json
          import sys
          import urllib.error
          import urllib.request
          import urllib.parse
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          
          def main():
              _require_api_key()
              url = f"{BASE_URL}/api/v1/market/data/stock-list"
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.5 KB
        ---
        name: stock-list-all-stocks
        description: "获取所有股票代码列表。当用户需要获取所有 A 股股票的代码和名称列表,支持沪深京股票,自动返回最新交易日的数据,或了解所有股票代码列表时使用。"
        ---
        
        # 获取所有股票代码列表
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 获取所有股票代码列表 |
        | 外部接口 | `/api/v1/market/data/stock-list` |
        | 请求方式 | GET |
        | 适用场景 | 获取所有 A 股股票的代码和名称列表,支持沪深京股票,自动返回最新交易日的数据 |
        
        ## 请求参数
        
        无需任何参数。
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> stock-list-all-stocks
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应结构
        
        ```json
        {
            "items": [
                {
                    "stock_code": "000001.SZ",
                    "stock_name": "平安银行"
                },
                {
                    "stock_code": "000002.SZ",
                    "stock_name": "万科A"
                }
            ]
        }
        ```
        
        ### 字段说明
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |---|---|---|---|
        | `stock_code` | String | 否 | 股票交易代码,固定携带 `.SZ`/`.SH`/`.BJ` 市场后缀 |
        | `stock_name` | String | 否 | 股票名称 |
        
        ## 注意事项
        
        - 返回全量 A 股,数据量较大(通常 5000 条以上),按需筛选
        - `stock_code` 固定含市场后缀,不要自行增减
        - 当查询没有数据时,返回空数组 `[]`
        
    • stock-minutes
      • scripts
        • handler.py 3 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v2/market/data/stock_minutes"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询股票历史分钟行情")
              parser.add_argument("--symbol", required=True)
              parser.add_argument("--interval-value", dest="interval_value", type=int, default=1)
              parser.add_argument("--adjust-kind", dest="adjust_kind")
              parser.add_argument("--since-ts-millis", dest="since_ts_millis", required=True, type=int)
              parser.add_argument("--until-ts-millis", dest="until_ts_millis", required=True, type=int)
              parser.add_argument("--limit", type=int, default=50)
              args = parser.parse_args()
              if args.interval_value <= 0 or not 1 <= args.limit <= 1000:
                  parser.error("interval-value 须大于 0,limit 须在 1~1000 之间")
              params = {"symbol": args.symbol, "interval_value": args.interval_value,
                        "since_ts_millis": args.since_ts_millis, "until_ts_millis": args.until_ts_millis,
                        "limit": args.limit}
              if args.adjust_kind is not None:
                  params["adjust_kind"] = args.adjust_kind
              request = urllib.request.Request(BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params),
                                               headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 874 B
        ---
        name: stock-minutes
        description: 股票历史分钟行情。必填 --symbol;可选日期和分页参数。 用户询问股票历史分钟行情时使用。
        ---
        
        # 股票历史分钟行情
        
        外部接口:`GET /api/v2/market/data/stock_minutes`。
        
        必填 --symbol;可选日期和分页参数。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> stock-minutes --symbol 000001.SZ --since-ts-millis 1787189400000 --until-ts-millis 1787191200000 --limit 5
        ```
        
        接口返回 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        响应为数组结构(非分页),每根 K 线字段:open / high / low / close / ts_millis / ts_millis_open / turnover / volume / turnover_rate(换手率)。
        
    • stock-minutes-batch
      • scripts
        • handler.py 1.7 KB
          #!/usr/bin/env python3
          import argparse,json,os,sys,urllib.parse,urllib.request
          BASE_URL=os.environ.get("FTSHARE_BASE_URL","https://market.ft.tech/gateway").rstrip("/");_REQUEST_HEADERS={"FTSHARE_API_KEY":os.environ.get("FTSHARE_API_KEY", ""),"Content-Type":"application/json"}; ENDPOINT="/api/v2/market/data/stock_minutes/batch"
          def main():
           p=argparse.ArgumentParser(); p.add_argument("--symbols",required=True); p.add_argument("--since-ts-millis",required=True,type=int); p.add_argument("--until-ts-millis",required=True,type=int); p.add_argument("--interval-value",type=int,default=1); p.add_argument("--adjust-kind",choices=["None","Forward","Backward"],default="None"); p.add_argument("--limit",type=int,default=50); a=p.parse_args()
           if a.interval_value<1 or not 1<=a.limit<=1000:p.error("interval-value 须大于 0,limit 须在 1~1000 之间")
           key=os.environ.get("FTSHARE_API_KEY")
           if not key: print("FTSHARE_API_KEY environment variable is required",file=sys.stderr); raise SystemExit(2)
           params=[("symbols",s.strip()) for s in a.symbols.split(",") if s.strip()]+[("since_ts_millis",a.since_ts_millis),("until_ts_millis",a.until_ts_millis),("interval_value",a.interval_value),("limit",a.limit)]
           if a.adjust_kind!="None":params.append(("adjust_kind",a.adjust_kind))
           req=urllib.request.Request(BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params),headers={"FTSHARE_API_KEY":key,"Content-Type":"application/json"},method="GET")
           try:
            with urllib.request.urlopen(req,timeout=30) as r: print(json.dumps(json.loads(r.read().decode()),ensure_ascii=False,indent=2))
           except Exception as e: print(f"请求失败: {e}",file=sys.stderr); raise SystemExit(1)
          if __name__=="__main__":main()
          
      • SKILL.md 840 B
        ---
        name: stock-minutes-batch
        description: 批量查询 股票历史分钟 K 线。用户询问多只股票的分钟行情时使用。
        ---
        
        # 批量股票历史分钟行情
        
        接口:`GET /api/v2/market/data/stock_minutes/batch`。必填 `--symbols`、`--since-ts-millis`、`--until-ts-millis`;`--symbols` 使用逗号分隔,最多 20 只;可选 `--interval-value`、`--limit`,时间跨度不超过 3 天,`limit` 范围 1~1000。
        
        ```bash
        python <RUN_PY> stock-minutes-batch --symbols 600519.SH,000001.SZ --since-ts-millis 1788919020000 --until-ts-millis 1788920820000 --limit 5
        ```
        
        返回 `code/message/data`,`data` 为每只标的的 `symbol`、`total` 和 `items` 列表;不是分页接口。每根 K 线字段:open / high / low / close / ts_millis / ts_millis_open / turnover / volume / turnover_rate(换手率)。
        
    • stock-performance-express-all-stocks-specific-period
      • scripts
        • handler.py 2.9 KB
          #!/usr/bin/env python3
          """查询指定报告期所有股票业绩快报(分页)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          VALID_REPORT_TYPES = ["q1", "q2", "q3", "annual"]
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询指定报告期全市场业绩快报(分页)")
              parser.add_argument("--year", type=int, required=True, help="报告所属年度,如 2025")
              parser.add_argument("--report-type", required=True, choices=VALID_REPORT_TYPES,
                                  help="报告期类型:q1(一季报)/ q2(半年报)/ q3(三季报)/ annual(年报)")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始(默认 1)")
              parser.add_argument("--page-size", type=int, default=20, help="每页记录数(默认 20)")
              args = parser.parse_args()
          
              params = {
                  "year": args.year,
                  "report_type": args.report_type,
                  "page": args.page,
                  "page_size": args.page_size,
              }
              url = f"{BASE_URL}/api/v1/market/data/finance/stock-performance-express?" + urllib.parse.urlencode(params)
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.4 KB
        ---
        name: stock-performance-express-all-stocks-specific-period
        description: Get performance express (业绩快报) for all stocks in a specific reporting period. Use when user asks about 指定报告期业绩快报, 全市场业绩快报, all stocks performance express for a specific year and period.
        ---
        
        # 查询指定报告期全市场业绩快报(分页)
        
        ## 参数
        
        | 参数 | 类型 | 必填 | 说明 | 示例 |
        |------|------|------|------|------|
        | `--year` | int | 是 | 报告所属年度 | `2024` |
        | `--report-type` | string | 是 | 报告期类型:`q1`/`q2`/`q3`/`annual` | `annual` |
        | `--page` | int | 否 | 页码,从 1 开始(默认 1) | `1` |
        | `--page-size` | int | 否 | 每页记录数(默认 20) | `20` |
        
        ## 用法
        
        从用户问题中提取年度和报告期类型,执行:
        
        ```bash
        python scripts/handler.py --year 2024 --report-type annual --page 1 --page-size 20
        ```
        
        脚本输出带分页信息的 JSON,每项含 `stock_code`、`stock_name`、`eps`、`total_revenue`、`net_profit`、`roe` 等字段,以表格展示给用户。
        
        ## 注意
        
        - `report-type` 取值:`q1`(一季报)、`q2`(半年报)、`q3`(三季报)、`annual`(年报)
        - 需要全量数据时,循环请求直到 `page > total_pages`
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-performance-express-all-stocks-specific-period --year 2025 --report-type annual --page 1 --page-size 5
        ```
        
    • stock-performance-forecast-all-stocks-specific-period
      • scripts
        • handler.py 2.9 KB
          #!/usr/bin/env python3
          """查询指定报告期所有股票业绩预告(分页)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          VALID_REPORT_TYPES = ["q1", "q2", "q3", "annual"]
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询指定报告期全市场业绩预告(分页)")
              parser.add_argument("--year", type=int, required=True, help="报告所属年度,如 2025")
              parser.add_argument("--report-type", required=True, choices=VALID_REPORT_TYPES,
                                  help="报告期类型:q1(一季报)/ q2(中报)/ q3(三季报)/ annual(年报)")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始(默认 1)")
              parser.add_argument("--page-size", type=int, default=20, help="每页记录数(默认 20)")
              args = parser.parse_args()
          
              params = {
                  "year": args.year,
                  "report_type": args.report_type,
                  "page": args.page,
                  "page_size": args.page_size,
              }
              url = f"{BASE_URL}/api/v1/market/data/finance/stock-performance-forecast?" + urllib.parse.urlencode(params)
          
              try:
                  with safe_urlopen(url) as resp:
                      data = json.loads(resp.read().decode())
                  print(json.dumps(data, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 1.4 KB
        ---
        name: stock-performance-forecast-all-stocks-specific-period
        description: Get performance forecast (业绩预告) for all stocks in a specific reporting period. Use when user asks about 指定报告期业绩预告, 全市场业绩预告, all stocks performance forecast for a specific year and period.
        ---
        
        # 查询指定报告期全市场业绩预告(分页)
        
        ## 参数
        
        | 参数 | 类型 | 必填 | 说明 | 示例 |
        |------|------|------|------|------|
        | `--year` | int | 是 | 报告所属年度 | `2024` |
        | `--report-type` | string | 是 | 报告期类型:`q1`/`q2`/`q3`/`annual` | `annual` |
        | `--page` | int | 否 | 页码,从 1 开始(默认 1) | `1` |
        | `--page-size` | int | 否 | 每页记录数(默认 20) | `20` |
        
        ## 用法
        
        从用户问题中提取年度和报告期类型,执行:
        
        ```bash
        python scripts/handler.py --year 2024 --report-type annual --page 1 --page-size 20
        ```
        
        脚本输出带分页信息的 JSON,每项含 `stock_code`、`stock_name`、`forecast_type`、`net_profit_min`、`net_profit_max`、`net_profit_yoy` 等字段,以表格展示给用户。
        
        ## 注意
        
        - `report-type` 取值:`q1`(一季报)、`q2`(中报)、`q3`(三季报)、`annual`(年报)
        - 需要全量数据时,循环请求直到 `page > total_pages`
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-performance-forecast-all-stocks-specific-period --year 2025 --report-type annual --page 1 --page-size 5
        ```
        
    • stock-prospectuses
      • scripts
        • handler.py 5.2 KB
          #!/usr/bin/env python3
          """招股书列表(GET /api/v2/market/data/announcements/stock-prospectuses)"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v2/market/data/announcements/stock-prospectuses"
          
          DEFAULT_PAGE_SIZE = 20
          MAX_PAGE_SIZE = 500
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              base_parsed = urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base_parsed.scheme or parsed.netloc != base_parsed.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          
          def build_params(args):
              params = {"page": args.page, "page_size": args.page_size}
              if args.stock_code is not None:
                  params["stock_code"] = args.stock_code
              if args.start_date is not None:
                  params["start_date"] = args.start_date
              if args.end_date is not None:
                  params["end_date"] = args.end_date
              return params
          
          
          def fetch_page(params):
              query = urllib.parse.urlencode(params)
              req = urllib.request.Request(
                  f"{BASE_URL}{ENDPOINT}?{query}",
                  headers={**HEADERS, **_REQUEST_HEADERS},
                  method="GET",
              )
              try:
                  with safe_urlopen(req) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  print(f"HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Request failed: {e}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(
                  description="招股书列表:按标的(--stock-code)或按单日披露日期(--start-date)查询"
              )
              parser.add_argument("--stock-code", dest="stock_code", default=None,
                                  help="股票代码,支持裸代码/短后缀/长后缀,如 600000、600000.SH、600000.XSHG")
              parser.add_argument("--start-date", dest="start_date", default=None,
                                  help="披露日期 YYYYMMDD(未提供 stock-code 时必填,仅支持单日)")
              parser.add_argument("--end-date", dest="end_date", default=None,
                                  help="披露日期 YYYYMMDD;不填默认等于 start-date,传入时必须相等")
              parser.add_argument("--page", type=int, default=1, help="页码,从 1 开始,默认 1")
              parser.add_argument("--page-size", dest="page_size", type=int, default=DEFAULT_PAGE_SIZE,
                                  help=f"每页条数,默认 {DEFAULT_PAGE_SIZE},最大 {MAX_PAGE_SIZE}")
              parser.add_argument("--all", action="store_true", dest="fetch_all", help="自动翻页获取全量数据")
              args = parser.parse_args()
          
              if args.stock_code is None and args.start_date is None:
                  print("必须提供 --stock-code(按标的查)或 --start-date(按日期查)", file=sys.stderr)
                  raise SystemExit(2)
              if args.page < 1:
                  print("--page 须大于等于 1", file=sys.stderr)
                  raise SystemExit(2)
              if not 1 <= args.page_size <= MAX_PAGE_SIZE:
                  print(f"--page-size 须在 1~{MAX_PAGE_SIZE} 之间", file=sys.stderr)
                  raise SystemExit(2)
              if args.stock_code is None and args.end_date is not None and args.end_date != args.start_date:
                  print("按日期查询仅支持单日:--end-date 必须等于 --start-date", file=sys.stderr)
                  raise SystemExit(2)
          
              params = build_params(args)
              if args.fetch_all:
                  first = fetch_page(params)
                  data = first.get("data") or {}
                  records = list(data.get("records", []))
                  total_pages = data.get("pages") or 1
                  for page in range(2, total_pages + 1):
                      page_data = fetch_page({**params, "page": page})
                      records.extend((page_data.get("data") or {}).get("records", []))
                  result = {
                      "records": records,
                      "pages": total_pages,
                      "total": data.get("total", len(records)),
                  }
              else:
                  result = fetch_page(params)
          
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
        • test_handler.py 3.6 KB
          #!/usr/bin/env python3
          """Tests for stock-prospectuses handler"""
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          def _run(argv):
              with patch.dict(os.environ, {"FTSHARE_API_KEY": "test-key"}):
                  with patch.object(sys, "argv", ["handler.py"] + argv):
                      with patch("sys.stdout", new_callable=StringIO) as out:
                          handler.main()
                          return json.loads(out.getvalue())
          
          
          class TestModes(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_requires_code_or_date(self):
                  with self.assertRaises(SystemExit):
                      _run(["--page", "1", "--page-size", "5"])
          
              def test_rejects_page_size_over_max(self):
                  with self.assertRaises(SystemExit):
                      _run(["--stock-code", "600000.SH", "--page-size", "501"])
          
              def test_rejects_date_range(self):
                  with self.assertRaises(SystemExit):
                      _run(["--start-date", "20240517", "--end-date", "20240518"])
          
              @patch.object(handler, "safe_urlopen")
              def test_by_symbol(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = (
                      b'{"code":200,"data":{"records":[],"pages":1,"total":0}}'
                  )
                  _run(["--stock-code", "600000.SH", "--page", "1", "--page-size", "5"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("/api/v2/market/data/announcements/stock-prospectuses", req.full_url)
                  self.assertIn("stock_code=600000.SH", req.full_url)
                  self.assertIn("page=1", req.full_url)
                  self.assertIn("page_size=5", req.full_url)
          
              @patch.object(handler, "safe_urlopen")
              def test_by_single_date(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = (
                      b'{"code":200,"data":{"records":[],"pages":1,"total":0}}'
                  )
                  _run(["--start-date", "20240517", "--page", "1", "--page-size", "5"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("start_date=20240517", req.full_url)
                  self.assertNotIn("end_date", req.full_url)
          
              @patch.object(handler, "safe_urlopen")
              def test_symbol_wins_over_date(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = (
                      b'{"code":200,"data":{"records":[],"pages":1,"total":0}}'
                  )
                  _run(["--stock-code", "600000.SH", "--start-date", "20240517"])
                  req = mock_open.call_args[0][0]
                  self.assertIn("stock_code=600000.SH", req.full_url)
                  self.assertIn("start_date=20240517", req.full_url)
          
          
          class TestFetchAll(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_all_aggregates_pages(self, mock_open):
                  page1 = {"code": 200, "data": {"records": [{"url_hash": "a"}], "pages": 2, "total": 2}}
                  page2 = {"code": 200, "data": {"records": [{"url_hash": "b"}], "pages": 2, "total": 2}}
                  mock_open.return_value.__enter__.return_value.read.side_effect = [
                      json.dumps(page1).encode(), json.dumps(page2).encode(),
                  ]
                  result = _run(["--start-date", "20240517", "--page", "1", "--page-size", "1", "--all"])
                  self.assertEqual([r["url_hash"] for r in result["records"]], ["a", "b"])
                  self.assertEqual(result["total"], 2)
                  second_req = mock_open.call_args_list[1][0][0]
                  self.assertIn("page=2", second_req.full_url)
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 4.2 KB
        ---
        name: stock-prospectuses
        description: 查询 A 股招股书及相关发行公告列表(stock_prospectuses,GET /api/v2/market/data/announcements/stock-prospectuses)。用户问招股书、招股说明书、招股意向书、某只股票的发行公告、某个披露日的全市场招股书、招股书文件下载标识时使用。必填 --stock-code 或 --start-date 之一;可选 --end-date、--page、--page-size、--all。
        ---
        
        # 招股书列表
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 招股书列表(stock_prospectuses) |
        | 外部接口 | `GET /api/v2/market/data/announcements/stock-prospectuses` |
        | 请求方式 | GET(query 参数) |
        | 适用场景 | 按标的查单只股票的招股书及发行公告,或按披露日查全市场记录;文件可按返回的 `url_hash` 另行下载 |
        | 数据范围 | 上交所、深交所、北交所公开披露的招股书及相关发行文件 |
        
        两种模式二选一:
        
        1. **按标的**:传 `stock_code`,查该股票的全部记录(此时忽略 `start_date` / `end_date`)。
        2. **按日期**:未传 `stock_code` 时,按 `start_date` 查该披露日的全市场记录;仅支持单日。
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | stock_code | string | 二选一 | 股票代码 | 600000.SH | 支持裸代码(`600000`)、短后缀(`.SH`/`.SZ`/`.BJ`)和长后缀(`.XSHG`/`.XSHE`/`.BJSE`),后缀大小写不敏感 |
        | start_date | string | 二选一 | 披露日期 | 20240517 | `YYYYMMDD`;未提供 `stock_code` 时必填,仅支持单日 |
        | end_date | string | 否 | 披露日期 | 20240517 | 不填默认等于 `start_date`,传入时必须与 `start_date` 相同 |
        | page | int | 否 | 页码 | 1 | 从 1 开始,默认 1 |
        | page_size | int | 否 | 每页条数 | 20 | 默认 20,最大 500 |
        | --all | - | 否 | 自动翻页拉全量 | - | 仅本子 skill 扩展参数,按 `pages` 翻页 |
        
        ## 3. 响应说明
        
        外层固定为 `code`(成功 200)/ `message`(成功 `success`)/ `data`(失败为 `null`)。`data` 为分页对象:`pageNum` / `pageSize` / `total` / `pages` / `records`(无记录时 `pages` 为 0、`records` 为 `[]`)。
        
        `records` 元素:
        
        | 字段名 | 类型 | 说明 |
        |--------|------|------|
        | stock_code | string | 股票代码,统一返回 `.SH` / `.SZ` / `.BJ` 短后缀 |
        | stock_name | string | 股票名称;源数据未提供时为空字符串 |
        | announcement_title | string | 公告标题 |
        | announcement_time | string | 公告披露时间,格式 `YYYY-MM-DD HH:MM:SS` |
        | column_type | string | 栏目类型,固定为 `stock` |
        | url_hash | string | 文件下载标识,用于下载关联文件 |
        
        使用 `--all` 时输出聚合结构 `{"records": [...], "pages": N, "total": M}`,不再保留逐页信封。
        
        ## 4. 调用方式
        
        ```bash
        python <RUN_PY> stock-prospectuses --stock-code 600000.SH --page 1 --page-size 5
        python <RUN_PY> stock-prospectuses --stock-code 600000 --page 1 --page-size 5
        python <RUN_PY> stock-prospectuses --start-date 20240517 --page 1 --page-size 20
        python <RUN_PY> stock-prospectuses --stock-code 600000.SH --all
        ```
        
        `<RUN_PY>` 为主 SKILL.md 同级 `run.py` 的绝对路径。输出 JSON;HTTP 错误输出到 stderr 并以非零状态退出。
        
        ## 5. 注意事项
        
        - 必须提供 `stock_code` 或 `start_date` 之一,否则服务端返回 400「需要 stock_code 或 start_date 参数」(handler 会本地校验并拒绝)。
        - 同时提供两者时按标的查询,`start_date` / `end_date` 被忽略。
        - 按日期查询仅支持单日:`end_date` 不填默认等于 `start_date`,填了则必须相等,否则服务端返回 400。
        - `page_size` 超过 500 返回 400;`--page` / `--page-size` 可不传,默认 `1` / `20`。
        - `stock_name` 可能为空字符串,不是缺失字段。
        - 文件下载:`GET /api/v2/market/data/announcements/stock-prospectuses/{url_hash}`,响应为附件(PDF 为 `application/pdf`,另可能为 `text/plain` 或 `text/html`);`url_hash` 须取自本接口返回值且为 64 位小写十六进制 sha256,勿硬编码。本子 skill 不直接下载,需要时用 `curl -OJ` 取文件。
    • stock-quotes-list
      • scripts
        • handler.py 5.3 KB
          #!/usr/bin/env python3
          """查询 A 股行情列表(分页,daec stocks)"""
          import argparse
          import json
          import sys
          import time
          import urllib.error
          import urllib.parse
          import urllib.request
          from datetime import datetime, timedelta, timezone
          from http.client import IncompleteRead
          from typing import Optional
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BEIJING_TZ = timezone(timedelta(hours=8))
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v1/market/data/daec/stocks"
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          HEADERS = {
              "X-Client-Name": "ft-claw",
              "Content-Type": "application/json",
          }
          
          
          def ms_to_iso(ms: Optional[int]) -> Optional[str]:
              """将毫秒时间戳转为北京时间 ISO 字符串(YYYY-MM-DDTHH:mm:ss)。"""
              if ms is None:
                  return None
              return datetime.fromtimestamp(ms / 1000.0, tz=BEIJING_TZ).strftime("%Y-%m-%dT%H:%M:%S")
          
          
          def build_params(order_by=None, ob=None, filter_=None, masks=None, page_size=None, page_no=None, filter_index=None):
              """构造 daec 分页列表查询参数。
          
              关键映射:v2 的 `page_no` → daec 的 `page`(daec 忽略 page_no,不转则永远返回第一页);
              `ob` 作为 `order_by` 的别名。注:daec 当前忽略 masks(返回全字段)。
              """
              params = {}
              sort = order_by if order_by else ob
              if sort:
                  params["order_by"] = sort
              if filter_:
                  params["filter"] = filter_
              if masks:
                  params["masks"] = masks
              if page_size is not None:
                  params["page_size"] = page_size
              if page_no is not None:
                  params["page"] = page_no  # v2 page_no → daec page
              if filter_index is not None:
                  params["filter_index"] = filter_index
              return params
          
          
          MAX_RETRIES = 5
          _SLEEP_BASE = 0.3  # 指数退避基准秒
          
          
          def _get_json(url: str):
              """发起 GET 并解析 JSON。daec 大响应偶发截断,对传输类错误重试。"""
              last_exc = None
              for attempt in range(MAX_RETRIES):
                  req = urllib.request.Request(url, headers={**HEADERS, **_REQUEST_HEADERS})
                  try:
                      with safe_urlopen(req) as resp:
                          return json.loads(resp.read().decode())
                  except urllib.error.HTTPError as e:
                      body = e.read().decode()
                      print(f"HTTP {e.code}: {body}", file=sys.stderr)
                      sys.exit(1)
                  except (IncompleteRead, OSError) as e:
                      last_exc = e
                      if attempt < MAX_RETRIES - 1:
                          time.sleep(_SLEEP_BASE * (2 ** attempt))
              print(f"请求失败(重试 {MAX_RETRIES} 次仍截断或网络错误): {last_exc}", file=sys.stderr)
              sys.exit(1)
          
          
          def fetch(
          order_by=None, ob=None, filter_=None, masks=None, page_size=None, page_no=None, filter_index=None):
              params = build_params(order_by, ob, filter_, masks, page_size, page_no, filter_index)
              url = f"{BASE_URL}{ENDPOINT}?{urllib.parse.urlencode(params)}"
              data = _get_json(url)
              # items 每条的交易所时间戳 ts_millis(毫秒)转为北京时间 ISO 字符串
              for item in data.get("items", []):
                  if isinstance(item, dict) and "ts_millis" in item:
                      item["ts_millis"] = ms_to_iso(item["ts_millis"])
              return data
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询 A 股行情列表(分页,daec)")
              parser.add_argument("--order_by", required=True, help='排序规则,如 change_rate desc')
              parser.add_argument("--page_no", type=int, required=True, help="页码,从 1 开始")
              parser.add_argument("--page_size", type=int, required=True, help="每页记录数")
              parser.add_argument("--filter", default="", help="筛选条件表达式,可选")
              parser.add_argument("--masks", default="", help="返回字段掩码(daec 当前忽略,返回全字段)")
              args = parser.parse_args()
          
              result = fetch(
                  order_by=args.order_by,
                  filter_=args.filter,
                  masks=args.masks,
                  page_size=args.page_size,
                  page_no=args.page_no,
              )
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 5.1 KB
          #!/usr/bin/env python3
          """Tests for stock-quotes-list handler"""
          import json
          import sys
          import unittest
          import urllib.error
          from http.client import IncompleteRead
          from io import BytesIO, StringIO
          from unittest.mock import patch
          
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestBuildParams(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_page_no_mapped_to_page(self):
                  p = handler.build_params(page_no=2, page_size=10)
                  self.assertEqual(p["page"], 2)
                  self.assertEqual(p["page_size"], 10)
                  self.assertNotIn("page_no", p)  # 关键:daec 不认 page_no
          
              def test_no_page_when_page_no_none(self):
                  self.assertNotIn("page", handler.build_params(order_by="change_rate desc"))
          
              def test_ob_to_order_by(self):
                  self.assertEqual(handler.build_params(ob="change_rate desc"), {"order_by": "change_rate desc"})
          
              def test_order_by_preferred_over_ob(self):
                  p = handler.build_params(order_by="close desc", ob="change_rate desc")
                  self.assertEqual(p["order_by"], "close desc")
          
              def test_filter_masks_injected(self):
                  p = handler.build_params(filter_="close != null", masks="name,close")
                  self.assertEqual(p["filter"], "close != null")
                  self.assertEqual(p["masks"], "name,close")
          
              def test_empty_filter_masks_omitted(self):
                  p = handler.build_params(filter_="", masks="")
                  self.assertNotIn("filter", p)
                  self.assertNotIn("masks", p)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
                  patch.object(handler.time, "sleep").start()
                  self.addCleanup(patch.stopall)
          
              @patch.object(handler, "safe_urlopen")
              def test_url_uses_daec_with_page_from_page_no(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'{"items":[]}'
                  handler.fetch(order_by="change_rate desc", page_no=2, page_size=10)
                  url = mock_open.call_args[0][0].full_url
                  self.assertIn("daec/stocks", url)
                  self.assertIn("page=2", url)
                  self.assertNotIn("page_no", url)  # 关键
                  self.assertIn("page_size=10", url)
                  self.assertIn("order_by=change_rate", url)
                  self.assertNotIn("/api/v2/", url)
          
              @patch.object(handler, "safe_urlopen")
              def test_items_ts_millis_converted(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = (
                      b'{"items":[{"symbol":"600000.XSHG","close":"9.32","ts_millis":1780300795000}],"page":1,"total_pages":1,"total_items":1}'
                  )
                  result = handler.fetch(order_by="change_rate desc", page_no=1, page_size=10)
                  self.assertEqual(len(result["items"]), 1)
                  self.assertIsInstance(result["items"][0]["ts_millis"], str)
                  self.assertIn("T", result["items"][0]["ts_millis"])
          
              @patch.object(handler, "safe_urlopen")
              def test_http_error_exits(self, mock_open):
                  mock_open.side_effect = urllib.error.HTTPError(
                      "https://fake", 500, "Internal Error", {}, BytesIO(b"server error")
                  )
                  with self.assertRaises(SystemExit):
                      handler.fetch(order_by="change_rate desc", page_no=1, page_size=10)
          
              @patch.object(handler, "safe_urlopen")
              def test_retries_on_incomplete_read(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.side_effect = [
                      IncompleteRead(b"partial"),
                      b'{"items":[],"page":1,"total_pages":1,"total_items":0}',
                  ]
                  handler.fetch(order_by="change_rate desc", page_no=1, page_size=10)
                  self.assertEqual(mock_open.call_count, 2)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
                  patch.object(handler.time, "sleep").start()
                  self.addCleanup(patch.stopall)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_basic(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b'{"items":[],"page":1,"total_pages":1,"total_items":0}'
                  with patch.object(sys, "argv", ["handler.py", "--order_by", "change_rate desc", "--page_no", "1", "--page_size", "10"]):
                      with patch("sys.stdout", new_callable=StringIO) as fake_out:
                          handler.main()
                          result = json.loads(fake_out.getvalue())
                          self.assertEqual(result["page"], 1)
          
              def test_main_missing_page_no_exits(self):
                  with patch.object(sys, "argv", ["handler.py", "--order_by", "change_rate desc", "--page_size", "10"]):
                      with self.assertRaises(SystemExit):
                          handler.main()
          
          
          class TestSafeUrlopen(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              def test_rejects_non_https(self):
                  with self.assertRaises(SystemExit):
                      handler.safe_urlopen("http://market.ft.tech/api")
          
              def test_rejects_wrong_host(self):
                  with self.assertRaises(SystemExit):
                      handler.safe_urlopen("https://evil.com/api")
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 5.2 KB
        ---
        name: stock-quotes-list
        description: "查询 A 股行情列表(分页)。当用户需要获取 A 股(沪深京)股票行情列表,支持按板块筛选、多字段排序与分页,用于行情中心「个股行情」等列表展示,或了解A 股行情列表(分页)时使用。"
        ---
        
        # 查询 A 股行情列表(分页)
        
        ## 接口说明
        
        | 项目 | 说明 |
        |---|---|
        | 接口名称 | 查询 A 股行情列表(分页) |
        | 外部接口 | `/api/v1/market/data/daec/stocks` |
        | 请求方式 | GET |
        | 适用场景 | 获取 A 股(沪深京)股票行情列表,支持按板块筛选、多字段排序与分页,用于行情中心「个股行情」等列表展示 |
        
        请求头要求:必须携带 `X-Client-Name: ft-claw`,否则返回参数错误。
        
        ## 请求参数
        
        说明:`order_by`、`page_no`、`page_size` 为必填项;`filter`、`masks` 为可选项,用于筛选与字段控制。
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |---|---|---|---|---|---|
        | order_by | string | 是 | 排序规则,格式为「字段 排序方向」 | change_rate desc | 常用:change_rate desc、change desc、latest desc、turnover desc、volume desc、market_cap_total desc、turnover_rate desc、change_rate_5d desc、amplitude desc;方向为 asc/desc |
        | page_no | int | 是 | 页码,从 1 开始 | 1 | 必须大于等于 1 |
        | page_size | int | 是 | 每页记录数 | 30 | 必须大于等于 1,建议不超过 100 |
        | filter | string | 否 | 筛选条件表达式 | (ex_id = "XSHE" OR ex_id = "XSHG" OR ex_id = "BJSE") AND (latest != null) | 见下方「filter 常用取值」 |
        | masks | string | 否 | 返回字段掩码/控制 | - | 不传则返回默认字段集 |
        
        ### filter 常用取值(按板块)
        
        | 板块 | filter 取值 |
        |---|---|
        | 全部股票 | (ex_id = "XSHE" OR ex_id = "XSHG" OR ex_id = "BJSE") AND (latest != null) |
        | 上交主板 | ex_id = "XSHG" AND latest != null |
        | 深交主板 | ex_id = "XSHE" AND latest != null |
        | 北交主板 | ex_id = "BJSE" AND latest != null |
        | 科创板 | board = "XSHG_STAR" AND latest != null |
        | 创业板 | board = "XSHE_CHI_NEXT" AND latest != null |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> stock-quotes-list --order_by "change_rate desc" --page_no 1 --page_size 30
        ```
        
        可选参数:`--filter`、`--masks`。示例(仅科创板):
        
        ```bash
        python <RUN_PY> stock-quotes-list --order_by "change_rate desc" --page_no 1 --page_size 30 --filter 'board = "XSHG_STAR" AND latest != null'
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径,参见主 SKILL.md 的「调用方式」说明。
        
        ## 响应说明
        
        返回当前页股票列表及总条数,数据模型如下:
        
        ```json
        {
            "total_size": 5000,
            "stocks": [ { "StockInfo" } ]
        }
        ```
        
        ### 顶层字段
        
        | 字段名 | 类型 | 是否可为空 | 说明 |
        |---|---|---|---|
        | total_size | int | 否 | 符合筛选条件的总记录数,用于分页计算 |
        | stocks | array | 否 | 当前页股票列表,元素为 StockInfo |
        
        ### StockInfo 结构
        
        | 字段名 | 类型 | 是否可为空 | 说明 | 单位 |
        |---|---|---|---|---|
        | name | string | 是 | 股票名称 | - |
        | symkey | string | 是 | 股票唯一标识,带市场后缀(如 920036.BJ、002445.SZ) | - |
        | symbol_id | string | 是 | 证券代码(不含市场后缀) | - |
        | ex_id | string | 是 | 交易所标识:XSHG(上交所)、XSHE(深交所)、BJSE(北交所) | - |
        | board | string | 是 | 板块标识,如 XSHG_STAR(科创板)、XSHE_CHI_NEXT(创业板) | - |
        | latest | number | 是 | 最新价 | 元 |
        | open | number | 是 | 今开;非交易时段或未开盘时可能为空 | 元 |
        | high | number | 是 | 最高价 | 元 |
        | low | number | 是 | 最低价 | 元 |
        | prev_close | number | 是 | 前收盘价 | 元 |
        | close | number | 是 | 收盘价 | 元 |
        | change | number | 是 | 涨跌额 | 元 |
        | change_rate | number | 是 | 涨跌幅,小数值(如 0.1175 表示 11.75%) | 小数 |
        | turnover | number | 是 | 成交额;非交易时段或停牌可为 0 | 元 |
        | volume | number | 是 | 成交量(股);前端常除以 100 显示为「手」 | 股 |
        | market_cap_total | number | 是 | 总市值 | 元 |
        | turnover_rate | number | 是 | 换手率,小数值(如 0.05 表示 5%);非交易时段可为 0 | 小数 |
        | change_rate_5d | number | 是 | 五日涨跌幅,小数值 | 小数 |
        | amplitude | number | 是 | 振幅,小数值 | 小数 |
        | limit_up | number | 是 | 涨停价 | 元 |
        | limit_down | number | 是 | 跌停价 | 元 |
        | is_in_default_choice_group | boolean | 是 | 是否在用户默认自选列表中;未登录时为 null | - |
        | trading_status | string | 是 | 交易状态:LIMIT_UP(涨停)、LIMIT_DOWN(跌停)、NORMAL(正常) | - |
        | symbol_status | object | 是 | 标的状态,含 base、extra 等 | - |
        | margin_status | string | 是 | 融资融券状态:BOTH、NONE 等 | - |
        | industry_sector | object | 是 | 行业板块,含 code、name | - |
        
        ## 注意事项
        
        - 涨跌幅、换手率、振幅等比率为小数值,展示时乘以 100 转为百分比
        - `filter` 中含空格、引号、括号,通过 run.py 传参时需按 shell 规则正确引号包裹(如单引号包裹整段 filter)
        
    • stock-realtime-day-kline
      • scripts
        • handler.py 2.2 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v4/market/data/stock-realtime-day-kline"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询股票实时日 K 线")
              parser.add_argument("--symbols", nargs="+", required=True)
              args = parser.parse_args()
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode({"symbols": json.dumps(args.symbols, ensure_ascii=False)})
              request = urllib.request.Request(url, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 861 B
        ---
        name: stock-realtime-day-kline
        description: 查询股票当前交易日实时日 K 线。必填 --symbols,按空格分隔。接口:GET /api/v4/market/data/stock-realtime-day-kline。
        ---
        
        # 股票实时日K线
        
        查询股票当前交易日实时日 K 线。必填 --symbols,按空格分隔。接口:GET /api/v4/market/data/stock-realtime-day-kline。
        
        每根 K 线字段:symbol / open / high / low / close / ts_millis / ts_millis_open / turnover / volume / turnover_rate(换手率),返回当前交易日截至最新行情时刻的数据。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> stock-realtime-day-kline --symbols 600519.SH 000001.SZ --symbols 600519.SH 000001.SZ
        ```
        
    • stock-realtime-minute-kline
      • scripts
        • handler.py 2.3 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v4/market/data/stock-realtime-minute-kline"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询股票实时分钟 K 线")
              parser.add_argument("--symbols", nargs="+", required=True)
              args = parser.parse_args()
              if len(args.symbols) > 20:
                  parser.error("symbols 单次最多 20 个")
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode({"symbols": json.dumps(args.symbols, ensure_ascii=False)})
              request = urllib.request.Request(url, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 875 B
        ---
        name: stock-realtime-minute-kline
        description: 查询股票当前交易日实时 1 分钟 K 线。必填 --symbols,按空格分隔,单次最多 20 个。接口:GET /api/v4/market/data/stock-realtime-minute-kline。
        ---
        
        # 股票实时分钟K线
        
        查询股票当前交易日实时 1 分钟 K 线。必填 --symbols,按空格分隔,单次最多 20 个。接口:GET /api/v4/market/data/stock-realtime-minute-kline。
        
        每根 K 线字段:symbol / open / high / low / close / ts_millis / ts_millis_open / turnover / volume / turnover_rate(换手率)。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> stock-realtime-minute-kline --symbols 600519.SH 000001.SZ --symbols 600519.SH 000001.SZ
        ```
        
    • stock-reports
      • scripts
        • handler.py 3 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v2/market/data/report/stock-reports"
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description="查询 A 股研报列表")
              parser.add_argument("--stock-code", dest="stock_code")
              parser.add_argument("--type")
              parser.add_argument("--start-date", dest="start_date")
              parser.add_argument("--end-date", dest="end_date")
              parser.add_argument("--page", type=int, required=True)
              parser.add_argument("--page-size", dest="page_size", type=int, required=True)
              args = parser.parse_args()
              if args.stock_code is None and (args.type is None or args.start_date is None):
                  parser.error("未提供 stock-code 时,type 与 start-date 必须同时提供")
              if args.stock_code is not None and args.type is not None and args.type.lower() != "stockreport":
                  parser.error("按 stock-code 查询时 type 必须为 StockReport")
              params = {"page": args.page, "page_size": args.page_size}
              for name in ("stock_code", "type", "start_date", "end_date"):
                  value = getattr(args, name)
                  if value is not None:
                      params[name] = value
              request = urllib.request.Request(BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params),
                                               headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      print(json.dumps(json.loads(response.read().decode()), ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 957 B
        ---
        name: stock-reports
        description: 查询 A 股研报列表。按 stock-code 查询个股历史,或按 type 与日期范围查询;page 和 page-size 必填。
        ---
        
        # 研报列表
        
        外部接口:`GET /api/v2/market/data/report/stock-reports`。
        
        支持两种模式:
        
        - 按个股查询:`--stock-code`,`--type` 可省略,传入时必须为 `StockReport`。
        - 按类型和日期查询:不传 `--stock-code` 时,必须提供 `--type` 与 `--start-date`;`--end-date` 可省略,日期范围最多 3 天。
        
        `--page` 与 `--page-size` 必填。返回 `code`、`message` 和分页 `data`,记录含研报评级、研究机构、研究员及用于下载正文的 `url_hash`。所有请求必须设置 `FTSHARE_API_KEY`,缺失凭据时不会发起请求。
        
        ```bash
        python <RUN_PY> stock-reports --stock-code 600036.SH --page 1 --page-size 20
        python <RUN_PY> stock-reports --type MacroReport --start-date 20260821 --page 1 --page-size 20
        ```
        
    • stock-share
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          """获取单票指定日期股本信息。stock_code 与 date 均必填,date 格式 YYYYMMDD。"""
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          SAFE_URLOPENER = urllib.request.build_opener()
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v2/market/data/share/get-stock-share"
          
          
          def fetch(
          stock_code, date):
              params = urllib.parse.urlencode({"stock_code": stock_code, "date": date})
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="获取单票指定日期股本信息")
              parser.add_argument("--stock_code", required=True, help="股票代码,如 000001.SZ")
              parser.add_argument("--date", required=True, help="日期 YYYYMMDD")
              args = parser.parse_args()
          
              result = fetch(args.stock_code, args.date)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 1.5 KB
          #!/usr/bin/env python3
          """Tests for stock-share handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_forwards_stock_code_and_date(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("000001.SZ", "20260716")
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v2/market/data/share/get-stock-share", url)
                  self.assertIn("stock_code=000001.SZ", url)
                  self.assertIn("date=20260716", url)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  with patch.object(sys, "argv", ["handler.py", "--stock_code", "000001.SZ", "--date", "20260716"]):
                      with patch("sys.stdout", new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 4 KB
        ---
        name: stock-share
        description: "获取单票指定日期股本信息。当用户需要查询某只 A 股股票在某日的总股本、A 股流通/限售/无限售股本、B 股股本、H 股股本、境外上市股本等股本快照时使用。"
        ---
        
        # 获取单票指定日期股本信息
        
        ## 接口说明
        
        | 项目     | 说明                                                              |
        |----------|-------------------------------------------------------------------|
        | 接口名称 | 获取单票指定日期股本信息                                            |
        | 外部接口 | `/api/v1/market/data/share/get-stock-share`                 |
        | 请求方式 | GET                                                               |
        | 适用场景 | 按日期快照查询单票股本结构                                          |
        
        ## 请求参数
        
        | 参数名     | 类型   | 是否必填 | 描述            | 取值示例  | 备注                                                                                       |
        |------------|--------|----------|-----------------|-----------|--------------------------------------------------------------------------------------------|
        | stock_code | string | 是       | 单个股票代码    | 000001.SZ | 支持 6 位数字+后缀(SH/SZ/BJ)                                                              |
        | date       | string | 是       | 日期 YYYYMMDD   | 20260716  | 按指定日期快照查询                                                                          |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> stock-share --stock_code 000001.SZ --date 20260716
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        返回分页结构(items/total_pages/total_items),命中时仅返回 1 条;未命中 items 为空数组。
        
        ```json
        {
            "items": [
                {
                    "stock_code": "000001.SZ",
                    "stock_name": "平安银行",
                    "totshare_num": "19405918198",
                    "ashare_circ_num": "19405918198",
                    "ashare_circ_limit_num": "317545",
                    "ashare_circ_unlimit_num": "19405600653",
                    "bshare_num": "0",
                    "bshare_circ_num": "0",
                    "bshare_uncirc_num": "0",
                    "hshare_num": "0",
                    "osshare_num": "0",
                    "share_circ_num": "19405918198"
                }
            ],
            "total_pages": 1,
            "total_items": 1
        }
        ```
        
        ### 顶层字段说明
        
        | 字段名       | 类型  | 是否可为空 | 说明     |
        |--------------|-------|------------|----------|
        | items        | Array | 否         | 股本信息列表 |
        | total_pages | int   | 否         | 总页数   |
        | total_items | int   | 否         | 总记录数 |
        
        ### items 元素字段
        
        | 字段名                | 类型   | 是否可为空 | 说明                  |
        |-----------------------|--------|------------|-----------------------|
        | stock_code            | string | 否         | 股票代码              |
        | stock_name            | string | 否         | 股票名称              |
        | totshare_num          | string | 否         | 总股本                |
        | ashare_circ_num       | string | 否         | A 股流通股本           |
        | ashare_circ_limit_num | string | 否         | A 股限售流通股本       |
        | ashare_circ_unlimit_num | string | 否       | A 股无限售流通股本     |
        | bshare_num            | string | 否         | B 股股本              |
        | bshare_circ_num       | string | 否         | B 股流通股本          |
        | bshare_uncirc_num     | string | 否         | B 股非流通股本        |
        | hshare_num            | string | 否         | H 股股本              |
        | osshare_num           | string | 否         | 境外上市股本          |
        | share_circ_num        | string | 否         | 流通股本合计          |
        
        ## 注意事项
        
        - `stock_code` 与 `date` 均必填,`date` 格式 YYYYMMDD。
        - 未命中数据时 `items` 为空数组。
        - 金额/股数字段以字符串返回,保持精度。
        
    • stock-share-chg
      • scripts
        • handler.py 3.5 KB
          #!/usr/bin/env python3
          """查询 A 股股东增减持明细。
          
          两种模式:
          1) 指定 stock_code:分页查询该标的全部历史增减持记录。
          2) is_last=true:返回所有标的最新一期增减持数据(分页)。
          """
          import argparse
          import json
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          SAFE_URLOPENER = urllib.request.build_opener()
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          def safe_urlopen(req_or_url):
              if isinstance(req_or_url, urllib.request.Request):
                  url = req_or_url.full_url
              else:
                  url = str(req_or_url)
              parsed = urllib.parse.urlparse(url)
              if parsed.scheme != urllib.parse.urlparse(BASE_URL).scheme or parsed.netloc != urllib.parse.urlparse(BASE_URL).netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  sys.exit(1)
              if not isinstance(req_or_url, urllib.request.Request):
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              if isinstance(req_or_url, urllib.request.Request):
                  for key, value in _REQUEST_HEADERS.items():
                      req_or_url.add_unredirected_header(key, value)
              else:
                  req_or_url = urllib.request.Request(str(req_or_url), headers=_REQUEST_HEADERS, method="GET")
              return SAFE_URLOPENER.open(req_or_url)
          
          ENDPOINT = "/api/v1/market/data/holder/stock-share-chg"
          
          
          def build_params(stock_code, is_last, page, page_size):
              params = {}
              if stock_code:
                  params["stock_code"] = stock_code
              if is_last:
                  params["is_last"] = "true"
              if page is not None:
                  params["page"] = page
              if page_size is not None:
                  params["page_size"] = page_size
              return params
          
          
          def fetch(
          stock_code, is_last, page, page_size):
              params = urllib.parse.urlencode(build_params(stock_code, is_last, page, page_size))
              url = f"{BASE_URL}{ENDPOINT}?{params}"
              try:
                  with safe_urlopen(url) as resp:
                      return json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
          
          
          def main():
              _require_api_key()
              parser = argparse.ArgumentParser(description="查询 A 股股东增减持明细")
              parser.add_argument(
                  "--stock_code",
                  help="股票代码,需携带市场后缀,如 603323.SH / 000001.SZ / 833171.BJ;不传时需配合 --is_last",
              )
              parser.add_argument(
                  "--is_last",
                  action="store_true",
                  help="返回所有标的最新一期增减持数据(分页)",
              )
              parser.add_argument("--page", type=int, help="页码,从 1 开始(默认 1)")
              parser.add_argument("--page_size", type=int, help="每页记录数(默认 50)")
              args = parser.parse_args()
          
              if not args.stock_code and not args.is_last:
                  parser.error("需指定 --stock_code 或 --is_last 至少其一")
          
              result = fetch(args.stock_code, args.is_last, args.page, args.page_size)
              print(json.dumps(result, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
        • test_handler.py 2 KB
          #!/usr/bin/env python3
          """Tests for stock-share-chg handler"""
          import json
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          import importlib.util
          import os
          
          _dir = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_dir, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          
          
          class TestFetch(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_by_stock_code(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch("000001.SZ", False, 1, 50)
                  url = mock_open.call_args[0][0]
                  self.assertIn("/api/v1/market/data/holder/stock-share-chg", url)
                  self.assertIn("stock_code=000001.SZ", url)
                  self.assertIn("page=1", url)
          
              @patch.object(handler, "safe_urlopen")
              def test_fetch_is_last_paging(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  handler.fetch(None, True, 1, 50)
                  url = mock_open.call_args[0][0]
                  self.assertIn("is_last=true", url)
                  self.assertIn("page=1", url)
          
          
          class TestMain(unittest.TestCase):
              def setUp(self):
                  spec.loader.exec_module(handler)
          
              @patch.object(handler, "safe_urlopen")
              def test_main_emits_json_for_stock_code(self, mock_open):
                  mock_open.return_value.__enter__.return_value.read.return_value = b"[]"
                  with patch.object(sys, "argv", ["handler.py", "--stock_code", "000001.SZ"]):
                      with patch("sys.stdout", new_callable=StringIO) as fake_out:
                          handler.main()
                          self.assertEqual(json.loads(fake_out.getvalue()), [])
          
              def test_main_requires_stock_code_or_is_last(self):
                  with patch.object(sys, "argv", ["handler.py"]):
                      with self.assertRaises(SystemExit):
                          handler.main()
          
          
          if __name__ == "__main__":
              unittest.main()
          
      • SKILL.md 5.8 KB
        ---
        name: stock-share-chg
        description: "查询 A 股股东增减持明细。当用户需要获取 A 股股东增减持数据(按 stock_code 分页查询全部历史,或以 is_last=true 查全市场最新一期),支持沪深京股票时使用。"
        ---
        
        # 查询 A 股股东增减持
        
        ## 接口说明
        
        | 项目     | 说明                                                                            |
        |----------|---------------------------------------------------------------------------------|
        | 接口名称 | 查询 A 股股东增减持                                                              |
        | 外部接口 | `/api/v1/market/data/holder/stock-share-chg`                               |
        | 请求方式 | GET                                                                             |
        | 适用场景 | 指定 stock_code 分页查询该标的全部历史增减持,或 is_last=true 查全市场最新一期 |
        
        ## 请求参数
        
        | 参数名     | 类型   | 是否必填 | 描述            | 取值示例  | 备注                                                                                     |
        |------------|--------|----------|-----------------|-----------|------------------------------------------------------------------------------------------|
        | stock_code | string | 否       | 单个股票代码    | 603323.SH | 不传时需配合 `--is_last`;支持沪深京 A 股,需 6 位数字+后缀(SH/SZ/BJ),单次仅支持一个代码 |
        | is_last    | flag   | 否       | 取所有标的最新一期 | (flag)  | 传字符串 `"true"`;与 `stock_code` 二选一                                                    |
        | page       | int    | 否       | 页码            | 1         | 默认 1                                                                                    |
        | page_size  | int    | 否       | 每页记录数      | 50        | 默认 50                                                                                   |
        
        ## 执行方式
        
        通过根目录的 `run.py` 调用(推荐):
        
        ```bash
        python <RUN_PY> stock-share-chg --stock_code 603323.SH
        python <RUN_PY> stock-share-chg --stock_code 603323.SH --page 2 --page_size 20
        python <RUN_PY> stock-share-chg --is_last --page 1 --page_size 50
        ```
        
        > `<RUN_PY>` 为主 `SKILL.md` 同级的 `run.py` 绝对路径。
        
        ## 响应结构
        
        ```json
        {
            "items": [
                {
                    "trade_code": "000001",
                    "stock_name": "平安银行",
                    "holder_name": "张某某",
                    "shareholding_change_info": "增持",
                    "change_quantity": "30000",
                    "pre_change_quantity": "0",
                    "pre_change_total_capital_ratio": null,
                    "post_change_quantity": "30000",
                    "post_change_total_capital_ratio": null,
                    "transfer_method": null,
                    "latest_price": "18.45",
                    "price_change_rate": "0",
                    "transaction_price": null,
                    "transaction_amount": null,
                    "progress_description": null,
                    "change_start_date": "2021-09-06",
                    "change_end_date": "2021-09-06",
                    "announcement_date": "2021-09-07"
                }
            ],
            "total_pages": 1,
            "total_items": 11036
        }
        ```
        
        ### items 元素字段
        
        | 字段名                          | 类型   | 是否可为空 | 说明                                        |
        |---------------------------------|--------|------------|---------------------------------------------|
        | trade_code                      | String | 否         | 股票交易代码,6 位数字                       |
        | stock_name                      | String | 否         | 上市公司官方简称                            |
        | holder_name                     | String | 否         | 持股变动主体(股东名称)                     |
        | shareholding_change_info        | String | 否         | 持股变动类型:增持/减持                       |
        | change_quantity                 | string | 否         | 持股变动数量                                |
        | pre_change_quantity             | string | 否         | 变动前持股数量                              |
        | pre_change_total_capital_ratio  | String | 是         | 变动前持股占总股本比例(暂空)               |
        | post_change_quantity            | string | 否         | 变动后持股数量                              |
        | post_change_total_capital_ratio | String | 是         | 变动后持股占总股本比例(暂空)               |
        | transfer_method                 | String | 是         | 股份转让方式(暂空)                         |
        | latest_price                    | string | 否         | 股票最新价                                  |
        | price_change_rate               | string | 否         | 股票涨跌幅                                  |
        | transaction_price               | String | 是         | 股份交易价格(暂空)                         |
        | transaction_amount              | String | 是         | 股份交易金额(暂空)                         |
        | progress_description            | String | 是         | 持股变动进展说明(暂空)                     |
        | change_start_date               | String | 否         | 变动开始日期 YYYY-MM-DD                      |
        | change_end_date                 | String | 否         | 变动截止日期 YYYY-MM-DD                      |
        | announcement_date               | String | 否         | 公告发布日期 YYYY-MM-DD                      |
        
        ## 注意事项
        
        - 必须至少指定 `--stock_code` 或 `--is_last` 其一。
        - `shareholding_change_info` 仅为「增持」「减持」两种取值。
        - `pre_change_total_capital_ratio`/`post_change_total_capital_ratio`/`transfer_method`/`transaction_price`/`transaction_amount`/`progress_description` 当前接口均暂为空值。
        - `is_last` 在 query string 中传字符串 `"true"`。
        
    • stock-signal-latest-snapshot
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          ENDPOINT = "/api/v3/market/data/stock-signal-latest-snapshot"
          TYPES = {"new_high_month","new_high_60d","new_high_120d","new_high_250d","new_low_month","new_low_60d","new_low_120d","new_low_250d","consecutive_up","consecutive_down","consecutive_vol_up","consecutive_vol_down","break_up_ma5","break_up_ma10","break_up_ma20","break_down_ma5","break_down_ma10","break_down_ma20","vol_price_rise","vol_price_fall"}
          def fetch(params):
              if not os.environ.get("FTSHARE_API_KEY"): print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              url = BASE_URL + ENDPOINT + "?" + urllib.parse.urlencode(params)
              p, b = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if (p.scheme, p.netloc) != (b.scheme, b.netloc): print("Invalid URL for safe_urlopen", file=sys.stderr); raise SystemExit(1)
              req = urllib.request.Request(url, headers={"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type":"application/json"}, method="GET")
              try:
                  with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read().decode())
              except (urllib.error.HTTPError, urllib.error.URLError) as e: print(f"请求失败: {e}", file=sys.stderr); raise SystemExit(1)
          def main():
              p=argparse.ArgumentParser(); p.add_argument("--signal-type", choices=sorted(TYPES)); p.add_argument("--page",type=int,default=1); p.add_argument("--page-size",type=int,default=50); p.add_argument("--all",action="store_true",dest="fetch_all"); a=p.parse_args()
              if not 1 <= a.page_size <= 200: p.error("page-size 须在 1~200 之间")
              base={"signal_type":a.signal_type} if a.signal_type else {}
              result=fetch({**base,"page":1 if a.fetch_all else a.page,"page_size":a.page_size})
              if a.fetch_all:
                  d=result.get("data") or {}; records=list(d.get("records",[]))
                  for page in range(2,int(d.get("pages",1))+1): records.extend((fetch({**base,"page":page,"page_size":a.page_size}).get("data") or {}).get("records",[]))
                  result["data"]={**d,"records":records}
              print(json.dumps(result,ensure_ascii=False,indent=2))
          if __name__ == "__main__": main()
          
      • SKILL.md 1.1 KB
        ---
        name: stock-signal-latest-snapshot
        description: 查询股票最新计算信号快照。用户询问创新高/新低、连续上涨下跌、均线突破或量价信号时使用。
        ---
        
        # 信号最新快照
        
        接口:`GET /api/v3/market/data/stock-signal-latest-snapshot`
        
        参数:`--signal-type` 可选,支持 `new_high_month`、`new_high_60d`、`new_high_120d`、`new_high_250d`、`new_low_month`、`new_low_60d`、`new_low_120d`、`new_low_250d`、`consecutive_up`、`consecutive_down`、`consecutive_vol_up`、`consecutive_vol_down`、`break_up_ma5`、`break_up_ma10`、`break_up_ma20`、`break_down_ma5`、`break_down_ma10`、`break_down_ma20`、`vol_price_rise`、`vol_price_fall`;`--page` 默认 1,`--page-size` 默认 50,范围 1~200;支持 `--all` 自动翻页。
        
        ```bash
        python <RUN_PY> stock-signal-latest-snapshot --signal-type new_high_60d --page 1 --page-size 20
        ```
        
        返回统一 `code/message/data` 分页信封,记录位于 `data.records`,包含 `symbol`、`code`、`latest_trade_date`、`signal_type`、`signal_name`、`signal_detail`、`close_price` 和 `volume`。
        
    • stock-st-history
      • scripts
        • handler.py 1.9 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL=os.environ.get("FTSHARE_BASE_URL","https://market.ft.tech/gateway").rstrip("/"); ENDPOINT="/api/v1/market/data/stock-st-history"
          _REQUEST_HEADERS={"Content-Type":"application/json"}
          def safe_urlopen(request):
           url=request.full_url if isinstance(request,urllib.request.Request) else str(request)
           parsed=urllib.parse.urlparse(url); base=urllib.parse.urlparse(BASE_URL)
           if (parsed.scheme,parsed.netloc)!=(base.scheme,base.netloc): print("Invalid URL for safe_urlopen",file=sys.stderr); raise SystemExit(1)
           if not isinstance(request,urllib.request.Request): request=urllib.request.Request(url,headers=_REQUEST_HEADERS,method="GET")
           return urllib.request.urlopen(request,timeout=30)
          def fetch(params):
           key=os.environ.get("FTSHARE_API_KEY")
           if not key: print("FTSHARE_API_KEY environment variable is required",file=sys.stderr); raise SystemExit(2)
           url=BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params); b,p=urllib.parse.urlparse(BASE_URL),urllib.parse.urlparse(url)
           if (b.scheme,b.netloc)!=(p.scheme,p.netloc): print("Invalid URL for safe_urlopen",file=sys.stderr); raise SystemExit(1)
           try:
            with safe_urlopen(urllib.request.Request(url,headers={"FTSHARE_API_KEY":key,"Content-Type":"application/json"},method="GET")) as r:return json.loads(r.read().decode())
           except urllib.error.HTTPError as e: print(f"HTTP {e.code}: {e.read().decode()}",file=sys.stderr); raise SystemExit(1)
           except urllib.error.URLError as e: print(f"请求失败: {e.reason}",file=sys.stderr); raise SystemExit(1)
          def main():
           p=argparse.ArgumentParser(description="查询股票历史ST状态"); p.add_argument("--symbol",required=True); p.add_argument("--st-type"); a=p.parse_args()
           if not a.symbol.strip(): p.error("symbol 不能为空")
           params={"symbol":a.symbol}
           if a.st_type: params["st_type"]=a.st_type
           print(json.dumps(fetch(params),ensure_ascii=False,indent=2))
          if __name__=="__main__":main()
          
        • test_handler.py 1.3 KB
          import importlib.util
          import json
          import os
          import sys
          import unittest
          from io import StringIO
          from unittest.mock import patch
          
          _DIR = os.path.dirname(os.path.abspath(__file__))
          spec = importlib.util.spec_from_file_location("handler", os.path.join(_DIR, "handler.py"))
          handler = importlib.util.module_from_spec(spec)
          spec.loader.exec_module(handler)
          
          class TestHandler(unittest.TestCase):
              @patch.object(handler, "fetch")
              def test_required_symbol_and_optional_filter(self, mock_fetch):
                  mock_fetch.return_value = {"code": 200, "data": []}
                  with patch.object(sys, "argv", ["handler.py", "--symbol", "600735.SH,000004.SZ", "--st-type", "退市整理期"]), patch("sys.stdout", new_callable=StringIO) as out:
                      handler.main()
                  self.assertEqual(mock_fetch.call_args.args[0], {"symbol": "600735.SH,000004.SZ", "st_type": "退市整理期"})
                  self.assertEqual(json.loads(out.getvalue())["code"], 200)
          
              @patch.object(handler, "fetch")
              def test_symbol_only_omits_filter(self, mock_fetch):
                  mock_fetch.return_value = {"code": 200, "data": []}
                  with patch.object(sys, "argv", ["handler.py", "--symbol", "600735.SH"]), patch("sys.stdout", new_callable=StringIO):
                      handler.main()
                  self.assertEqual(mock_fetch.call_args.args[0], {"symbol": "600735.SH"})
          
          if __name__ == "__main__": unittest.main()
          
      • SKILL.md 1.1 KB
        ---
        name: stock-st-history
        description: 查询 A 股股票历史风险警示(ST/*ST/PT/退市整理期)状态区间。Use when user asks about 股票历史ST状态、ST 区间、风险警示区间、摘帽或退市整理期。
        ---
        
        # 股票历史ST状态
        
        接口:`GET /api/v1/market/data/stock-st-history`。`symbol` 必填,支持逗号分隔批量;`st_type` 可选过滤;不分页,一次返回全部阶段区间。
        
        ```bash
        python <RUN_PY> stock-st-history --symbol 600735.SH
        python <RUN_PY> stock-st-history --symbol 600735.SH,000004.SZ
        python <RUN_PY> stock-st-history --symbol 000004.SZ --st-type 退市整理期
        ```
        
        - `--symbol`:必填,带 `.SH`/`.SZ` 后缀的股票代码,逗号分隔去重后最多 50 只。
        - `--st-type`:可选,阶段类型过滤,取值 `ST`、`*ST`、`PT`、`退市整理期`。
        
        成功响应为 `code/message/data`,`data` 为阶段区间数组,元素包含 `symbol`、`st_type`、`st_name`、`start_date`、`end_date`、`delist_end`、`reason`、`publish_date`。`end_date` 为 `null` 表示仍在警示中;`delist_end=true` 表示该阶段以终止上市日结束。摘帽后的无警示区间不产生记录。
        
    • stock-unlock
      • scripts
        • handler.py 5.9 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/unlock/stock_unlock'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='限售解禁')
              parser.add_argument("--stock_code")
              parser.add_argument("--start_date")
              parser.add_argument("--end_date")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--total", required=False)
              parser.add_argument("--stock-code", required=True)
              parser.add_argument("--stock-name", required=True)
              parser.add_argument("--unlock-date")
              parser.add_argument("--holder-count")
              parser.add_argument("--able-free-shares")
              parser.add_argument("--current-free-shares")
              parser.add_argument("--non-free-shares")
              parser.add_argument("--lift-market-cap")
              parser.add_argument("--total-ratio")
              parser.add_argument("--free-ratio")
              parser.add_argument("--new-price")
              parser.add_argument("--free-shares-type", required=False)
              parser.add_argument("--b20-adjchrate")
              parser.add_argument("--a20-adjchrate")
              parser.add_argument("--crawl-date", required=False)
              parser.add_argument("--source", required=False)
              parser.add_argument("--holders", required=False)
              parser.add_argument("--holder-name", required=False)
              parser.add_argument("--add-listing-shares")
              parser.add_argument("--actual-listed-shares")
              parser.add_argument("--add-listing-cap")
              parser.add_argument("--lock-month")
              parser.add_argument("--residual-limited-shares")
              parser.add_argument("--plan-feature", required=False)
              args = parser.parse_args()
              params = {}
              if args.stock_code is not None: params["stock_code"] = args.stock_code
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.stock_code is not None: params["stock_code"] = args.stock_code
              if args.stock_name is not None: params["stock_name"] = args.stock_name
              if args.unlock_date is not None: params["unlock_date"] = args.unlock_date
              if args.holder_count is not None: params["holder_count"] = args.holder_count
              if args.able_free_shares is not None: params["able_free_shares"] = args.able_free_shares
              if args.current_free_shares is not None: params["current_free_shares"] = args.current_free_shares
              if args.non_free_shares is not None: params["non_free_shares"] = args.non_free_shares
              if args.lift_market_cap is not None: params["lift_market_cap"] = args.lift_market_cap
              if args.total_ratio is not None: params["total_ratio"] = args.total_ratio
              if args.free_ratio is not None: params["free_ratio"] = args.free_ratio
              if args.new_price is not None: params["new_price"] = args.new_price
              if args.free_shares_type is not None: params["free_shares_type"] = args.free_shares_type
              if args.b20_adjchrate is not None: params["b20_adjchrate"] = args.b20_adjchrate
              if args.a20_adjchrate is not None: params["a20_adjchrate"] = args.a20_adjchrate
              if args.crawl_date is not None: params["crawl_date"] = args.crawl_date
              if args.source is not None: params["source"] = args.source
              if args.holders is not None: params["holders"] = args.holders
              if args.holder_name is not None: params["holder_name"] = args.holder_name
              if args.add_listing_shares is not None: params["add_listing_shares"] = args.add_listing_shares
              if args.actual_listed_shares is not None: params["actual_listed_shares"] = args.actual_listed_shares
              if args.add_listing_cap is not None: params["add_listing_cap"] = args.add_listing_cap
              if args.lock_month is not None: params["lock_month"] = args.lock_month
              if args.residual_limited_shares is not None: params["residual_limited_shares"] = args.residual_limited_shares
              if args.plan_feature is not None: params["plan_feature"] = args.plan_feature
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 611 B
        ---
        name: stock-unlock
        description: 查询限售解禁。接口:GET /api/v1/market/data/unlock/stock_unlock。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 限售解禁
        
        接口:GET `/api/v1/market/data/unlock/stock_unlock`。参数和响应以 `ftshare-doc/api-doc/股票数据/参考数据/限售解禁.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> stock-unlock --page 1 --stock-code 600519 --stock-name 1
        ```
        
    • supply-chain-companies
      • scripts
        • handler.py 2.6 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v3/market/data/supply-chain/company-supply-chain-companies'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='供应链公司候选')
              parser.add_argument("--trade_code", required=True)
              parser.add_argument("--direction", required=True)
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              args = parser.parse_args()
              params = {}
              if args.trade_code is not None: params["trade_code"] = args.trade_code
              if args.direction is not None: params["direction"] = args.direction
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 717 B
        ---
        name: supply-chain-companies
        description: 查询供应链公司候选。接口:GET /api/v3/market/data/supply-chain/company-supply-chain-companies。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 供应链公司候选
        
        接口:GET `/api/v3/market/data/supply-chain/company-supply-chain-companies`。参数和响应以 `ftshare-doc/api-doc/股票数据/特色数据/供应链公司候选.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> supply-chain-companies --trade_code 600519.SH --direction downstream --page 1
        ```
        
    • supply-chain-one-hop
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v3/market/data/supply-chain/subindustry-supply-chain'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='供应链一跳关系')
              parser.add_argument("--industry_name", required=True)
              parser.add_argument("--direction", required=True)
              args = parser.parse_args()
              params = {}
              if args.industry_name is not None: params["industry_name"] = args.industry_name
              if args.direction is not None: params["direction"] = args.direction
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 692 B
        ---
        name: supply-chain-one-hop
        description: 查询供应链一跳关系。接口:GET /api/v3/market/data/supply-chain/subindustry-supply-chain。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 供应链一跳关系
        
        接口:GET `/api/v3/market/data/supply-chain/subindustry-supply-chain`。参数和响应以 `ftshare-doc/api-doc/股票数据/特色数据/供应链一跳关系.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> supply-chain-one-hop --industry_name 银行 --direction downstream
        ```
        
    • supply-chain-parent-subindustries
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v3/market/data/supply-chain/subsubindustry-parent-subindustries'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='供应链子子行业父行业反查')
              parser.add_argument("--subindustry_name", required=True)
              args = parser.parse_args()
              params = {}
              if args.subindustry_name is not None: params["subindustry_name"] = args.subindustry_name
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 765 B
        ---
        name: supply-chain-parent-subindustries
        description: 查询供应链子子行业父行业反查。接口:GET /api/v3/market/data/supply-chain/subsubindustry-parent-subindustries。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 供应链子子行业父行业反查
        
        接口:GET `/api/v3/market/data/supply-chain/subsubindustry-parent-subindustries`。参数和响应以 `ftshare-doc/api-doc/股票数据/特色数据/供应链子子行业父行业反查.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> supply-chain-parent-subindustries --subindustry_name 银行
        ```
        
    • supply-chain-subsubindustries
      • scripts
        • handler.py 2.3 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v3/market/data/supply-chain/subindustry-subsubindustries'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='供应链子行业层级展开')
              parser.add_argument("--industry_name", required=True)
              args = parser.parse_args()
              params = {}
              if args.industry_name is not None: params["industry_name"] = args.industry_name
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 722 B
        ---
        name: supply-chain-subsubindustries
        description: 查询供应链子行业层级展开。接口:GET /api/v3/market/data/supply-chain/subindustry-subsubindustries。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 供应链子行业层级展开
        
        接口:GET `/api/v3/market/data/supply-chain/subindustry-subsubindustries`。参数和响应以 `ftshare-doc/api-doc/股票数据/特色数据/供应链子行业层级展开.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> supply-chain-subsubindustries --industry_name 银行
        ```
        
    • supply-chain-subsubindustry-companies
      • scripts
        • handler.py 2.4 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v3/market/data/supply-chain/subsubindustry-companies'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='供应链子子行业公司映射')
              parser.add_argument("--subindustry_name", required=True)
              args = parser.parse_args()
              params = {}
              if args.subindustry_name is not None: params["subindustry_name"] = args.subindustry_name
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 742 B
        ---
        name: supply-chain-subsubindustry-companies
        description: 查询供应链子子行业公司映射。接口:GET /api/v3/market/data/supply-chain/subsubindustry-companies。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 供应链子子行业公司映射
        
        接口:GET `/api/v3/market/data/supply-chain/subsubindustry-companies`。参数和响应以 `ftshare-doc/api-doc/股票数据/特色数据/供应链子子行业公司映射.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> supply-chain-subsubindustry-companies --subindustry_name 银行
        ```
        
    • suspension-list
      • scripts
        • handler.py 2.3 KB
          #!/usr/bin/env python3
          import argparse
          import json
          import os
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/suspension-list'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              return key
          
          
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
                  raise SystemExit(1)
              if not isinstance(request, urllib.request.Request):
                  request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              return SAFE_URLOPENER.open(request, timeout=timeout)
          
          
          def main():
              key = _require_api_key()
              parser = argparse.ArgumentParser(description='查询停牌列表')
              parser.add_argument("--trade-date")
          
              args = parser.parse_args()
              params = {}
              if args.trade_date is not None:
                  params['trade_date'] = args.trade_date
          
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={"FTSHARE_API_KEY": key, "X-Client-Name": "ft-claw", "Content-Type": "application/json"}, method="GET")
              try:
                  with safe_urlopen(request) as response:
                      payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr)
                  raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr)
                  raise SystemExit(1)
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 495 B
        ---
        name: suspension-list
        description: 查询停牌股票列表。可选 --trade-date。接口:GET /api/v1/market/data/suspension-list。
        ---
        
        # 停牌列表
        
        查询停牌股票列表。可选 --trade-date。接口:GET /api/v1/market/data/suspension-list。
        
        所有请求必须设置环境变量 `FTSHARE_API_KEY`;handler 将其作为 `FTSHARE_API_KEY` 请求头发送。缺失凭据时不会发起请求。
        
        通过主目录 `run.py` 调用:
        
        ```bash
        python <RUN_PY> suspension-list
        ```
        
    • sw-index-history-minutes
      • scripts
        • handler.py 1.8 KB
          #!/usr/bin/env python3
          import argparse,json,os,sys,urllib.parse,urllib.request
          BASE_URL=os.environ.get("FTSHARE_BASE_URL","https://market.ft.tech/gateway").rstrip("/");_REQUEST_HEADERS={"FTSHARE_API_KEY":os.environ.get("FTSHARE_API_KEY", ""),"Content-Type":"application/json"}; ENDPOINT="/api/v1/market/data/sw-index/history-minutes"
          def fetch(params):
           key=os.environ.get("FTSHARE_API_KEY")
           if not key: print("FTSHARE_API_KEY environment variable is required",file=sys.stderr);raise SystemExit(2)
           url=BASE_URL+ENDPOINT+"?"+urllib.parse.urlencode(params); p,b=urllib.parse.urlparse(url),urllib.parse.urlparse(BASE_URL)
           if (p.scheme,p.netloc)!=(b.scheme,b.netloc):print("Invalid URL for safe_urlopen",file=sys.stderr);raise SystemExit(1)
           with urllib.request.urlopen(urllib.request.Request(url,headers={"FTSHARE_API_KEY":key,"Content-Type":"application/json"},method="GET"),timeout=30) as r:return json.loads(r.read().decode())
          def main():
           p=argparse.ArgumentParser();p.add_argument("--index-code",required=True);p.add_argument("--start-date",required=True);p.add_argument("--end-date",required=True);p.add_argument("--page",type=int,default=1);p.add_argument("--page-size",type=int,default=50);p.add_argument("--all",action="store_true",dest="fetch_all");a=p.parse_args()
           if not 1<=a.page_size<=200:p.error("page-size 须在 1~200 之间")
           base={"index_code":a.index_code,"start_date":a.start_date,"end_date":a.end_date};result=fetch({**base,"page":1 if a.fetch_all else a.page,"page_size":a.page_size})
           if a.fetch_all:
            d=result.get("data") or {};records=list(d.get("records",[]))
            for page in range(2,int(d.get("pages",1))+1):records.extend((fetch({**base,"page":page,"page_size":a.page_size}).get("data")or{}).get("records",[]))
            result["data"]={**d,"records":records}
           print(json.dumps(result,ensure_ascii=False,indent=2))
          if __name__=="__main__":main()
          
      • SKILL.md 605 B
        ---
        name: sw-index-history-minutes
        description: 查询申万指数历史一分钟 K 线。用户询问申万行业指数分钟行情时使用。
        ---
        
        # 申万指数历史分钟 K 线
        
        接口:`GET /api/v1/market/data/sw-index/history-minutes`。必填 `--index-code`、`--start-date`、`--end-date`;可选 `--page` 和 `--page-size`(最大 200),支持 `--all` 自动翻页。
        
        ```bash
        python <RUN_PY> sw-index-history-minutes --index-code 801001 --start-date 20260811 --end-date 20260811 --page 1 --page-size 50
        ```
        
        返回 `code/message/data` 分页信封,分钟记录位于 `data.records`。
        
    • sw-industry-constituent-history
      • scripts
        • handler.py 3.6 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/sw-industry/constituent-history'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='申万行业成份股历史')
              parser.add_argument("--industry_code", required=True)
              parser.add_argument("--stock-code", required=True)
              parser.add_argument("--stock-name", required=True)
              parser.add_argument("--in-date")
              parser.add_argument("--out-date")
              parser.add_argument("--sw-level1-code", required=True)
              parser.add_argument("--sw-level1-name", required=True)
              parser.add_argument("--sw-level2-code")
              parser.add_argument("--sw-level2-name")
              parser.add_argument("--sw-level3-code")
              parser.add_argument("--sw-level3-name")
              args = parser.parse_args()
              params = {}
              if args.industry_code is not None: params["industry_code"] = args.industry_code
              if args.stock_code is not None: params["stock_code"] = args.stock_code
              if args.stock_name is not None: params["stock_name"] = args.stock_name
              if args.in_date is not None: params["in_date"] = args.in_date
              if args.out_date is not None: params["out_date"] = args.out_date
              if args.sw_level1_code is not None: params["sw_level1_code"] = args.sw_level1_code
              if args.sw_level1_name is not None: params["sw_level1_name"] = args.sw_level1_name
              if args.sw_level2_code is not None: params["sw_level2_code"] = args.sw_level2_code
              if args.sw_level2_name is not None: params["sw_level2_name"] = args.sw_level2_name
              if args.sw_level3_code is not None: params["sw_level3_code"] = args.sw_level3_code
              if args.sw_level3_name is not None: params["sw_level3_name"] = args.sw_level3_name
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 770 B
        ---
        name: sw-industry-constituent-history
        description: 查询申万行业成份股历史。接口:GET /api/v1/market/data/sw-industry/constituent-history。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 申万行业成份股历史
        
        接口:GET `/api/v1/market/data/sw-industry/constituent-history`。参数和响应以 `ftshare-doc/api-doc/指数专题/申万行业/申万行业成份股历史.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> sw-industry-constituent-history --industry_code 银行 --stock-code 600519 --stock-name 1 --sw-level1-code 1 --sw-level1-name 1
        ```
        
    • sw-industry-daily-metrics
      • scripts
        • handler.py 2.9 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/sw-industry/daily-metrics'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='申万行业日度指标')
              parser.add_argument("--level")
              parser.add_argument("--start_date", required=True)
              parser.add_argument("--end_date", required=True)
              parser.add_argument("--industry_code", required=True)
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              args = parser.parse_args()
              params = {}
              if args.level is not None: params["level"] = args.level
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.industry_code is not None: params["industry_code"] = args.industry_code
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 715 B
        ---
        name: sw-industry-daily-metrics
        description: 查询申万行业日度指标。接口:GET /api/v1/market/data/sw-industry/daily-metrics。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 申万行业日度指标
        
        接口:GET `/api/v1/market/data/sw-industry/daily-metrics`。参数和响应以 `ftshare-doc/api-doc/指数专题/申万行业/申万行业日度指标.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> sw-industry-daily-metrics --start_date 20260828 --end_date 20260828 --industry_code 银行 --page 1
        ```
        
    • sw-industry-overview
      • scripts
        • handler.py 4.2 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/sw-industry/overview'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='申万行业总览')
              parser.add_argument("--date", required=True)
              parser.add_argument("--level")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--industry-code", required=True)
              parser.add_argument("--industry-name", required=True)
              parser.add_argument("--parent-industry-name")
              parser.add_argument("--constituent-count")
              parser.add_argument("--trade-date")
              parser.add_argument("--lyr-pe")
              parser.add_argument("--lyr-pe-percentile")
              parser.add_argument("--ttm-pe")
              parser.add_argument("--ttm-pe-percentile")
              parser.add_argument("--pb")
              parser.add_argument("--pb-percentile")
              parser.add_argument("--dv-ratio")
              parser.add_argument("--dv-ratio-percentile")
              args = parser.parse_args()
              params = {}
              if args.date is not None: params["date"] = args.date
              if args.level is not None: params["level"] = args.level
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.industry_code is not None: params["industry_code"] = args.industry_code
              if args.industry_name is not None: params["industry_name"] = args.industry_name
              if args.level is not None: params["level"] = args.level
              if args.parent_industry_name is not None: params["parent_industry_name"] = args.parent_industry_name
              if args.constituent_count is not None: params["constituent_count"] = args.constituent_count
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.lyr_pe is not None: params["lyr_pe"] = args.lyr_pe
              if args.lyr_pe_percentile is not None: params["lyr_pe_percentile"] = args.lyr_pe_percentile
              if args.ttm_pe is not None: params["ttm_pe"] = args.ttm_pe
              if args.ttm_pe_percentile is not None: params["ttm_pe_percentile"] = args.ttm_pe_percentile
              if args.pb is not None: params["pb"] = args.pb
              if args.pb_percentile is not None: params["pb_percentile"] = args.pb_percentile
              if args.dv_ratio is not None: params["dv_ratio"] = args.dv_ratio
              if args.dv_ratio_percentile is not None: params["dv_ratio_percentile"] = args.dv_ratio_percentile
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 674 B
        ---
        name: sw-industry-overview
        description: 查询申万行业总览。接口:GET /api/v1/market/data/sw-industry/overview。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 申万行业总览
        
        接口:GET `/api/v1/market/data/sw-industry/overview`。参数和响应以 `ftshare-doc/api-doc/指数专题/申万行业/申万行业总览.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> sw-industry-overview --date 20260828 --page 1 --industry-code 银行 --industry-name 银行
        ```
        
    • tdx-board-daily
      • scripts
        • handler.py 4.5 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/tdx-board-daily'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='通达信板块日线')
              parser.add_argument("--start_date", required=True)
              parser.add_argument("--end_date", required=True)
              parser.add_argument("--ts_code")
              parser.add_argument("--idx_name")
              parser.add_argument("--idx_type")
              parser.add_argument("--idx_type_code")
              parser.add_argument("--market")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--total", required=False)
              parser.add_argument("--trade_date", required=True)
              parser.add_argument("--open", required=False)
              parser.add_argument("--high", required=False)
              parser.add_argument("--low", required=False)
              parser.add_argument("--close", required=False)
              parser.add_argument("--vol", required=False)
              parser.add_argument("--amount", required=False)
              parser.add_argument("--float_shares", required=False)
              args = parser.parse_args()
              params = {}
              if args.start_date is not None: params["start_date"] = args.start_date
              if args.end_date is not None: params["end_date"] = args.end_date
              if args.ts_code is not None: params["ts_code"] = args.ts_code
              if args.idx_name is not None: params["idx_name"] = args.idx_name
              if args.idx_type is not None: params["idx_type"] = args.idx_type
              if args.idx_type_code is not None: params["idx_type_code"] = args.idx_type_code
              if args.market is not None: params["market"] = args.market
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.ts_code is not None: params["ts_code"] = args.ts_code
              if args.idx_name is not None: params["idx_name"] = args.idx_name
              if args.idx_type is not None: params["idx_type"] = args.idx_type
              if args.idx_type_code is not None: params["idx_type_code"] = args.idx_type_code
              if args.market is not None: params["market"] = args.market
              if args.open is not None: params["open"] = args.open
              if args.high is not None: params["high"] = args.high
              if args.low is not None: params["low"] = args.low
              if args.close is not None: params["close"] = args.close
              if args.vol is not None: params["vol"] = args.vol
              if args.amount is not None: params["amount"] = args.amount
              if args.float_shares is not None: params["float_shares"] = args.float_shares
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 671 B
        ---
        name: tdx-board-daily
        description: 查询通达信板块日线。接口:GET /api/v1/market/data/tdx-board-daily。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 通达信板块日线
        
        接口:GET `/api/v1/market/data/tdx-board-daily`。参数和响应以 `ftshare-doc/api-doc/股票数据/打板专题数据/通达信板块日线.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> tdx-board-daily --start_date 20260828 --end_date 20260828 --page 1 --trade_date 20260828
        ```
        
    • tdx-board-index
      • scripts
        • handler.py 4.6 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/tdx-board-index'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='通达信板块指数最新快照')
              parser.add_argument("--ts_code")
              parser.add_argument("--idx_name")
              parser.add_argument("--idx_type")
              parser.add_argument("--idx_type_code")
              parser.add_argument("--market")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--total", required=False)
              parser.add_argument("--trade_date", required=True)
              parser.add_argument("--price", required=False)
              parser.add_argument("--rise_speed", required=False)
              parser.add_argument("--pre_close", required=False)
              parser.add_argument("--lead_market", required=False)
              parser.add_argument("--lead_code", required=False)
              parser.add_argument("--lead_name", required=False)
              parser.add_argument("--lead_price", required=False)
              parser.add_argument("--lead_rise_speed", required=True)
              parser.add_argument("--lead_pre_close", required=True)
              args = parser.parse_args()
              params = {}
              if args.ts_code is not None: params["ts_code"] = args.ts_code
              if args.idx_name is not None: params["idx_name"] = args.idx_name
              if args.idx_type is not None: params["idx_type"] = args.idx_type
              if args.idx_type_code is not None: params["idx_type_code"] = args.idx_type_code
              if args.market is not None: params["market"] = args.market
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.ts_code is not None: params["ts_code"] = args.ts_code
              if args.idx_name is not None: params["idx_name"] = args.idx_name
              if args.idx_type is not None: params["idx_type"] = args.idx_type
              if args.idx_type_code is not None: params["idx_type_code"] = args.idx_type_code
              if args.market is not None: params["market"] = args.market
              if args.price is not None: params["price"] = args.price
              if args.rise_speed is not None: params["rise_speed"] = args.rise_speed
              if args.pre_close is not None: params["pre_close"] = args.pre_close
              if args.lead_market is not None: params["lead_market"] = args.lead_market
              if args.lead_code is not None: params["lead_code"] = args.lead_code
              if args.lead_name is not None: params["lead_name"] = args.lead_name
              if args.lead_price is not None: params["lead_price"] = args.lead_price
              if args.lead_rise_speed is not None: params["lead_rise_speed"] = args.lead_rise_speed
              if args.lead_pre_close is not None: params["lead_pre_close"] = args.lead_pre_close
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 704 B
        ---
        name: tdx-board-index
        description: 查询通达信板块指数最新快照。接口:GET /api/v1/market/data/tdx-board-index。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 通达信板块指数最新快照
        
        接口:GET `/api/v1/market/data/tdx-board-index`。参数和响应以 `ftshare-doc/api-doc/股票数据/打板专题数据/通达信板块指数最新快照.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> tdx-board-index --page 1 --trade_date 20260828 --lead_rise_speed 1 --lead_pre_close 1
        ```
        
    • tdx-board-members
      • scripts
        • handler.py 3.8 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/tdx-board-members'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='通达信板块成分股最新快照')
              parser.add_argument("--ts_code")
              parser.add_argument("--idx_name")
              parser.add_argument("--idx_type")
              parser.add_argument("--idx_type_code")
              parser.add_argument("--con_code")
              parser.add_argument("--con_name")
              parser.add_argument("--market")
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              parser.add_argument("--total", required=False)
              parser.add_argument("--trade_date", required=True)
              args = parser.parse_args()
              params = {}
              if args.ts_code is not None: params["ts_code"] = args.ts_code
              if args.idx_name is not None: params["idx_name"] = args.idx_name
              if args.idx_type is not None: params["idx_type"] = args.idx_type
              if args.idx_type_code is not None: params["idx_type_code"] = args.idx_type_code
              if args.con_code is not None: params["con_code"] = args.con_code
              if args.con_name is not None: params["con_name"] = args.con_name
              if args.market is not None: params["market"] = args.market
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              if args.total is not None: params["total"] = args.total
              if args.trade_date is not None: params["trade_date"] = args.trade_date
              if args.ts_code is not None: params["ts_code"] = args.ts_code
              if args.idx_name is not None: params["idx_name"] = args.idx_name
              if args.idx_type is not None: params["idx_type"] = args.idx_type
              if args.idx_type_code is not None: params["idx_type_code"] = args.idx_type_code
              if args.con_code is not None: params["con_code"] = args.con_code
              if args.con_name is not None: params["con_name"] = args.con_name
              if args.market is not None: params["market"] = args.market
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 682 B
        ---
        name: tdx-board-members
        description: 查询通达信板块成分股最新快照。接口:GET /api/v1/market/data/tdx-board-members。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 通达信板块成分股最新快照
        
        接口:GET `/api/v1/market/data/tdx-board-members`。参数和响应以 `ftshare-doc/api-doc/股票数据/打板专题数据/通达信板块成分股最新快照.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> tdx-board-members --page 1 --trade_date 20260828
        ```
        
    • ths-all-board-kline
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          """查询同花顺全板块K线-按日期范围(market.ft.tech)"""
          import argparse
          import json
          import re
          import sys
          import urllib.error
          import urllib.parse
          import urllib.request
          import os
          
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = "/api/v1/market/data/ths-all-board-kline"
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          
          
          def _normalize_date(d):
              """Normalize YYYYMMDD or YYYY-MM-DD to YYYY-MM-DD."""
              if not d:
                  return d
              d = d.strip()
              if re.match(r"^\d{8}$", d):
                  return f"{d[:4]}-{d[4:6]}-{d[6:8]}"
              if re.match(r"^\d{4}-\d{2}-\d{2}$", d):
                  return d
              print(f"Invalid date format: {d} (expected YYYY-MM-DD or YYYYMMDD)", file=sys.stderr)
              sys.exit(1)
          
          
          def main():
              parser = argparse.ArgumentParser(description="查询同花顺全板块K线-按日期范围")
              parser.add_argument("--start-date", default=None, help="起始日期(YYYY-MM-DD 或 YYYYMMDD)")
              parser.add_argument("--end-date", default=None, help="截止日期(YYYY-MM-DD 或 YYYYMMDD)")
              parser.add_argument("--page", type=int, default=1, help="页码,默认 1")
              parser.add_argument("--page-size", type=int, default=50, help="每页数量,默认 50")
              args = parser.parse_args()
          
              params = {"page": args.page, "page_size": args.page_size}
              start = _normalize_date(args.start_date) if args.start_date else None
              end = _normalize_date(args.end_date) if args.end_date else None
              if start:
                  params["start_date"] = start
              if end:
                  params["end_date"] = end
          
              qs = urllib.parse.urlencode(params)
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr)
                  raise SystemExit(2)
              url = f"{BASE_URL}{ENDPOINT}?{qs}"
              req = urllib.request.Request(url, headers={"FTSHARE_API_KEY": key, "Content-Type": "application/json"}, method="GET")
          
              try:
                  with urllib.request.urlopen(req, timeout=30) as resp:
                      data = json.loads(resp.read().decode())
              except urllib.error.HTTPError as e:
                  body = e.read().decode()[:500] if e.fp else ""
                  print(f"HTTP {e.code}: {body}", file=sys.stderr)
                  sys.exit(1)
              except urllib.error.URLError as e:
                  print(f"Connection error: {e.reason}", file=sys.stderr)
                  sys.exit(1)
          
              print(json.dumps(data, ensure_ascii=False, indent=2))
          
          
          if __name__ == "__main__":
              main()
          
      • SKILL.md 2.1 KB
        ---
        name: ths-all-board-kline
        description: Get THS all boards K-line by date range (同花顺全板块 K 线). Use when user asks about 同花顺全板块行情、近N天板块K线、某日期范围所有板块K线.
        ---
        
        # 同花顺全板块 K 线
        
        ## 1. 接口描述
        
        | 项目 | 说明 |
        |------|------|
        | 接口名称 | 查询同花顺全板块 K 线 |
        | 外部接口 | `GET /api/v1/market/data/ths-all-board-kline` |
        | 请求方式 | GET |
        | 适用场景 | 查询同花顺所有板块在指定日期范围内的 K 线数据(分页) |
        
        ## 2. 请求参数
        
        | 参数名 | 类型 | 是否必填 | 描述 | 取值示例 | 备注 |
        |--------|------|----------|------|----------|------|
        | start_date | string | 否 | 起始日期(含) | 2021-10-01 | 格式 YYYY-MM-DD |
        | end_date | string | 否 | 截止日期(含) | 2021-10-31 | 格式 YYYY-MM-DD |
        | page | int | 否 | 页码,从 1 开始 | 1 | 默认 1 |
        | page_size | int | 否 | 每页数量 | 50 | 默认 50 |
        
        ## 3. 响应说明
        
        | 字段名 | 类型 | 说明 |
        |--------|------|------|
        | items | array | 分页数据列表 |
        | items[].board_code | string | 板块代码 |
        | items[].board_name | string | 板块名称 |
        | items[].module | string | 所属模块 |
        | items[].date | string | 日期 YYYY-MM-DD |
        | items[].open | string | 开盘价 |
        | items[].high | string | 最高价 |
        | items[].low | string | 最低价 |
        | items[].close | string | 收盘价 |
        | items[].volume | string | 成交量 |
        | total_pages | int | 总页数 |
        | total_items | int | 总记录数 |
        
        ## 4. 用法
        
        ```bash
        python <RUN_PY> ths-all-board-kline --start-date 2026-05-20 --end-date 2026-05-22 --page 1 --page-size 20
        python <RUN_PY> ths-all-board-kline --start-date 2026-05-20 --page 1 --page-size 20
        ```
        
        ## 5. 请求示例
        
        ```
        GET /api/v1/market/data/ths-all-board-kline?start_date=2026-05-20&end_date=2026-05-22&page=1&page_size=50
        ```
        
        ## 6. 注意事项
        
        - 与 `ths-board-kline` 的区别:本接口按日期范围返回所有板块的 K 线,无需指定 `board_code`
        - 日期格式为 `YYYY-MM-DD`,均可选
        - K 线数值均为字符串
        - 支持分页
        
    • ths-board-kline
      • scripts
        • handler.py 2.5 KB
          #!/usr/bin/env python3
          import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
          BASE_URL = os.environ.get("FTSHARE_BASE_URL", "https://market.ft.tech/gateway").rstrip("/")
          ENDPOINT = '/api/v1/market/data/ths-board-kline'
          SAFE_URLOPENER = urllib.request.build_opener()
          _REQUEST_HEADERS = {"FTSHARE_API_KEY": os.environ["FTSHARE_API_KEY"], "Content-Type": "application/json"} if os.environ.get("FTSHARE_API_KEY") else {}
          def _require_api_key():
              key = os.environ.get("FTSHARE_API_KEY")
              if not key:
                  print("FTSHARE_API_KEY environment variable is required", file=sys.stderr); raise SystemExit(2)
              return key
          def safe_urlopen(request, timeout=30):
              url = request.full_url if isinstance(request, urllib.request.Request) else str(request)
              parsed, base = urllib.parse.urlparse(url), urllib.parse.urlparse(BASE_URL)
              if parsed.scheme != base.scheme or parsed.netloc != base.netloc:
                  print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr); raise SystemExit(1)
              if not isinstance(request, urllib.request.Request): request = urllib.request.Request(url, method="GET")
              request.add_unredirected_header("FTSHARE_API_KEY", _require_api_key())
              request.add_unredirected_header("Content-Type", "application/json")
              return SAFE_URLOPENER.open(request, timeout=timeout)
          def main():
              key = _require_api_key(); parser = argparse.ArgumentParser(description='同花顺板块K线')
              parser.add_argument("--board_code", required=True)
              parser.add_argument("--page")
              parser.add_argument("--page_size")
              args = parser.parse_args()
              params = {}
              if args.board_code is not None: params["board_code"] = args.board_code
              if args.page is not None: params["page"] = args.page
              if args.page_size is not None: params["page_size"] = args.page_size
              query = ("?" + urllib.parse.urlencode(params)) if params else ""
              request = urllib.request.Request(BASE_URL + ENDPOINT + query, headers={**_REQUEST_HEADERS, "FTSHARE_API_KEY": key, "Content-Type": "application/json", "X-Client-Name": "ft-claw"}, method="GET")
              try:
                  with safe_urlopen(request) as response: payload = json.loads(response.read().decode())
                  print(json.dumps(payload, ensure_ascii=False, indent=2))
              except urllib.error.HTTPError as error:
                  print(f"HTTP {error.code}: {error.read().decode()}", file=sys.stderr); raise SystemExit(1)
              except urllib.error.URLError as error:
                  print(f"请求失败: {error.reason}", file=sys.stderr); raise SystemExit(1)
          if __name__ == "__main__": main()
          
      • SKILL.md 621 B
        ---
        name: ths-board-kline
        description: 查询同花顺板块K线。接口:GET /api/v1/market/data/ths-board-kline。所有请求必须设置 FTSHARE_API_KEY。
        ---
        
        # 同花顺板块K线
        
        接口:GET `/api/v1/market/data/ths-board-kline`。参数和响应以 `ftshare-doc/api-doc/股票数据/打板专题数据/同花顺板块K线.md` 为准。
        
        请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
        
        ## 调用示例
        
        ```bash
        python <RUN_PY> ths-board-kline --board_code 886056 --page 1
        ```
        
    • SKILL.md 603 B
      ---
      name: sub-skills
      description: 查询股票列表。接口:GET /api/v1/market/data/stock-list。所有请求必须设置 FTSHARE_API_KEY。
      ---
      
      # 股票列表
      
      接口:GET `/api/v1/market/data/stock-list`。
      
      请求参数通过同名 kebab-case CLI 选项传入;详细参数和响应字段以 `ftshare-doc/api-doc/股票数据/基础数据/股票列表.md` 为准。
      
      所有请求必须从环境变量 `FTSHARE_API_KEY` 读取凭据,并通过请求头发送 `FTSHARE_API_KEY` 和 `Content-Type: application/json`;缺少凭据时不会发起请求。
      
      ```bash
      python <RUN_PY> sub-skills
      ```
      
  • README.md 12.2 KB
    # ftshare-market-data
    
    [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
    [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
    
    `ftshare-market-data` 的当前可用范围不包含暂不支持的美股资产负债表、美股现金流、美股利润表、美股复权因子、港股资产负债表、港股现金流量表、港股利润表、港股估值分析、港股市值和炸板池接口。当前 README 及 `sub-skills/` 仅介绍和提供仍在支持范围内的接口。
    
    ## 在 ftshare 生态中的位置
    
    `ftshare-market-data` 处于 ftshare 生态的数据 Skill 层。它连接 FTShare 数据服务 `market.ft.tech`,为投研任务、MCP 工具和 Agent 应用提供结构化、可直接调用的行情、财报和宏观数据。
    
    ```text
    FTShare 数据服务 (market.ft.tech)
        ↓  HTTP GET(Python 标准库 urllib)
    ftshare-market-data        # run.py 统一路由 + sub-skills 目录中的实际子 skill
        ↓
    Claude Code / Codex / OpenClaw   # Agent 运行时加载本 Skill
        ↓
    用户                       # 自然语言提问 → JSON 结果
    ```
    
    > 这是一份 **Skill**(给 Agent 运行时消费),不是给人 `import` 的 Python 库。如果你需要在数据分析脚本里编程调用、想要 pandas `DataFrame`,请使用独立维护的 `ftshare-python-sdk`;本次 Skill 调整不修改 SDK。
    
    ## 作为 Skill 加载
    
    本目录已包含标准 Skill 描述文件 `SKILL.md`(带 `name` / `description` frontmatter),把它作为一个 Skill 放进你的 Agent 运行时即可,无需安装任何包。
    
    **Claude Code**:将本目录放入 skills 路径——项目级 `.claude/skills/ftshare-market-data/`,或用户级 `~/.claude/skills/ftshare-market-data/`。Claude Code 会自动读取 `SKILL.md`,在用户提问匹配到行情 / 财报 / 宏观等数据需求时触发。
    
    **Codex / OpenClaw**:同样将本目录作为一个 Skill 加载,运行时读取 `SKILL.md` 的 frontmatter 完成路由(各家具体加载命令请以对应运行时文档为准)。
    
    获取仓库:
    
    ```bash
    git clone https://github.com/ftshare-lab/ftshare-skills.git
    ```
    
    运行时只需要 Python 3:子 skill 仅使用标准库 `urllib`、`json`,**零第三方依赖**,不需要 `pandas`、`requests`。
    
    ## 快速开始
    
    加载 Skill 后,用户用自然语言提问即可。运行时根据 `SKILL.md` 匹配子 skill、执行 `run.py`,并将返回的 JSON 交回用户。
    
    ```bash
    python <RUN_PY> stock-list-all-stocks
    python <RUN_PY> stock-realtime-minute-kline --symbols 600519.SH 000001.SZ
    python <RUN_PY> hk-candlesticks --trade-code 00700.HK --interval-unit day --until-date 2026-03-24
    ```
    
    `<RUN_PY>` 是本目录下 `run.py` 的绝对路径。当前子 skill 仅对应《FTShare 四档套餐全量接口表》及其活动源文档;已下线、未发布和暂不支持的接口不在范围内。
    
    ```json
    {
        "items": [
            { "stock_code": "000001.SZ", "stock_name": "平安银行" },
            { "stock_code": "000002.SZ", "stock_name": "万科A" }
        ]
    }
    ```
    
    ## 调用方式(唯一规则)
    
    `run.py` 是统一调度入口,与 `SKILL.md` 同级。执行时:
    
    1. 取 `SKILL.md` 的绝对路径,将末尾 `/SKILL.md` 替换为 `/run.py`,得到 `<RUN_PY>`。
    2. 调用:`python <RUN_PY> <子skill名> [参数...]`
    
    ```bash
    # 示例
    python <RUN_PY> stock-list-all-stocks
    python <RUN_PY> stock-ipos --page 1 --page_size 20
    python <RUN_PY> semantic-search-news --query 人工智能
    python <RUN_PY> etf-pcfs --date 20260309
    python <RUN_PY> index-weight-summary --index-code 000300 --page 1 --page-size 20
    python <RUN_PY> index-weight-list --index-code 000300 --page 1 --page-size 20
    python <RUN_PY> hk-candlesticks --trade-code 00700.HK --interval-unit day --until-date 2026-03-24
    python <RUN_PY> nth-trade-date --n 5
    python <RUN_PY> eastmoney-shareholder-changes --symbol 股东增持 --page 1 --page-size 20
    python <RUN_PY> ashare-code-change --trade-code 001872.SZ
    python <RUN_PY> ashare-status-change --trade-code 600848.SH --change-type 上市
    python <RUN_PY> economic-china-cpi-monthly
    ```
    
    所有 `market.ft.tech` handler 默认使用 `https://market.ft.tech/gateway`。认证 key 从环境变量 `FTSHARE_API_KEY` 读取,并作为 `FTSHARE_API_KEY` 请求头发送;同时发送 `Content-Type: application/json`。缺少凭据时 handler 不会发起请求。本地或内网服务可通过环境变量切换 API 地址:
    
    ```bash
    FTSHARE_BASE_URL=http://127.0.0.1:8000/ python <RUN_PY> stock-list-all-stocks
    ```
    
    > `run.py` 内部通过 `__file__` 自定位,无论安装在何处都能正确找到各子 skill 的脚本。
    
    ## 返回类型
    
    所有子 skill 一律向 **标准输出打印 JSON**,运行时(Agent)会直接读取这份 JSON,再决定如何以表格 / 要点形式展示给用户。
    
    需要手工解析时(如冒烟测试、定时任务):Shell 中配合 `jq`:
    
    ```bash
    python run.py stock-list-all-stocks | jq '.items[0:3]'
    ```
    
    响应中常见的表格数据信封包括:`data.records`、`data.items`、顶层 `items`、顶层数组。各子 skill 的 `SKILL.md` 会标注其具体响应结构与字段含义。
    
    ## 分页
    
    分页接口同时支持传统 `--page / --page_size` 和更方便的 `--all` 自动翻页。
    
    取单页:
    
    ```bash
    python run.py stock-ipos --page 1 --page_size 20
    ```
    
    自动翻页拉取全量数据:
    
    ```bash
    python run.py stock-ipos --all
    ```
    
    `--all` 仅在具体子 skill 的 `SKILL.md` 明确说明时可用;分页参数也以对应源文档为准。
    
    ## 能力总览
    
    子 skill 以《FTShare 四档套餐全量接口表》为范围依据,按股票、ETF、指数、基金、可转债、期货、港股、板块、资金流、财务、公司治理、新闻公告研报和宏观经济等领域组织。每个接口的参数、响应字段和限制见对应子目录的 `SKILL.md` 及活动源文档;`api-doc/已下线`、`api-doc/未发布` 不计入范围。
    
    
    | 域 | 代表子 skill |
    |---|---|
    | **交易日 / 财经日历 / 新闻公告研报** | `nth-trade-date`、`trading-calendar`、`financial-calendar`、`semantic-search-news`、`news-reaction-snapshot`、`stock-announcements`、`stock-reports`、`stock-prospectuses` |
    | **A 股行情 / 基础** | `stock-list-all-stocks`、`stock-description-all`、`stock-quotes-list`、`stock-ipos`、`stock-intraday`、`eastmoney-all-board-daily-ohlc`、`block-trades`、`margin-trading-details`、`continuous-auction-volume`、`intraday-auction-volume` |
    | **A 股财报 / 业绩** | `stock-income-*`、`stock-balance-*`、`stock-cashflow-*`、`stock-performance-express-*`、`stock-performance-forecast-*` |
    | **A 股股东 / 质押 / 增减持** | `stock-holder-ten`、`stock-holder-ften`、`stock-holder-nums`、`pledge-summary`、`pledge-detail`、`stock-share-chg`、`executive-holdings-changes`、`eastmoney-shareholder-changes` |
    | **A 股公司行动 / 代码与状态** | `shareholder-meeting`、`stock-dividends`、`stock-dividends-effective`、`major-contract-by-date`、`major-contract-by-symbol`、`major-contract-summary`、`ashare-code-change`、`ashare-status-change`、`stock-st-history` |
    | **A 股估值 / 千股千评 / 热度 / 资金流** | `eastmoney-stock-valuation`、`eastmoney-market-valuation`、`stock-comment-index/score/org-participate/desire/focus`、`stock-rank-xueqiu`、`stock-rank-eastmoney`、`stock-capital-flows` |
    | **A 股涨跌停** | `limit-up-pool`、`limit-up-pool-yesterday`、`limit-down-pool` |
    | **A 股商誉** | `stock-goodwill-detail`、`stock-goodwill-impairment`、`stock-goodwill-industry`、`stock-goodwill-market-overview`、`stock-goodwill-predict` |
    | **可转债** | `cb-lists`、`cb-base-data`、`convertible-bond-candlesticks`、`convertible-bond-candlesticks-batch`、`convertible-bond-minutes`、`convertible-bond-realtime-day-kline`、`convertible-bond-realtime-minute-kline`、`convertible-bond-szse-matching-trades` 等深交所成交明细 |
    | **ETF** | `etf-description-all`、`etf-components-all`、`etf-component-details`、`etf-pre-single`、`etf-pcfs`、`etf-pcf-infos`、`etf-share`、`etf-net-value`、`etf-announcements`、`etf-adjust-factor`、`etf-candlesticks`、`etf-candlesticks-batch`、`etf-minutes`、`etf-minutes-batch`、`etf-realtime-minute-kline`、`etf-realtime-day-kline` |
    | **基金** | `fund-basicinfo-single-fund`、`fund-cal-return-...`、`fund-nav-single-fund-paginated`、`fund-overview-all-funds-paginated`、`fund-support-symbols-all-funds-paginated` |
    | **指数** | `index-detail`、`index-list-paginated`、`index-ohlcs`、`index-prices`、`index-candlesticks`、`index-candlesticks-batch`、`index-minutes`、`index-minutes-batch`、`sw-index-history-minutes`、`index-realtime-minute-kline`、`index-realtime-day-kline`、`index-description-all/paginated/download`、`index-weight-summary/list/download` |
    | **板块(东财 / 同花顺)** | `eastmoney-concept-boards`、`eastmoney-board-constituents/daily-ohlc/latest-ohlc`、`10jqk-board-list/kline/all-kline`、`ths-industry-constituents` |
    | **港股** | `company-hk`、`hk-candlesticks`、`northbound`、`southbound`、`eastmoney-hk-index-daily-kline`、`hsi-daily-weight` |
    | **美股** | `eastmoney-us-stock-list`、`eastmoney-us-stock-daily-ohlc`、`us-basic` |
    | **期货** | `futures-base-data`、`futures-lists`、`futures-limit`、`futures-settle`、`futures-weekly-detail`、`futures-warehouse-receipt`、`futures-contract-kline`、`eastmoney-futures-position`、`eastmoney-futures-strange`、`member-build-process`、`member-position-ranking` |
    | **宏观经济(中国 + 美国)** | `economic-china-gdp/cpi/ppi/pmi/lpr/...-monthly`(15 项)、`economic-us-economic-by-type`(16 类,按 `--type`) |
    
    ## 名称 → 代码映射
    
    部分接口只接受代码而非名称。具体代码格式与映射方式以对应子 skill 文档为准。
    
    ## 查看可用接口
    
    不带参数运行 `run.py` 会打印用法并列出全部可用子 skill:
    
    ```bash
    python run.py
    ```
    
    查看某个接口的详细参数、响应结构与字段说明:
    
    ```bash
    cat sub-skills/stock-list-all-stocks/SKILL.md
    ```
    
    ## Base URL 配置
    
    `market.ft.tech` 接口默认以 `https://market.ft.tech/gateway` 为基础地址,使用 HTTP GET;每个请求均需在请求头携带 `FTSHARE_API_KEY` 和 `Content-Type: application/json`:
    
    ```text
    /api/v1/market/data/<接口路径>
    ```
    
    如需切到本地或内网服务,设置 `FTSHARE_BASE_URL`。变量里是否带 `/gateway` 会原样保留:
    
    ```bash
    FTSHARE_BASE_URL=http://127.0.0.1:8000/ python <RUN_PY> stock-list-all-stocks
    FTSHARE_BASE_URL=http://127.0.0.1:8000/gateway/ python <RUN_PY> stock-list-all-stocks
    ```
    
    - 所有 `market.ft.tech` 接口都通过 `FTSHARE_BASE_URL` 切换基础地址。
    
    ## 安全与约束
    
    - **域名白名单**:使用 `safe_urlopen` 的 handler 会校验请求协议和主机匹配当前基础地址;设置 `FTSHARE_BASE_URL` 后按该地址校验。
    - **子 skill 白名单**:`run.py` 仅允许 `sub-skills/<名称>/scripts/handler.py` 形态的子 skill,防止路径遍历。
    - **下载落盘限制**:含 `--output` 的下载类接口仅允许写入**当前工作目录**下的路径。
    - **依赖前序接口的参数**:下载类接口的 `url_hash` / `filename` 须先由对应的列表接口取得,勿硬编码。
    
    ## 项目结构
    
    ```text
    ftshare-market-data/
      SKILL.md                # Skill 入口文档:frontmatter(name/description)+ 能力总览 + 调用规则
      run.py                  # 统一调度入口:校验并执行子 skill 的 handler
      README.md               # 本文档
      sub-skills/
        <子skill名>/
          SKILL.md            # 该接口的参数、响应结构、字段说明
          scripts/
            handler.py        # 具体实现:HTTP GET → 打印 JSON
    ```
    
    ## 与 ftshare-python-sdk 的关系
    
    本仓库是 **Skill(命令行驱动,给 Agent 运行时)**;`ftshare-python-sdk` 是 **Python 库(给开发者编程)**。两者覆盖同一批 FTShare 数据接口,按使用形态区分:
    
    | | ftshare-market-data(本仓库) | ftshare-python-sdk |
    |---|---|---|
    | 形态 | Skill(被运行时加载) | pip 包 |
    | 入口 | `python run.py <子skill>` | `ft.market_api().<方法>()` |
    | 返回 | 原始 JSON(stdout) | pandas `DataFrame` |
    | 消费者 | Claude Code / Codex / OpenClaw | 数据分析脚本 / 量化研究 |
    | 依赖 | 仅 Python 标准库 | `pandas`、`requests` |
    
  • run.py 2.7 KB
    #!/usr/bin/env python3
    """
    ftshare-market-data 统一调度入口。
    
    用法:
        python run.py <subskill名> [handler参数...]
    
    示例:
        python run.py stock-list-all-stocks
        python run.py stock-quotes-list --order_by "change_rate desc" --page_no 1 --page_size 30
        python run.py stock-ipos --page 1 --page_size 20
        python run.py block-trades
        python run.py margin-trading-details --page 1 --page_size 20
        python run.py semantic-search-news --query 人工智能
        python run.py cb-lists
        python run.py etf-pcfs --date 20260309
        python run.py fund-basicinfo-single-fund --fund-code 000001
        python run.py hk-candlesticks --trade-code 00700.HK --interval-unit day --until-date 2026-03-24 --since-date 2026-03-01 --limit 20
        python run.py index-weight-summary --page 1 --page-size 20
        python run.py index-weight-list --index-code 000300 --page 1 --page-size 20
    
    """
    import os
    import runpy
    import sys
    
    SKILL_ROOT = os.path.dirname(os.path.abspath(__file__))
    
    
    def _allowed_subskills():
        """仅允许 sub-skills 目录下存在 scripts/handler.py 的名称,防止路径遍历。"""
        sub_skills_dir = os.path.join(SKILL_ROOT, "sub-skills")
        allowed = set()
        if not os.path.isdir(sub_skills_dir):
            return allowed
        for name in os.listdir(sub_skills_dir):
            if os.path.isfile(os.path.join(sub_skills_dir, name, "scripts", "handler.py")):
                allowed.add(name)
        return allowed
    
    
    
    
    def _execute_handler(handler: str, handler_args: list) -> int:
        """在当前进程执行 handler.py,避免 subprocess 告警。"""
        old_cwd = os.getcwd()
        old_argv = sys.argv[:]
        try:
            os.chdir(SKILL_ROOT)
            sys.argv = [handler] + handler_args
            runpy.run_path(handler, run_name="__main__")
            return 0
        except SystemExit as e:
            code = e.code
            if isinstance(code, int):
                return code
            return 1 if code else 0
        finally:
            sys.argv = old_argv
            os.chdir(old_cwd)
    
    def main():
        sub_skills_dir = os.path.join(SKILL_ROOT, "sub-skills")
        allowed = _allowed_subskills()
    
        if len(sys.argv) < 2:
            print("用法: python run.py <subskill名> [参数...]", file=sys.stderr)
            print("\n可用 subskill:")
            for name in sorted(allowed):
                print(f"  {name}")
            sys.exit(1)
    
        subskill = sys.argv[1]
        if subskill not in allowed:
            print(f"错误:未找到 subskill '{subskill}',或名称不合法。", file=sys.stderr)
            sys.exit(1)
    
        handler = os.path.join(sub_skills_dir, subskill, "scripts", "handler.py")
    
        exit_code = _execute_handler(handler, sys.argv[2:])
        sys.exit(exit_code)
    
    
    if __name__ == "__main__":
        main()
    
  • SKILL.md 4.3 KB
    ---
    name: ftshare-market-data
    description: FTShare 市场数据技能集。根据用户对股票、ETF、指数、基金、可转债、期货、港股、美股、资金流、财务数据或宏观数据的查询意图,匹配并执行对应子 skill。
    ---
    
    # FTShare Market Data Skills
    
    本 skill 是统一路由入口。子 skill 位于 `sub-skills/`,由同目录的 `run.py` 动态发现和执行。
    
    ## 调用
    
    ```bash
    python <RUN_PY> <子skill名> [参数...]
    ```
    
    `<RUN_PY>` 是本文件同级的 `run.py` 绝对路径。不带参数运行会列出当前可用子 skill。
    
    ## 认证与请求约束
    
    - 所有请求必须提供 `FTSHARE_API_KEY` 环境变量。
    - handler 将其作为 `FTSHARE_API_KEY` 请求头发送,并设置 `Content-Type: application/json`。
    - 未设置凭据时,handler 在发起请求前以非零状态退出。
    - 默认基础地址为 `https://market.ft.tech/gateway`,可通过 `FTSHARE_BASE_URL` 覆盖。
    - 请求使用各子 skill 文档声明的 `/api/v1`、`/api/v2`、`/api/v3` 或 `/api/v4` 路由;handler 会限制请求 scheme 和 host 与基础地址一致。
    
    ## 常用示例
    
    ```bash
    python <RUN_PY> stock-description-all --symbol-id 600000.SH --page 1 --page-size 1
    python <RUN_PY> stock-realtime-minute-kline --symbols 600519.SH 000001.SZ
    python <RUN_PY> stock-minutes --symbol 600519.SH --since-ts-millis 1787189400000 --until-ts-millis 1787191200000
    python <RUN_PY> etf-realtime-day-kline --symbols 510300.SH
    python <RUN_PY> index-minutes --symbol 000300.SH --since-ts-millis 1787189400000 --until-ts-millis 1787191200000
    python <RUN_PY> stock-reports --stock-code 600036.SH --page 1 --page-size 20
    python <RUN_PY> stock-announcements --stock-code 600000 --page 1 --page-size 20
    python <RUN_PY> stock-prospectuses --stock-code 600000.SH --page 1 --page-size 20
    python <RUN_PY> stock-candlesticks-batch --symbols 600519.SH,510300.SH --interval-unit Day --since-ts-millis 1787000000000 --until-ts-millis 1787191200000
    python <RUN_PY> etf-candlesticks-batch --symbols 510300.XSHG,159915.XSHE --interval-unit Day --since-ts-millis 1787000000000 --until-ts-millis 1787191200000
    python <RUN_PY> index-candlesticks-batch --symbols 000300.XSHG,399001.XSHE --interval-unit Day --since-ts-millis 1787000000000 --until-ts-millis 1787191200000
    python <RUN_PY> futures-contract-kline --symbol A2605.DCE --interval daily --limit 5
    python <RUN_PY> etf-pcf-infos --symbol 510300.SH --trade-date 20260909
    python <RUN_PY> etf-net-value --etf-code 510300 --nav-date 20260909
    python <RUN_PY> etf-share --etf-code 510300 --page 1 --page-size 5
    python <RUN_PY> etf-announcements --etf-code 159915 --page 1 --page-size 5
    python <RUN_PY> etf-component-details --symbol 510300.SH
    python <RUN_PY> stock-dividends-effective --symbol 600519.XSHG --page 1 --page-size 20
    python <RUN_PY> executive-holdings-changes --stock-code 600519 --page 1 --page-size 20
    python <RUN_PY> eastmoney-shareholder-changes --symbol 股东增持 --page 1 --page-size 20
    python <RUN_PY> ashare-code-change --trade-code 001872.SZ
    python <RUN_PY> ashare-status-change --trade-code 600848.SH --change-type 上市
    python <RUN_PY> stock-st-history --symbol 600735.SH,000004.SZ
    python <RUN_PY> nth-trade-date --n 5
    python <RUN_PY> continuous-auction-volume --trade-date 20260620 --page 1 --page-size 50
    python <RUN_PY> news-reaction-snapshot --symbol 600519.SH --start-date 20260818 --end-date 20260828 --lookback-hours 48 --page 1 --page-size 5
    python <RUN_PY> semantic-search-news --query 人工智能
    ```
    
    ## 能力范围
    
    当前子 skill 仅覆盖《FTShare 四档套餐全量接口表》中的接口;接口详细参数、响应字段和限制以对应子目录的 `SKILL.md` 及 `ftshare-doc/api-doc/` 活动源文档为准。`api-doc/已下线` 和 `api-doc/未发布` 不属于当前范围。
    
    ## 输出与错误
    
    - 成功响应以格式化 JSON 输出到 stdout。
    - HTTP、网络、参数和认证诊断输出到 stderr。
    - 请求失败时返回非零退出状态。
    - 下载类子 skill 只允许将文件写入当前工作目录及其子目录。
    
    ## 运行时发现
    
    `run.py` 仅执行 `sub-skills/<名称>/scripts/handler.py` 形式的动态发现结果,不维护重复的静态注册表。新增或移除子 skill 时,应同步其目录中的 `SKILL.md` 和 handler,并以套餐接口表和源接口文档进行核对。
    
    本包是 Agent Skill,不是 Python SDK;SDK 的同步由独立任务负责。
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related