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

Pine Script(256):唐奇安趋势策略与时间退出改进

#Pine Script入门教学

如何在TradingView中编写唐奇安趋势策略

交易N周期突破是一种常见的趋势跟踪方法。然而,其问题在于,相当一部分的突破最终会失败。这不仅会降低策略的胜率,还会直接造成资金损失。但是,如果我们利用市场的整体趋势来过滤高低点的突破信号呢?本文将探讨如何编写这样一个TradingView交易策略。

唐奇安趋势(Donchian Trend)策略:为突破交易增加过滤

柯蒂斯·费思(Curtis Faith)在其著作《海龟交易法则》(Way of the Turtle)中,分享了他作为第一代海龟成员所学到的宝贵经验。在二十世纪八十年代,名噪一时的海龟们是一群由极度成功的交易员理查德·丹尼斯亲自挑选并训练的普通人。丹尼斯向他们传授了两种盈利丰厚的策略,并为他们提供了高达200万美元的真实账户进行交易。最终,费思本人为丹尼斯创造了超过3000万美元的利润。

海龟们是坚定的趋势跟踪者。这类交易者致力于从持续数月的大级别价格波段中获利。他们在市场创下历史新高或新低时入场,其核心假设是价格动能在突破后仍会持续。趋势跟踪是一种能够盈利、但执行起来异常困难的交易方式。其胜率通常徘徊在30-35%之间,资金回撤的幅度可能巨大,错过几笔关键的大幅盈利交易,就可能毁掉一整年的努力。

幸运的是,其回报也可能相当可观——费思分享的策略据称能达到每年约30-40%的收益。其中之一便是唐奇安趋势(Donchian Trend)策略。该系统是著名的海龟们所使用的交易策略的一个简化版本。使用唐奇安通道(Donchian Channel)交易的基本思想是:当价格突破过去20日的最高高点时建立多头仓位,当价格跌破过去20日的最低低点时建立空头仓位。

有一个关键原因使得交易突破变得困难:在短期内,价格很可能在突破后朝相反方向大幅移动。这种情况尤其容易在逆势突破时发生,例如在熊市中出现向上突破新高。为了规避这些低胜率的交易,费思在原始的唐奇安通道基础上,增加了一个趋势过滤器。通过这种方式,我们只在市场看涨时进行多头突破交易,只在市场疲软时进行空头突破交易。

交易突破的另一个难题是何时离场。如果我们使用同一个20周期的通道来入场和出场,那么我们将不得不放弃相当大一部分的浮动利润,才能等到离场信号的出现。然而,缩短通道周期又会降低入场信号的质量。为了解决这个问题,费思采用了非对称的通道周期:使用一个长周期的唐奇安通道来入场,并使用一个短周期的唐奇安通道来出场。接下来,让我们看看该策略具体的交易规则。

唐奇安趋势策略的交易规则

唐奇安趋势系统包含以下交易规则。做多入场:当价格上涨并突破过去20日的最高高点时,建立多头仓位,并且25日指数移动平均线(EMA)必须在350日EMA之上。做多离场:当价格跌破过去10日的最低价时,平掉多头仓位,同时在入场价格下方2倍20日ATR的位置设置止损。做空入场:当价格下跌并跌破过去20日的最低低点时,建立空头仓位,并且25日EMA必须在350日EMA之下。做空离场:当价格上涨并突破过去10日的最高价时,回补空头仓位,同时在入场价格上方2倍20日ATR的位置设置止损。头寸规模:对于多头和空头头寸,将权益的0.5%除以该市场20周期平均真实波幅(ATR)所代表的美元价值,来确定头寸大小。

费思在他的书中分享的、被证明盈利的该策略回测,是建立在日线数据以及一系列高流动性的美国期货市场之上的,包括外汇期货(澳元、英镑、欧元)、大宗商品(黄金、原油、铜、天然气)、软商品(棉花、咖啡、活牛、大豆)以及固定收益期货(美国国库券和国债)。

编写唐奇安趋势交易策略

现在,让我们在TradingView中来编写唐奇安趋势策略。使用模板是简化策略编写过程的有效方法。它能将一项大任务分解成更小、更易于管理的工作区块,不仅可以防止我们面对空白脚本时无从下手,还能为我们的思路增加一些结构性,减少遗漏。

我们将使用以下模板来编写唐奇安趋势策略:

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

如果你想跟随本文一起操作,请在TradingView的Pine编辑器中创建一个新的策略脚本,并将上述模板粘贴进去。

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

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

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

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

