程序员量化交易实战 03:量化交易中的核心概念

程序员量化交易实战 03:量化交易中的核心概念 前两篇做了两件事先把量化交易放回工程语境再把 Python 项目骨架搭起来。从这一篇开始我们要统一词汇。量化系统里很多词看着熟悉但如果不落到代码对象很快就会混在一起股票池和持仓不是一回事因子和策略不是一回事信号和订单也不是一回事。这一篇的目标很简单把后面会反复出现的概念翻译成 ZiQuant 里的模型、字段和函数。股票不是一个字符串程序里最容易犯的第一个错误是把股票当成一个字符串。600519.SH当然是股票代码但一个可运行的平台还需要知道它的名称、市场、行业、交易单位和元数据。ZiQuant 里用Stock表保存这些主数据class Stock(Base): __tablename__ zi_quant_stocks symbol mapped_column(String(16), primary_keyTrue) name mapped_column(String(80), indexTrue) market mapped_column(String(8), indexTrue) sector mapped_column(String(80), indexTrue) lot_size mapped_column(Integer, default100) metadata_json mapped_column(JSONB, defaultdict)这里的lot_size100不是装饰字段。A 股普通买入按 100 股整数手处理后面订单检查、回测成交和模拟盘都会用到它。股票主数据解决的是“这个标的是什么”。它不回答“今天多少钱”也不回答“该不该买”。股票池是策略运行范围股票池不是持仓。股票池表示策略允许观察和选择的范围。比如公共 A 股 500 股票池是给策略、因子刷新和推荐接口使用的候选集合持仓则是某个账户已经买入的股票。ZiQuant 里用StockPool和StockPoolMember表达这个关系class StockPool(Base): __tablename__ zi_quant_stock_pools name mapped_column(String(120), indexTrue) visibility mapped_column(Enum(Visibility), defaultVisibility.private) refresh_strategy_id mapped_column(UUID(as_uuidTrue), nullableTrue) class StockPoolMember(Base): __tablename__ zi_quant_stock_pool_members pool_id mapped_column(ForeignKey(zi_quant_stock_pools.id)) symbol mapped_column(ForeignKey(zi_quant_stocks.symbol)) weight mapped_column(Float, nullableTrue) reason mapped_column(Text, default)这层设计的好处是公共股票池、用户私有股票池、按策略重建的股票池可以共存。后面我们做 500 只 A 股股票池时会继续完善这部分。K 线是行情事实不是策略结论行情数据通常会以 K 线形式进入系统。日频系统里最常用的是日 K开盘价、最高价、最低价、收盘价、成交量、成交额。ZiQuant 的MarketBar表是这样设计的class MarketBar(Base): __tablename__ zi_quant_market_bars symbol mapped_column(String(16), indexTrue) trade_date mapped_column(Date, indexTrue) frequency mapped_column(String(16), default1d, indexTrue) open mapped_column(Float) high mapped_column(Float) low mapped_column(Float) close mapped_column(Float) volume mapped_column(Float, default0) amount mapped_column(Float, default0) source mapped_column(String(40), defaultunknown, indexTrue) payload mapped_column(JSONB, defaultdict)注意两个字段source和payload。source用来记录数据来自 QVeris、东方财富、Tushare、同花顺还是 fallback。payload用来保留供应商原始字段或清洗信息。后面排查数据问题时这两个字段会救命。行情数据只是事实记录。它告诉我们某天发生了什么不直接告诉我们该不该买。复权要尽早想清楚A 股会分红、送股、拆股。价格序列如果不处理复权很多因子会被历史价格跳变污染。例如一只股票除权后价格从 100 变成 50并不代表真实跌了 50%。如果策略直接用原始价格计算 20 日动量就可能误判。本系列前期会先把字段和来源留好后面接入真实行情时再明确前复权、后复权和不复权的口径。这里先定一个工程原则同一张行情表不能混入不同价格口径而不标记。供应商返回的复权方式要进入payload或单独字段。因子不是策略因子是一个可排序的证据不是完整决策。20 日动量可以是因子ROE 可以是因子波动率可以是因子财报质量分也可以是因子。但“动量前 20 名等权买入、每周调仓、跌破止损卖出”才更接近策略。ZiQuant 里用FactorDefinition描述因子定义用FactorValue保存某日某股票的因子值class FactorValue(Base): __tablename__ zi_quant_factor_values factor_id mapped_column(ForeignKey(zi_quant_factor_definitions.id)) symbol mapped_column(String(16), indexTrue) trade_date mapped_column(Date, indexTrue) value mapped_column(Float) payload mapped_column(JSONB, defaultdict)当前项目里已经有一个早期的compute_factor()会把 MACD、RSI、动量、波动率和质量分合成一个FactorRowrow compute_factor(stock) print(row.symbol, row.momentum, row.volatility, row.quality, row.score)这还不是最终策略只是后续推荐、回测和模拟盘会消费的证据层。信号不是订单信号是策略输出的观察结论订单是账户动作。这个区分很重要。我们会用BUY_WATCH、HOLD_WATCH、RISK_WATCH这样的名字而不是直接叫BUY、SELL。原因很简单系统做的是研究和模拟观察不做真实下单。一个信号至少应该包含股票代码。信号类型。置信度或排序分。触发原因。风控提示。数据来源和交易日期。类似这样的结构比一句“建议买入”可靠得多signal { symbol: 600519.SH, signal: BUY_WATCH, score: 0.73, reasons: [20日动量靠前, 财报质量分较高, 波动率未超限], risk: {max_position_pct: 0.10, paper_trading_only: True}, }后面推荐工作台、飞书提醒和昨日复盘都会围绕这种结构展开。持仓、订单和账户要分开模拟盘里至少有三类对象。账户是PaperPortfolio现金、关联策略、配置。持仓是PaperPosition某只股票持有多少股平均成本是多少已实现盈亏是多少。订单是PaperOrder某次纸面买入或卖出的价格、数量、费用、状态和原因。如果把这三者混成一张表后面净值曲线、风险检查、复盘都会很痛苦。比如持仓只表示当前状态订单才是历史流水账户现金变化也应该能追到具体订单。回测是一次实验记录回测不是“跑一个函数得到收益率”。它应该是一条实验记录。ZiQuant 用BacktestRun保存回测参数、区间、初始资金、最终权益和指标class BacktestRun(Base): __tablename__ zi_quant_backtest_runs strategy_id mapped_column(ForeignKey(zi_quant_strategies.id), nullableTrue) stock_pool_id mapped_column(ForeignKey(zi_quant_stock_pools.id), nullableTrue) start_date mapped_column(Date, indexTrue) end_date mapped_column(Date, indexTrue) initial_cash mapped_column(Float, default100000.0) final_equity mapped_column(Float, default0) metrics mapped_column(JSONB, defaultdict) params mapped_column(JSONB, defaultdict)交易明细则放在BacktestTrade。这样做是为了以后回答这次回测为什么赚钱费用花了多少哪些股票贡献了收益哪些交易被涨跌停或成交量限制拒绝。胜率不等于赚钱很多人喜欢问策略胜率多少。胜率当然可以看但它不是唯一指标。一个策略可能 90% 的交易都赚小钱剩下 10% 一次亏掉全部利润。另一个策略可能胜率只有 45%但亏损很小、盈利交易更大整体仍然赚钱。所以我们后面会一起看总收益、最大回撤、Sharpe、交易次数、样本外表现和基准对比。单个指标都不够组合起来才接近策略健康状况。可运行基础校验概念统一以后需要能在代码里找到对应对象。当前统一用这条命令复现第 01-08 篇的基础能力uv run python -m scripts.chapter_examples foundation-check本章对应输出如下这里列出的表不是完整 schema而是概念主线股票、行情、因子、回测和模拟盘订单。后面的信号链路也不会直接从signal变成真实订单而是经过风险检查后落到 paper order。本篇实战任务这一篇不要求新增复杂功能但要确认当前项目里核心表已经存在。运行测试cd ~/projects/zi-quant-platform uv run pytest tests/test_services.py -k schema如果从 GitHub 拉取这一章对应代码git clone https://github.com/ax2/zi-quant-platform.git cd zi-quant-platform git checkout chapter-03 uv sync --extra dev uv run pytest tests/test_services.py -k schema已有测试会检查关键表名def test_schema_contains_core_tables(): names table_names() assert zi_quant_stocks in names assert zi_quant_stock_pools in names assert zi_quant_market_bars in names assert zi_quant_backtest_runs in names assert zi_quant_paper_portfolios in names如果你从零实现也可以先补这类测试。它不验证业务正确性但能保证工程骨架里已经有承载后续业务的对象。本章更新与代码仓库本章更新内容梳理股票、股票池、K 线、因子、策略、回测和模拟盘这些核心概念。对应 ZiQuant 里的核心 ORM 表和测试入口。明确回测应该是一条可追溯的实验记录而不是一次函数调用。代码仓库https://github.com/ax2/zi-quant-platform本章代码git clone https://github.com/ax2/zi-quant-platform.git cd zi-quant-platform git checkout chapter-03 uv sync --extra dev uv run pytest tests/test_services.py -k schema本篇小结这一篇做的是概念对齐。股票主数据、股票池、K 线、因子、信号、持仓、订单、回测和指标都应该有清晰边界。边界清楚后面写数据同步、因子计算和策略回测才不会互相污染。下一篇进入 A 股交易规则100 股整数手、T1、涨跌停、停牌、ST、手续费和印花税。这些规则会直接进入订单检查、回测成交和模拟盘风控。