第 8 课 第一次逐行读懂一条完整策略¶
前六课先学习代码的零件,本课才把数据、指标、信号、持仓和收益连接起来。我们只使用十天虚构价格,并且把每个关键日期手算一遍。
先把研究任务写成自然语言¶
我们拥有一只虚构资产的十天收盘价,准备检查一条教学规则。每天收盘以后,计算最近三天和最近五天的平均收盘价。三日平均高于五日平均时,记录“希望持有”;条件不成立时,记录“希望空仓”。今天收盘以后才知道的判断,从下一天开始影响持仓。
这条规则来自一个简单想法。三日平均对近期价格变化反应较快,五日平均反应较慢,短平均高于长平均时,近期价格相对更强。这个解释只说明规则试图捕捉什么,没有证明未来会继续上涨。我们先学习代码时间关系,收益有效性要留给后面的回测与验证课程。
还要说明本课的“下一天持仓”只是一种向量化教学假设。真实市场中,收盘后生成信号,最早可能在下一个开盘成交;开盘成交以后经历的收益区间与收盘到收盘并不完全相同。第 14 课会把信号、订单和成交拆开,本课先让读者看清信息后移。
任何策略代码都先拆成五个阶段¶
一段策略代码即使写得很长,也可以先寻找五类对象。数据描述程序读到了什么,指标把原始数据转换成可比较数值,信号表达某个时点的判断,持仓说明接下来承担多少市场变化,结果记录这段持仓经历了什么。
flowchart LR
accTitle: 从价格到策略收益的五个阶段
accDescr: 收盘价先计算三日和五日均线,同日比较产生信号,信号后移一天成为持仓,持仓与对应收益相乘得到策略收益
a[数据<br/>十天收盘价] --> b[指标<br/>三日与五日均线]
b --> c[信号<br/>短均线是否更高]
c --> d[持仓<br/>信号向后移动一天]
d --> e[结果<br/>持仓乘对应市场收益]
读别人代码时,先按这五种作用标记变量,暂时不用理解每个函数内部。若 signal 与 position 混成同一列,或者收益列无法说清对应哪段持有时间,应该先画出数据流和时间线,再继续看细节。
十天数据在纸面上长什么样¶
代码中的价格是教学数据,没有真实资产和日期。为了方便口头说明,我们把列表位置叫作第 1 天到第 10 天。
| 教学日 | 收盘价 | 这一天能够计算什么 |
|---|---|---|
| 第 1 天 | 10.0 | 只有一个价格,均线都不完整 |
| 第 2 天 | 10.2 | 仍不足三天 |
| 第 3 天 | 10.1 | 第一个三日均线出现 |
| 第 4 天 | 10.4 | 三日均线继续更新 |
| 第 5 天 | 10.7 | 第一个五日均线出现,可以首次比较 |
| 第 6 天 | 10.8 | 信号继续更新,持仓来自第 5 天 |
| 第 7 天 | 10.6 | 持仓来自第 6 天 |
| 第 8 天 | 10.9 | 继续沿同一规则处理 |
| 第 9 天 | 11.1 | 继续沿同一规则处理 |
| 第 10 天 | 11.0 | 得到本表最后一段结果 |
三日均线要等三个价格,五日均线要等五个价格,所以第 5 天以前无法同时比较两条均线。缺失出现在这里有明确原因,历史长度不足。把早期缺失随手填成某个价格,会制造原本不存在的信号。
完整代码出现以前先认识输入和输出¶
下面代码的输入只有十个收盘价,输出是一张增加了六列的 pandas 表。ma3 和 ma5 是指标,signal 是当日收盘后的判断,position 是后移一天的持仓,market_return 是相邻收盘收益,strategy_return 是教学持仓经历的收益。
阅读时不必一次记住全部标点。先确认等号左侧新增了哪一列,再看等号右侧使用哪些旧列。代码按照数据、指标、信号、持仓和结果排列,每一行都将在后面单独拆解。
第一段怎样把列表变成数据表¶
import pandas as pd 取得 pandas 工具包并使用常见简称 pd。pd.DataFrame 随后把一个字典转换成表格,字典键 close 成为列名,右侧列表中的十个价格成为十行。真实研究至少还要有日期与资产代码,本课省略它们是为了把注意力放在策略流水线。
数据建立以后应先打印 prices.shape 和 prices.head()。预期形状为十行一列,前五个价格要与纸面表相同。若输入顺序已经错误,后面每个函数仍能运行,策略含义却不会自动修复。
rolling 和 mean 怎样得到移动平均¶
移动平均会在每一天回看最近若干项,再计算平均值。prices["close"] 先取出收盘价列,rolling(3) 为每一行准备最近三项窗口,mean() 求窗口平均,最后把结果保存为 ma3。
先手算第 3 天。窗口包含第 1 至第 3 天,价格为 10.0、10.2 和 10.1,相加得到 30.3,再除以 3,三日均线为 10.1。第 1 和第 2 天凑不满三项,所以 ma3 显示缺失。
第 5 天第一次同时拥有两条均线。三日窗口使用第 3 至第 5 天,平均值为 (10.1 + 10.4 + 10.7) ÷ 3 = 10.4。五日窗口使用第 1 至第 5 天,平均值为 (10.0 + 10.2 + 10.1 + 10.4 + 10.7) ÷ 5 = 10.28。短均线高于长均线,所以第 5 天收盘后第一次得到真信号。
公式在这里没有引入新概念,它只把刚才的加总与平均写得更紧凑。若窗口长度为 n,时点 T 的简单移动平均等于最近 n 个价格之和除以 n。
P_T 表示当前日收盘价,P_{T-1} 表示前一日,最远一项为 P_{T-n+1}。默认 rolling(n) 只使用当前行与以前数据。若加入 center=True,窗口会向两边展开,历史行可能读到未来价格,量化特征通常不应这样设置。
两列比较怎样产生信号¶
三日和五日均线已经是同一张表中的两列。pandas 会逐行执行大于比较,短均线更高时得到 True,其他情况得到 False。早期五日均线缺失时,比较也不会产生可用真信号。
signal 只表达“这一行数据完整以后,条件是否成立”。第 5 天的真信号要等第 5 天收盘价出现、两条均线计算完成以后才知道。它没有自动下单,也没有改动账户持仓。把信号、订单、成交和持仓区分开,是后面阅读回测的基础。
shift 为什么把信号移动一天¶
本课用相邻收盘收益演示时间关系。market_return 第 6 行代表第 5 天收盘到第 6 天收盘的变化。若第 5 天收盘后才知道信号,希望它影响下一段收益,就要把第 5 行信号移动到第 6 行持仓。
shift(1) 把整列向下移动一行。移动以后,第 6 天 position 读取第 5 天 signal。第一行没有更早信号,得到缺失;fillna(False) 在这里把初始状态明确设为空仓。这个填充由业务约定支持,不能把同一种处理复制到价格列或任何未知字段。
flowchart LR
accTitle: 第 5 天信号怎样影响第 6 天持仓
accDescr: 第 5 天收盘价先完成均线和信号,信号向后移动一行,第 6 天持仓才读取这项判断并经历下一段收益
a[第 5 天收盘<br/>价格已经完整] --> b[计算两条均线<br/>短均线更高]
b --> c[第 5 天信号<br/>True]
c --> d[shift 1<br/>向后移动一行]
d --> e[第 6 天持仓<br/>True]
e --> f[对应第 5 至第 6 天<br/>收盘价格变化]
删除 shift(1) 后,第 6 天收盘价参与生成第 6 天信号,信号又立即乘上第 5 天到第 6 天已经发生的收益。程序让刚刚知道的信息影响它形成以前的价格变化,产生时间穿越。策略表现常会因此虚高。
市场收益和策略收益怎样连接¶
pct_change() 计算当前收盘价相对前一收盘价的比例变化。第 6 天市场收益为 10.8 ÷ 10.7 - 1,约 0.93%。第一行没有前一价格,所以市场收益缺失。
持仓列目前是布尔值。astype(int) 把 True 转成 1,把 False 转成 0。持仓为 1 时,乘法保留对应市场收益;持仓为 0 时,结果变成 0。下面代码因此表达“一份资产或空仓”的教学组合。
第 5 天信号为真,所以第 6 天持仓为真,第 6 天策略收益保留约 0.93%。第 6 天信号也为真,因此第 7 天继续持有;第 7 天价格从 10.8 降到 10.6,策略会经历约负 1.85%。信号规则不会只保留上涨日,持仓一旦建立,就要承担下一段真实涨跌。
把第 6 天逐列走完¶
第 6 天三日均线使用第 4 至第 6 天,结果为 (10.4 + 10.7 + 10.8) ÷ 3,约 10.6333。五日均线使用第 2 至第 6 天,结果为 (10.2 + 10.1 + 10.4 + 10.7 + 10.8) ÷ 5,等于 10.44。短均线高于长均线,所以第 6 天收盘后的新信号仍为真。
第 6 天当前持仓来自第 5 天信号。第 5 天的三日均线为 10.4,五日均线为 10.28,信号已经为真,因此第 6 天持有一份资产。第 6 天市场收益约为 0.93%,持仓乘市场收益以后,教学策略收益也约为 0.93%。
| 第 6 天检查项 | 使用的信息 | 结果 |
|---|---|---|
| 三日均线 | 第 4、5、6 天收盘价 | 约 10.6333 |
| 五日均线 | 第 2 至第 6 天收盘价 | 10.44 |
| 当日信号 | 第 6 天两条均线 | 真,收盘后才知道 |
| 当日持仓 | 第 5 天信号 | 真 |
| 当日市场收益 | 第 5 天到第 6 天收盘变化 | 约 0.93% |
| 当日策略收益 | 当日持仓乘市场收益 | 约 0.93% |
这张表是代码审查的核心。每一列都能说出输入日期、形成时点和业务含义,程序结果才容易相信。变量名很漂亮却无法回答这些问题时,仍然需要回到小表和时间线。
一行 pandas 代码会同时决定什么¶
rolling(3).mean() 同时决定窗口长度、窗口方向、凑不满时的缺失和平均方式。pct_change() 默认按当前行顺序比较相邻值,日期未排序时会连接错误两天。fillna(False) 在本例表示初始空仓,换到其他列可能完全没有这种含义。
陌生函数要检查输入对象、参数、默认值、输出类型和缺失位置。最可靠的入门方法是构造五至十行小表,把全部中间列打印出来,再与手算并排。官方文档负责说明函数一般行为,小样例负责证明当前数据上的具体关系。
当前代码离真实成交还差什么¶
本课把持仓直接乘以收盘到收盘收益,属于向量化信号回测。它适合讲解信息后移,也适合快速筛查想法。真实交易还需要把目标持仓转换成订单,选择下一个可交易时点,判断成交数量,并从现金中扣除手续费、价差和滑点。
若第 6 天收盘后出现买入信号,策略通常无法使用已经结束的第 6 天收盘成交。较保守的教学假设可以安排第 7 天开盘提交订单,此时持有区间从第 7 天开盘以后开始,已经不同于本页的收盘到收盘收益。研究说明应把当前版本明确称为“信号时间演示”,避免把它包装成可执行回测。
成交量、涨跌停和停牌也可能让订单无法完成。后面的市场与订单课程会先解释这些机制,第 14 课再建立逐日账本。读者现在只需指出缺少哪些环节,无需在一段入门代码里同时解决全部问题。

