跳转至

第 5 课 让程序按日期重复判断,并且不连错前后两天

列表已经保存了行情顺序。条件负责回答某一天是否满足规则,循环负责让同一条规则依次检查每一天。本课先处理上涨与成交量,不提前引入完整策略。

适合谁会读取列表和字典的读者
前置知识变量、列表、索引和布尔值
建议用时110 分钟,可以分两次完成
本课成果逐日计算相邻收益,并找出价量同时上升的日期

一次比较怎样得到真或假

假设今天收盘价为 10.5 元,昨天为 10.2 元。人会说“今天高于昨天”,Python 需要一个比较表达式。today > yesterday 会先读取两个变量,再比较大小,最后产生布尔值 True。布尔值可以保存到变量,也可以直接交给后面的条件语句。

today = 10.5
yesterday = 10.2

price_rises = today > yesterday
same_price = today == yesterday

print(price_rises)
print(same_price)

运行后会看到 TrueFalse。第一项说明上涨条件成立,第二项说明两天价格并不相等。双等号 == 负责比较,单等号 = 仍然只负责把结果保存到变量中。把这两个符号混用,是 Python 入门最常见的语法错误之一。

比较写法 它提出的问题 示例结果
a > b a 是否大于 b 10.5 > 10.2 为真
a < b a 是否小于 b 10.5 < 10.2 为假
a >= b a 是否大于或等于 b 10.5 >= 10.5 为真
a <= b a 是否小于或等于 b 10.2 <= 10.5 为真
a == b 两边当前值是否相等 10.2 == 10.2 为真
a != b 两边当前值是否不同 10.5 != 10.2 为真

比较价格时还要考虑浮点精度。两个计算结果都应该是 0.3,却可能留下极小尾差,此时直接用 == 未必合适。第 3 课已经介绍过容许误差,可以比较 abs(a - b) < tolerance。比较明确的整数数量、文字标签或已经离散化的状态时,双等号通常更直接。

条件语句怎样选择一条执行路径

比较只产生真假,条件语句会根据真假决定执行哪些代码。if 后面放问题,冒号表示接下来是一组从属步骤,缩进的代码只在条件成立时执行。else 负责接住条件不成立的情况。

change_rate = 0.03

if change_rate > 0:
    print("今天上涨")
else:
    print("今天没有上涨")

变量为 0.03,所以 change_rate > 0 得到 True,程序只执行第一条打印语句。若把变化率改成负 0.02,第一条路径会被跳过,程序转到 else 并显示“今天没有上涨”。两条路径不会在同一次判断中同时执行。

四个空格的缩进会改变程序结构。下面代码把第二条打印放在 if 内部,因此只在上涨时执行;最后一条没有缩进,每次都会执行。Python 不使用大括号标记代码块,读者必须通过缩进看出一行属于哪个条件。

if change_rate > 0:
    print("今天上涨")
    print("记录上涨标签")

print("当天检查结束")

复制代码时若混用制表符和空格,可能出现 IndentationError。编辑器通常会自动插入四个空格,初学者仍要养成观察对齐的习惯。同一级代码应从同一列开始,进入条件内部时统一向右缩进一级。

三种结果怎样用 elif 表达

价格变化有上涨、下跌和不变三种情况。两个分支不够时,可以在 ifelse 中间加入 elif,它表示“前面的条件不成立时,再检查这个条件”。程序从上向下寻找第一个成立分支,执行以后跳过同组其他分支。

change_rate = -0.01

if change_rate > 0:
    label = "上涨"
elif change_rate < 0:
    label = "下跌"
else:
    label = "不变"

print(label)

本例先检查大于零,结果为假;随后检查小于零,结果为真,所以变量 label 保存“下跌”。最后的 else 不再提出问题,它接住前两个条件都不成立的情况,也就是变化率恰好等于零。

条件顺序会影响结果。若先写一个宽松条件 change_rate > 0,后面再写 change_rate > 0.05,涨幅 8% 会在第一项就被接住,永远到不了第二项。更具体的条件通常应放在更宽条件以前,或者改写成互不重叠的区间。

and、or 与 not 怎样组合条件

量化规则经常同时考虑价格和成交量。and 要求左右两边都成立,or 只要求至少一边成立,not 会把真假结果反过来。使用以前先把每个基础条件单独保存,打印并核对,再进行组合,排错会简单很多。

price_rises = True
volume_grows = False

both_rise = price_rises and volume_grows
either_rises = price_rises or volume_grows
price_not_rise = not price_rises

print(both_rise, either_rises, price_not_rise)

