程序员量化交易实战 02:从零搭建 Python 量化项目结构
原创 · 约 25 分钟阅读 · 阅读 --
Last updated on

程序员量化交易实战 02:从零搭建 Python 量化项目结构

作者: Alex Xiang


程序员量化交易实战 02:从零搭建 Python 量化项目结构

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

上一篇我们先把边界讲清楚:这个系列只做研究、回测、提醒和模拟盘,不连接券商,不下真实订单。

这一篇开始搭工程骨架。量化项目最容易从一个 notebook 或一个 main.py 开始,然后越写越乱:数据同步脚本放在桌面,策略参数写死在代码里,数据库连接散落在函数里,测试靠手工点页面。等策略稍微复杂一点,项目就很难复现。

所以第二篇不急着写策略。我们先把 ZiQuant 变成一个能安装、能启动、能配置、能测试的 Python 项目。

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

为什么量化项目也需要工程化

很多量化入门教程会从一段回测代码开始:

prices = get_price("600519.SH")
signal = prices.close > prices.close.rolling(20).mean()

这当然直观,但它只解决了“我能不能算出一个指标”。真正做成平台以后,还要回答更多问题:

  • 数据从哪里来,失败时怎么知道?
  • 数据库结构变更怎么升级?
  • 策略参数改过没有,谁改的?
  • 回测结果能不能复现?
  • 定时任务失败后怎么补跑?
  • 模拟盘订单为什么被拒绝?
  • 项目换一台机器后能不能启动?

这些问题和金融没什么关系,都是工程问题。程序员做量化,第一优势就在这里:不把策略当成一段孤立代码,而是把它放进一个可运行系统里。

ZiQuant 第一版工程骨架

第一版目录结构

当前 ZiQuant 项目的核心目录是这样的:

zi-quant-platform/
  app/
    __init__.py
    db.py
    main.py
    models.py
    services.py
    settings.py
    static/
  scripts/
    check_migrations.py
    db_backup.py
    production_smoke.py
    run_due_jobs.py
  tests/
    test_db_backup.py
    test_production_smoke.py
    test_run_due_jobs.py
    test_services.py
  migrations/
    env.py
    versions/
  .env.example
  .gitignore
  alembic.ini
  pyproject.toml
  README.md
  uv.lock

这个结构不是为了好看。每一层都有明确职责。

app/ 是服务主体。FastAPI 入口、配置、数据库连接、ORM 模型和核心业务服务都放在这里。后面股票池、行情、因子、策略、回测、模拟盘和管理接口都会从这里长出来。

scripts/ 放可重复执行的运维动作。比如到期任务 runner、数据库备份、生产 smoke 检查、迁移检查。脚本和 Web API 一样重要,因为量化平台每天都要自动跑任务。

tests/ 放单元测试和轻量集成测试。这个系列后面会经常改交易规则、因子逻辑和策略准入条件,如果没有测试,很容易在某篇文章里把前面的能力写坏。

migrations/ 放数据库迁移。量化平台的数据模型会持续演进,不能靠“删库重来”解决问题。

pyproject.tomluv.lock 用来固定依赖和命令入口。项目只要进入长期维护,就必须让依赖可复现。

用 uv 管理依赖

ZiQuant 使用 uv 管理 Python 环境。当前 pyproject.toml 的主体依赖包括:

[project]
name = "zi-quant-platform"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
  "alembic>=1.13.0",
  "asyncpg>=0.31.0",
  "fastapi>=0.115.0",
  "httpx>=0.27.0",
  "pydantic>=2.8.0",
  "pydantic-settings>=2.4.0",
  "sqlalchemy[asyncio]>=2.0.30",
  "uvicorn[standard]>=0.30.0",
]

开发依赖单独放在 dev extra:

[project.optional-dependencies]
dev = [
  "pytest>=8.0",
  "pytest-asyncio>=0.24.0",
  "httpx>=0.27.0",
]

安装命令:

cd /home/alex/work/yswx/zi-quant-platform
uv sync --extra dev

