基础入门
本讲把价格与均线关系翻译成第一列可检查的买入布尔信号,覆盖规则定义、布尔列生成、命中样本检查和信号解释。目标是让入门者理解“信号”本质上是规则表达,而不是直接等于最终回测结果。
第 4 讲开始第一次把“指标”推进成“信号”。本讲的目标是:基于第 3 讲已经算出的 5 日均线,构造一列最简单、最容易解释的买入信号。这里的买入信号不追求复杂,不涉及完整策略,只要求你第一次明确看到:某个条件成立时,程序可以把它标成 True。
学完这一讲后,你可以直接完成下面几件事:
这是从“看到指标”走向“能用指标做决策”的关键一步。
如果一上来就同时做买入、卖出、仓位和回测结果,新手很容易在一个步骤里混入四五层逻辑,最后连错误出在哪里都看不出来。第 4 讲故意只做买入信号,就是为了让你先学会:
向量化回测的很多复杂性,最后都建立在这种最基础的布尔列之上。
signal_df = work_df.copy()
print(signal_df[['date', 'close', 'ma_5']].head(10))
当前表里至少应该已经有:
dateclosema_5如果这三列都还没稳住,就不应该急着往下做信号。
买入信号这一讲的重点不是追求复杂规则,而是把“条件成立时生成一列布尔值”这个动作真正走通。
本讲用最简单的口径:当收盘价大于 5 日均线时,认为满足买入条件。这条规则不意味着它就是最好的策略,只是它足够简单,适合入门练习。
signal_df['buy_signal'] = signal_df['close'] > signal_df['ma_5']
这一步之后,你就会得到一列 True / False。这就是第 4 讲最核心的产物。
print(signal_df[['date', 'close', 'ma_5', 'buy_signal']].head(12))
一定要把这四列一起看,因为你现在关心的不是“有没有生成一列布尔值”,而是“这个布尔值到底是不是按我理解的逻辑出现的”。
buy_rows = signal_df[signal_df['buy_signal']]
print(buy_rows[['date', 'close', 'ma_5']].head())
这一步特别有用,因为它会把“规则命中的样本点”直接拉出来,让你快速判断是否有明显不合理的地方。
验证时别只盯着有没有新列,更要确认这列真假值和价格、均线之间的关系说得通。
assert 'buy_signal' in signal_df.columns
assert signal_df['buy_signal'].dtype == bool or str(signal_df['buy_signal'].dtype) == 'bool'
assert signal_df['buy_signal'].sum() >= 0
如果你当前样本较短,也可能没有太多 True,但你至少要能解释为什么。
print(signal_df[['close', 'ma_5', 'buy_signal']].head(10))
你应该看到前几行因为 ma_5 还没稳定,信号解释价值也有限。
信号列第一次出现时,最常见的疑问都和数量多少、前几行表现,以及触发位置是否自然有关。
这很可能只是因为 ma_5 前几行还是空值,比较结果自然不会有意义。
如果规则写得太宽,比如单纯用 close > ma_5,在趋势向上的小样本里 True 可能很多。这不是错误,只是说明当前规则非常基础。
先别怀疑 vectorbt。先看样本长度、排序、以及 ma_5 是否真的算出来了。
>= 或 >,只要你自己清楚口径即可。import pandas as pd
def add_buy_signal(df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
out['buy_signal'] = out['close'] > out['ma_5']
return out
signal_df = add_buy_signal(work_df)
print(signal_df[['date', 'close', 'ma_5', 'buy_signal']].head(12))
如果你已经能把一条价格和均线的比较规则,稳定变成一列可检查的 buy_signal,那第 4 讲就已经完成了它的任务。
新手很容易把“信号”误以为已经接近最终收益结果。其实不是。第 4 讲里的 buy_signal,本质上只是把一句规则翻译成一列布尔值。比如“收盘价大于 5 日均线”,程序就把满足的行标成 True。它还没有回答你最终赚没赚钱,也还没有处理卖出、持仓、手续费等现实问题。
第 4 讲故意把它单独拎出来,就是为了让你先把“规则表达”这层学清楚。因为后面所有回测动作,无论多复杂,最后都绕不开这种布尔列。你越早把这件事看清楚,后面越不容易把所有逻辑混成一团。
很多人生成 buy_signal 后,第一反应是统计有多少个 True。这个动作不是没用,但对第 4 讲来说,更关键的是看这些 True 出现在什么位置、对应什么价格和均线关系。建议你固定做两步:
print(signal_df[['date', 'close', 'ma_5', 'buy_signal']].head(12))
print(signal_df[signal_df['buy_signal']][['date', 'close', 'ma_5']].head())
第一步让你看整体,第二步让你看命中的样本。只有当这两步都看过,你才真正知道程序是不是按照你的理解在打标记。
入门阶段暂时还不展开完整回测理论,但可以先埋一个很重要的观念:当前信号列最好始终建立在当期可见的数据上。第 4 讲里我们用的是同一行 close 和 ma_5 去比较,这在入门层面足够简单,也容易解释。你现在不一定要马上处理所有时点对齐问题,但至少要养成一个意识:不要不知不觉把未来数据带进当前决策。
这也是为什么第 4 讲要先让你肉眼检查每一行的价格、均线和信号对应关系。你一旦看得见它们怎么对齐,后面在真正做回测时,就更容易理解哪些写法会引入偏差,哪些写法更稳。
有时候单看 True / False 还是有点抽象,你可以临时加一列解释字段:
signal_df['buy_reason'] = signal_df['buy_signal'].map({True: 'close 高于 ma_5', False: 'close 不高于 ma_5'})
print(signal_df[['date', 'close', 'ma_5', 'buy_signal', 'buy_reason']].head(12))
这列不一定要一直保留,但它对新手非常友好。因为你会明显感觉到,原来所谓“构造信号”,就是把一条自然语言规则变成程序里一列可检查的结果。
第 4 讲里如果你的 buy_signal 很多,不代表错;如果很少,也不一定错。你更应该问的是:这和当前样本走势是否一致。比如在一段持续上行的小样本里,close > ma_5 很可能经常成立;在震荡或下行样本里,信号自然会少一些。第 4 讲当前的目标不是追求“好看”的命中数量,而是确认布尔规则确实在按预期运行。
下一讲要做卖出信号时,你会发现思路几乎是镜像的。买入信号这边已经帮你建立了三件事:
只要这三步已经跑顺了,第 5 讲其实就是在同一张表上再构造另一列布尔信号。也就是说,第 4 讲真正帮你搭的,不只是一个 buy_signal,而是一套后面能重复用的构造方法。
signal_df['buy_signal'] = signal_df['close'] > signal_df['ma_5']
print(signal_df[['date', 'close', 'ma_5', 'buy_signal']].head(12))
print('buy_signal 个数 =', int(signal_df['buy_signal'].sum()))
print('第一次出现 True 的样本:')
print(signal_df[signal_df['buy_signal']][['date', 'close', 'ma_5']].head(1))
这段片段的价值在于,它把“生成信号”和“解释信号”绑在一起了。对入门者来说,这比单独写出那一行比较表达式更完整,也更容易真正形成理解。
本讲是《vectorbt回测入门短课》的第 4/8 讲,当前主题是《构造买入信号》。
上一讲:第 3 讲《计算5日均线》。
下一讲:第 5 讲《构造卖出信号》。
后续安排:第 6 讲《跑第一轮向量化回测》;第 7 讲《查看收益回撤和交易次数》。
风险揭示与免责声明
本页面内容仅用于量化研究与技术交流,旨在展示研究方法与流程,不构成对任何金融产品、证券或衍生品的要约、招揽、推荐或保证。
本文所涉历史数据、回测结果与示例参数不代表未来表现,也不应作为投资决策依据。
市场存在波动、流动性与执行偏差等不确定性,任何策略均可能出现收益波动或阶段性失效。
读者应结合自身风险承受能力进行独立判断,并在必要时咨询持牌专业机构意见。