输出依次为 FalseTrueFalse。价量同时上升没有成立,因为成交量条件为假;至少一个上升成立,因为价格条件为真;“价格没有上升”则与已知条件相反。

多个条件写在一行时,括号能明确阅读顺序。例如 (price_rises and volume_grows) or event_day 表示价量同时上升,或者当天属于事件日。规则变长以后,应把基础条件拆成有含义的变量名,避免一行同时出现许多比较和逻辑运算。

循环为什么能够处理很多天

条件只检查一次,循环会让一组代码对多个项目重复执行。for 循环依次从列表中取值,每次把当前项目保存到临时变量,然后执行缩进代码。下面列表有三个价格,所以循环体会执行三次。

prices = [10.0, 10.2, 10.1]

for price in prices:
    print("当前价格", price)

第一次循环中 price 为 10.0,第二次为 10.2,第三次为 10.1。循环结束以后,列表本身没有改变。price 只是当前这一轮正在处理的项目,名字可以更换,但应说明含义。

flowchart LR
    accTitle: for 循环怎样逐项读取列表
    accDescr: 循环从列表左侧开始,每轮取一个价格交给临时变量,执行同一组步骤后再移动到下一项

    a[价格列表<br/>10.0, 10.2, 10.1] --> b[第一轮<br/>price 等于 10.0]
    b --> c[第二轮<br/>price 等于 10.2]
    c --> d[第三轮<br/>price 等于 10.1]
    d --> e[列表结束<br/>离开循环]

若只需要每个当前值,直接写 for price in prices 最清楚。计算相邻收益还需要知道当前位置,从而读取前一项,这时可以遍历索引。

相邻计算为什么从索引 1 开始

三天价格只能产生两段相邻收益。第一天没有更早价格,因此收益未知;第二天可以和第一天比较,第三天可以和第二天比较。正索引 1 正好对应第二项,所以循环从 1 开始。

range(1, len(prices)) 会产生从 1 到列表长度以前的整数。本例长度为 3,因此索引依次为 1 和 2。每一轮用 i 读取当前价格,用 i - 1 读取前一价格。

prices = [10.0, 10.2, 10.1]

for i in range(1, len(prices)):
    previous_price = prices[i - 1]
    current_price = prices[i]
    daily_return = current_price / previous_price - 1

    print(i, previous_price, current_price, daily_return)

第一轮打印索引 1、前值 10.0、当前值 10.2 和收益 2%。第二轮打印索引 2、前值 10.2、当前值 10.1 和约负 0.98%。把索引和两个价格一起打印,是在验证时间关系。只打印最终收益会掩盖“当前日与前一日是否真的相邻”。

若从索引 0 开始,prices[i - 1] 会变成 prices[-1],Python 合法地取到末项 10.1,第一天便与未来第三天连接。这个错误不会产生报错,所以手算小列表和打印日期比语法检查更重要。

enumerate 怎样同时给出位置和值

enumerate 会在遍历列表时同时提供索引与当前值。下面代码中的 start=1 只改变显示编号,让第一项显示为第 1 天,它不会改变列表本身的索引。需要读取前一项时,仍要清楚显示编号和真实索引的区别。

prices = [10.0, 10.2, 10.1]

for day_number, price in enumerate(prices, start=1):
    print("第", day_number, "天价格", price)

输出会写第 1 天、第 2 天和第 3 天,更接近日常语言。若程序需要列表位置,可以省略 start=1,让 enumerate(prices) 直接产生索引 0、1、2。选择哪种写法取决于变量要表达给人看的天数,还是给程序读取列表的位置。

把日期和字段放进逐日判断

现在使用第 4 课的列表套字典结构。每行包含日期、收盘价和成交量,我们要从第二天开始比较价格与成交量,并找出两者都高于前一天的日期。开始写循环以前先手算,周二价格上涨但成交量下降,不满足;周三价格与成交量都上涨,应该被记录。

rows = [
    {"date": "周一", "close": 10.0, "volume": 1000},
    {"date": "周二", "close": 10.2, "volume": 900},
    {"date": "周三", "close": 10.4, "volume": 1200},
]

matched_dates = []

for i in range(1, len(rows)):
    previous_row = rows[i - 1]
    current_row = rows[i]

    price_rises = current_row["close"] > previous_row["close"]
    volume_grows = current_row["volume"] > previous_row["volume"]

    if price_rises and volume_grows:
        matched_dates.append(current_row["date"])

print(matched_dates)

程序先准备一个空列表 matched_dates,用来保存满足条件的日期。循环每次取前后两行,分别计算两个基础条件,随后使用 and 组合。只有组合结果为真时,append 才把当前日期加入结果。最终输出应为 ["周三"]

