程序员量化交易实战 05:量化系统最重要的是数据,不是策略
原创 · 约 18 分钟阅读 · 阅读 --
Last updated on

程序员量化交易实战 05:量化系统最重要的是数据,不是策略

作者: Alex Xiang


程序员量化交易实战 05:量化系统最重要的是数据,不是策略

古董级程序员,大厂出来后一直在创业公司,现在仍活跃在一线做 AI 相关的开发。这个专栏会把一个 A 股量化平台从 0 到 1 拆开写:数据、策略、回测、模拟盘、提醒和生产化,尽量用真实代码和真实运行结果说话。更完整的更新也会同步到微信公众号「字与码」。

前四篇已经把边界、工程骨架、核心概念和 A 股交易规则铺好了。

现在很多人会想:是不是终于可以写策略了?还不急。量化系统里最容易被低估的部分不是策略,而是数据。脏数据、缺数据、错口径数据,会让任何策略看起来像有用,也会让任何回测失去意义。

这一篇开始进入数据层。

程序员量化交易实战第五篇封面

策略之前先问数据从哪来

一个简单的均线策略看起来只需要收盘价。

但真落到系统里,问题会立刻变多:

  • 股票列表从哪里来?
  • 退市股票怎么处理?
  • 停牌日有没有 K 线?
  • 成交量单位是股、手,还是供应商自定义单位?
  • 日线是前复权、后复权,还是不复权?
  • 财报数据是否有发布时间滞后?
  • 数据源失败时要不要 fallback?
  • fallback 数据能不能进入正式回测?

这些问题如果不在数据层解决,策略层就会到处打补丁。

数据层先解决什么问题

数据源不能写死在策略里

策略只应该消费统一后的数据,不应该知道供应商细节。

比如策略想要一段日线,它不应该自己去拼 QVeris、东方财富、Tushare 或同花顺的请求参数。策略应该调用统一接口,拿到标准化后的 MarketBar

ZiQuant 里用 DataSourceConfig 管理数据源配置:

class DataSourceConfig(Base):
    __tablename__ = "zi_quant_data_source_configs"

    name = mapped_column(String(80), unique=True)
    adapter = mapped_column(String(40), index=True)
    enabled = mapped_column(Boolean, default=True)
    priority = mapped_column(Integer, default=100)
    config_json = mapped_column(JSONB, default=dict)
    secret_ref = mapped_column(String(200), nullable=True)
    last_status = mapped_column(JSONB, default=dict)

几个字段很关键。

adapter 表示供应商类型,比如 qveriseastmoneytusharepriority 表示优先级。secret_ref 不直接存密钥,而是引用环境变量或密钥系统。last_status 保存最近一次调用状态,方便排障。

这样设计以后,策略不需要关心谁提供数据。供应商可以替换,策略逻辑不动。

数据源抽象要留替换空间

行情数据要记录来源

行情表不是只有价格。

ZiQuant 的 MarketBar 里有 sourcepayload

class MarketBar(Base):
    __tablename__ = "zi_quant_market_bars"

    symbol = mapped_column(String(16), index=True)
    trade_date = mapped_column(Date, index=True)
    frequency = mapped_column(String(16), default="1d", index=True)
    open = mapped_column(Float)
    high = mapped_column(Float)
    low = mapped_column(Float)
    close = mapped_column(Float)
    volume = mapped_column(Float, default=0)
    amount = mapped_column(Float, default=0)
    source = mapped_column(String(40), default="unknown", index=True)
    payload = mapped_column(JSONB, default=dict)

如果一条数据来自东方财富,另一条来自 QVeris,还有一条来自 fallback,系统必须能区分。否则后面发现回测异常时,很难判断是策略问题,还是数据混了口径。

数据来源不是审计洁癖。它是排障入口。

财报数据有滞后性

价格数据通常按交易日更新,财报数据不是。

财报有报告期,也有披露时间。2025 年年报不等于 2025 年最后一个交易日就可用。如果回测时提前使用未来才披露的财报,就会出现未来函数。

ZiQuant 当前的 FinancialReport 先保存基础字段:

class FinancialReport(Base):
    __tablename__ = "zi_quant_financial_reports"

    symbol = mapped_column(String(16), index=True)
    report_date = mapped_column(Date, index=True)
    report_type = mapped_column(String(40), default="income")
    revenue = mapped_column(Float, nullable=True)
    net_profit = mapped_column(Float, nullable=True)
    roe = mapped_column(Float, nullable=True)
    source = mapped_column(String(40), default="unknown", index=True)
    payload = mapped_column(JSONB, default=dict)

后续我们会继续补披露时间和口径字段。现在先建立原则:财报因子不能直接用“报告期”当作“可交易日可见信息”。

fallback 数据只能救急,不能糊弄

