程序员量化交易实战 02:从零搭建 Python 量化项目结构
程序员量化交易实战 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 项目的核心目录是这样的:
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.toml 和 uv.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 项目骨架。
- 引入
uv、pyproject.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 线、因子、信号、持仓、回测指标这些词翻译成后面真正会落库和传参的数据结构。