跳转至

场景示例

每个片段都是完整可跑的。共同前提:

import asyncio
from pathlib import Path
from pytdxdata import TdxData
from pytdxdata.models import Adjust, KlinePeriod

全市场日线批量落库

传多标的时,连接池会把不同股票分配到不同服务器——这正是它比逐个 await 快的原因。

async def dump_all_daily():
    async with TdxData(cache_dir=Path("./tdx_cache")) as td:
        infos = await td.get_universe("sz")                    # 缓存 1 天
        symbols = [i.symbol for i in infos]                    # ["sz000001", ...]

        bars = await td.get_bars(
            symbols,
            KlinePeriod.DAY,
            count=800,
            adjust=Adjust.QFQ,
            concurrency=12,
        )
        got = {b.symbol for b in bars}
        print(f"{len(got)} / {len(symbols)} 只拿到数据")

        # bars 是扁平 list[SecurityBar](按标的/时间排序),落库 / 写 parquet 都行
        if bars:
            print(bars[-1].symbol, bars[-1].datetime, bars[-1].close)

分页与并发的取舍

concurrency 不是越大越好。单台服务器有连接上限,concurrency 超过 pool_max × 服务器数 之后只会排队。实践值:8~16。

分钟线增量更新

只拉「上次之后」的部分,用 get_bars(start_date=...) 省掉全量重拉。

async def update_minute(symbol: str, last_dt):
    async with TdxData(cache_dir=Path("./tdx_cache")) as td:
        rows = await td.get_bars(symbol, KlinePeriod.MIN_1, start_date=last_dt)
        return [r for r in rows if r.datetime > last_dt]

1 分钟 K 线走 MAC 单连接串行分页

这是刻意设计:并发分页会把不同页分到不同服务器,而各服务器历史深度不一致, 拼接后会出现非确定性数据空洞。所以 MIN_1 强制单连接串行(见 通道与数据源)。

报价轮询

async def poll_quotes(symbols, interval=5.0):
    async with TdxData() as td:
        while True:
            quotes = await td.get_quotes(symbols)      # >80 只自动切批;缓存 TTL 5s
            for q in quotes:
                print(q.code, q.price, q.bid[0], q.ask[0])
            await asyncio.sleep(interval)

缓存 TTL 与轮询间隔对齐时,重复轮询几乎 0 网络开销。但注意通达信公开源本身是 15 分钟延时数据,别拿它做实时交易决策。

板块轮动 / 领涨股

async def sector_rotation():
    async with TdxData() as td:
        # 近 5 日概念板块涨幅排行
        top = await td.get_board_change_ranking(kind="concept", days=5, top_n=10)
        for row in top:
            print(row)

        # 拿第一名板块的成分股
        symbol = top[0]["symbol"] if "symbol" in top[0] else "881001"
        members = await td.get_board_members(symbol, count=20)
        for m in members[:5]:
            print(m.code, m.name, m.fields)

        # 板块资金汇总
        print(await td.get_board_summary(symbol))

个股所属板块(反向查询)

async def belong(symbol: str):
    async with TdxData() as td:
        for b in await td.get_board_of(symbol):
            print(b)

逐笔复盘

async def review_transactions(symbol: str, ymd: int):
    async with TdxData() as td:
        rows = await td.get_ticks(symbol, date=ymd, count=2000)
        total = sum(r.trade_count or 0 for r in rows)
        buy = sum(r.vol for r in rows if r.bs_flag == 0)     # 主动买
        sell = sum(r.vol for r in rows if r.bs_flag == 1)    # 主动卖
        print(f"{ymd} 总成交笔数={total} 主动买={buy} 主动卖={sell}")

财务因子快照

async def finance_snapshot(symbols):
    async with TdxData() as td:
        for fin in await td.get_finance(symbols):   # 标准通道,最新快照
            print(fin.symbol, fin)

历史专业财报(gpcw*,含多报告期)走文件接口。

