保持敬畏之心
交易是一场持久战

Pine Script(258):SMA交叉策略与周线均线交叉

#Pine Script入门教学

TradingView的简单移动平均线(SMA)交叉策略

移动平均线是一种广受欢迎的技术指标。它们能抚平价格的毛刺,揭示市场在更长时间维度上的行为,从而帮助我们将当前价格置于更宏观的视角中。这一特性也使得它们在趋势跟踪者中备受青睐。本文将探讨一个基于移动平均线交叉的经典趋势跟踪策略:SMA交叉策略。

SMA交叉策略:一种经典的趋势跟踪系统

在其著作《趋势跟踪》(Trend Following)中,迈克尔·柯威尔(Michael Covel)深入研究了全球顶尖的趋势跟踪交易者。趋势跟踪的目标很简单:为了盈利而捕捉到一轮上涨或下跌趋势的主体部分。然而,趋势跟踪者们实现这一目标的方式却各有千秋。

与其他试图预测趋势或估算支撑阻力位的交易风格不同,趋势跟踪者只关注价格本身。他们会预先定义何种价格行为构成了趋势,然后当他们看到新趋势出现时便建立仓位。这确实使得趋势跟踪者总是会错过趋势的初始阶段,他们也不会在顶部附近离场。但这没关系,因为他们能够抓住大趋势中最主要、最肥美的那一段。

典型的趋势跟踪策略有几个共同的特点:它们通过观察价格变化来识别主要趋势;让盈利的头寸持续奔跑,直到趋势发生改变;在预设的止损位果断了结亏损的交易;并通过头寸规模管理来限制亏损。

柯威尔在他的书中分享的趋势跟踪策略之一,便是SMA交叉策略。该策略使用两条移动平均线:一条用于跟踪短期趋势,另一条则反映市场的长期趋势。当快线移动平均线上穿或下穿慢线时,通常被认为是市场的长期趋势方向可能发生了改变。接下来,让我们仔细看看该策略的交易规则。

SMA交叉策略的交易规则

该策略包含以下交易规则。做多入场:当50周期简单移动平均线(SMA)上穿100周期SMA后的次日开盘时,以市价单做多(并反转任何已有的空头仓位)。做多离场:为多头仓位设置一个止损,其价位为入场价格减去4倍的10周期ATR。做空入场:当50周期SMA下穿100周期SMA后的次日开盘时,以市价单做空(并平掉任何已有的多头仓位)。做空离场:空头仓位的止损价位为入场价格加上4倍的10周期ATR。

头寸规模的确定是一个双重风控机制:其一是基于风险的规模,每笔头寸的初始风险(入场价与止损价之差)被设定为权益的2%;其二是最大风险敞口限制,单个头寸的最大风险敞口(即保证金与权益的比率)被限制在权益的10%以内。

我们首先根据2%的风险来计算头寸大小,但如果计算出的头寸所需的保证金超过了权益的10%,则会相应地缩减头寸规模,以确保单笔交易的风险敞口不会过大。

柯威尔的盈利回测,是在长达15年的日线数据上,对一系列美国期货品种进行的,包括货币(英镑、日元、欧元)、大宗商品(原油、黄金、白银、玉米、小麦)、软商品(咖啡、糖)以及金融产品(标普500、纳斯达克100、5年期美国国债)。

在TradingView中编写SMA交叉策略

现在,让我们将上述交易规则转化为一个功能完备的TradingView策略脚本。一个有效的方法是使用模板,它能将一项大任务分解成更小、更易于管理的工作区块,也能避免我们面对空白脚本时无从下手。

这是我们将用于SMA交叉策略的模板:

//@version=5
// 步骤1. 定义策略设置
// 步骤2. 计算策略所需指标值
// 步骤3. 在图表上输出数据
// 步骤4. 定义多头交易条件
// 步骤5. 定义空头交易条件
// 步骤6. 提交入场订单
// 步骤7. 提交出场订单

如果你想跟随本文一起操作,请在TradingView的Pine编辑器中创建一个新的策略脚本,并将上述模板粘贴进去。(或者,你也可以直接跳到文末查看完整的策略代码。)

为了让你对我们即将编写的代码有个直观的认识,以下是最终完成的SMA交叉策略在图表上的样子:

现在,让我们开始将上述交易规则,一步步地转化为一个规范的TradingView策略脚本。

步骤1:定义策略设置和输入选项

在第一步,我们需要配置策略的整体属性,并创建用户可调的参数设置,以便我们能方便地调整策略参数。首先,我们指定策略的基础设置。代码如下:

// 步骤1. 定义策略设置
strategy(title="SMA Crossover", overlay=true,
     pyramiding=0, initial_capital=100000,
     commission_type=strategy.commission.cash_per_order,
     commission_value=4, slippage=2)

我们使用 strategy() 函数来配置脚本属性。title 参数为策略命名。overlay=true 使策略直接叠加在主图表上。根据交易规则,我们通过 pyramiding=0 禁止加仓。initial_capital 则将策略的初始资金设为100,000。在交易成本方面,我们设定每笔单边交易(strategy.commission.cash_per_order)收取4个货币单位的固定佣金(commission_value=4),并假设市价单和止损单会产生2个最小跳动点的滑点(slippage=2)。这里的成本设置相对悲观,但高估交易成本总比低估要安全。

接下来,我们定义一些脚本的输入选项:

// SMA 输入项
fastMALen = input.int(50, title="快线SMA周期")
slowMALen = input.int(100, title="慢线SMA周期")

我们使用 input.int() 函数创建了两个整数输入项,分别用于设置快、慢两条均线的计算周期,默认值分别为50和100。

我们还为ATR止损创建了两个输入项:

// 止损输入项
atrLen     = input.int(10, title="ATR周期")
stopOffset = input.float(4, title="止损偏移倍数", step=.25)

ATR周期是一个整数输入,用于指定计算平均真实波幅(ATR)的K线数量,默认值为10。止损偏移倍数则是一个浮点数输入,用于指定止损距离入场价多少个ATR的倍数,默认值为4。

然后,我们为策略的仓位管理功能创建设置:

// 仓位管理输入项
usePosSize  = input.bool(true, title="启用仓位管理?")
maxRisk     = input.float(2, title="单笔最大风险 %", step=.25)
maxExposure = input.float(10, title="最大风险敞口 %", step=1)
marginPerc  = input.int(10, title="保证金预估 %")