这张图比本课代码多出订单、成交与现金。它提醒读者,信号列只是研究流水线的一站,完整回测还要继续记录实际事件。
怎样阅读别人项目中的策略文件¶
先找到数据入口,确认使用哪些字段、日期怎样排序、价格采用什么口径。随后沿表格列名追踪指标、信号、目标持仓和收益。看到 signal、position 或 weight 时,搜索它在哪里创建,又在哪里被使用;看到收益列时,说明它对应哪两个价格时点。
项目拆成多个文件以后,可以按职责寻找。data 常负责读取和清洗,indicators 计算均线与波动,strategies 把指标变成目标持仓,backtest 模拟成交与账本,metrics 计算收益和回撤,reporting 输出图表。名称可能不同,数据流和责任大致相似。
最后查找测试。可靠的策略项目应该有答案已知的小例子,证明均线何时首次出现、信号何时改变、订单何时生效和费用怎样扣除。没有测试不等于代码一定错误,它表示关键关系仍缺少可重复证据,读者需要自己补上。
容易弄错的地方¶
| 阅读时的误解 | 为什么会出错 | 需要找到的证据 |
|---|---|---|
signal 为真说明账户已经持有 |
信号只是判断 | 订单、成交和持仓更新在哪里发生 |
删除 shift(1) 让策略更及时 |
当日信息会影响已经发生的收益 | 信号形成时点与收益区间 |
| 均线规则很经典,所以应该有效 | 常见程度不能代替检验 | 基线、费用和样本外结果 |
| 最后一列收益正确便说明全部正确 | 错位可能在中间列抵消或隐藏 | 均线、信号、持仓逐列手算 |
fillna 能消除所有缺失 |
填充值会改变业务含义 | 每个缺失的原因与处理依据 |
一分钟自测¶
为什么第 6 天 position 要读取第 5 天 signal?第 6 天刚算出的新信号最早影响哪一行?
查看答案
第 6 天信号要等第 6 天收盘以后才完整,不能决定第 5 天到第 6 天已经发生的收盘收益。后移一行以后,第 6 天持仓读取第 5 天信号,第 6 天新信号最早影响第 7 天持仓。本课术语¶
| 词语 | 现在应该怎样理解 |
|---|---|
| 滚动窗口 | 随日期前进并只保留最近若干项的数据范围 |
| 移动平均 | 每个滚动窗口内价格的平均值 |
| 指标 | 根据原始数据计算出的描述性数值 |
| 信号 | 规则在某个信息时点给出的判断 |
| 持仓 | 某段时间实际承担的市场暴露 |
shift |
把一列按行移动,使信息落到正确生效时间 |
| 市场收益 | 资产价格在指定区间的比例变化 |
| 策略收益 | 按持仓规则实际经历的收益变化 |
Python 阶段综合作业¶
1 标注五段¶
给完整代码每一行标上数据、指标、信号、持仓或结果,并用一句话说明理由。
提示
先看等号左侧保存到哪一列,再问它在流水线中承担什么作用。答案
创建 `close` 属于数据,`ma3` 与 `ma5` 属于指标,`signal` 属于信号,`position` 属于持仓,`market_return` 与 `strategy_return` 属于结果。2 手算第 5 天¶
计算第 5 天的三日均线、五日均线和信号,并判断它影响哪一天持仓。
提示
三日窗口使用第 3 至第 5 天,五日窗口使用第 1 至第 5 天。答案
三日均线为 10.4,五日均线为 10.28,信号为真。它经过 `shift(1)` 后影响第 6 天持仓。3 寻找未来信息¶
删除 shift(1) 会把哪一项后来才知道的信息放进哪一段已经发生的收益?
提示
观察第 6 天信号使用第 6 天收盘价,却会乘上哪段收益。答案
第 6 天收盘后才知道的信号会立即乘上第 5 天到第 6 天收盘收益,时间顺序被倒置。4 动手修改¶
把短均线改为 2 日、长均线改为 4 日,先预测第一项可比较信号在哪一天,再运行核对。
提示
两条均线必须同时存在,较长窗口决定最早日期。答案
把两个窗口改为 `rolling(2)` 和 `rolling(4)`,第一项可比较信号在第 4 天出现,并从第 5 天开始影响持仓。5 开放思考¶
给这条规则写一个可检验研究问题,并写出至少两个会让你放弃当前版本的结果。
评价要点
问题应说明资产范围、信息时点、成交假设、费用和比较基线。放弃条件可以包括样本外优势消失、加入合理费用后失效、只在单一时期成立或依赖未来数据。下一部分会把本页简化掉的市场机制补回来,从资产、交易所、K 线和订单开始。继续阅读第 9 课 市场、K 线和订单。