结课项目把书中各部分集中到一个文件:移动平均线、输入、条件、自定义函数、订单和显示。目标是让每项决策都能追溯到数据与文字规则。

不写代码,先描述程序

策略观察两条 EMA 的交叉,再用第三条更长的 SMA 检查收盘价是否高于平均值。

多头入场要求快 EMA 上穿慢 EMA,并且收盘价高于趋势过滤 SMA。三个长度应满足快速 < 慢速 < 趋势。若不满足,程序不发入场订单。

EMA 下穿时发送市价平仓。入场还关联以 tick 指定的止损和目标。模拟使用固定一单位数量、0.1% 手续费和两个 tick 滑点。

这些是任务规则,不据此推断程序会在任何市场盈利。检查使用本书固定演示数据。

完整程序

创建 final_strategy.py:

"""
@pyne edge
"""
from pynecore.lib import close, color, input, location, plot, plotshape, script, shape, strategy, ta


def lengths_are_ordered(fast: int, slow: int, trend: int) -> bool:
    return fast < slow and slow < trend


@script.strategy(
    "Filtered Crossing Strategy",
    overlay=True,
    initial_capital=10000,
    default_qty_type=strategy.fixed,
    default_qty_value=1,
    pyramiding=1,
    commission_type=strategy.commission.percent,
    commission_value=0.1,
    slippage=2,
)
def main(
    fast_length: int = input.int(9, title="Fast EMA length", minval=1, maxval=200),
    slow_length: int = input.int(21, title="Slow EMA length", minval=1, maxval=200),
    trend_length: int = input.int(50, title="Trend SMA length", minval=1, maxval=200),
    use_trend_filter: bool = input.bool(True, title="Use trend filter"),
    stop_ticks: int = input.int(100, title="Stop distance (ticks)", minval=1),
    target_ticks: int = input.int(200, title="Target distance (ticks)", minval=1),
):
    fast_average = ta.ema(close, fast_length)
    slow_average = ta.ema(close, slow_length)
    trend_average = ta.sma(close, trend_length)
    crosses_up = ta.crossover(fast_average, slow_average)
    crosses_down = ta.crossunder(fast_average, slow_average)
    valid_lengths = lengths_are_ordered(fast_length, slow_length, trend_length)
    trend_passes = not use_trend_filter or close > trend_average
    entry_signal = valid_lengths and crosses_up and trend_passes
    if entry_signal:
        strategy.entry("Long", strategy.long)
    if crosses_down:
        strategy.close("Long")
    strategy.exit("Bracket", "Long", loss=stop_ticks, profit=target_ticks)
    plot(fast_average, "Fast EMA", color=color.blue, linewidth=2)
    plot(slow_average, "Slow EMA", color=color.orange, linewidth=2)
    plot(trend_average, "Trend SMA", color=color.gray, linewidth=2)
    plotshape(entry_signal, "Entry signal", style=shape.triangleup,
              location=location.belowbar, color=color.green)

保存并在演示数据运行。图表显示三条均线和代表入场条件的绿色三角形。实际成交的入场和出场由策略交易标记显示。

final_strategy.py 策略图表显示快慢 EMA、趋势 SMA、入场信号和已成交交易。
结课项目使用三条均线、可开关过滤器,以及关联入场的出场规则。

哪些内容每根都计算?

三条均线和两种交叉先于条件执行。即使过滤器阻止入场,也仍然计算,使其状态沿完整 K 线序列推进。

自定义 lengths_are_ordered 检查三个数并返回逻辑值,没有自己的 K 线状态。valid_lengths 保存当前检查结果,供后续表达式使用。

过滤器关闭,或收盘价高于 SMA 时,trend_passes 为真。input.bool 提供可切换逻辑输入。过滤器关闭时 not use_trend_filter 为真,不再需要第二个条件。

过滤器关闭后,均线依然计算和绘制。开关改变入场检查,不改变线条显示。

不合法的长度关系

单项输入限制只要求每个长度位于 1 到 200,因此仍可输入快速 30、慢速 10。这违反任务规定的顺序。

所以检查函数要额外验证关系。顺序错误时线条仍绘制,但 entry_signal 不可能为真,避免程序在不符规则的长度关系下发出入场。

调试时可直接检查 valid_lengths。保存的输入不合要求时,它会在整个运行中保持为假。

信号不一定产生新交易

绿色三角形代表入场条件,策略将订单交给模拟器。成交还可能要求有下一根 K 线、足够资金、允许的数量等。

由于 pyramiding=1,已有多头持仓时,模拟器可能不允许再开多头。因此,不要默认信号数等于交易数。

数据开头,五十期趋势 SMA 尚缺值。过滤器开启时,无法认定价格高于它,所以入场也不满足这一条件。

检查程序行为

先用默认值运行,在 Properties 检查数量、成本与成交模式。以前保存的覆盖设置可能改变源码配置。

找一个绿色信号。通过调试器或图表值,查看当前和上一根的两条 EMA,检查交叉、SMA 过滤与长度关系,三者都应为真。

随后在 Trades 找到对应入场,检查哪根 K 线、什么价格成交。出场时确认是交叉、止损还是目标解释了平仓。

尝试结果可预测的情况

检查行为时,选择能够预判结果的修改:

修改检查内容
快慢长度相同长度验证使程序没有新入场信号
关闭趋势过滤EMA 交叉不再要求价格高于 SMA
增大止损和目标距离距离变化可能带来不同出场 K 线
改变数量价格信号相同,金额规模可能不同

一次运行只改一项。每次先记录预期,再记实际结果。不一致时,在调试器找到第一个不同的决策。

比较结果

比较过滤开关时,保持数据、数量、成本和长度相同。同时查看 Performance 曲线与 Trades 表。交易少或最后仍有持仓,也会影响汇总。

使用真实历史数据时,在查看结果前就确定调参时段和独立检验时段。看过检验结果后再调参,这段数据也就参与了调参。

输入数据开头要有足够预热 K 线。截取时段单独运行时,均线与持仓状态重新建立,因此起始阶段可能不同于该时段在更长数据中段运行的结果。

自己试一试

添加 Use crossings exit 开关。开启时保留下穿平仓,关闭时只由止损和目标平仓。两种状态都要计算下穿。

参考答案

在 main() 参数中加入:

use_crossing_exit: bool = input.bool(True, title="Use crossings exit"),

平仓分支:

if use_crossing_exit and crosses_down:
    strategy.close("Long")

crosses_down = ta.crossunder(...) 保留在条件之前。strategy.exit 两种开关状态都保留。

关闭交叉平仓时,EMA 下穿后持仓也可能继续开放。下一个入场信号仍可能出现,但由于已有持仓,不会新增交易。

创建自己的版本

另存程序,先用文字写出一条想增加的新规则。说明它根据哪些数据、在哪根 K 线上决策,怎样手算检查,以及哪个输入控制它。

先将新规则算成独立变量,绘制或在调试器观察,再把它用于入场或出场条件。这样先排查计算错误,再解释整个策略结果。

源码、保存的设置和使用的数据共同描述实验。保留它们,运行结果才能重现并核查。