这段代码已经包含“数据、逐日比较、条件和结果记录”,仍然没有形成交易策略。它没有定义何时下单、买多少、何时退出、成交价格和费用。此处只练习程序怎样重复判断,避免 Python 入门阶段被完整回测的许多概念同时压住。

continue 和 break 分别改变什么

循环中偶尔需要跳过某一行。continue 会结束当前这一轮,直接进入下一轮;break 会结束整个循环。两者都会改变程序处理的数据范围,使用时要写清原因。

假设收盘价缺失时无法计算相邻收益,可以先检查当前值。下面代码遇到 None 会跳过该轮,后面的除法不会执行。

for row in rows:
    if row["close"] is None:
        continue

    print("可用价格", row["close"])

break 更适合找到目标后不再继续搜索,或者遇到必须停止的状态。量化数据清洗中不宜随便用它,因为后面的日期可能因此全部被忽略。任何提前结束都应在日志中留下日期和原因。

循环结果怎样用小测试检查

运行完成以后,可以用 assert 检查结果是否符合手算。教学数据已经知道只有周三满足条件,所以写下这条预期。若以后改动循环导致结果不同,程序会立即提醒。

assert matched_dates == ["周三"]

一个测试不能证明所有情况都正确,还可以分别准备价格不涨、成交量不涨、两者都涨和缺失值四种小数据。条件有多少条重要路径,测试就应尽量覆盖多少条。第 6 课会把重复计算整理成函数,让这些测试更容易复用。

容易弄错的地方

常见错误 程序会发生什么 检查方法
= 写比较条件 单等号负责赋值,条件语法会报错 相等判断统一使用 ==
缩进层级不一致 代码归属改变或产生缩进错误 同级行从同一列开始
宽条件写在具体条件以前 后面的具体分支永远到不了 手算每个区间并调整顺序
相邻循环从索引 0 开始 第一项会与列表末项连接 从索引 1 开始并打印前后值
组合条件直接写成很长一行 很难知道哪一项为假 先给基础条件取名并逐项打印
break 跳出清洗循环 后续日期可能全部被忽略 记录停止原因并核对处理行数

一分钟自测

列表有五天价格时,相邻收益应该有几项?循环的 range 应从哪个索引开始,到哪里停止?

查看答案 五天价格只有四段相邻变化,所以得到四项收益。循环从索引 1 开始,到列表长度以前停止,可以写 `range(1, len(prices))`。

本课术语

词语 现在应该怎样理解
比较表达式 比较两个值并产生真或假的代码
布尔值 TrueFalse,表示条件是否成立
条件语句 根据真假选择执行路径的 ifelifelse
缩进 表明一行代码属于哪个条件或循环的空格层级
逻辑运算 使用 andornot 组合真假条件
循环 让同一组步骤依次处理多个项目
range 产生一段整数位置供循环使用的工具
continue 跳过当前一轮并进入下一轮

课后练习

1 判断结果

price = 10.2ma = 10.0,表达式 price > ma 的结果是什么?

提示比较左边是否大于右边。
答案结果为 `True`,因为 10.2 大于 10.0。

2 检查分支顺序

若先写 if return_rate > 0,随后写 elif return_rate > 0.05,涨幅 8% 会进入哪一项?

提示程序执行第一个成立分支以后会跳过同组剩余分支。
答案它会进入第一项 `return_rate > 0`,第二项永远收不到正收益。若要区分大涨,应先检查 `> 0.05`。

3 排查索引

为什么 for i in range(len(prices)) 不适合直接计算 prices[i] / prices[i - 1] - 1

提示观察第一轮 `i` 的值和负索引含义。
答案第一轮 `i` 为 0,`i - 1` 为负 1,会取到未来的末项。应从 `range(1, len(prices))` 开始。

4 动手修改

把完整例子改成“价格上涨并且成交量不低于前一天”,需要修改哪一处?

提示只改变成交量比较符号,其他时间关系保持不变。
答案把 `current_row["volume"] > previous_row["volume"]` 改成 `>=`。周二仍不满足,周三满足。

5 开放思考

为什么一个逐日条件能够找出日期,却还不能被称为完整量化策略?

评价要点回答应提到信号与交易的区别,还缺下单时点、成交价格、仓位、退出、费用、风险与账本。能够逐日判断只解决了流程中的一部分。

下一课会把本课的相邻收益计算整理成可以反复调用的小工具,并学习怎样阅读报错。继续阅读第 6 课 函数、模块和报错