第一个输入项是一个布尔型的复选框,可以方便地一键开启或关闭仓位管理算法,默认开启,关闭时每笔交易固定为1个合约。第二个是浮点数输入,用于设定在单笔交易中我们愿意承担的权益风险比例,默认值为2%。第三个是另一个浮点数输入,用于限制总仓位规模相对于策略权益的比例,防止因止损过近而导致仓位过大,默认值为10%。最后一个是整数输入,由于Pine Script无法直接获取经纪商提供的确切保证金率,我们在此创建一个输入项,以便用户根据实际情况或典型值进行估算,默认值为10%。

步骤2:计算交易策略所需的值

接下来,我们计算策略运行所需的数据,包括均线、ATR、交易窗口和头寸规模。首先,我们确定均线和ATR的值:

// 步骤2. 计算策略所需值
fastMA = ta.sma(close, fastMALen)
slowMA = ta.sma(close, slowMALen)

atrValue = ta.atr(atrLen)

我们使用 ta.sma() 分别计算出快、慢两条简单移动平均线(SMA)。并使用 ta.atr() 计算出ATR值,其周期由用户设定的 atrLen 决定。

然后,我们计算策略的有效交易时间窗口:

tradeWindow = time <= timenow - (86400000 * 3)

设置交易窗口的目的是为了让策略在回测结束时自动平仓,从而避免在绩效报告中出现未平仓的交易。一种实现方式是让策略在执行回测的那个时刻往前推几天就停止交易。这样既能给策略足够的时间去平仓,也考虑到了周末等休市情况。

我们通过 time <= timenow - (86400000 * 3) 来实现。time 是当前K线的时间,timenow 是执行回测的当前时刻,而86,400,000是一天的毫秒数。这个条件确保了策略只在距离当前时间三天之前的历史数据上运行。

接下来,我们计算策略的头寸规模,这是本策略的一个核心部分:

// 计算头寸规模
riskEquity = (maxRisk * 0.01) * strategy.equity
riskTrade  = (atrValue * stopOffset) * syminfo.pointvalue

maxPos = ((maxExposure * 0.01) * strategy.equity) /
     ((marginPerc * 0.01) * (close * syminfo.pointvalue))

posSize = usePosSize ? math.min(math.floor(riskEquity / riskTrade), maxPos) : 1

要计算头寸规模,我们分步进行。第一步,计算单笔交易风险金额 riskEquity:我们将用户设定的单笔最大风险百分比 maxRisk 转换为小数,再乘以策略当前总权益 strategy.equity,得出我愿意在这一笔交易上最多亏多少钱。第二步,计算每手交易风险 riskTrade:我们将ATR值(atrValue)乘以止损偏移倍数(stopOffset),得到以点为单位的风险,再乘以 syminfo.pointvalue(每点价值),得出如果我交易一手(或一个合约),我的初始止损代表了多少钱的风险。第三步,计算最大允许仓位 maxPos:这是为了控制总的杠杆和风险敞口。我们用最大风险敞口比例 maxExposure 乘以总权益,得出最大可投资金额,然后除以每手合约所需的预估保证金,得出最大允许的合约数。第四步,确定最终头寸规模 posSize:如果用户启用了仓位管理,我们先用 riskEquity / riskTrade 计算出基于单笔风险的头寸规模,并向下取整。然后,我们使用 math.min() 函数,在这个值和最大允许仓位 maxPos 之间取其较小者,以确保最终的仓位规模同时满足这两个风控维度。如果未启用仓位管理,则头寸规模固定为1。

举个例子,假设我们交易E-mini S&P 500期货(ES),当前价格2,500,点值为$50。策略权益为$1,000,000,最大风险敞口设为12.5%,预估保证金为15%。那么最大可投资金额 = $1,000,000 * 12.5% = $125,000;每手合约所需保证金 = 15% * (2,500 * $50) = $18,750;最大允许仓位 maxPos = $125,000 / $18,750 ≈ 6个合约。这意味着,无论单笔风险计算出的仓位是多少,我们的持仓都不能超过6个合约。

顺带再解释一下 posSize 变量。它会根据 usePosSize 这个输入变量的值来决定大小。如果 usePosSize 设置为 false,那么默认的仓位大小就是1份合约。

如果 usePosSize 设置为 true,我们就会计算策略的实际仓位大小。计算方法是把 riskEquity(可风险资金)除以 riskTrade(每笔交易风险)。我们会用 math.floor() 函数对结果进行向下取整,确保仓位大小是整数。同时,我们还会用 math.min() 函数,让系统在计算出的仓位大小和设定的最大仓位大小之间选择较小的一个,这样可以避免开仓过大的风险。

第三步:编写做多交易规则

现在,我们将策略的交易规则用TradingView的代码来实现。这一步主要讲解做多部分的逻辑,做空部分将在第四步介绍。我们首先编写做多入场的具体条件:

// 第三步:确定做多交易条件
enterLong = ta.crossover(fastMA, slowMA) and 
     tradeWindow

这里我们定义了一个 enterLong 变量。它的值是根据两个布尔表达式(即真/假判断)的结果来决定的。因为我们使用了 and 逻辑运算符,所以只有当这两个表达式都为 true 时,enterLong 才会变成 true。如果其中任何一个或两个都为 false,那么 enterLong 就会是 false

第一个表达式使用了 ta.crossover() 函数,并传入了 fastMAslowMA 这两个变量。这个函数的作用是监测50周期简单移动平均线(SMA)何时向上穿越100周期简单移动平均线。当发生这种穿越时,ta.crossover() 函数会返回 true,否则返回 false

第二个表达式是 tradeWindow。我们之前已经定义过这个变量,当脚本计算的价格K线距离当前日期和时间超过3天时,tradeWindow 的值就会被设置为 true。通过将 tradeWindow 包含在做多入场条件中,我们限制了策略只在特定时间窗口内进行交易。

在编写TradingView策略的第三个环节,我们还需要确定做多的止损价格。我们用以下代码来实现:

longStop = 0.0
longStop := enterLong ? close - (stopOffset * atrValue) :
     longStop[1]

首先,我们声明了 longStop 变量,并给它一个初始的浮点数值 0.0。这样做是为了让这个变量能够存储小数(而不仅仅是整数)。

