你已经知道 K 线区间的计算:high - low。如果多个地方都需要,可以把它放入自定义函数。调用名称能说明计算目的,公式也只需维护一处。

函数的输入与结果

def price_range(top: float, bottom: float) -> float:
    return top - bottom

def 定义函数,名称是 price_range。它接收 top 和 bottom 两个输入,均为可以带小数的数值。

-> float 表示返回结果的类型。return 计算并返回结果,然后结束本次函数调用。

price_range(105, 100) 的结果是 5。函数不知道这些数是不是价格,也不会选择 K 线。两个数值由调用方提供。

函数体缩进四个空格。本例的定义与 main() 并列,位于文件最外层,所以计算所需的两个价格通过参数传入。函数也可以定义在 main() 内部,后面会尝试。

名称的作用域

top 和 bottom 只在 price_range 内可用,main() 不能直接引用它们,而是使用函数返回的结果。

名称的作用域指它在代码的哪些位置可见。两个独立函数可以使用相同参数名,不会因此变成同一个共享变量。

文件最外层定义的 price_range 不会自动访问 main() 的局部变量,但可以使用文件开头导入的名称,例如 close。所以可见性也与函数定义的位置有关。

定义与调用也不同。定义说明规则,调用则用具体值执行规则。

完整程序

创建 price_functions.py。同一个函数分别计算 K 线完整区间,以及开盘到收盘带正负号的变化:

"""
@pyne edge
"""
from pynecore.lib import close, color, high, low, open, plot, script


def price_range(top: float, bottom: float) -> float:
    return top - bottom


@script.indicator("Price Functions", overlay=False)
def main():
    full_range = price_range(high, low)
    body_change = price_range(close, open)
    plot(full_range, "Full range", color=color.blue)
    plot(body_change, "Body change", color=color.orange)

在演示数据上运行。完整区间不为负,因为最高价至少等于最低价。开盘到收盘的变化则可能为负。函数不会修改传入价格,只返回两个数的差。

price_functions.py 的两次函数调用绘制完整 K 线区间和带正负号的实体变化。
同一个自定义函数从不同输入计算出两个结果。

本例函数很短,也容易手算验证。大型程序可以给复杂子任务命名,但单次加法并不是一定要封装成函数。

参数顺序

price_range(high, low) 将 high 传给 top,将 low 传给 bottom。如果交换位置,减法的符号也会反转。

也可以写出参数名:

full_range = price_range(top=high, bottom=low)

写法更长,但在复杂调用中可能更易读。名称必须与函数参数名完全一致。

在 main() 内定义函数

内部定义的函数还能访问外层函数的变量。当多个计算使用同一个输入设置时,这很方便。

我们用移动平均来平滑上一个程序的两条线。平均长度只通过一个输入设置,两项计算共用。内部函数只接收两个价格;长度从 main() 参数中读取。

在 price_functions.py 保留外部 price_range 和指标装饰器。在 pynecore.lib 导入中添加 input 和 ta,然后将 main() 定义替换为:

def main(
    length: int = input.int(5, title="Average length", minval=1, maxval=200),
):
    def average_difference(top: float, bottom: float) -> float:
        return ta.sma(price_range(top, bottom), length)

    average_range = average_difference(high, low)
    average_body_change = average_difference(close, open)
    plot(average_range, "Average range", color=color.blue)
    plot(average_body_change, "Average body change", color=color.orange)

length 是此前学过的整数输入。内部函数 average_difference 的两个参数 top 和 bottom 由调用传入,而 length 从外层 main() 读取,所以调用时不用再传一次。

内部函数定义属于 main(),行首缩进四个空格;其自己的 return 函数体行缩进八个。average_difference 只在 main() 内可用,不会出现在文件最外层。

price_range 先计算两个价格的差,ta.sma 返回差值的移动平均。蓝线因此表示完整 K 线区间的平均值,橙线表示开收盘带方向变化的平均值。两个调用都逐根执行,所以两条平均线都持续计算。

运行修改后的程序,点击齿轮打开 Inputs。将 Average length 从 5 改为 20,点击 Save。已打开图表自动重算,两条线都会改成二十根 K 线的平均值,不需要修改内部函数的两个调用。若把长度设为 1,就会得到最初的两条线。

这种访问外层变量的方式称为闭包,英文为 closure。能够读取,不代表在内部赋值就能自动改写外部变量。本例只读取 length。

参数版本能从调用处看出传入的全部值,也容易换用另一对价格。若多个计算依赖 main() 中相同变量,内部函数可能更方便。

自定义函数也可以有状态

price_range 只根据收到的值计算,没有跨 K 线保留的变量,也不使用历史。因此,可以随时传入两个数调用。

上面的 average_difference 则已经带状态,因为其中的 ta.sma 使用此前的值计算。自定义函数也能直接创建 Persistent 或 Series 变量。

事件计数函数

分别统计上涨和下跌 K 线。我们编写一个接收逻辑值的函数,记住至今收到过多少次真值。

在文件开头导入 Persistent:

from pynecore import Persistent

把下面函数写在 main() 之前、文件最外层:

def count_events(event: bool) -> int:
    count: Persistent[int] = 0
    if event:
        count += 1
    return count

count 初值为零。event 为真时加一,否则保留。无论哪种情况,return 都返回累计结果。函数调用结束后,Persistent 变量值会保留给下一次调用。

保留 overlay=False。将原有 main() 定义及函数体替换为:

def main():
    bullish_count = count_events(close > open)
    bearish_count = count_events(close < open)
    plot(bullish_count, "Bullish count", color=color.green)
    plot(bearish_count, "Bearish count", color=color.red)

同一个函数在两个不同代码位置调用。两个调用位置各自拥有独立的 count 状态:第一个统计上涨,第二个统计下跌。第一个调用不会增加第二个的计数。

例如,前四根 K 线按下表排列时,结果如下:

K 线开盘价与收盘价关系bullish_countbearish_count
第一根收盘价更高10
第二根收盘价更低11
第三根两者相同11
第四根收盘价更高21

在演示数据运行。绿线只在上涨 K 线增加一,红线只在下跌 K 线增加一。新的运行会让两者从零开始。

两个函数调用都在每根 K 线上执行,增加计数的条件放在函数内部。带状态函数的调用顺序与频率也是行为的一部分。如果只在部分 K 线上调用,其内部历史也只会在这些调用中推进。

如果需要单个共享计数器,就只调用一次函数,再在多个地方使用结果变量。第二个调用位置意味着另一个独立计数器。

函数中的多条语句

需要中间结果时,可以在 return 前计算:

def midpoint(top: float, bottom: float) -> float:
    total = top + bottom
    return total / 2

total 是局部变量。调用结束后,它不会变成 main() 的变量;函数只返回计算结果。

自己试一试

添加 midpoint 函数,创建一个叠加在主图的指标,绘制 K 线中间价。使用函数的 top 和 bottom 参数接收最高价与最低价。

参考答案

将上面的 midpoint 定义写到 main() 前。指标装饰器使用 overlay=True,函数体为:

center = midpoint(high, low)
plot(center, "Midpoint", color=color.blue, linewidth=2)

这个版本可以删除此前两条线的计算和绘图,也删去不再使用的导入。

解释一次调用

针对 price_range(close, open),说明哪个值进入哪个参数、return 行做什么,以及 main() 在哪里拿到结果。更长的函数调用也能按这三步分析。