// 1. 定义策略设置
strategy(title="唐奇安趋势策略", 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 来禁止金字塔式加仓,因为我们的交易规则没有设计加仓逻辑。我们设定回测的初始资金为100,000(initial_capital=100000)。在交易成本方面,我们设定每笔单边订单(strategy.commission.cash_per_order)收取4个货币单位的固定佣金(commission_value=4),并假设市价单和止损单会产生2个最小跳动点的滑点(slippage=2)。这里的佣金和滑点设置都相对悲观,这样能让策略在更严苛的条件下证明其有效性。

接下来,我们为策略的各项参数编写输入选项:

// 唐奇安通道输入项
hiLenEntry = input.int(20, title="高点周期 (入场)")
loLenEntry = input.int(20, title="低点周期 (入场)")
hiLenExit  = input.int(10, title="高点周期 (出场)")
loLenExit  = input.int(10, title="低点周期 (出场)")

// EMA趋势过滤器输入项
fastMALen = input.int(25, title="快线EMA周期")
slowMALen = input.int(350, title="慢线EMA周期")

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

// 仓位管理输入项
usePosSize = input.bool(true, title="启用仓位管理?")
riskPerc   = input.float(0.5, title="风险百分比 %", step=0.25)

我们来逐一审视这些输入项。前两组是为唐奇安通道设置的,分别用于入场和出场。我们使用 input.int() 创建整数输入,用于定义计算高点和低点的周期,入场周期默认为20,出场周期默认为10。

然后是为EMA趋势过滤器创建的输入项,同样是整数输入,用于设置快慢EMA的周期,默认值分别为25和350。

接下来是ATR止损的配置项。ATR周期是一个整数输入,默认为20。止损偏移倍数则是一个浮点数输入,由 input.float() 创建,初始值为2.0,用于设定止损距离入场价多少个ATR的倍数。

最后两个输入项用于仓位管理。启用仓位管理是一个布尔型的复选框,由 input.bool() 创建,默认勾选(true)。另一个风险百分比是浮点数输入,默认值为0.5,用于设定每笔交易愿意承担的权益风险比例。

请注意,我们将每个输入项的返回值都赋给了一个独立的变量。这样,在脚本的后续部分,我们就可以通过变量名方便地引用用户在设置面板中的当前选择。

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

接下来,我们计算策略运行所需的各项数据。由于唐奇安趋势策略需要的数据较多,我们分块进行讨论。首先,我们确定用于交易的最高高点和最低低点:

// 2. 计算策略所需值
// 计算最高高点和最低低点
highsEntry = ta.highest(high, hiLenEntry)[1]
lowsEntry  = ta.lowest(low, loLenEntry)[1]

highsExit = ta.highest(high, hiLenExit)[1]
lowsExit  = ta.lowest(low, loLenExit)[1]

我们使用 ta.highest()ta.lowest() 函数来计算指定周期内的最高高点和最低低点,周期由我们之前创建的输入项决定。

这里有一个至关重要的细节:请注意,我们在每个函数后面都加上了 [1]。这是因为,例如 ta.highest(high, 20),它计算的是包含当前K线在内的最近20根K线的最高价。这意味着,当前K线的价格永远不可能向上突破一个包含了它自己的最高价。为了能够捕捉到真实的突破信号,我们需要获取不包含当前K线的前20根K线的最高价,而 [1] 正是实现这一目的的历史引用操作符,它代表上一根K线的值。对最低低点的计算同理。

接下来,我们计算作为趋势过滤器和止损依据的EMA和ATR:

// 获取EMA和ATR值
fastMA = ta.ema(close, fastMALen)
slowMA = ta.ema(close, slowMALen)

atrValue = ta.atr(atrLen)

我们调用 ta.ema() 函数,根据用户设定的 fastMALenslowMALen 周期,计算出快慢两条EMA均线。然后调用 ta.atr() 函数,计算出用于设定止损的ATR值。

然后,我们来计算头寸规模:

// 计算头寸大小
riskEquity  = (riskPerc / 100) * strategy.equity
atrCurrency = atrValue * syminfo.pointvalue
posSize     = usePosSize ? math.floor(riskEquity / atrCurrency) : 1

我们这里采用的仓位管理算法基于两个核心部分:一部分是策略权益的固定百分比,另一部分是ATR的货币价值。

首先,我们计算出本次交易愿意承担的风险金额 riskEquity。我们将输入的风险百分比 riskPerc 转换为小数(例如,0.5%变为0.005),然后乘以 strategy.equity(策略的当前总权益,包括初始资本、已实现盈亏和浮动盈亏)。

接着,我们将ATR值转换为具体的货币价值 atrCurrency。我们用ATR的数值 atrValue 乘以 syminfo.pointvaluesyminfo.pointvalue 是一个内置变量,它返回当前交易品种价格每波动一个完整点位所代表的货币价值(例如,E-mini S&P 500期货是50,而股票通常是1)。

最后,我们通过条件运算符 ?: 来确定最终的头寸规模 posSize。如果用户在输入项中勾选了启用仓位管理(usePosSizetrue),我们就用风险金额 riskEquity 除以每单位风险(即ATR的货币价值 atrCurrency),并用 math.floor() 向下取整,得到最终的下单量。如果用户未启用仓位管理,那么 posSize 就固定为1。

接下来,我们计算策略的止损价位:

// 确定ATR止损价
atrLongEntry = strategy.position_size == 0 ?
     close - (atrValue * stopOffset) :
     na
longStop = fixnan(atrLongEntry)

atrShortEntry = strategy.position_size == 0 ?
     close + (atrValue * stopOffset) :
     na
shortStop = fixnan(atrShortEntry)

这段代码的核心目的是计算一个在入场时就确定下来,并在整个持仓期间保持不变的固定止损价格。我们通过一个巧妙的两变量技巧来实现它。

首先,我们计算多头的止损。变量 atrLongEntry 只在策略没有持仓时(strategy.position_size == 0)才计算止损价;它的计算方式是当前收盘价 – (ATR值 × 止损偏移倍数)。一旦进入持仓,它的值就变为 na(即不可用)。

这里的奥妙在于 longStop = fixnan(atrLongEntry) 这一行。fixnan() 函数能获取一个序列中最近的那个非 na 值。因此,当 atrLongEntry 变为 na 时,fixnan(atrLongEntry) 会持续返回它变为 na 之前的那最后一个有效数值——也就是我们入场前计算好的那个止损价。通过这种方式,我们便成功地锁定了止损价格,并将其存入 longStop 变量,以备后续下单时使用。

(我们以完全相同的方式计算了空头止损的 atrShortEntryshortStop 变量,其逻辑与上述解释一致。)

剩下唯一需要计算的,就是判断当前是上升趋势还是下降趋势:

// 根据移动平均线确定趋势
upTrend   = fastMA > slowMA
downTrend = fastMA < slowMA

这两个布尔变量根据策略的交易规则,定义了市场的趋势方向。当25周期EMA(fastMA)高于350日EMA(slowMA)时,upTrend 变量为 true,否则为 false

同理,当25周期EMA低于350日EMA时,downTrend 变量为 true,否则为 false。我们将在下一步绘制策略数据时使用这两个变量。

步骤3:在图表上输出数据并可视化信号

在第三步中,我们将在图表上显示策略的各项数据。这能帮助我们追踪其交易行为,并验证策略是否如预期般运行。为了直观地看到信号在何处发生以及策略所使用的数值,我们使用 plot() 函数在图表上显示一些关键数据:

// 3. 在图表上输出数据
// 绘制用于入场的20周期高点和低点突破通道
plot(upTrend ? highsEntry : na, style=plot.style_linebr,
     color=color.green, linewidth=2, title="多头入场通道")
plot(downTrend ? lowsEntry : na, style=plot.style_linebr,
     color=color.red, linewidth=2, title="空头入场通道")

第一个 plot() 函数调用负责显示我们用来开多仓的20周期最高高点(highsEntry)。但为了避免图表显得过于杂乱,我们只在市场处于上升趋势时才显示它。我们通过条件运算符(?:)实现了这一效果:如果 upTrendtrue,则绘制 highsEntry;否则,返回 na 来关闭绘图。我们将这条代表多头触发水平的线设置为绿色的断点线。

第二个 plot() 语句也基于条件进行绘制。这次我们检查 downTrend 变量。因为我们只在下降趋势中寻找空头入场机会,所以我们只在这种情况下才绘制20周期的最低低点(lowsEntry)。这条线则被设为红色。

接着,我们绘制策略所用的EMA:

// 绘制25周期EMA
plot(fastMA, linewidth=2, color=upTrend ? color.green : color.red, 
     title="快线EMA")

这条 plot() 语句在图表上显示了25周期的EMA。我们通过 linewidth 参数将其线宽调大。其颜色则由条件决定:当 upTrendtrue 时,线条为绿色;否则为红色。这样我们只需看一眼移动平均线的颜色,就能知道当前市场处于上升趋势还是下降趋势。

接下来,我们绘制用于出场的10周期最高高点和最低低点:

// 绘制用于突破出场的10周期高点和低点通道
plot(downTrend ? highsExit : na, style=plot.style_circles,
     title="空头出场通道", color=color.fuchsia, linewidth=3)
plot(upTrend ? lowsExit : na, style=plot.style_circles,
     title="多头出场通道", color=color.fuchsia, linewidth=3)

由于唐奇安趋势策略包含趋势过滤器,用于空头离场的10周期高点在上升趋势中并无意义,反之亦然。因此,我们只在合适的趋势方向上绘制相应的数据。

第一个 plot() 调用只在下降趋势(downTrendtrue)时,才绘制作为空头离场依据的10周期最高高点(highsExit)。第二个 plot() 则只在上升趋势(upTrendtrue)时,绘制作为多头离场依据的10周期最低低点(lowsExit)。

我们将这两组数据都绘制成紫红色(color.fuchsia)的实心圆点(style=plot.style_circles),并通过 linewidth=3 使其比默认尺寸大几号,以示醒目。

我们要在图表上显示的最后一项数据是基于ATR计算出的止损水平:

// 绘制ATR止损水平
stopPrice = if strategy.position_size > 0
    longStop
else if strategy.position_size < 0
    shortStop

plot(stopPrice, style=plot.style_linebr, title="ATR止损", 
     color=color.blue, linewidth=2)

这里,我们首先根据策略的当前持仓来决定要绘制哪一个止损价格。我们通过 strategy.position_size 变量来判断:当策略持多仓时(值大于0),我们选择 longStop;当策略持空仓时(值小于0),我们则选择 shortStop

我们将这个止损价格以蓝色的(color.blue)断点线(style=plot.style_linebr)形式显示出来。

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

接下来,我们编写策略的多头交易条件,并将它们存入布尔变量中,以便后续提交实际订单时使用。

// 4. 定义多头交易条件
enterLong = upTrend and 
     close > highsEntry and 
     barstate.ishistory

exitLong = close < lowsExit

要使 enterLong 变量为 true,必须同时满足三个条件。第一,upTrend 变量必须为 true,即市场处于上升趋势(25周期EMA大于350周期EMA)。第二,当前K线的收盘价必须向上突破(>)20周期的最高高点(highsEntry),这是策略实际的多头入场信号。第三,barstate.ishistory 必须为 true,该变量的作用是确保我们的策略只在历史数据上进行回测;在后续的代码中,我们还会让策略在实时数据K线上自动平掉所有仓位,这样便能让策略在图表的最后一根K线上自动停止交易并清仓。

由于我们使用了 and 运算符,只有当这三个条件全部满足时,enterLong 才为 true

我们创建的 exitLong 变量则更为直接:我们只需判断当前K线的收盘价是否低于(<)10周期的最低低点(lowsExit)。当这种情况发生时,策略便会按照交易规则生成其多头离场信号。

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

在执行实际交易之前,我们首先需要明确定义策略的做空入场和出场条件:

// 5. 定义空头交易条件
enterShort = downTrend and 
     close < lowsEntry and 
     barstate.ishistory

exitShort = close > highsExit

我们在这里定义的布尔变量 enterShort 只有在三个条件同时满足时才为 true。第一,downTrend 变量为 true,即25周期EMA低于350周期EMA,确认市场处于下降趋势。第二,当前K线的收盘价 close 向下跌破了20周期的唐奇安通道下轨(lowsEntry),这是策略的核心做空触发信号。第三,barstate.ishistorytrue,确保此入场逻辑只在历史数据回测期间有效,避免在实时K线上产生非预期的信号。

我们使用 and 逻辑与运算符将这三个要求连接起来,意味着三者必须全部成立,enterShort 才会为 true

另一个布尔变量 exitShort 用于判断平掉空仓的条件。当收盘价 close 向上穿越10周期的唐奇安通道上轨(highsExit)时,exitShorttrue。我们稍后会用这个变量来提交实际的平仓指令。

步骤6:提交开仓订单

接下来,在策略代码中,我们根据前面定义的条件来开立仓位:

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

第一个 if 语句检查 enterLong 是否为 true。当市场处于上升趋势且价格向上突破20周期上轨时,此条件成立。在这种情况下,我们执行 strategy.entry() 函数来开立一个多头仓位(strategy.long)。我们将该订单命名为”EL”(id="EL"),并使用我们之前计算好的 posSize 变量作为其数量。

第二个 if 语句与此类似。我们检查 enterShort 是否为 true。当市场处于下降趋势且价格向下跌破20周期下轨时,此条件成立。在这种情况下,我们调用 strategy.entry() 开立一个ID为”ES”、数量为 posSize 的空头仓位(strategy.short)。

步骤7:提交平仓订单

策略代码的最后一部分负责平掉已有的仓位:

// 7. 提交平仓订单
if exitLong
    strategy.close("EL")
if exitShort
    strategy.close("ES")
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 barstate.islastconfirmedhistory
    strategy.close_all()

我们在这里实现了三种不同的平仓机制:基于交易信号的平仓、止损平仓,以及在回测结束时清空所有仓位。

信号平仓:前两个 if 语句用于处理基于信号的平仓。我们检查 exitLong 是否为 true(价格下穿10周期下轨)。若是,则调用 strategy.close() 函数,通过指定订单ID”EL”来平掉对应的多头仓位。同理,当 exitShort 为真时,平掉ID为”ES”的空头仓位。

止损平仓:接下来的两个 if 语句用于设置保护性止损。第一个检查当前是否持有多仓(strategy.position_size > 0)。若是,则调用 strategy.exit() 函数,提交一个ID为”XL”的止损单,其止损价格由 longStop 变量决定,并明确该止损单是针对ID为”EL”的开仓订单。第二个 if 语句同理,为ID为”ES”的空仓设置一个止损价格为 shortStop 的止损单。

期末平仓:最后一个 if 语句检查 barstate.islastconfirmedhistory 变量,该变量只在脚本运行于图表的最后一根历史K线时才为 true。在这一刻,我们调用 strategy.close_all() 函数,无条件平掉所有持仓,确保策略在回测结束时是空仓状态。

唐奇安趋势策略的表现

让我们先从积极的方面说起。唐奇安趋势策略能很好地捕捉长期趋势。

例如,在下方的图表中,该策略成功交易了E-mini S&P 500期货一波350点的上涨行情。图中用于平仓的10周期最低低点(紫红色点)也很好地跟随了上升趋势,使策略在价格最终崩盘前成功离场:

但该策略有时也表现不佳。和大多数趋势跟踪策略一样,当市场进入横盘震荡时,唐奇安趋势策略会遭受损失。

例如,在下图中,由于价格没有形成有效趋势,且即便有所移动,幅度也很小,导致策略产生了两次亏损交易:

下表展示了该策略在E-迷你标普500指数期货(ES)和大豆期货(ZS)上的回测结果。这些结果是在禁用头寸规模管理功能的情况下得出的。这样做的目的是为了让策略能执行它生成的每一个信号,从而在回测中获得更大的交易样本量,同时也避免了策略跳过那些可能最终成为亏损交易的信号。

表现指标 E-迷你标普500指数期货 (ES) 大豆期货 (ZS)
首次交易 1999-03-07 1971-06-22
最后交易 2018-09-14 2018-09-14
时间框架 日线 日线
净利润 -$9,634 $59,974
总利润 $99,994 $213,894
总亏损 -$109,628 -$153,920
最大回撤 $32,020 $13,941
盈利因子 0.912 1.39
总交易数 98 222
胜率 32.65% 36.94%
平均每笔交易 -$98 $270
平均盈利交易 $3,124 $2,608
平均亏损交易 -$1,661 -$1,099
平均盈亏比 1.881 2.373
支付佣金 $784 $1,776
每笔订单滑点 2个跳动点 2个跳动点

改进思路与新策略方向

虽然上表的表现优于大多数其他的TradingView示例策略,但仍有相当大的改进空间。以下是一些你可能会觉得有价值去进一步探索的思路。

唐奇安趋势策略所使用的10周期高低点离场规则,其初衷是降低交易风险,但这并不一定能同时提升利润。例如,在编写该策略时,E-迷你标普500指数期货的回测在不使用10周期离场规则时是盈利的。但一旦加入了这条离场规则,结果就变成了上表所示的亏损。或许,这条10周期离场规则需要加入一些过滤器,以避免它过早地将那些未来可能转为大幅盈利的交易平仓。

费思在他的书中并未具体说明他选择这些参数的理由。这些参数可能是当时的最优解,也可能仅仅是出于方便而选择的。因此,测试策略在其他参数设置下的表现,似乎是一个不错的想法。

尽管唐奇安趋势策略已经包含了一个趋势过滤器,但它仍然受困于失败的突破信号。这可能意味着现有的趋势过滤器在剔除虚假突破方面效果不佳,或者需要一个额外的过滤器来减少那些胜算不高的交易。无论如何,一旦我们能跳过那些没有前景的交易,策略的表现很可能会得到显著提升。

唐奇安趋势策略使用的350周期EMA对于变化的市场环境反应相当迟缓。也许,一个更快速的移动平均线能让策略更好地响应趋势的变化。

改善策略表现的另一种方法是加强风险管理。例如,我们可以限制策略的最大持仓规模以防止巨额亏损,或者为最大资金回撤设定一个上限。

你也可以查阅带时间退出的唐奇安趋势策略,以了解一个交易高低价格突破的类似策略。

完整代码:TradingView的唐奇安趋势策略

以下是TradingView的唐奇安趋势策略的完整代码。关于代码各部分的详细信息,请参阅上文的讨论。

//@version=5
// 1. 定义策略设置
strategy(title="唐奇安趋势", overlay=true,
     pyramiding=0, initial_capital=100000,
     commission_type=strategy.commission.cash_per_order,
     commission_value=4, slippage=2)

// --- 唐奇安通道输入项 ---
hiLenEntry = input.int(20, title="高点周期(入场)")
loLenEntry = input.int(20, title="低点周期(入场)")
hiLenExit  = input.int(10, title="高点周期(出场)")
loLenExit  = input.int(10, title="低点周期(出场)")

// --- EMA趋势过滤器输入项 ---
fastMALen = input.int(25, title="快线EMA周期")
slowMALen = input.int(350, title="慢线EMA周期")

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

// --- 头寸规模管理输入项 ---
usePosSize = input.bool(true, title="是否启用头寸规模管理?")
riskPerc   = input.float(0.5, title="风险百分比%", step=0.25)

// 2. 计算策略所需指标值
// 计算最高高点和最低低点
highsEntry = ta.highest(high, hiLenEntry)[1]
lowsEntry  = ta.lowest(low, loLenEntry)[1]

highsExit = ta.highest(high, hiLenExit)[1]
lowsExit  = ta.lowest(low, loLenExit)[1]

// 获取EMA和ATR值
fastMA = ta.ema(close, fastMALen)
slowMA = ta.ema(close, slowMALen)

atrValue = ta.atr(atrLen)

// 计算头寸规模
riskEquity  = (riskPerc / 100) * strategy.equity
atrCurrency = atrValue * syminfo.pointvalue
posSize     = usePosSize ? math.floor(riskEquity / atrCurrency) : 1

// 确定ATR止损价
atrLongEntry = strategy.position_size == 0 ?
     close - (atrValue * stopOffset) :
     na
longStop = fixnan(atrLongEntry)

atrShortEntry = strategy.position_size == 0 ?
     close + (atrValue * stopOffset) :
     na
shortStop = fixnan(atrShortEntry)

// 根据移动平均线确定趋势
upTrend   = fastMA > slowMA
downTrend = fastMA < slowMA

// 3. 在图表上输出数据
// 绘制用于入场的20周期高点和低点突破通道
plot(upTrend ? highsEntry : na, style=plot.style_linebr,
     color=color.green, linewidth=2, title="多头入场通道")
plot(downTrend ? lowsEntry : na, style=plot.style_linebr,
     color=color.red, linewidth=2, title="空头入场通道")

// 绘制25周期EMA
plot(fastMA, linewidth=2, color=upTrend ? color.green : color.red, 
     title="快线EMA")

// 绘制用于突破出场的10周期高点和低点通道
plot(downTrend ? highsExit : na, style=plot.style_circles,
     title="空头出场通道", color=color.fuchsia, linewidth=3)
plot(upTrend ? lowsExit : na, style=plot.style_circles,
     title="多头出场通道", color=color.fuchsia, linewidth=3)

// 绘制ATR止损水平
stopPrice = if strategy.position_size > 0
    longStop
else if strategy.position_size < 0
    shortStop
plot(stopPrice, style=plot.style_linebr, title="ATR止损", 
     color=color.blue, linewidth=2)

// 4. 定义多头交易条件
enterLong = upTrend and 
     close > highsEntry and 
     barstate.ishistory

exitLong = close < lowsExit

// 5. 定义空头交易条件
enterShort = downTrend and 
     close < lowsEntry and 
     barstate.ishistory

exitShort = close > highsExit

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

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

// 7. 提交出场订单
if exitLong
    strategy.close("EL")

if exitShort
    strategy.close("ES")

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 barstate.islastconfirmedhistory
    strategy.close_all()

为TradingView编写的唐奇安趋势与时间退出策略

交易员之间流传着一句俗语:入场点位无关紧要,如何出场才决定一切。我们可以通过带时间退出的唐奇安趋势策略(Donchian Trend with Time Exit strategy)来检验这一观点。该策略采用趋势跟踪的逻辑入场,但其离场规则却仅仅是一个固定的时间止损。让我们看看这样的策略表现究竟如何。

带时间退出的唐奇安趋势策略:一场关于出场的实验

在《海龟交易法则》(Way of the Turtle)一书中,作者柯蒂斯·费思(Curtis Faith)分享了自己作为第一代海龟成员所学到的宝贵经验。二十世纪八十年代,传奇交易员理查德·丹尼斯发起了一项著名的实验,亲自挑选并训练了一群背景各异的普通人,他们后来被称为海龟。在他们学会了两种盈利丰厚的策略后,每人都得到了一个高达200万美元的真实账户进行交易。最终,费思本人为丹尼斯创造了超过3000万美元的利润。

海龟们是坚定的趋势跟踪者。这类交易方法致力于从持续数月的大级别价格波段中获利。他们在市场创下历史新高或新低时入场,并推断价格动能将会持续。趋势跟踪并非一种轻松的交易风格:其胜率很低(通常在30-35%),且资金回撤可能非常巨大。

费思在他的书中分享了几个趋势跟踪策略的范例,其中之一便是带时间退出的唐奇安趋势策略,它是我们之前讨论过的唐奇安趋势策略的一个变体。这个策略的独特之处在于,它只有一个离场规则:无论盈亏,在持仓达到80天后便平掉所有多头或空头仓位。

这个离场规则看起来有些随意和奇怪。但费思设计这个策略,正是为了回应一个在交易界经久不衰的争论。据他所说,有大量的交易者坚信,对于一个策略的表现而言,入场并不重要,只有出场才起决定性作用。带时间退出的唐奇安趋势策略便是费思对这一论点的回应。

其理念是,通过将这个仅有简单时间退出的策略,与其他拥有更复杂离场机制的趋势策略进行绩效对比,我们就能更清晰地看到,相对于入场而言,出场规则究竟有多重要。

接下来,让我们看看该策略具体的交易规则,然后将其编写为TradingView Pine脚本。

带时间退出的唐奇安趋势策略的交易规则

该策略包含以下交易规则。做多入场:当价格上涨并突破过去20日的最高高点时,建立多头仓位,并且25日指数移动平均线(EMA)必须在350日EMA之上。做多离场:持仓80天后,平掉多头仓位。做空入场:当价格下跌并跌破过去20日的最低低点时,建立空头仓位,并且25日EMA必须在350日EMA之下。做空离场:持仓80天后,回补空头仓位。头寸规模:对于多头和空头头寸,将权益的0.5%除以该市场20周期平均真实波幅(ATR)所代表的美元价值,来确定头寸大小。

费思是在日线数据以及一系列高流动性的美国期货市场上,对带时间退出的唐奇安趋势策略进行回测的,包括外汇期货(澳元、英镑、欧元)、大宗商品(黄金、原油、铜、天然气)、软商品(棉花、咖啡、活牛、大豆)以及固定收益期货(美国国库券和国债)。

编写带时间退出的唐奇安趋势策略

现在,让我们看看如何将上述交易规则转化为代码。在编写TradingView策略时,使用模板能让过程更轻松。它将一项大任务分解成更小、更易于管理的工作区块,也能避免我们面对空白脚本时无从下手。

在本文中,我们将使用以下模板来编写带时间退出的唐奇安趋势策略:

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

如果你想跟随本文一起操作,请在TradingView的Pine编辑器中创建一个新的策略脚本,并将上述模板粘贴进去。

为了让你对我们即将编写的代码有个直观的认识,以下是最终完成的带时间退出的唐奇安趋势策略在图表上的样子:

现在,让我们开始将策略的交易规则,一步步转化为TradingView的代码。

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

在第一步中,我们将配置策略,并创建一些输入选项,以便能灵活地调整策略的各项参数。与往常一样,我们首先通过 strategy() 函数来定义策略的各项属性:

// 步骤1. 定义策略设置
strategy(title="带时间退出的唐奇安趋势", overlay=true,
     pyramiding=0, initial_capital=100000,
     commission_type=strategy.commission.cash_per_order,
     commission_value=4, slippage=2)

我们使用 title 参数为脚本命名。将 overlay 设为 true,可以使策略的绘图叠加在主图表上。

根据策略的交易规则,我们禁用了金字塔式加仓(pyramiding=0)。initial_capital 则将策略的初始资金设为100,000个货币单位。

在交易成本方面,我们设定每笔单向订单的佣金为4个货币单位(commission_value=4commission_type=strategy.commission.cash_per_order)。我们还为市价单和止损单预估了2个跳动点(tick)的滑点(slippage=2)。这些交易成本的设置略显悲观,这样做是为了让策略必须表现出足够好的性能,才能在扣除成本后依然盈利。

接下来,我们创建几个输入选项:

hiHighsLen = input.int(20, title="最高高点周期")
loLowsLen  = input.int(20, title="最低低点周期")
fastMALen  = input.int(25, title="快线MA周期")
slowMALen  = input.int(350, title="慢线MA周期")

timeExitLong  = input.int(80, title="多头时间退出(K线数)")
timeExitShort = input.int(80, title="空头时间退出(K线数)")

usePosSize = input.bool(true, title="是否启用头寸规模管理?")
riskPerc   = input.float(0.5, title="风险百分比%", step=0.25)

我们在这里使用了一系列输入函数来创建各项手动设置。每个函数都会返回该输入项的当前值,我们将这些值存入变量中,以便后续代码随时调用。

前两个输入项用于设置突破通道的周期。我们分别命名为最高高点周期和最低低点周期,两者都是由 input.int() 创建的整数输入项,默认值为20。接着是两个用于移动平均线的整数输入项,分别命名为快线MA周期和慢线MA周期,默认值设为25和350。

然后,我们创建了另外一对整数输入项,名为多头时间退出(K线数)和空头时间退出(K线数),用以指定策略在持有多头和空头仓位多少根K线后,应执行平仓。这两个输入的默认值均为80。

最后两个输入项用于头寸规模管理。第一个是 input.bool() 创建的布尔型(是/否)输入项,它会在策略设置中生成一个复选框,我们将其默认设为勾选(true)。我们将使用这个选项,来方便地在启用海龟式头寸规模算法和使用固定的1手下单之间切换。最后一个输入项风险百分比,是一个由 input.float() 创建的浮点数输入项。通过它,我们可以指定每笔交易愿意承担的权益风险百分比。我们将其默认值设为0.5,代表每笔交易承担0.5%的权益风险。

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

接下来是策略的核心计算部分。我们将计算三样东西:移动平均线与高低点极值、策略的头寸规模,以及一个回测窗口。设置回测窗口,可以确保我们的策略在回测结束时没有任何持仓。

我们如下计算移动平均线和高低点极值:

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

hiHighs = ta.highest(high, hiHighsLen)[1]
loLows  = ta.lowest(low, loLowsLen)[1]

我们使用 ta.ema() 函数来计算快线和慢线指数移动平均线(EMA),它们都基于收盘价(close)进行计算,周期分别由 fastMALenslowMALen 两个输入变量决定(默认值为25和350)。

然后,我们计算价格的突破水平。我们使用 ta.highest() 函数来获取过去 hiHighsLen 周期内的最高价,使用 ta.lowest() 来获取过去 loLowsLen 周期内的最低价。

请注意,我们在 ta.highest()ta.lowest() 函数后面都加上了 [1] 这个历史引用操作符。我们这样做是因为:在计算N周期最高价时,函数默认会包含当前K线的数据。这对我们来说是个问题,因为策略需要寻找价格向上突破20周期最高点的时刻,如果这个最高点本身就包含了当前K线,那么价格就永远无法突破它自己。

因此,我们通过 [1] 将函数的计算范围向右平移一根K线,从而获取到不包含当前K线的前20根K线的最高高点和最低低点。

接下来,我们计算策略的头寸规模:

// 计算头寸规模
riskEquity  = (riskPerc / 100) * strategy.equity
atrCurrency = ta.atr(20) * syminfo.pointvalue
posSize     = usePosSize ? math.floor(riskEquity / atrCurrency) : 1

费思提出的头寸规模算法包含两个核心部分:一部分是策略权益的风险百分比,另一部分是转换成该品种货币价值的平均真实波幅(ATR)。

首先,我们将 riskPerc 输入变量除以100,将其从百分比转换为小数(例如,1%变为0.01)。然后,我们用这个小数乘以 strategy.equity(一个包含初始资金、累计盈亏和浮动盈亏的内置变量)。我们将计算结果存入 riskEquity 变量。

接着,我们将ATR值转换为该品种的货币价值。我们先用 ta.atr(20) 计算20周期的ATR,然后将其乘以 syminfo.pointvaluesyminfo.pointvalue 是一个内置变量,它返回该品种价格每变动一个完整点所对应的货币金额。例如,对于E-mini标普500指数期货,该值为50;对于原油期货,为1,000;而对于股票,则为1。

现在,我们有了算法的两个组成部分,可以确定最终的头寸规模了。但你可能还记得,我们创建了一个输入项来启用或禁用头寸规模管理。因此,我们在创建 posSize 变量时,需要根据这个选项,在使用算法计算和使用固定的1手之间进行切换。

我们使用条件运算符(?:)来实现这个逻辑。我们首先判断 usePosSize 输入变量是否为 true。如果是,我们就用 riskEquity 除以 atrCurrency 来计算头寸规模,并用 math.floor() 函数向下取整。如果 usePosSizefalse,则条件运算符返回1。这样,策略就会始终以1手下单。

我们需要计算的最后一点是策略的回测窗口:

// 设置回测窗口
backtestWindow = time < (timenow - 86400000 * 5)

回测窗口背后的思想是,确保在回测结束时没有任何持仓,从而使策略测试器窗口中的表现报告能包含所有交易的数据。要实现这一点,方法有多种。对于本策略,我们让策略只交易到当前日期和时间之前的5天。

设置这个五天的间隔,是为了应对市场休市的日子。否则,如果我们在周日进行回测,策略将因没有新的价格K线而无法平掉回测中的仓位。

为了实现这个窗口,我们创建了 backtestWindow 变量。我们判断当前K线的时间戳(time)是否小于当前时间戳(timenow)减去5天(即 86400000 * 5,因为时间戳以毫秒计)。

其效果是,对于所有发生在当前时间5天之前的K线,backtestWindowtrue。对于此后的任何K线,该变量为 false,我们便会在这些K线上让策略平仓。

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

现在,让我们将策略的交易规则转化为TradingView代码。首先,我们编写多头的相关逻辑:

// 步骤3. 定义多头交易条件
enterLong = close > hiHighs and 
     fastMA > slowMA and
     backtestWindow and 
     strategy.position_size < 1

exitLong = ta.barssince(enterLong) > timeExitLong and
     strategy.position_size > 0

我们在这里创建了两个布尔变量:enterLongexitLong。要使 enterLongtrue,必须同时满足四个条件:第一,价格突破,即当前K线的收盘价高于20周期的最高高点(hiHighs);第二,趋势向上,即25周期EMA(fastMA)大于350周期EMA(slowMA);第三,信号处于回测窗口内(backtestWindowtrue);第四,当前无持仓或持空仓(strategy.position_size 小于1),这样可以避免在已经持有多仓时,重复收到买入信号。

我们使用 and 运算符将这些条件连接起来,只有当所有条件都满足时,enterLong 才为 true

我们创建的另一个变量 exitLong,其真假值由两个表达式决定。首先,我们检查自入场信号出现以来是否已经过去了超过80根K线。我们通过 ta.barssince(enterLong) 来实现这一点,该函数会返回 enterLong 上一次为 true 至今所经过的K线数量。我们将这个数量与我们设置的 timeExitLong 输入变量(默认为80)进行比较。另一个影响 exitLong 值的条件是,检查策略当前是否持有多仓(strategy.position_size > 0)。增加这个条件,是为了确保我们的策略只在确实有仓位可卖时才发送卖出订单。(如果策略当前无持仓,而我们提交了一个平多订单,结果会是建立一个空头仓位。)

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

接下来是空头交易的入场和出场条件。我们按如下方式编写代码:

// 步骤4. 定义空头交易条件
enterShort = close < loLows and 
     fastMA < slowMA and
     backtestWindow and 
     strategy.position_size > -1

exitShort = ta.barssince(enterShort) > timeExitShort and
     strategy.position_size < 0

我们在这里定义的第一个变量 enterShort,只有在四个条件同时满足时才为 true:当前K线的收盘价低于(<)20周期的唐奇安通道下轨(loLows);25周期的快线EMA位于350周期的慢线EMA下方,确认市场处于下降趋势;信号发生在设定的回测时间窗口内(backtestWindow);并且,策略当前不是已经持有空头仓位(strategy.position_size > -1),以避免重复开仓。

只要有任意一个条件不满足,enterShort 变量的值就会是 false

另一个变量 exitShort 则用于判断平掉空仓的条件。它在以下两个条件同时满足时为 true:自从最近一次做空信号(enterShort)出现以来,已经过去了超过80根K线,我们使用 ta.barssince() 函数来计算经过的K线数,并将其与我们之前设定的输入变量 timeExitShort(默认值为80)进行比较;并且,策略当前正持有空头仓位(strategy.position_size < 0),这可以确保我们只在有仓位时才发出平仓指令。

如果这两个条件不同时满足,exitShort 就为 false

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

在下一步,我们输出策略所需的值,以便在图表上直观地识别交易设置并验证策略的行为。首先,我们将唐奇安通道和快线EMA绘制在图表上:

// 步骤5. 输出策略数据
plot(hiHighs, color=color.green, title="最高高点")
plot(loLows, color=color.red, title="最低低点")
plot(fastMA, color=fastMA > slowMA ? color.green : color.red,
     linewidth=2, title="快速EMA")

我们使用 plot() 函数来输出数据。前两行分别用绿色和红色绘制了20周期的唐奇安通道上下轨。第三行代码绘制了25周期的快线EMA。我们通过条件运算符(?:)为这条线赋予了动态颜色:当快线EMA高于慢线EMA时,它显示为绿色,表示上升趋势;反之则为红色,表示下降趋势。

我们还可以用背景色来高亮显示策略的交易信号:

bgColour = enterLong ? color.new(color.green, 90) :
     enterShort ? color.new(color.red, 90) :
     na
bgcolor(bgColour)

我们首先定义一个 bgColour 变量来储存背景颜色。通过嵌套的条件运算符,我们判断:如果 enterLongtrue,就返回一个90%透明度的绿色;否则,如果 enterShorttrue,就返回一个90%透明度的红色;如果两者都不成立,则返回 na(即无颜色)。最后,我们调用 bgcolor() 函数,将图表背景设置为 bgColour 变量所代表的颜色。

步骤6:提交开仓订单

现在,让我们利用之前定义的交易逻辑来提交开仓订单。代码如下:

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

第一个 if 语句检查 enterLong 是否为 true。如果是(即价格在上升趋势中突破上轨),我们就执行 strategy.entry() 来开立一个ID为”EL”、数量为 posSize 的多头仓位。posSize 是我们之前通过仓位管理算法计算出的,或默认为1。

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

步骤7:提交平仓订单

最后要编写的是策略如何平掉市场仓位。首先,我们根据时间信号来生成市价平仓订单:

// 步骤7. 提交平仓订单
if exitLong
    strategy.close("EL", comment="XL")
if exitShort
    strategy.close("ES", comment="XS")

第一个 if 语句判断 exitLong 是否为 true(即距离多头开仓已过去80根K线)。如果是,我们就调用 strategy.close() 函数,通过指定订单ID”EL”来平掉对应的多头仓位,并将该平仓订单命名为”XL”。

第二个 if 语句同理,当 exitShorttrue 时,平掉ID为”ES”的空头仓位,命名为”XS”。

最后一行代码确保策略在回测时间窗口结束时是空仓状态:

if not backtestWindow
    strategy.close_all()

我们通过 not backtestWindow 这个条件来判断回测窗口是否已经结束。根据我们之前的代码,当脚本运行到距离当前时间不足5天的K线时,backtestWindow 会变为 false,此时 not backtestWindow 就为 true,从而触发 strategy.close_all(),将所有剩余仓位以市价单清空。

请注意,我们在开仓前也检查了 backtestWindow 的值。这样就确保了在策略即将被 strategy.close_all() 清仓时,不会再开立新的仓位。这实际上是在回测期末尾停止了策略的交易活动,为我们分析策略表现报告时提供了一个干净的最终状态。

带时间退出的唐奇安趋势策略的表现

让我们先从积极的方面看起:带时间退出的唐奇安趋势策略在长期的上升和下降趋势中表现良好。这个特性与许多其他趋势跟踪策略相似,所以并不完全出人意料。但令我惊讶的是,考虑到其随机的时间退出机制,该策略的表现相当不错。

下方的图表显示了S&P 500 E-mini期货的一段长期上升趋势。在此期间,该策略表现出色,取得了7次连续的盈利交易(并在2.5年内无一亏损):

当然,这个策略也有其弱点。和大多数趋势跟踪策略一样,当市场进入横盘震荡时,它的表现就会不佳。在那种环境下,策略主要因为两个原因亏损:第一,当趋势未能有效展开时,交易会因触及时间退出而被亏损平仓。第二,当趋势确实启动但持续时间不够长时,策略会过早地放弃浮动盈利(甚至可能转盈为亏)。

在下图中,市场横向移动,策略因此连续产生了数笔亏损交易:

以下是在两个交易品种上的一个迷你回测结果。乍一看,表现似乎还不错。但值得注意的是,交易次数有些偏低——尤其是对于盈利的E-mini S&P 500期货回测。在我看来,为了对结果有更强的信心,我们需要看到更多的交易数据。

顺便一提,下表中的结果是在没有启用仓位管理的情况下得出的。这样可以确保策略执行了每一个交易信号,没有因为仓位管理而跳过任何交易。

绩效指标 ES (E-mini S&P 500) CL (原油)
首次交易 1999-03-07 1984-09-05
最后交易 2018-09-24 2018-09-24
时间周期 日线 日线
净利润 $48,921 -$3,330
总盈利 $156,258 $327,522
总亏损 -$107,336 -$330,852
最大回撤 $37,876 $107,750
盈利因子 1.456 0.99
总交易数 55 98
胜率 56.36% 44.9%
平均每笔交易 $889 -$33.98
平均盈利 $5,040 $7,443
平均亏损 -$4,472 -$6,126
盈亏比 1.127 1.215
支付佣金 $416 $660
滑点 2 跳 2 跳

改进思路与新策略方向

尽管我们见过许多比带时间退出的唐奇安趋势策略表现差得多的TradingView策略,但它仍有很大的改进空间。以下是一些你可能会觉得有价值的探索方向。

优化退出机制:虽然时间止损本身不失为一个好主意,但简单地在80根K线后平掉所有仓位,似乎限制了策略的潜力。一个更优化的思路是:对亏损单快速止损,而让盈利单持有更长时间。或者,可以尝试一种更智能的计时方式,例如只计算那些对我们持仓不利的K线。

引入移动止损:从该策略所交易的趋势来看,使用移动止损(trailing stop-loss)代替武断的时间止损,其表现很可能会更好。至少,移动止损可以减少因时间到了就平仓、下一根K线又马上开仓这种行为所产生的额外交易成本。

参数优化:原作者费思并未说明他选择这些参数的原因。这些参数可能是深度优化的结果,也可能只是为了方便而随意选择的。无论如何,探索该策略在不同参数设置下的表现,似乎都是一个值得尝试的方向。

增加市场过滤器:和大多数趋势跟踪策略一样,当市场进入横盘震荡时,该策略的表现会受到影响。如果我们能增加一个过滤器,以识别并避开市场处于区间震荡时的低概率交易,策略的整体表现将得到显著提升。

整合其他风险管理:另一种可能性是,限制那些对提升表现无益的策略风险。例如,为了防止大的亏损,我们可以限制策略的最大回撤,或者在连续亏损n天后停止交易。

完整代码:带时间退出的唐奇安趋势策略

带时间退出的唐奇安趋势策略的完整代码如下所示。关于代码的详细解释和更多信息,请参阅前文的讨论。

//@version=5
// 步骤1. 定义策略设置
strategy(title="带时间退出的唐奇安趋势策略", overlay=true,
     pyramiding=0, initial_capital=100000,
     commission_type=strategy.commission.cash_per_order,
     commission_value=4, slippage=2)

hiHighsLen = input.int(20, title="最高高点周期")
loLowsLen  = input.int(20, title="最低低点周期")
fastMALen  = input.int(25, title="快线MA周期")
slowMALen  = input.int(350, title="慢线MA周期")

timeExitLong  = input.int(80, title="多头持仓时间 (K线数)")
timeExitShort = input.int(80, title="空头持仓时间 (K线数)")

usePosSize = input.bool(true, title="启用仓位管理?")
riskPerc   = input.float(0.5, title="风险百分比 %", step=0.25)

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

hiHighs = ta.highest(high, hiHighsLen)[1]
loLows  = ta.lowest(low, loLowsLen)[1]

// 计算仓位规模
riskEquity  = (riskPerc / 100) * strategy.equity
atrCurrency = ta.atr(20) * syminfo.pointvalue
posSize     = usePosSize ? math.floor(riskEquity / atrCurrency) : 1

// 设置回测时间窗口
backtestWindow = time < (timenow - 86400000 * 5)

// 步骤3. 定义多头交易条件
enterLong = close > hiHighs and 
     fastMA > slowMA and
     backtestWindow and 
     strategy.position_size < 1

exitLong = ta.barssince(enterLong) > timeExitLong and
     strategy.position_size > 0

// 步骤4. 定义空头交易条件
enterShort = close < loLows and 
     fastMA < slowMA and
     backtestWindow and 
     strategy.position_size > -1

exitShort = ta.barssince(enterShort) > timeExitShort and
     strategy.position_size < 0

// 步骤5. 输出策略数据并可视化
plot(hiHighs, color=color.green, title="最高高点")
plot(loLows, color=color.red, title="最低低点")
plot(fastMA, color=fastMA > slowMA ? color.green : color.red,
     linewidth=2, title="快线EMA")

bgColour = enterLong ? color.new(color.green, 90) :
     enterShort ? color.new(color.red, 90) :
     na
bgcolor(bgColour)

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

// 步骤7. 提交平仓订单
if exitLong
    strategy.close("EL", comment="XL")
if exitShort
    strategy.close("ES", comment="XS")

if not backtestWindow
    strategy.close_all()
赞(0)
未经允许不得转载:图道交易 » Pine Script(256):唐奇安趋势策略与时间退出改进
分享到

评论 抢沙发

登录

找回密码

注册