接着,我们使用 := 赋值运算符来给 longStop 变量赋予实际的值。它的值取决于 enterLong 变量的状态。如果 enterLongtrue,那么条件运算符(?:)就会将做多止损价设置为 close - (stopOffset * atrValue)。这表示我们从当前价格(close)中减去一个特定倍数(stopOffset)的10周期平均真实波动范围(ATR,由 atrValue 代表),从而计算出初始止损价。

如果 enterLongfalse,条件运算符就会返回 longStop 在前一根K线上的值(即 longStop[1])。这意味着只要没有新的做多入场信号出现,止损价格就会保持在之前的水平。

第四步:编写做空交易条件

接下来,我们编写做空头寸的代码。首先,我们实现做空入场条件:

// 第四步:编写做空交易条件
enterShort = ta.crossunder(fastMA, slowMA) and 
     tradeWindow

enterShort 变量要变为 true,需要满足两个条件。首先,50周期简单移动平均线(SMA)必须跌破100周期SMA。我们使用 ta.crossunder() 函数并传入 fastMAslowMA 变量来判断这一点。

其次,tradeWindow 变量必须为 true。这种情况发生在脚本处理的K线时间距离当前日期和时间超过3天时。如果K线时间晚于这个设定,或者没有出现移动平均线向下交叉的情况,那么 enterShort 变量就会是 false

然后我们计算做空止损价格:

shortStop = 0.0
shortStop := enterShort ? close + (stopOffset * atrValue) :
     shortStop[1]

这段代码与我们计算做多止损的方式非常相似。首先,我们创建一个名为 shortStop 的浮点型变量。然后,我们使用 := 运算符将该变量更新为其真实值。

shortStop 的值取决于 enterShort 变量。当该变量为 true 时,我们使用 close + (stopOffset * atrValue) 来计算止损价。这意味着止损价被设定为当前价格(close)加上10周期平均真实波动范围(ATR)(atrValue)的某个倍数(stopOffset)。

如果 enterShortfalse,我们则使用TradingView的历史引用运算符 [] 来获取 shortStop 在前一根K线上的值。这使得止损在多根K线上保持在同一价格,只有当出现新的做空入场信号时,我们才会更新它。

第五步:输出策略数据并可视化信号

接下来,我们将在图表上显示策略的数据。这样可以方便我们检查策略是否按预期运行。首先,我们显示简单移动平均线:

// 第五步:输出策略数据
plot(fastMA, color=color.orange, title="快速移动平均线")
plot(slowMA, color=color.teal, linewidth=2, title="慢速移动平均线")

我们使用TradingView的 plot() 函数来绘制简单移动平均线(SMA)。第一个 plot() 语句显示50周期SMA(来自 fastMA 变量),这条常规线图显示为橙色。

第二个 plot() 函数调用则显示100周期SMA(slowMA)。这条线显示为青色。并且通过将 linewidth 参数设置为 2,使线图比平时更粗一些。

我们还绘制了策略的止损价格:

plot(strategy.position_size > 0 ? longStop : na, color=color.green, 
     linewidth=2, style=plot.style_circles)
plot(strategy.position_size < 0 ? shortStop : na, color=color.red, 
     linewidth=2, style=plot.style_circles)

这里,plot() 函数再次在图表上显示数值。但这一次,我们创建的是圆形图(style=plot.style_circles)。绘制这些值的时机也有所不同:我们不会在每根K线上都绘制,而只在存在做多或做空头寸时才绘制。

为了实现这一点,我们将 plot() 函数的第一个参数设置为一个条件值。在第一个 plot() 语句中,我们判断 strategy.position_size 变量是否大于0。当策略处于做多状态时,就会出现这种情况。在这种情况下,我们让条件运算符(?:)返回做多止损值(longStop)。

当脚本不做多时,我们让该运算符返回 na 来禁用绘图。这样,做多止损价格只会在策略实际持有做多头寸时才显示。

对于第二个 plot() 语句,我们也判断 strategy.position_size。但这次我们看该变量是否小于0,这表示策略处于做空状态。在这种情况下,我们让条件运算符(?:)返回 shortStop 值(这些值随后会绘制在图表上)。否则,该运算符返回 na 以禁用圆形图的显示。

第六步:通过入场订单开仓交易

在策略数据计算并绘制完毕后,现在是时候进行实际交易了。为此,我们首先编写用于开仓交易的策略逻辑:

// 第六步:提交入场订单
if enterLong
    strategy.entry("EL", strategy.long, qty=posSize)
if enterShort
    strategy.entry("ES", strategy.short, qty=posSize)

第一个 if 语句检查 enterLong 变量是否为 true。当50周期简单移动平均线(SMA)向上穿越100周期SMA时,该变量的值为 true。在这种情况下,我们调用 strategy.entry() 函数来开立一个做多交易(strategy.long)。我们将这个订单命名为”EL”。订单的大小由 posSize 变量决定,我们之前已经将其设置为计算出的仓位大小或默认值1。

第二个 if 语句则检查 enterShort 变量是否为 true。如果为 true,则表示50周期SMA已经跌破100周期均线。在这种情况下,我们发起一个做空交易(strategy.short)。我们将该订单命名为”ES”,并提交 posSize 份合约。

需要注意的是,如果目前已经有反方向的持仓,strategy.entry() 函数会自动将现有交易反转。所以,如果我们的策略当前是做多,而50周期SMA跌破了100周期SMA,那么 strategy.entry() 就会将当前的做多头寸转化为做空交易。(同样地,如果策略当前是做空,而出现了做多信号,也会发生类似的反转。)

第七步:通过出场订单平仓

在最后一步中,我们编写用于平仓的策略代码。首先,我们提交止损订单:

// 第七步:提交出场订单
if strategy.position_size > 0
    strategy.exit("XL", from_entry="EL", stop=longStop)
if strategy.position_size < 0
    strategy.exit("XS", from_entry="ES", stop=shortStop)

第一个 if 语句检查策略是否处于做多状态,也就是当 strategy.position_size 大于0时。如果条件成立,我们就会调用 strategy.exit() 函数,提交一个以 longStop 价格为依据的止损订单。我们将这个做多退出订单命名为”XL”,并将其关联到”EL”入场订单(from_entry="EL"),从而平仓该头寸。

第二个 if 语句则判断策略是否处于做空状态(strategy.position_size < 0)。如果条件成立,我们同样会调用 strategy.exit(),提交一个基于 shortStop 价格的止损订单。我们将这个做空退出订单命名为”XS”,并将其关联到”ES”入场订单,从而平仓该头寸。