索引里的文件名与服务器实际不一致

tdxfin/gpcw.txt 索引里写的是 gpcwYYYYMMDD.zip,但服务器实际只提供未压缩的 tdxfin/gpcwYYYYMMDD.dat —— 这些差异已由 get_financial_records_parsed() 内部处理, 直接把索引里的 filename 传进去即可。

另外索引里含有尚未生成的占位条目(filesize 只有百来字节),取它们会得到空列表。 想拿最新一期可用数据,先按体积过滤:

usable = [fi for fi in infos if fi.filesize > 500_000]   # 排除占位条目
records = await td.get_financial_records_parsed(usable[0].filename)
async def history_financials(code: str):
    async with TdxData(cache_dir=Path("./tdx_cache")) as td:
        infos = await td.get_financial_file_infos()             # 索引
        records = await td.get_financial_records_parsed(infos[-1].filename)
        row = next((r for r in records if r.code == code), None)
        if row:
            print(row.report_date, row.fields[:8])

行业分类映射

async def industry_lookup(codes):
    async with TdxData(cache_dir=Path("./tdx_cache")) as td:
        hy = await td.get_industry_map()          # {6位代码: IndustryInfo}
        for c in codes:
            info = hy.get(c)
            if info:
                print(c, info.tdx_industry, info.sw_industry)

板块成分(离线解析)

get_block_parsed 拿到的是纯 dataclass,落成本地 JSON 后完全脱网可用—— 适合做「板块成分快照」的定时任务。

import json

async def snapshot_blocks():
    async with TdxData(cache_dir=Path("./tdx_cache")) as td:
        all_blocks = []
        for fn in ("block_zs.dat", "block_gn.dat", "block_fg.dat"):
            for b in await td.get_block_parsed(fn):
                all_blocks.append({
                    "name": b.name,
                    "category": b.category,
                    "codes": b.codes,
                })
        Path("blocks.json").write_text(
            json.dumps(all_blocks, ensure_ascii=False), encoding="utf-8"
        )

港股 / 美股 / 期货 / 期权

async def ex_markets():
    async with TdxData() as td:
        hk = await td.get_bars("hk00700", KlinePeriod.DAY, count=700)
        us = await td.get_bars("usAAPL", KlinePeriod.DAY, count=700)
        fut = await td.get_bars("cffex:IFL0", KlinePeriod.DAY, count=700)
        opt = await td.get_bars("sh_stock_option:10010971", KlinePeriod.DAY, count=700)
        print(len(hk), len(us), len(fut), len(opt))

        # 港股全量逐笔(自动翻页,≤50 页 / 9 万条)
        ticks = await td.get_ticks("hk00700", count=None)
        print(len(ticks))

转成 pandas

包本身不带 pandas 依赖,但转换是一行的事:

import pandas as pd

async def to_dataframe(symbol: str):
    async with TdxData() as td:
        bars = await td.get_bars(symbol, KlinePeriod.DAY, count=800)
    df = pd.DataFrame([{
        "dt": b.datetime, "open": b.open, "high": b.high,
        "low": b.low, "close": b.close, "vol": b.vol, "amount": b.amount,
    } for b in bars])
    return df.set_index("dt")

长驻服务(FastAPI)

不要每次请求都新建 TdxData

池与缓存都挂在实例上,每请求新建 = 每请求重建连接池 + 缓存全部失效。

from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request

@asynccontextmanager
async def lifespan(app: FastAPI):
    async with TdxData(cache_dir=Path("./tdx_cache"), pool_min=4, pool_max=16) as td:
        app.state.td = td
        yield

app = FastAPI(lifespan=lifespan)

@app.get("/kline/{symbol}")
async def kline(symbol: str, request: Request):
    td = request.app.state.td
    bars = await td.get_bars(symbol, KlinePeriod.DAY, count=30)
    return [{"dt": str(b.datetime), "close": b.close} for b in bars]

下一步