这里我不建议一开始就把 pandas、机器学习框架、回测框架、绘图库全部塞进去。依赖越多,项目越重,环境问题越难排查。真正需要时再加,并且每次加依赖都要说明它解决什么问题。

配置必须从第一天分层

量化项目会接触数据库、数据源 API Key、DeepSeek Key、飞书群 ID、生产模式开关和访问 Token。配置如果散在代码里,后面一定会出事。

ZiQuant 用 pydantic-settings 读取 .env

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    database_url: str = "postgresql+asyncpg:///postgres?host=/var/run/postgresql"
    host: str = "127.0.0.1"
    port: int = 8092
    qveris_data_api_key: str = ""
    deepseek_api_key: str = ""
    zi_api_token: str = ""
    zi_deployment_mode: str = "development"
    auto_create_schema: bool = True

    model_config = SettingsConfigDict(env_file=".env", extra="ignore")


settings = Settings()

这段代码看起来普通,但它确立了几条规则。

第一,默认值可以让本地开发尽快跑起来。

第二,密钥默认是空字符串,不写死在代码里。

第三,生产模式和开发模式从配置上区分。比如 AUTO_CREATE_SCHEMA=true 对本地开发方便,但生产环境应该显式跑 Alembic 迁移,不应该让服务启动时偷偷改表。

第四,.env.example 可以入库,.env 不能入库。

.gitignore 至少要包含:

.env
.venv/
__pycache__/
.pytest_cache/

以后我们接入 QVeris、DeepSeek 和飞书时,都会沿用这个配置边界。

配置、启动和测试闭环

数据库连接先保持简单

项目现在的数据库连接放在 app/db.py

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from app.models import Base
from app.settings import settings

engine = create_async_engine(settings.database_url, pool_pre_ping=True)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)


async def init_schema() -> None:
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)


async def get_session():
    async with SessionLocal() as session:
        yield session

这里先用 SQLAlchemy async。原因很直接:后面的 Web API、数据同步、任务 runner 都会频繁访问数据库,用 async 栈更容易和 FastAPI 对齐。

init_schema() 是本地开发便利入口。它让新环境能先跑起来。但这不等于生产环境也应该自动建表。生产部署时要跑:

uv run alembic upgrade head

项目里也已经准备了迁移检查脚本:

uv run python scripts/check_migrations.py

这就是工程骨架和一次性脚本的区别:一次性脚本只关心“现在跑通”;工程项目要关心“下次怎么升级”。

先做健康检查

一个服务能启动,不代表它能工作。ZiQuant 先放了两个检查接口。

/health 做轻量检查:

@app.get("/health")
async def health(session: AsyncSession = Depends(get_session)) -> dict:
    db_ok = True
    try:
        await session.execute(select(Stock).limit(1))
    except Exception:
        db_ok = False
    return {"status": "ok" if db_ok else "degraded", "database": db_ok, "tables": table_names()}

它不做复杂业务判断,只回答一个问题:服务和数据库是否基本可用。

/ready 会更严格,后面会检查迁移、股票池、数据源、策略、模拟盘和任务状态。上线前不能只看进程还活着,要看系统是否真的具备运行条件。

启动服务:

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

第一阶段如果 /health 都不稳定,就不要急着写策略。

写一个最小测试

第二篇的项目验收,不是“文件都创建了”,而是测试能跑。

项目当前的 pytest 配置写在 pyproject.toml

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

运行:

uv run pytest

这一章对应的项目 tag:

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

第一批测试不需要复杂。比如股票池种子数据数量、核心表是否存在、配置脱敏是否生效、权限检查是否符合预期,都适合作为早期测试。

类似这样的测试就很有价值:

def test_seed_pool_has_500_unique_stocks():
    stocks = build_seed_stocks()
    assert len(stocks) == 500
    assert len({s["symbol"] for s in stocks}) == 500
    assert stocks[0]["symbol"] == "600519.SH"