我们脚本的最后一部分是确保在回测期结束时,策略会自动平仓:

if not tradeWindow
    strategy.close_all()

这个 if 语句会检查 not tradeWindow 这个条件。我们之前设定 tradeWindow 变量的布尔值,是根据当前K线时间距离脚本运行时的实时时间是否超过3天来决定的(超过则为 true,否则为 false)。由于我们希望在回测的最后阶段关闭所有订单,因此一旦回测时间结束,我们需要一个 true 的信号来执行平仓。

然而,在那个时间点之后,tradeWindow 本身会变为 false。所以,我们在 tradeWindow 前面加上了 not 逻辑运算符。这样,当 tradeWindowfalse 时,not tradeWindow 就会变为 true;而当 tradeWindowtrue 时,not tradeWindow 则为 false

因此,当脚本计算到距离实时时间3天以内时,tradeWindowfalse,此时 not tradeWindow 就变成了 true

有了这个 true 值,if 语句就会执行 strategy.close_all() 函数。这个函数会关闭所有当前持有的交易。这样,策略在回测结束时就能完全清仓。由于我们也在开仓交易前检查了 tradeWindow,所以过了这个时间点,策略也不会再开新仓。

TradingView SMA交叉策略的性能表现

让我们从积极的一面来回顾SMA交叉策略的性能。和大多数趋势跟踪策略一样,当市场价格长时间保持同一方向的趋势时,SMA交叉策略的表现非常出色。在这样的市场条件下,策略能够以极低的交易成本获得超额收益。

以下图表就是一个很好的例子,该策略在E-mini S&P 500期货中获得了500点的利润:

然而,SMA交叉策略也有其不足之处。例如,当市场处于横盘震荡(区间波动)时,由于频繁的来回打脸(whipsaw)交易,策略的表现会受到影响。在这些情况下,策略亏损的原因有三点:趋势未能形成、趋势方向错误,以及趋势过早结束。

以下图表展示了这种不佳表现的一个例子,策略连续出现了多次亏损。

下方展示的是一次迷你回测的结果。这些结果是在没有设定仓位大小的情况下进行的。这意味着策略会交易所有接收到的信号,这也最大化了回测报告中的交易数量(以及因此而产生的数据)。

但正如你可以从下表中的交易数据看出,每年大约只有2.5笔交易。这使得仅凭这两个回测数据几乎不可能得出任何确切结论。因此,还需要进行更进一步的测试。

性能指标 E-mini S&P 500 (ES) 原油期货 (CL)
首次交易 1998-09-07 1983-10-17
最后交易 2018-10-23 2018-10-16
时间周期 1 天 1 天
净利润 $27,432 -$37,526
毛利润 $144,093 $217,158
毛亏损 -$116,661 -$254,684
最大回撤 $25,173 $76,790
利润因子 1.235 0.853
总交易次数 46 90
胜率 34.78% 25.56%
平均每笔交易 $596 -$416
平均盈利交易 $9,005 $9,441
平均亏损交易 -$3,888 -$3,801
盈亏比 2.316 2.484
已付佣金 $268 $536
滑点 2 ticks 2 ticks

策略改进与新思路探讨

正如上面回测结果所显示,简单移动平均线(SMA)交叉策略确实需要进一步优化。以下是一些值得你深入研究的改进方向。

SMA交叉策略的一个明显问题是其平仓速度较慢。这是因为我们使用的移动平均线周期较长(50和100根K线),导致退出信号滞后。这种滞后会放大亏损,因为在退出信号出现前,亏损可能已经进一步扩大。也许更好的做法是:在入场时仍使用50和100周期的SMA,但在出场时改用周期更短的移动平均线;或者考虑引入追踪止损机制,以保护部分已实现的浮动利润。

柯威尔在他的书中分享了优化研究的结果,表明快速SMA的最佳周期通常在40到60之间,而慢速SMA的最佳周期范围在100到120根K线。我们可以以这些数值作为初始参考,但很可能需要根据具体的交易品种和当前市场状况进行重新调整和优化。

与大多数趋势跟踪策略一样,SMA交叉策略在市场处于横盘震荡时表现不佳。如果我们能有效过滤掉这些低胜率的交易,策略的整体表现将大幅提升。或许可以考虑引入ADX(平均趋向指数)或平均真实波动范围(ATR)作为过滤器,帮助我们判断市场何时从横盘转变为趋势行情。

SMA交叉策略目前使用的是简单移动平均线(SMA)。这种平均线是对数据进行直接算术平均,每个数据点权重相同,因此响应速度不够灵敏。如果我们尝试使用其他类型的移动平均线,例如指数移动平均线(EMA),策略的性能可能会有显著改善。

其他优化策略的方法还包括风险管理。例如,我们可以设定一个最大回撤限制,或者限制连续亏损的天数。

与SMA交叉策略相似的策略还有周线级别的SMA交叉策略(使用周线级别的简单移动平均线)和SMA交叉金字塔加仓策略(在盈利头寸上进行逐步加仓)。此外,其他同样基于移动平均线的趋势跟踪策略包括双移动平均线策略和三移动平均线策略。

完整代码:TradingView SMA交叉策略

以下是SMA交叉策略的完整和最终代码。有关代码的详细说明和解释,请参阅上文的讨论。

//@version=5
// 第一步:定义策略设置
strategy(title="SMA Crossover", overlay=true,
     pyramiding=0, initial_capital=100000,
     commission_type=strategy.commission.cash_per_order,
     commission_value=4, slippage=2)

// SMA 输入参数
fastMALen = input.int(50, title="快速SMA周期")
slowMALen = input.int(100, title="慢速SMA周期")

// 止损输入参数
atrLen     = input.int(10, title="ATR周期")
stopOffset = input.float(4, title="止损偏移倍数", step=.25)

// 仓位大小输入参数
usePosSize  = input.bool(true, title="是否使用仓位管理?")
maxRisk     = input.float(2, title="最大风险百分比", step=.25)
maxExposure = input.float(10, title="最大敞口百分比", step=1)
marginPerc  = input.int(10, title="保证金百分比")

// 第二步:计算策略值
fastMA = ta.sma(close, fastMALen)
slowMA = ta.sma(close, slowMALen)

atrValue = ta.atr(atrLen)