开发早期,系统可能用 fallback 数据让页面和接口先跑起来。这没问题,但 fallback 必须显式标记。

ZiQuant 的 README 里已经写清楚:真实行情和财报会标记真实来源,fallback 数据会显式标记为 simulated_fallback。生产质量检查里也会统计 fallback 比例。

这条规则非常重要。fallback 可以用于本地开发、界面联调、测试空链路,但不能假装成真实行情进入正式策略结论。

数据质量要变成接口

数据质量不应该靠人打开数据库看几眼。

ZiQuant 已经准备了几个接口:

GET  /api/data/quality
GET  /api/data/provenance
POST /api/data/coverage
POST /api/stocks/sync
POST /api/data/sync
POST /api/financials/sync
POST /api/financials/coverage

这些接口服务于不同问题:

  • quality 看整体质量:覆盖率、新鲜度、fallback 比例。
  • provenance 看数据来源:各来源多少行,最近同步状态如何。
  • coverage 看某批股票有没有足够行情。
  • sync 系列负责触发股票、行情和财报同步。

后面写策略前,我们会先用这些接口确认数据能不能支撑回测。

数据源接口先定义清楚

第五篇先不实现完整供应商 SDK,但要把接口边界想清楚。

一个数据源适配器至少应该提供三类能力:

class MarketDataProvider:
    async def get_stock_basic(self) -> list[dict]:
        ...

    async def get_daily_bars(self, symbol: str, start: str, end: str) -> list[dict]:
        ...

    async def get_financial_reports(self, symbol: str, limit: int = 8) -> list[dict]:
        ...

注意这里返回的还不是最终 ORM 对象,而是供应商数据经过第一层解析后的结构。真正写入数据库前,还需要标准化、去重、校验和来源标记。

数据层最小验收

数据层不是写完一个下载脚本就结束。至少要能回答下面几个问题:

  • 股票主数据有多少只?
  • 公共股票池有多少只?
  • 每只股票有多少根日 K?
  • 最近一根 K 线是哪天?
  • 多少数据来自真实供应商?
  • fallback 比例是多少?
  • 财报覆盖多少股票?
  • 同步失败时最近错误是什么?

ZiQuant 的 /ready/api/data/quality/api/data/provenance 会逐步承担这些检查。

本篇实战任务

这一篇先确认现有数据模型和质量接口入口。

运行表结构测试:

cd /home/alex/work/yswx/zi-quant-platform
uv run pytest tests/test_services.py -k "schema or data_source or data_provenance"

如果从 GitHub 拉取这一章对应代码:

git clone https://github.com/ax2/zi-quant-platform.git
cd zi-quant-platform
git checkout chapter-05
uv sync --extra dev
uv run pytest

启动服务后看健康状态:

uv run uvicorn app.main:app --host 127.0.0.1 --port 8092
curl http://127.0.0.1:8092/health
curl http://127.0.0.1:8092/ready

如果配置了 API Token,后续同步类接口要带 X-Zi-Api-Token。这一点后面写真实数据同步时会展开。

前五篇阶段 review

到这里,第一组五篇完成了一个小闭环。

第 1 篇回答为什么程序员适合做量化,定下“研究、回测、模拟盘,不真实下单”的边界。

第 2 篇搭 Python 项目骨架,让 ZiQuant 可以安装、启动、配置和测试。

第 3 篇把核心概念落到模型:股票、股票池、K 线、因子、策略、回测和模拟盘。

第 4 篇把 A 股交易规则落到代码,新增了可测试的 trading_rules 模块。

第 5 篇开始数据层,强调数据源抽象、来源记录、质量检查和 fallback 标记。

这五篇没有重复写“量化不是玄学”这类开场,而是逐步把项目从认知边界推进到工程骨架、领域模型、交易规则和数据层入口。后面第二组文章会继续沿着数据层展开:PostgreSQL 表设计、股票基础信息表、500 只 A 股股票池、真实行情接入和数据清洗。

本章更新与代码仓库

本章更新内容:

  • 明确数据层优先于策略层。
  • 梳理数据源抽象、行情来源、财报滞后、fallback 标记和质量接口。
  • 完成前五篇阶段 review,确认第一组文章风格、边界和项目进度一致。

代码仓库:

https://github.com/ax2/zi-quant-platform

本章代码:

git clone https://github.com/ax2/zi-quant-platform.git
cd zi-quant-platform
git checkout chapter-05
uv sync --extra dev
uv run pytest

本篇小结

策略之前先做数据。

没有来源记录、质量检查、覆盖率和 fallback 标记,策略输出就没有可信基础。程序员做量化,真正的起点不是写一个买卖公式,而是让数据链路可替换、可检查、可复盘。

下一篇我们进入 PostgreSQL,开始把股票、行情、财报和股票池这些对象稳定地存下来。