它看起来不像“高大上量化”,但它保护了后续所有策略的运行范围。股票池如果重复、缺失或混入无效代码,后面的因子和回测都会被污染。

为什么现在就要 console scripts

pyproject.toml 里还有一段命令入口:

[project.scripts]
zi-quant-platform = "app.main:run"
zi-quant-run-due-jobs = "scripts.run_due_jobs:main"
zi-quant-production-smoke = "scripts.production_smoke:main"
zi-quant-db-backup = "scripts.db_backup:main"

这不是为了少敲几个字。它的价值是让常用操作有稳定入口。

以后部署到服务器、写 cron、做 systemd service、让别人接手项目时,不应该让对方从 README 里复制一长串 Python 文件路径。入口稳定,运维脚本才容易沉淀。

例如后面定时任务会这样跑:

uv run zi-quant-run-due-jobs --limit 3

生产 smoke 会这样跑:

uv run zi-quant-production-smoke --base-url http://127.0.0.1:8092

数据库备份会这样跑:

uv run zi-quant-db-backup --output-dir /var/backups/zi-quant

这些命令会在后续文章里逐个展开。现在先把入口留好。

本篇实战任务

如果你从零跟做,第二篇的任务是把项目骨架搭到可以验收。

创建目录:

mkdir -p zi-quant-platform/{app,scripts,tests,migrations/versions}
touch zi-quant-platform/app/__init__.py
touch zi-quant-platform/scripts/__init__.py

初始化项目:

cd zi-quant-platform
uv init --package

添加依赖:

uv add fastapi "uvicorn[standard]" "sqlalchemy[asyncio]" asyncpg alembic pydantic pydantic-settings httpx
uv add --dev pytest pytest-asyncio

.env.example

DATABASE_URL=postgresql+asyncpg:///postgres?host=/var/run/postgresql
HOST=127.0.0.1
PORT=8092
ZI_DEPLOYMENT_MODE=development
AUTO_CREATE_SCHEMA=true
ZI_API_TOKEN=

创建 .gitignore

.env
.venv/
__pycache__/
.pytest_cache/

然后跑:

uv sync --extra dev
uv run pytest

如果测试还没写,至少先让 pytest 能启动,并补一个最小测试。空项目里测试为 0,不算真正完成。

常见坑

第一个坑,是把 .env 提交进仓库。

哪怕现在里面只有本地数据库地址,后面也会出现真实 API Key。.env.example 可以提交,.env 不可以。

第二个坑,是配置默认值太“生产化”。

本地开发可以自动建表、使用默认端口、允许空 Token。但生产环境应该反过来:必须显式迁移、必须设置 Token、必须关掉自动建表。后面我们会用 /ready 和 production smoke 检查这些差异。

第三个坑,是把 notebook 当项目结构。

notebook 适合探索,不适合承载长期运行的平台。等我们开始同步真实行情、写入数据库、跑定时任务时,核心逻辑必须回到 app/scripts/

第四个坑,是测试只测接口返回 200。

接口能返回 200 不代表策略正确。量化项目要测业务约束:股票池数量、A 股整数手、费用计算、数据去重、回测指标、策略晋升规则。越早建立这种习惯,后面越省事。

本章更新与代码仓库

本章更新内容:

  • 搭建 ZiQuant 的 Python 项目骨架。
  • 引入 uvpyproject.toml.env.example、FastAPI 和 pytest。
  • 确认项目可以安装、启动并运行测试。

代码仓库:

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

本章代码:

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

本篇小结

第二篇没有写任何赚钱策略,但它完成了更重要的事:把 ZiQuant 放进一个可维护的 Python 工程结构里。

现在项目应该具备四个最小能力:

  • 可以用 uv sync --extra dev 安装。
  • 可以通过 .env 管理本地配置。
  • 可以启动 FastAPI 并访问 /health
  • 可以用 uv run pytest 做回归验证。

下一篇我们会进入量化交易核心概念,把股票池、K 线、因子、信号、持仓、回测指标这些词翻译成后面真正会落库和传参的数据结构。