tradeWindow = time <= timenow - (86400000 * 3)

// 计算仓位大小
riskEquity = (maxRisk * 0.01) * strategy.equity
riskTrade  = (atrValue * stopOffset) * syminfo.pointvalue

maxPos = ((maxExposure * 0.01) * strategy.equity) /
     ((marginPerc * 0.01) * (close * syminfo.pointvalue))

posSize = usePosSize ? math.min(math.floor(riskEquity / riskTrade), maxPos) : 1

// 第三步:确定做多交易条件
enterLong = ta.crossover(fastMA, slowMA) and 
     tradeWindow

longStop = 0.0
longStop := enterLong ? close - (stopOffset * atrValue) :
     longStop[1]

// 第四步:编写做空交易条件
enterShort = ta.crossunder(fastMA, slowMA) and 
     tradeWindow

shortStop = 0.0
shortStop := enterShort ? close + (stopOffset * atrValue) :
     shortStop[1]

// 第五步:输出策略数据
plot(fastMA, color=color.orange, title="快速SMA")
plot(slowMA, color=color.teal, linewidth=2, title="慢速SMA")
plot(strategy.position_size > 0 ? longStop : na, color=color.green, 
     linewidth=2, style=plot.style_circles)
plot(strategy.position_size < 0 ? shortStop : na, color=color.red, 
     linewidth=2, style=plot.style_circles)

// 第六步:提交入场订单
if enterLong
    strategy.entry("EL", strategy.long, qty=posSize)
if enterShort
    strategy.entry("ES", strategy.short, qty=posSize)

// 第七步:提交出场订单
if strategy.position_size > 0
    strategy.exit("XL", from_entry="EL", stop=longStop)
if strategy.position_size < 0
    strategy.exit("XS", from_entry="ES", stop=shortStop)

if not tradeWindow
    strategy.close_all()

为TradingView Pine编写的SMA周线交叉交易策略

移动平均线是一种广受欢迎的技术指标。它们能抚平价格毛刺,帮助我们穿透日常的市场噪音,更清晰地洞察价格行为。这一特性也使得它们在趋势跟踪者中备受青睐。本文将探讨一个基于移动平均线的趋势跟踪策略。

SMA周线交叉策略:一种更高时间框架的趋势跟踪

在其著作《趋势跟踪》(Trend Following)中,迈克尔·柯威尔(Michael Covel)深入研究了全球顶尖的趋势跟踪交易者。作为一种交易方法,趋势跟踪的目标很简单(但执行起来并不容易):为了盈利而捕捉到一轮上涨或下跌趋势的主体部分。趋势跟踪者看待市场的方式,与其他交易风格有所不同。

趋势跟踪者不会去猜测趋势何时开始,或是支撑和阻力位在何处,他们只简单地关注价格。他们预先定义何种价格行为构成了趋势,然后在他们看到新趋势出现时便建立仓位。这确实使得他们总是会错过趋势的开端,也无法在顶部附近离场。但这没关系:当我们能抓住大趋势的主升浪或主跌浪时,便可以获取可观的利润。

大多数趋势跟踪策略有几个共同的特点:它们通过观察价格变化来识别主要趋势;让盈利的头寸持续奔跑,直到趋势发生改变;在预设的止损位果断了结亏损;并通过头寸规模管理来降低交易风险。(尽管如此,大多数趋势跟踪策略确实会经历大幅度的资金回撤。)

柯威尔在他的书中分享的策略之一,便是SMA周线交叉策略(SMA Crossover Weekly strategy)。该策略使用周线价格数据和两条移动平均线。一条均线跟随短期趋势,另一条则追踪长期趋势。当它们交叉时,策略便顺着短周期均线的方向开仓。接下来,让我们看看该策略的完整交易规则。

SMA周线交叉策略的交易规则

该策略包含以下交易规则。做多入场:当10周简单移动平均线(SMA)上穿20周SMA后的次日开盘时,以市价单做多(并平掉任何已有的空头仓位)。做多离场:多头仓位的止损价位为入场价格减去4倍的10周平均真实波幅(ATR)。做空入场:当10周SMA下穿20周SMA后的次日开盘时,以市价单做空(并平掉任何已有的多头仓位)。做空离场:空头仓位的止损价位为入场价格加上4倍的10周ATR。

头寸规模的确定是一个双重风控机制:其一是基于风险的规模,每笔头寸的初始风险(入场价与止损价之差)被设定为权益的3%;其二是最大风险敞口限制,单个头寸的最大风险敞口(即保证金与权益的比率)被限制在权益的10%以内。

我们首先根据3%的风险来计算头寸大小,但如果计算出的头寸所需的保证金超过了权益的10%,则会相应地缩减头寸规模,以确保单笔交易的风险敞口不会过大。

柯威尔分享的SMA周线交叉策略的盈利回测,是基于20个美国期货市场长达15年的日线数据完成的,包括货币(英镑、日元、欧元)、大宗商品(原油、黄金、白银、玉米、小麦)、软商品(咖啡、糖)以及金融产品(标普500、纳斯达克100、5年期美国国债)。

在TradingView中编写SMA周线交叉策略

现在,让我们将上述交易规则转化为一个功能完备的TradingView脚本。一个有效的方法是使用模板,它能将一项大任务分解成更小、更易于管理的工作区块,也能避免我们面对空白脚本时无从下手。

这是我们将用于SMA周线交叉策略的策略模板:

//@version=5
// 步骤1. 定义策略设置
// 步骤2. 计算策略所需指标值
// 步骤3. 定义多头交易条件
// 步骤4. 定义空头交易条件
// 步骤5. 在图表上输出数据
// 步骤6. 提交入场订单
// 步骤7. 提交出场订单

如果你想跟随本文一起操作,请在TradingView的Pine编辑器中创建一个新的策略脚本,并将上述模板粘贴进去。(或者,你也可以直接跳到文末查看完整的策略代码。)

为了让你对我们即将编写的代码有个直观的认识,以下是最终完成的SMA周线交叉策略在图表上的样子:

现在,让我们开始将上述交易规则,一步步地转化为一个规范的TradingView策略脚本。

步骤1:定义策略设置和输入选项

在第一步,我们需要配置策略的整体属性,并创建用户可调的参数设置,以便我们能方便地调整策略参数。我们用 strategy() 函数作为代码的开端,来配置策略的默认行为:

