自定义函数
将代码拆成有名称的计算,并在多个位置使用。
你已经知道 K 线区间的计算:high - low。如果多个地方都需要,可以把它放入自定义函数。调用名称能说明计算目的,公式也只需维护一处。
函数的输入与结果
def price_range(top: float, bottom: float) -> float:
return top - bottomdef 定义函数,名称是 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_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。
阅读由 Pine Script 转换而来的代码时: PyneComp 会把函数定义放到 main() 内。Pine Script 顶层变量也会放到 main(),从而让内部函数能访问它们。这就是转换结果采用这种结构的原因。自己编写 Pyne code 时,两种方式都可以使用:独立函数通过参数接收数据,内部函数还可以读取周围的变量。
参数版本能从调用处看出传入的全部值,也容易换用另一对价格。若多个计算依赖 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 countcount 初值为零。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_count | bearish_count |
|---|---|---|---|
| 第一根 | 收盘价更高 | 1 | 0 |
| 第二根 | 收盘价更低 | 1 | 1 |
| 第三根 | 两者相同 | 1 | 1 |
| 第四根 | 收盘价更高 | 2 | 1 |
在演示数据运行。绿线只在上涨 K 线增加一,红线只在下跌 K 线增加一。新的运行会让两者从零开始。
两个函数调用都在每根 K 线上执行,增加计数的条件放在函数内部。带状态函数的调用顺序与频率也是行为的一部分。如果只在部分 K 线上调用,其内部历史也只会在这些调用中推进。
如果需要单个共享计数器,就只调用一次函数,再在多个地方使用结果变量。第二个调用位置意味着另一个独立计数器。
函数中的多条语句
需要中间结果时,可以在 return 前计算:
def midpoint(top: float, bottom: float) -> float:
total = top + bottom
return total / 2total 是局部变量。调用结束后,它不会变成 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() 在哪里拿到结果。更长的函数调用也能按这三步分析。