程序员量化交易实战 09:从 K 线到第一个可解释因子信号
第 8 篇把原始 K 线清洗成了统一的CleanMarketBar。现在可以写因子了。这里先不追求复杂。第一组因子只做四件事日收益、短均线、长均线、动量和波动率。它们足够简单也足够暴露量化工程的几个关键问题窗口、缺失值、信号解释和测试边界。因子不是神秘公式因子可以先理解成“把原始数据变成策略能消费的特征”。比如收盘价连续上涨动量可能为正。短均线高于长均线趋势可能偏强。波动率太高即使上涨也可能需要谨慎。这些判断都不是保证收益的规则。它们只是把行情数据变成更容易比较的工程对象。几个常见名词先放在这里后面文章会反复用到名词在本文里的含义K 线每个交易日的开盘、最高、最低、收盘、成交量等行情记录因子从行情、财务或其他数据里加工出来的特征例如动量、波动率、估值信号因子经过规则解释后的动作提示例如观察、买入观察、风险观察窗口计算指标时回看多少个交易日例如 5 日均线、20 日动量年化把日频指标按一年约 252 个交易日换算方便不同周期比较常见因子大致可以分成几类动量因子看“过去一段时间是不是在涨”反转因子看“短期是否过度下跌或上涨”波动率因子看“价格摆动是否过大”成交量或换手因子看“市场参与是否活跃”估值和质量因子则更多依赖财务数据。第 9 篇先做动量、均线和波动率是因为它们只依赖日线行情最容易和第 8 篇的数据清洗结果接上。先定义因子点第 9 章新增app/factors.py核心对象是FactorPointdataclass(frozenTrue) class FactorPoint: symbol: str trade_date: date close: float return_1d: float | None ma_short: float | None ma_long: float | None momentum: float | None volatility: float | None signal: str这里故意允许None。窗口不够时均线、动量、波动率都不应该硬算。很多回测 bug 就来自“前几天数据不够代码却填了 0”。均线要显式处理窗口短均线和长均线用同一个函数def simple_moving_average(values: list[float], window: int) - list[float | None]: if window 0: raise ValueError(window must be positive) out: list[float | None] [] running 0.0 for index, value in enumerate(values): running value if index window: running - values[index - window] out.append(round(running / window, 6) if index 1 window else None) return out这段代码没有 pandas便于读者直接理解窗口计算过程。真实生产里可以换成更高效的向量化实现但语义要保持一致。收益率和波动率日收益率从第二天开始才有def daily_returns(values: list[float]) - list[float | None]: out: list[float | None] [None] for previous, current in zip(values, values[1:], strictFalse): out.append(round(current / previous - 1, 6) if previous else None) return out波动率用滚动窗口最后乘以 252 做年化variance sum((value - mean) ** 2 for value in sample) / (len(sample) - 1) out.append(round(math.sqrt(variance * 252), 6))这里不是为了预测未来只是给后续策略一个风险感知输入。从因子到信号build_factor_points()会把每一天标成三类信号if ma_short[index] ma_long[index] and momentum 0 and vol[index] 0.45: signal buy_watch elif momentum -0.08 or vol[index] 0.65: signal risk_watch else: signal observe这不是交易圣杯。它只是一个明确、可解释、可测试的第一版信号。我更关心的是工程性质信号为什么出现能不能复现边界条件能不能测。如果这三个问题回答不了策略复杂度越高越危险。和前后章节联动运行现在主线仓库里补了一个可运行示例用同一批样例日线把第 9-12 篇串起来git clone https://github.com/ax2/zi-quant-platform.git cd zi-quant-platform uv sync --extra dev uv run python -m scripts.chapter_examples factor-backtest --source sample这条命令会先输出第 9 篇的因子结果再继续跑第 10 篇最小回测、第 11 篇组合回测和第 12 篇指标。下面是第 9 篇对应的真实运行截图截图里000001.SZ最近 5 个交易日都给出buy_watch。原因可以直接从字段读出来短均线高于长均线20 日动量为正年化波动率低于当前阈值。这就是“可解释信号”的最低要求不只是输出一个动作还能说明它为什么出现。测试信号比测试收益更重要第 9 章的测试里有两个关键场景。稳定上行应该给出buy_watchcloses [10 index * 0.1 for index in range(40)] points build_factor_points(_bars(closes), short_window5, long_window20, volatility_window5) assert points[-1].signal buy_watch持续下行应该给出risk_watchcloses [20 - index * 0.2 for index in range(40)] points build_factor_points(_bars(closes), short_window5, long_window20, volatility_window5) assert points[-1].signal risk_watch这类测试不证明策略能赚钱但能证明代码没有把趋势方向、窗口边界和风险阈值写反。本篇实战任务拉取第 9 章代码git clone https://github.com/ax2/zi-quant-platform.git cd zi-quant-platform git checkout chapter-09 uv sync --extra dev uv run pytest只跑因子测试uv run pytest tests/test_factors.py第 9 章全量测试通过164 passed仍只有既有 FastAPI deprecation warning。本章更新与代码仓库本章更新内容新增app/factors.py。实现日收益、短均线、长均线、动量、年化波动率和三类信号。新增tests/test_factors.py覆盖窗口边界、上涨信号和风险信号。在当前主线补充scripts.chapter_examples factor-backtest联动示例可真实运行第 9-12 篇代码链路。补充因子、信号、窗口、年化等常见名词解释。代码仓库https://github.com/ax2/zi-quant-platform本章代码git clone https://github.com/ax2/zi-quant-platform.git cd zi-quant-platform git checkout chapter-09 uv sync --extra dev uv run pytest tests/test_factors.py本篇小结因子不是为了把数学公式堆上去而是把行情数据转成可解释、可测试、可复现的中间层。第 9 篇完成了第一版因子信号。下一篇我们把信号接进最小回测循环看它如何变成买入、卖出、现金、持仓和权益曲线。