// 步骤1. 定义策略设置
strategy(title="SMA周线交叉策略", overlay=true,
     pyramiding=0, initial_capital=100000,
     commission_type=strategy.commission.cash_per_order,
     commission_value=4, slippage=2)

我们通过 strategy() 函数来配置脚本的属性。title 参数为策略命名。overlay=true 使策略直接叠加在主图表上。根据交易规则,我们通过 pyramiding=0 禁止加仓。initial_capital 则将策略的初始资金设为100,000。在交易成本方面,我们设定每笔单边交易(strategy.commission.cash_per_order)收取4个货币单位的固定佣金(commission_value=4),并假设市价单和止损单会产生2个最小跳动点的滑点(slippage=2)。这里的成本设置对于期货交易来说略高,但这是一种保守的做法,可以避免人为地夸大策略的表现。

接下来,我们创建策略的输入选项,以便无需修改代码就能方便地调整参数。首先是移动平均线的设置:

// SMA 输入项
fastMALen = input.int(10, title="快线SMA周期")
slowMALen = input.int(20, title="慢线SMA周期")

我们使用 input.int() 函数创建了两个整数输入项,分别用于设置快、慢两条均线的计算周期,默认值分别为10和20。我们将输入值保存在变量中,以便后续引用。

然后,我们为ATR止损创建两个输入项:

// 止损输入项
atrLen     = input.int(10, title="ATR周期")
stopOffset = input.float(4, title="止损偏移倍数", step=.25)

ATR周期是一个整数输入,用于指定计算平均真实波幅(ATR)的K线数量,默认值为10。止损偏移倍数则是一个浮点数输入,用于指定止损距离当前价格多少个ATR的倍数,默认值为4。

接下来,我们为策略的仓位管理功能创建一些输入选项:

// 仓位管理输入项
usePosSize  = input.bool(true, title="启用仓位管理?")
maxRisk     = input.float(3, title="单笔最大风险 %", step=.25)
maxExposure = input.float(10, title="最大风险敞口 %", step=1)
marginPerc  = input.int(10, title="保证金预估 %")

第一个输入项是一个布尔型的复选框,可以方便地一键开启或关闭仓位管理算法。第二个是浮点数输入,用于设定在单笔交易中我们愿意承担的权益风险比例,默认值为3%。第三个是另一个浮点数输入,用于限制总仓位规模相对于策略权益的比例,默认值为10%。最后一个是整数输入,由于Pine Script无法直接获取经纪商提供的确切保证金率,我们在此创建一个输入项,以便用户根据实际情况或典型值进行估算,默认值为10%。

最后两个输入项用于设定策略的回测时间窗口:

// 时间范围输入项
endMonth = input.int(10, title="回测结束月份", minval=1, maxval=12)
endYear  = input.int(2018, title="回测结束年份", minval=1990, maxval=2025)

我们使用这两个输入项来定义策略应在何时结束回测。这样可以确保回测报告是基于已平仓的交易,同时也方便我们测试不同时间段的策略表现。默认设置为2018年10月。

步骤2:计算交易策略所需的值

在第二步,我们计算策略运行所需的数据,包括均线、ATR、交易窗口和头寸规模。首先是均线和ATR的计算:

// 步骤2. 计算策略所需值
fastMA = ta.sma(close, fastMALen)
slowMA = ta.sma(close, slowMALen)

atrValue = ta.atr(atrLen)

我们使用 ta.sma() 分别计算出10周期和20周期的简单移动平均线(SMA),并用 ta.atr() 计算出10周期的ATR值。

然后,我们定义策略的有效交易时间窗口:

tradeWindow = time <= timestamp(endYear, endMonth, 1, 0, 0)

设置交易窗口的目的是为了让策略在回测结束时自动平仓。我们通过 timestamp() 函数和用户的输入来创建一个具体的结束时间点,并判断当前K线的时间是否早于这个时间点,从而决定是否允许交易。

最后要计算的是策略的头寸规模,这是本策略的一个核心部分:

// 计算头寸规模
riskEquity = (maxRisk * 0.01) * strategy.equity
riskTrade  = (atrValue * stopOffset) * syminfo.pointvalue

maxPos = ((maxExposure * 0.01) * strategy.equity) /
     ((marginPerc * 0.01) * (close * syminfo.pointvalue))

posSize = usePosSize ? math.min(math.floor(riskEquity / riskTrade), maxPos) : 1

我们分步进行计算:首先,计算出本次交易愿意承担的风险金额 riskEquity。接着,计算出每手交易的初始风险 riskTrade(即止损距离代表的金额)。然后,计算出基于风险敞口和保证金预估的最大允许仓位 maxPos。最后,如果用户启用了仓位管理,我们就在基于单笔风险计算出的仓位和最大允许仓位之间取其较小者,作为最终的头寸规模 posSize。如果未启用,则固定为1。

步骤3:编写多头交易规则

在下一步,我们将策略的多头交易规则转化为Pine Script代码。首先,我们确定多头入场的条件,随后再计算多头的止损价格:

// 步骤3. 定义多头交易条件
enterLong = ta.crossover(fastMA, slowMA) and 
     tradeWindow

longStop = 0.0
longStop := enterLong ? close - (stopOffset * atrValue) :
     longStop[1]

enterLong 变量只有在两个条件同时满足时才为 true:一是发生了均线金叉(ta.crossover()),二是不超过我们设定的回测结束时间(tradeWindow)。

这个策略使用一个固定的止损,即止损位在开仓时计算一次后便不再改变。我们通过条件运算符(?:)来实现:只有当 enterLongtrue 时,我们才根据当前价格和ATR重新计算止损位;在其他所有时间,longStop 的值都保持为上一根K线的值。

步骤4:编写空头交易条件

接下来,我们编写空头交易的逻辑。首先,我们定义空头的入场条件:

// 步骤4. 定义空头交易条件
enterShort = ta.crossunder(fastMA, slowMA) and 
     tradeWindow

与多头类似,只有当发生均线死叉(ta.crossunder())并且处于交易窗口内时,enterShort 才为 true

然后,我们确定空头的止损价格:

shortStop = 0.0
shortStop := enterShort ? close + (stopOffset * atrValue) :
     shortStop[1]

空头止损的逻辑与多头完全对称。只有在产生新的做空信号时,我们才重新计算止损位,否则其值保持不变。

步骤5:输出数据并可视化信号

