结课项目:一套可核查的策略
组合可配置策略,完整检查信号、交易与结果。
结课项目把书中各部分集中到一个文件:移动平均线、输入、条件、自定义函数、订单和显示。目标是让每项决策都能追溯到数据与文字规则。
不写代码,先描述程序
策略观察两条 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)保存并在演示数据运行。图表显示三条均线和代表入场条件的绿色三角形。实际成交的入场和出场由策略交易标记显示。

哪些内容每根都计算?
三条均线和两种交叉先于条件执行。即使过滤器阻止入场,也仍然计算,使其状态沿完整 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 线上决策,怎样手算检查,以及哪个输入控制它。
先将新规则算成独立变量,绘制或在调试器观察,再把它用于入场或出场条件。这样先排查计算错误,再解释整个策略结果。
源码、保存的设置和使用的数据共同描述实验。保留它们,运行结果才能重现并核查。