在第五步,我们将策略计算出的数据绘制在图表上。这样,我们就可以直观地验证交易信号并跟踪策略的行为。首先,我们显示移动平均线:

// 步骤5. 输出策略数据
plot(fastMA, color=#87CEEB, linewidth=2, title="快线SMA")
plot(slowMA, color=#7B68EE, linewidth=3, title="慢线SMA")

我们使用 plot() 函数在图表上绘制均线。第一行代码用天蓝色(#87CEEB)绘制了10周SMA(快线),并将其线条加粗。第二行则用中暗石板蓝(#7B68EE)绘制了更粗的20周SMA(慢线)。

接下来,我们在图表上绘制策略的止损位:

plot(strategy.position_size > 0 ? longStop : na, color=#90EE90,
     style=plot.style_linebr, linewidth=2, title="多头止损")
plot(strategy.position_size < 0 ? shortStop : na, color=#F08080,
     style=plot.style_linebr, linewidth=2, title="空头止损")

为了保持图表整洁易读,我们只在策略实际持有多仓时才显示多头止损位,反之亦然。为了实现这一点,我们使用条件运算符(?:)来进行条件化绘图。

第一个 plot() 语句判断 strategy.position_size 是否大于0(即持有多仓),如果是,则绘制 longStop 的值;否则,返回 na 值以在该K线上隐藏绘图。我们使用带断点的线条样式(plot.style_linebr),并将其颜色设为浅绿色(#90EE90)。

第二个 plot() 语句的逻辑类似,但用于在持空仓时绘制红色的(#F08080)空头止损位。

步骤6:提交开仓订单

现在,我们到了编写开仓代码的环节。代码如下:

// 步骤6. 提交开仓订单
if enterLong
    strategy.entry("EL", strategy.long, qty=posSize)
if enterShort
    strategy.entry("ES", strategy.short, qty=posSize)

第一个 if 语句检查 enterLong 变量是否为 true。如果是(即发生10周与20周均线的金叉),我们就调用 strategy.entry() 函数开立一个ID为”EL”、数量为 posSize 的多头仓位。

第二个 if 语句同理,当 enterShorttrue 时,开立一个ID为”ES”的空头仓位。

这里有一个重要细节:strategy.entry() 函数会自动反转当前持有的反向仓位。因此,当我们的策略持有空仓,并且发生了均线金叉时,strategy.entry() 会自动先平掉空仓,然后再开立多仓,我们无需为此编写额外的代码。

步骤7:提交平仓订单

在策略代码的最后一部分,我们处理持仓的退出。首先,我们提交止损订单:

// 步骤7. 提交平仓订单
if strategy.position_size > 0 and not enterShort
    strategy.exit("XL", from_entry="EL", stop=longStop)
if strategy.position_size < 0 and not enterLong
    strategy.exit("XS", from_entry="ES", stop=shortStop)

if enterLong
    strategy.cancel("XS")
if enterShort
    strategy.cancel("XL")

第一个 if 语句用于提交多头止损。它有两个前置条件:一是策略当前确实持有多仓(strategy.position_size > 0);二是没有同时发生空头开仓信号(not enterShort)。加入第二个条件的原因是为了处理仓位反转的特殊情况。当策略持有多仓,并且一个做空信号出现时,strategy.entry() 会自动将多仓反转为空仓。如果在此时我们还提交一个多头止损单,可能会导致意料之外的成交和平仓行为。因此,在反转信号出现时,我们不再提交旧仓位的止损单。

第二个 if 语句同理,用于在持空仓且无多头开仓信号时提交空头止损单。

接下来的两个 if 语句用于主动撤销旧的止损单,这是一个良好的编程习惯。当产生新的多头信号(enterLong)时,我们使用 strategy.cancel() 函数取消掉之前可能存在的、ID为”XS”的空头止损挂单。反之亦然。这可以防止因旧的挂单未被处理而导致的意外成交。

我们SMA周线交叉策略的最后一行代码是:

if not tradeWindow
    strategy.close_all()

这个 if 语句通过 not tradeWindow 来判断回测窗口是否已经结束。根据我们之前的代码,当脚本运行到2018年10月1日之后的K线时,tradeWindow 会变为 false,此时 not tradeWindow 就为 true,从而触发 strategy.close_all(),将所有剩余仓位以市价单清空。由于我们在开仓前也检查了 tradeWindow,这就确保了在回测期末尾,策略会平掉所有仓位并且不再开新仓。

SMA周线交叉策略的表现

让我们先从这个策略表现出色的方面开始讨论。当市场出现清晰、长期的趋势时,该策略能捕捉到大级别波段的大部分利润。

例如,在下方的图表中,策略成功捕捉了原油期货的一波主要上涨趋势。随后当价格转跌时,策略简单地反转了仓位,同样在空头方向上获得了可观的利润。

然而不幸的是,这个策略也无法幸免于趋势跟踪系统常见的弱点。当市场进入横盘震荡时,其表现会受到影响,策略会开始亏损。

在这些市场条件下,亏损会因为多种原因累积:价格的走向与策略预期相反,趋势运行的距离不足以盈利出场,甚至一些不亏不盈的交易也会因为交易成本和滑点而变成实际亏损。

下方图表就是一个不利市场条件的例子。在这里,原油期货虽然整体向上,但伴随着大量的拉锯走势。这些反复的之字形波动给策略带来了很大的麻烦,导致了连续数笔亏损交易:

以下显示了两个回测的结果。对我而言,SMA周线交叉策略能取得盈利,有些出乎意料。但即便有这个惊喜,这里仍有一个重点需要注意:这些结果是在禁用头寸规模管理功能的情况下得出的。这样做的目的是为了让策略能执行它遇到的每一个信号。

但即便如此,该策略的交易频率也并不高:每年大约只有2.5次。如此小的交易样本量,让我们很难对以下表现结果抱有十足的信心。在我们可以肯定地说这个策略有效之前,我们需要收集更多的交易数据。

表现指标 原油期货 (CL) E-迷你标普500指数期货 (ES)
首次交易 1983-10-16 1998-09-06
最后交易 2018-10-07 2018-10-07
时间框架 周线 周线
净利润 $80,052 $35,771
总利润 $307,428 $153,210
总亏损 -$227,376 -$117,439
最大回撤 $65,961 $27,201
盈利因子 1.352 1.305
总交易数 93 50
胜率 36.56% 48%
平均每笔交易 $860 $715
平均盈利交易 $9,042 $6,383
平均亏损交易 -$3,853 -$4,516
平均盈亏比 2.346 1.413
支付佣金 $388 $216
每笔订单滑点 2个跳动点 2个跳动点

改进思路与新策略方向

因此,SMA周线交叉策略显然需要我们投入更多的关注和研究。以下是一些你可能会觉得有趣的、值得进一步探索的思路。

与大多数趋势跟踪策略一样,SMA周线交叉策略在市场横盘时表现不佳。如果我们能过滤掉那些低胜率的交易,策略的表现可能会得到显著提升。或许,我们可以用成交量突破、平均真实波幅(ATR)的值或ADX指标来过滤信号。

柯威尔并未讨论他选择这些特定参数设置的原因。也许10周/20周的SMA是最佳组合,但更有可能的是,当参数针对特定品种和当前市场状况进行优化后,策略的表现会更好(毕竟,柯威尔的书已经出版了十多年了)。

一个有趣的观察是,策略回测显示了多个实例:在一段盈利的多头趋势之后,接下来的1到2笔交易往往是亏损的。如果我们假设一段长趋势很少会立即被另一段大趋势所跟随,那么这个现象是合乎逻辑的。因此,如果我们能在一次盈利交易后跳过头一两个信号,策略的表现或许能得到改善。(或者在一次交易达到一定的利润额之后暂停。)

虽然10周SMA已经捕捉了超过2个月的价格数据,但策略仍然有大量交易未能有效过滤掉市场噪音。然而,使用更长的周期并非良策,因为那可能会使策略在周线级别上反应过于迟钝。一个可能的替代方案是,在日线图上进行交易决策,但使用周线级别的SMA数据作为过滤。这样,日线图便可以利用日线级别的移动平均线来过滤掉市场噪音。

改善策略表现的另一种方法是限制其所承担的风险。例如,我们可以为策略的资金回撤设置一个上限,或者使用最大持仓规模。

与SMA周线交叉策略相似的策略是SMA交叉策略,该脚本基于日线数据交易均线交叉。SMA交叉金字塔策略也使用移动平均线,但会在盈利的头寸上进行加仓。其他同样依赖移动平均线的趋势跟踪策略还包括双移动平均线策略和三重移动平均线策略。

完整代码:TradingView的SMA周线交叉策略

以下是SMA周线交叉策略的完整代码。关于代码的解释和更多细节,请参阅上文的讨论。

//@version=5
// 步骤1. 定义策略设置
strategy(title="SMA周线交叉", overlay=true,
     pyramiding=0, initial_capital=100000,
     commission_type=strategy.commission.cash_per_order,
     commission_value=4, slippage=2)

// --- SMA输入项 ---
fastMALen = input.int(10, title="快线SMA周期")
slowMALen = input.int(20, title="慢线SMA周期")

// --- 止损输入项 ---
atrLen     = input.int(10, title="ATR周期")
stopOffset = input.float(4, title="止损偏移倍数", step=.25)

// --- 头寸规模管理输入项 ---
usePosSize  = input.bool(true, title="是否启用头寸规模管理?")
maxRisk     = input.float(3, title="最大单笔风险%", step=.25)
maxExposure = input.float(10, title="最大头寸敞口%", step=1)
marginPerc  = input.int(10, title="保证金%")

// --- 时间范围输入项 ---
endMonth = input.int(10, title="结束月份", minval=1, maxval=12)
endYear  = input.int(2018, title="结束年份", minval=1990, maxval=2025)

// 步骤2. 计算策略所需指标值
// 请求周线级别的数据
[fastMA, slowMA, atrValue, tradeWindow, close] = request.security(syminfo.tickerid, "W",
     [ta.sma(close, fastMALen), ta.sma(close, slowMALen), ta.atr(atrLen),
     time <= timestamp(endYear, endMonth, 1, 0, 0), close], lookahead=barmerge.lookahead_on)

// 计算头寸规模 (双重风控:风险百分比 vs 最大敞口)
riskEquity = (maxRisk * 0.01) * strategy.equity
riskTrade  = (atrValue * stopOffset) * syminfo.pointvalue

maxPos = ((maxExposure * 0.01) * strategy.equity) /
     ((marginPerc * 0.01) * (close * syminfo.pointvalue))

posSize = usePosSize ? math.min(math.floor(riskEquity / riskTrade), maxPos) : 1

// 步骤3. 定义多头交易条件
enterLong = ta.crossover(fastMA, slowMA) and 
     tradeWindow

// 实现“粘性”止损:止损价在入场时确定,并在持仓期间保持不变
longStop = 0.0
longStop := enterLong ? close - (stopOffset * atrValue) :
     longStop[1]

// 步骤4. 定义空头交易条件
enterShort = ta.crossunder(fastMA, slowMA) and 
     tradeWindow

shortStop = 0.0
shortStop := enterShort ? close + (stopOffset * atrValue) :
     shortStop[1]

// 步骤5. 在图表上输出数据
plot(fastMA, color=#87CEEB, linewidth=2, title="快线SMA")
plot(slowMA, color=#7B68EE, linewidth=3, title="慢线SMA")
plot(strategy.position_size > 0 ? longStop : na, color=#90EE90,
     style=plot.style_linebr, linewidth=2, title="多头止损")
plot(strategy.position_size < 0 ? shortStop : na, color=#F08080,
     style=plot.style_linebr, linewidth=2, title="空头止损")

// 步骤6. 提交入场订单
if enterLong
    strategy.entry("EL", strategy.long, qty=posSize)

if enterShort
    strategy.entry("ES", strategy.short, qty=posSize)

// 步骤7. 提交出场订单
// ATR止损
if strategy.position_size > 0 and not enterShort
    strategy.exit("XL", from_entry="EL", stop=longStop)

if strategy.position_size < 0 and not enterLong
    strategy.exit("XS", from_entry="ES", stop=shortStop)

// 当新的反向信号出现时,取消之前方向的止损单
if enterLong
    strategy.cancel("XS")

if enterShort
    strategy.cancel("XL")

// 在回测窗口结束时平掉所有仓位
if not tradeWindow
    strategy.close_all()
赞(0)
未经允许不得转载:图道交易 » Pine Script(258):SMA交叉策略与周线均线交叉
分享到

评论 抢沙发

登录

找回密码

注册