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

Pine Script(259):趋势形态入场与金字塔加仓策略

#Pine Script入门教学

为TradingView编写的趋势与形态入场交易策略

移动平均线是趋势跟踪策略中用于产生入场和出场信号的常用工具。但如果我们将趋势指标与逆势入场信号相结合,会发生什么呢?本文将探讨这样一个策略的行为和表现。

带形态入场的趋势策略:一种顺大势、逆小势的系统

在其著作《趋势跟踪》(Trend Following)中,迈克尔·柯威尔(Michael Covel)分享了他对一些全球顶尖交易者的深刻见解。这些交易者都是趋势跟踪者,这是一种目标简单明确的交易风格:为了盈利而捕捉到一轮上涨或下跌趋势的主体部分。然而,趋势跟踪者实现这一目标的方式却各有千秋。

与其他试图预测趋势何时何地开始的交易风格不同,趋势跟踪者只简单地观察价格。他们会预先定义何种价格行为构成了趋势,然后在他们看到新趋势出现时便建立仓位。这确实使得他们总是会错过趋势的开端,也总是在趋势结束后才离场。但这没关系:大趋势的主升浪或主跌浪中,蕴含着大量的利润。

大多数趋势跟踪策略有四个共同的特点:它们通过监控价格来识别大趋势;让盈利的头寸持续奔跑,直到趋势发生改变;通过止损来截断亏损;并通过头寸规模管理来限制每个头寸的风险。

柯威尔书中介绍的一个趋势跟踪策略,是由迪翁·库尔切克(Dion Kurczek)和沃尔克·纳普(Volker Knapp)最初开发的带形态入场的趋势策略(Trend with Pattern Entry strategy)。该策略的有趣之处在于,它将顺势与逆势入场相结合。具体来说,策略使用一条简单移动平均线(SMA)来判断市场是看涨还是看跌,然后在主趋势的背景下,等待一个短期的回调或反弹时机入场。接下来,让我们仔细看看该策略的交易规则。

带形态入场的趋势策略的交易规则

该策略包含以下交易规则。做多入场:在上升趋势中(价格高于100周期SMA),等待一个短期回调(收盘价连续三日下跌)时,于次日以市价单做多。做多离场:为多头仓位设置一个追踪止损,其价位为收盘价减去4倍的10周期ATR。做空入场:在下降趋势中(价格低于100周期SMA),等待一个短期反弹(收盘价连续三日上涨)时,于次日以市价单做空。做空离场:为空头仓位设置一个追踪止损,其价位为收盘价加上4倍的10周期ATR。

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

我们首先根据2%的风险来计算头寸大小,但如果计算出的头寸所需的保证金超过了权益的10%,则会相应地缩减头寸规模。追踪止损的机制也使得随着价格朝有利方向移动,头寸的风险会随之降低。

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

为TradingView编写带形态入场的趋势交易策略

现在,让我们看看如何将上述交易规则转化为一个完整的TradingView策略。一个简单的方法是使用模板,它能为我们的脚本提供结构,并将一项大任务分解成更小的部分,也能避免我们面对空白脚本时无从下手。

这是我们将用于带形态入场的趋势策略的策略模板:

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

如果你想跟随本文一起操作,请在TradingView的Pine编辑器中创建一个新的策略脚本,并将上述模板粘贴进去。(如果你只想看完整的策略代码,请跳转至本文末尾。)

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

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

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

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

// 步骤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 禁止加仓。initial_capital 则将策略的初始资金设为100,000。在交易成本方面,我们设定每笔单边交易(strategy.commission.cash_per_order)收取4个货币单位的固定佣金(commission_value=4),并假设市价单和止损单会产生2个最小跳动点的滑点(slippage=2)。这里的成本设置相对悲观,但高估交易成本总比低估要安全。

然后,我们添加一些输入选项以便灵活配置策略。第一个用于设置移动平均线的周期:

smaLen = input.int(100, title="SMA周期")

我们使用 input.int() 函数创建了一个名为SMA周期的整数输入项,默认值为100,并将其当前值保存在 smaLen 变量中。

接着,我们为止损设置创建两个输入项:

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

ATR周期是一个整数输入,默认值为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="保证金预估 %")

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

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

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

// 步骤2. 计算策略所需值
smaValue  = ta.sma(close, smaLen)
stopValue = ta.atr(atrLen) * stopOffset

我们使用 ta.sma() 计算出100周期的简单移动平均线(SMA)。对于ATR止损距离,我们先用 ta.atr() 计算出10周期的ATR值,然后乘以 stopOffset(止损偏移倍数,默认为4)。

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

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

设置交易窗口的目的是为了让策略在回测结束时自动平仓。我们通过 time <= timenow - (86400000 * 3) 来实现。这个条件确保了策略只在距离当前时间三天之前的历史数据上运行。

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

// 计算头寸规模
riskEquity = (maxRisk * 0.01) * strategy.equity
riskTrade  = stopValue * 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. 定义多头交易条件
lowerCloses = close < close[1] and 
     close[1] < close[2] and
     close[2] < close[3]

enterLong = close > smaValue and 
     lowerCloses and
     tradeWindow

我们先定义一个 lowerCloses 变量来判断是否存在连续三根K线收盘价依次降低的形态。然后,我们将这个形态(lowerCloses)与当前收盘价高于SMA均线、处于交易窗口内这两个条件通过 and 连接起来,共同构成最终的多头入场信号 enterLong

这一步的另一个任务是确定多头的移动止损位。我们使用以下代码实现:

// 确定多头止损
longStop = 0.0

longStop := if enterLong and strategy.position_size < 1
    close - stopValue
else
    math.max(close - stopValue, longStop[1])

我们定义了一个 longStop 变量来存储止损价格。通过 if/else 语句,我们实现其移动(trailing)逻辑:当产生新的多头信号(enterLongtrue)且当前没有多头仓位时,我们将止损位初始化为当前收盘价减去止损距离(stopValue);在其他所有时间,我们将止损位更新为”当前收盘价减去止损距离”和”上一根K线的止损位”之间的较大值。这确保了止损位只会随着价格上涨而上移,而不会下调。

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

在第四步中,我们将根据之前讨论的交易规则,编写策略的空头交易逻辑。首先,我们定义做空的入场信号:

// 步骤4. 定义空头交易条件
higherCloses = close > close[1] and 
     close[1] > close[2] and
     close[2] > close[3]

enterShort = close < smaValue and 
     higherCloses and
     tradeWindow

布尔变量 higherCloses 的值由三个表达式共同决定,它检查价格是否连续三根K线收盘走高。只有当这三个条件都满足时,higherCloses 才为 true

enterShort 变量也由三个条件组合而成:首先,收盘价必须低于100周期SMA(表明处于长期下降趋势中);其次,必须出现连续三日收盘价上涨的短期反弹形态(higherCloses);最后,信号必须发生在我们的回测窗口之内(tradeWindow)。只有当这三个条件同时为 true 时,enterShort 才为 true

接下来,我们确定空头的追踪止损价:

// 确定空头止损价
shortStop = 0.0

shortStop := if enterShort and strategy.position_size > -1
    close + stopValue
else
    math.min(close + stopValue, shortStop[1])

我们首先声明了一个浮点型的持久变量 shortStop。然后,通过一个 if/else 语句来动态更新它的值。if 部分的条件是:当一个做空入场信号(enterShort)出现,并且策略当前处于平仓或持多仓状态时(即准备建立一个新的空头仓位时),我们将止损价初始化为当前收盘价加上止损价差。else 部分则在所有其他情况下(主要是持空仓期间)执行。在这里,我们使用 math.min() 函数来实现追踪止损:它会取”当前收盘价加上止损价差”和”前一根K线的止损价”两者中的较小值。这意味着,当价格下跌(对空头有利)时,止损价会跟随下移;但当价格反弹时,止损价会保持在之前记录的低位,绝不向上移动,从而锁定利润。

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

为了验证策略的行为,我们在图表上输出其数据。这一步,我们将在图表上显示移动平均线和止损订单。首先,我们绘制SMA:

// 步骤5. 在图表上输出数据
plot(smaValue, color=#4169e1, linewidth=2, title="SMA")

我们使用 plot() 函数来显示100周期的SMA(存放在 smaValue 变量中)。我们将其颜色设为宝蓝色(#4169e1),并加粗了线条以便观察。

我们也在图表上显示策略的止损价格:

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

我们根据策略的持仓状态来决定是否绘制止损线。这样,我们只在持多仓时显示多头止损线,在持空仓时显示空头止损线。我们通过条件运算符(?:)来实现这一逻辑。

第一个 plot() 语句判断 strategy.position_size 是否大于0。如果是,就绘制 longStop 的值;否则,用 na 来隐藏绘图。这条代表多头止损的断点线被设为石灰色。第二个 plot() 语句则以同样的方式处理空头止损,并将其设为红色。

步骤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() 函数,建立一个名为”EL”的多头仓位,其数量由 posSize 变量决定(这个变量要么是根据风险计算出的值,要么在禁用头寸规模管理时为1)。第二个 if 语句则以同样的方式处理空头入场。

步骤7:通过出场订单平掉市场头寸

策略代码的最后一部分是平仓逻辑。在这里,我们将设置策略的止损,并在回测结束时让策略退出市场。首先,我们提交止损:

// 步骤7. 提交出场订单
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() 函数,为名为”EL”的入场单提交一个追踪止损(stop=longStop)。第二个 if 语句则以同样的方式处理空头止损。

然后,我们取消之前可能存在的止损订单:

// 取消未成交的止损订单
if enterLong
    strategy.cancel("XS")

if enterShort
    strategy.cancel("XL")

当一个新的多头入场信号(enterLong)出现时,我们可能还存在一个之前空头仓位的、尚未触发的止损订单(名为”XS”)。为了防止这个旧订单干扰新仓位,我们使用 strategy.cancel() 函数将其取消。反之亦然。

脚本的最后一行,在回测结束时平掉所有仓位:

if not tradeWindow
    strategy.close_all()

这个 if 语句评估 not tradeWindow 表达式。之前,我们将 tradeWindow 设为 true(如果当前K线发生在回测截止日期之前)。我们希望在截止日期之后执行平仓,因此我们使用 not 运算符来反转逻辑。当 tradeWindow 变为 false 时,not tradeWindow 就为 true,从而触发 strategy.close_all() 函数,平掉所有当前持仓。

由于我们在开新仓前也检查了 tradeWindow,因此在截止日期之后,策略将完全停止交易。

TradingView的带形态入场的趋势策略的表现

让我们先从这个策略表现好的方面开始讨论。在长期的趋势中,该策略表现格外出色——这是它与其他趋势跟踪策略共享的一个特点。

例如,下方的图表展示了E-迷你标普500指数期货的一段强劲上升趋势。在此期间,带形态入场的趋势策略连续捕获了三笔盈利交易。由于趋势持续了很长时间,该策略在2017年没有任何亏损交易:

当然,该策略也有其缺点。其中之一便是在横盘市场中的糟糕表现。在这些市场条件下,策略表现不佳,因为趋势无法有效启动,价格走向错误的方向,并且趋势的持续时间不足以让交易盈利出场。

下方图表便是这种糟糕表现的一个例子。在这里,E-迷你标普500指数期货横盘整理了数月。由于那段时间只有小级别的趋势,该策略无法获利,并连续遭遇了三次亏损:

下表展示了在两个不同品种上的回测结果。结果是正向的,这算是一个惊喜。但这里有两点需要特别注意。

首先,此回测是在未启用仓位管理的情况下进行的,目的是为了确保策略执行每一个交易信号(从而在报告中获得尽可能多的交易样本)。但不幸的是,第二点是,即便如此,交易总数依然过少。因此,在我们能自信地断定该策略是否有效之前,还需要进行更多的测试。

绩效指标 E-mini S&P 500 期货 (ES) 原油期货 (CL)
首次交易 1998-03-29 1983-08-21
最后交易 2018-10-09 2018-10-22
时间周期 日线 日线
净利润 $23,217 $49,658
总盈利 $147,584 $316,752
总亏损 -$124,366 -$267,094
最大回撤 $33,717 $68,746
盈利因子 1.187 1.186
总交易数 82 166
胜率 34.15% 39.76%
平均每笔交易 $283 $299
平均盈利 $5,270 $4,799
平均亏损 -$2,303 -$2,670
盈亏比 2.289 1.797
支付佣金 $620 $1,272
滑点 2 跳 2 跳

改进思路与新策略方向

尽管回测结果是盈利的,但我们仍可以尝试一些方法来提升其表现并更深入地理解这个策略。以下是一些你可能会觉得有用的探索方向。

增加市场过滤器:和大多数趋势跟踪策略一样,带形态入场的趋势策略在市场横盘震荡时表现不佳。如果我们能增加一个过滤器(例如ATR或ADX指标)来识别并避开这种市场环境,策略的表现有望得到改善。

参数优化:原作者柯威尔并未解释他选择这些参数的原因。它们可能是深入优化的结果,也可能只是为了方便而随意选择的。因此,探索不同的参数组合,看看哪些最适合我们选择的交易品种和当前的市场状况,是一个很好的思路。

反思空头逻辑:虽然柯威尔的策略在多头方向上是盈利的,但在空头方向上却是亏损的。这提示我们,做空逻辑可能存在缺陷。连续三根K线收盘价抬高之后做空,是否是一个有效的逆势入场模式,值得商榷。或许100周期的SMA对于识别通常更为迅猛的下跌趋势来说反应过慢。因此,一个改进思路是测试调整后的做空规则,甚至完全放弃做空,只在多头方向上交易。

整合风险管理:为了改进策略,我们也可以从它所承担的风险入手。例如,我们可以限制策略的最大回撤或其连续亏损天数,以防止出现巨额亏损。

关于使用移动平均线的类似TradingView策略,可以参考双均线策略和三均线策略。关于基于柯威尔书籍的另外两个趋势跟踪策略,可以参考SMA交叉策略和SMA交叉金字塔策略。

完整代码: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)

smaLen = input.int(100, title="SMA周期")

// 止损输入项
atrLen     = input.int(10, title="ATR周期")
stopOffset = input.float(4, title="止损偏移倍数", step=0.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="保证金预估 %")

// 步骤2. 计算策略所需值
smaValue  = ta.sma(close, smaLen)
stopValue = ta.atr(atrLen) * stopOffset

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

// 计算仓位规模
riskEquity = (maxRisk * 0.01) * strategy.equity
riskTrade  = stopValue * 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. 定义多头交易条件
lowerCloses = close < close[1] and 
     close[1] < close[2] and
     close[2] < close[3]

enterLong = close > smaValue and 
     lowerCloses and
     tradeWindow

// 确定多头止损
longStop = 0.0

longStop := if enterLong and strategy.position_size < 1
    close - stopValue
else
    math.max(close - stopValue, longStop[1])

// 步骤4. 定义空头交易条件
higherCloses = close > close[1] and 
     close[1] > close[2] and
     close[2] > close[3]

enterShort = close < smaValue and 
     higherCloses and
     tradeWindow

// 确定空头止损
shortStop = 0.0

shortStop := if enterShort and strategy.position_size > -1
    close + stopValue
else
    math.min(close + stopValue, shortStop[1])

// 步骤5. 输出策略数据并可视化
plot(smaValue, color=#4169e1, linewidth=2, title="SMA")
plot(strategy.position_size > 0 ? longStop : na, color=color.lime,
     style=plot.style_linebr, title="多头止损", linewidth=2)
plot(strategy.position_size < 0 ? shortStop : na, color=color.red,
     style=plot.style_linebr, title="空头止损", linewidth=2)

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

// 步骤7. 提交平仓订单(止损)
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 enterLong
    strategy.cancel("XS")
if enterShort
    strategy.cancel("XL")

if not tradeWindow
    strategy.close_all()

在TradingView Pine中编写SMA交叉金字塔交易策略

我们经常使用三个基于价格的指标来定义趋势:唐奇安通道、布林带和移动平均线。一旦我们确定了趋势,我们便需要乘胜追击,以充分获取利润。本文将探讨如何利用一个移动平均线策略,在盈利的头寸上进行加仓。

SMA交叉金字塔策略:一种顺势加码的趋势跟踪系统

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

与其他试图预测趋势何时何地开始的交易风格不同,趋势跟踪者只简单地观察价格。他们知道何种价格行为定义了趋势,并只在他们看到新趋势出现之后才建立仓位。因此,他们确实总是会错过趋势的开端,也总是在趋势结束后才离场。但这不一定是问题:当趋势足够大时,抓住中间部分仍然能带来可观的利润。

大多数趋势跟踪策略有以下共同特点:它们通过监控价格来识别大趋势;让盈利的头寸持续奔跑,直到趋势结束;在预设的止损位果断了结亏损;并通过头寸规模管理来限制每个头寸的风险。

柯威尔在他的书中分享的趋势跟踪策略之一,便是SMA交叉金字塔策略(SMA Crossover Pyramiding strategy)。该策略建立在SMA交叉策略之上,但有一个关键区别:在第一笔入场单取得一定利润后,该策略会将头寸规模加倍。其背后的理念是,一旦一个趋势被证实存在,它将有更高的概率持续下去。因此,我们应该利用这些情况,扩大我们的持仓。

接下来,让我们看看该策略的交易规则,以及如何为TradingView编写它。

SMA交叉金字塔策略的交易规则

该策略包含以下交易规则。做多入场分两步:初始入场是当50周期简单移动平均线(SMA)上穿100周期SMA后的次日开盘时,以市价单做多(并反转任何已有的空头仓位);加仓则是在价格从初始入场价上涨1%后,于次日以市价单加仓一笔等量的多头头寸。做多离场时,根据止损价位(初始入场价格减去4倍的10周期ATR)平掉整个多头仓位。

做空入场同样分两步:初始入场是当50周期SMA下穿100周期SMA后的次日开盘时,以市价单做空(并平掉任何已有的多头仓位);加仓则是在价格从初始入场价下跌1%后,于次日以市价单加仓一笔等量的空头头寸。做空离场时,根据止损价位(初始入场价格加上4倍的10周期ATR)平掉整个空头仓位。

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

我们首先根据2%的风险来计算头寸大小,但如果计算出的头寸所需的保证金超过了权益的10%,则会相应地缩减头寸规模。当市场发生跳空,价格越过我们的止损价时,这个最大敞口限制能防止我们遭受过度的亏损。

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

在TradingView中编写SMA交叉金字塔策略

现在,让我们将上述交易规则转化为一个完整的TradingView策略脚本。一个实用的方法是使用模板,它能为我们提供一个框架,将编程任务分解成更小、更易于管理的部分。

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

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

如果你想跟随下方的代码讨论,请在TradingView的Pine编辑器中创建一个新的策略脚本,并将上述模板粘贴进去。(如果你只想看完整的策略代码,请跳转至本文末尾。)

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

现在,让我们开始为TradingView Pine编写这个SMA交叉金字塔策略的代码。

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

在第一步,我们定义策略的整体属性及其可由用户配置的输入参数。和所有TradingView策略一样,我们首先使用 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)。对于一个名为SMA交叉金字塔的策略来说,这听起来可能有些矛盾。但原因在于:我们将创建自定义的输入选项,以便能分别为多头和空头交易配置不同的加仓规则。因此,我们将实现自己的加仓逻辑,而不是使用 strategy() 函数中统一的 pyramiding 选项。

我们设定初始资金为100,000(initial_capital=100000)。在交易成本方面,我们设定每笔单边交易收取4个货币单位的固定佣金。这个成本设置相对悲观,但高估交易成本总比低估要安全。对于市价单和止损单,我们还设置了2个最小跳动点的滑点(slippage=2)。

接下来,我们创建一系列输入选项,以便能方便地调整策略参数。首先是移动平均线的设置:

// 移动平均线输入项
fastLen = input.int(50, title="快线SMA周期")
slowLen = input.int(100, title="慢线SMA周期")

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

然后,我们为策略的止损功能创建输入项:

// 止损输入项
atrLen       = input.int(10, title="ATR周期")
longStpMult  = input.float(4, title="多头止损倍数")
shortStpMult = input.float(4, title="空头止损倍数")

第一个是名为ATR周期的整数输入,用于设定计算平均真实波幅(ATR)的周期,默认值为10。

接着,我们用 input.float() 创建了两个浮点数输入。多头止损倍数指定了计算多头止损时所用的ATR倍数,默认值为4,意味着多头止损位将设在当前价格下方4倍ATR处。空头止损倍数同理,用于计算空头止损。

接下来,我们创建用于配置策略仓位管理的输入项:

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

第一个输入项是一个布尔型的复选框,可以方便地一键开启或关闭我们自定义的仓位管理算法。中间两个浮点数输入分别用于设定单笔交易的最大风险和总仓位的最大风险敞口,默认值分别为2%和10%。最后一个是整数输入,由于Pine Script无法直接获取经纪商提供的确切保证金率,我们在此创建一个输入项,以便用户根据实际情况进行估算,默认值为10%。

请注意,我们直接将所有基于百分比的输入项乘以0.01,将其转换为小数形式,以便于后续的计算。

下一组输入选项用于控制策略的金字塔式加仓行为:

// 金字塔式加仓设置
longThres       = input.float(1, title="多头加仓盈利阈值 %")
shortThres      = input.float(1, title="空头加仓盈利阈值 %")
maxLongEntries  = input.int(2, title="最大多头开仓次数")
maxShortEntries = input.int(2, title="最大空头开仓次数")

前两个浮点数输入用于设定加仓的触发条件,即持仓需要达到多少百分比的浮动盈利后才进行加仓,默认均为1%。后两个整数输入则用于限制在同一方向上的最大开仓(包括首次开仓和加仓)次数,默认均为2次,这与策略的原始规则相符。

最后的输入项用于配置策略的回测时间窗口:

// 回测时间范围设置
endMonth = input.int(11, title="回测结束月份", minval=1, maxval=12)
endYear  = input.int(2018, title="回测结束年份", minval=1950, maxval=2030)

设置回测结束时间的目的,是为了让策略在回测结束时自动平仓,从而确保所有绩效指标都是基于已平仓的交易计算得出。我们将结束月份和年份的默认值设为11和2018,即策略将在2018年11月停止交易。

顺便说一句,我们创建的所有这些输入选项,在TradingView平台中的设置界面看起来是这样的:

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

在这第二步中,我们将计算策略运行所需的核心数据。我们需要计算出:SMA和ATR指标、策略的回测时间窗口、头寸规模、可能的交易利润,以及多头和空头的入场次数。

首先,我们计算移动平均线和平均真实波幅(ATR):

// 步骤2. 计算策略所需指标值
// 计算移动平均线和ATR
fastMA   = ta.sma(close, fastLen)
slowMA   = ta.sma(close, slowLen)
atrValue = ta.atr(atrLen)

我们使用 ta.sma() 函数来计算50周期和100周期的简单移动平均线(SMA),其周期长度由我们之前创建的 fastLenslowLen 输入变量决定。同样,我们使用 ta.atr() 函数计算ATR,其周期由 atrLen 输入决定。我们将这些计算结果分别存入 fastMAslowMAatrValue 变量中,以备后用。

接下来,我们设定策略的回测时间窗口,以确保脚本只在特定时间段内交易。代码如下:

// 确定回测窗口
tradeWindow = time <= timestamp(endYear, endMonth, 1, 0, 0)

布尔变量 tradeWindow 的值由一个逻辑表达式决定。该表达式判断当前K线的时间戳(time)是否早于或等于一个由 timestamp() 函数构建的特定时间点。根据我们之前设定的输入项,这个时间点默认为2018年11月1日00:00。这意味着,在该日期之前的K线上,tradeWindowtrue;在该日期及之后,则为 false

然后,我们确定策略的头寸规模。这是本策略中最为复杂的部分,因为它包含了一个双重风控机制:

// 确定头寸规模
riskEquity = maxRisk * strategy.equity
riskLong   = (longStpMult * atrValue) * syminfo.pointvalue
riskShort  = (shortStpMult * atrValue) * syminfo.pointvalue

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

posSizeLong  = usePosSize ? math.min(math.floor(riskEquity / riskLong), maxPos) : 1
posSizeShort = usePosSize ? math.min(math.floor(riskEquity / riskShort), maxPos) : 1

SMA交叉金字塔策略的头寸规模算法,本质上是平衡两个核心要素:每笔交易愿意承担的风险金额,以及单个头寸允许的最大风险敞口。

首先是风险金额的计算。riskEquity 是将最大头寸风险百分比输入值 maxRisk 乘以 strategy.equity(策略总权益),得到本次交易愿意承担的绝对风险金额。riskLongriskShort 则是每手合约的初始风险额,其方法是用止损距离(ATR值乘以偏移倍数)乘以该品种的点值(syminfo.pointvalue)。

其次是最大敞口限制 maxPos 的计算。我们首先计算出用于保证金的权益上限(maxExposure * strategy.equity),然后估算一手合约所需的保证金(marginPerc * close * syminfo.pointvalue),两者相除,便得到了基于最大敞口原则所允许的最大持仓手数。

最后是最终头寸规模 posSizeLongposSizeShort 的确定。如果用户启用了头寸规模管理(usePosSize),我们先根据风险金额计算出一个初步的头寸规模(math.floor(riskEquity / riskLong)),再使用 math.min() 函数,取基于风险计算出的规模和基于最大敞口计算出的规模(maxPos)之间的较小值。这确保了我们的头寸规模同时满足两个风控条件。如果禁用头寸规模管理,则头寸大小简单地设为1。

我们还需要计算当前持仓的浮动盈亏百分比:

// 计算交易利润(无持仓时为NaN)
tradeProfit = ((close - strategy.position_avg_price) /
     strategy.position_avg_price) * 100

tradeProfit 变量的值是当前价格与策略平均入场价(strategy.position_avg_price)之间的百分比变化。这个值告诉我们持仓自开仓以来的价格涨跌幅。当没有持仓时,该值为 na

最后,我们需要计算多头和空头方向上的入场次数(用于金字塔加仓的判断):

// 计算仓位的入场次数
longEntries = 0
longEntries :=
     (strategy.position_size < 1) ? 0 :
     (strategy.position_size > strategy.position_size[1]) ? longEntries[1] + 1 :
     longEntries[1]

shortEntries = 0
shortEntries :=
     (strategy.position_size > -1) ? 0 :
     (strategy.position_size < strategy.position_size[1]) ? shortEntries[1] + 1 :
     shortEntries[1]

我们使用嵌套的条件运算符来动态追踪已加仓的次数。对于 longEntries:如果策略当前不持有任何多头仓位(strategy.position_size < 1),则计数器重置为0。如果策略持有多仓,我们便比较当前的多头仓位大小与前一根K线的大小。如果变大了,说明发生了一次加仓,计数器便加1;否则,计数器保持不变。shortEntries 的逻辑与此类似,只是判断方向相反。

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

在第三步中,我们编写策略的多头交易规则。首先,我们定义初始多头入场的信号:

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

这个 enterLong 变量由三个通过 and 连接的条件共同决定,只有当所有条件都为 true 时,它才为 true:一是50周期SMA上穿100周期均线;二是策略当前没有持有多头仓位(strategy.position_size < 1);三是信号发生在我们的回测窗口之内(tradeWindow)。

接下来,我们编写策略何时应该进行多头加仓:

extraLong = strategy.position_size > 0 and
     longEntries < maxLongEntries and
     tradeProfit / longEntries > longThres and
     tradeWindow

SMA交叉金字塔策略在一个头寸达到一定利润时会进行加仓。但我们也限制了加仓的次数,并执行了一些额外的检查。首先,策略必须已经持有多仓(strategy.position_size > 0);其次,当前的加仓次数(longEntries)必须小于设定的上限(maxLongEntries);接着,我们判断平均每笔入场单的利润(tradeProfit / longEntries)是否大于我们设定的加仓利润阈值(longThres,默认为1%);最后,加仓信号也必须发生在回测窗口之内。

多头逻辑的最后一部分是计算止损价格:

// 当有初始多头或加仓信号时,计算并更新止损价
longStop = 0.0
longStop := enterLong or extraLong ?
     close - (atrValue * longStpMult) :
     longStop[1]

这是一个粘性止损的实现方式。我们声明了一个持久变量 longStop。只有在初始入场信号(enterLong)或加仓信号(extraLong)出现的那一根K线上,longStop 才会被重新计算并赋值(当前收盘价减去ATR乘以止损倍数)。在后续的所有K线上,只要没有新的入场或加仓信号,它就会保持前一根K线的值不变,从而实现了在每次建仓或加仓时更新,并在持仓期间锁定止损价的效果。

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

接下来,我们将策略的空头交易逻辑转化为TradingView代码。首先,我们构建初始的空头入场信号:

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

enterShort 变量只有在以下三个条件同时满足时才为 true:一是发生了均线死叉,即50周期SMA下穿了100周期SMA;二是当前不是已经持有空头仓位,strategy.position_size > -1 这个条件可以确保我们只在空仓或持有多仓时才开立新的空仓,避免重复做空;三是处于回测时间窗口内(tradeWindow),确保交易发生在2018年11月1日之前。

然后,我们编写策略进行空头加仓的条件:

extraShort = strategy.position_size < 0 and
     shortEntries < maxShortEntries and
     tradeProfit / shortEntries < shortThres * -1 and
     tradeWindow

extraShort 变量只有在以下四个条件同时满足时才为 true:一是策略当前已持有空仓(strategy.position_size < 0);二是当前的空头开仓次数(shortEntries)小于设定的最大次数上限(maxShortEntries);三是持仓已足够盈利——我们将总的浮动盈利百分比(tradeProfit)除以开仓次数(shortEntries),得到平均每笔入场的利润,当这个平均利润低于我们设定的空头加仓阈值 shortThres 的负值时(例如-1%),条件成立,乘以-1是因为空头交易的盈利表现为负的价格变动;四是处于回测时间窗口内(tradeWindow)。

最后,我们计算并更新空头的止损价格:

// 当有初始空头或加仓信号时,计算并更新止损价格
shortStop = 0.0
shortStop := enterShort or extraShort ?
     close + (atrValue * shortStpMult) :
     shortStop[1]

我们首先声明 shortStop 变量。然后通过条件运算符(?:)来更新它的值:只有当有新的空头开仓信号(enterShortextraShorttrue)时,我们才根据当前价格和ATR重新计算止损位;在其他所有时间,shortStop 的值都保持为上一根K线的值,从而实现固定止损。

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

为了跟踪策略的行为,我们在这一步将它的关键数据绘制在图表上。首先,我们绘制两条移动平均线:

// 步骤5. 输出策略数据
// 在图表上显示移动平均线
plot(fastMA, color=color.orange, title="快线SMA")
plot(slowMA, color=color.teal, title="慢线SMA", linewidth=2)

我们用 plot() 函数分别绘制了橙色的50周期SMA(快线)和更粗的青色100周期SMA(慢线)。

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

// 绘制止损水平
plot(strategy.position_size > 0 ? longStop : na, color=color.green, 
     style=plot.style_cross, linewidth=2, title="多头止损位")
plot(strategy.position_size < 0 ? shortStop : na, color=color.red, 
     style=plot.style_cross, linewidth=2, title="空头止损位")

为了保持图表整洁,我们只在策略实际持有多仓时才显示多头止损,反之亦然。我们通过条件运算符(?:)实现这一功能,将止损位以醒目的绿色和红色十字星(plot.style_cross)形式标记在图表上。

步骤6:提交开仓订单

在第六步,我们提交入场订单,包括初始开仓和金字塔式加仓。这是开立初始仓位的代码:

// 步骤6. 提交入场订单
// 发送策略的初始开仓订单
if enterLong
    strategy.entry("EL", strategy.long, qty=posSizeLong)
if enterShort
    strategy.entry("ES", strategy.short, qty=posSizeShort)

enterLongtrue 时,我们调用 strategy.entry() 开立一个ID为”EL”的多头仓位,数量为我们计算好的 posSizeLong。当 enterShorttrue 时,则开立ID为”ES”的空头仓位。

接下来,我们提交加仓订单:

// 发送加仓订单
if extraLong
    strategy.order("EL Extra", strategy.long, qty=posSizeLong)
if extraShort
    strategy.order("ES Extra", strategy.short, qty=posSizeShort)

extraLong 条件满足时(持有多仓、未达加仓次数上限、利润达标等),我们调用 strategy.order() 函数提交一个ID为”EL Extra”的多头加仓订单。空头加仓同理。

步骤7:提交平仓订单

最后一步,我们处理仓位的退出。首先,我们提交止损订单:

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

我们通过两个 if 语句来确保只在持有多仓时提交多头止损,只在持空仓时提交空头止损。我们使用 strategy.exit() 函数,它会为整个仓位提交一个止损订单。

最后要编写的代码是在策略的交易窗口结束时清空所有持仓:

if not tradeWindow
    strategy.close_all()

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

SMA交叉金字塔策略的表现

现在,我们来看看SMA交叉金字塔策略的表现。和大多数趋势跟踪策略一样,该策略在长期的趋势行情中表现非常出色。在那些时期,由于移动平均线不会频繁交叉,策略能够很好地抓住并持有趋势。

例如,在下方的E-mini S&P 500期货图表中,策略在1,500点附近开仓做多,并在之后捕获了高达500点的巨大利润后才平仓:

这个策略一个很棒的特性是它如何进行金字塔式加仓。尽管原始规则只要求一次加仓,但我们的代码实现得更为灵活,允许在同一方向上进行任意次数的加仓。并且,由于我们每次加仓都会更新止损位,加仓的同时也上移了止损,从而锁定了利润,降低了风险。

下方图表展示了一个加仓效果很好的例子。在一个长期的上升趋势中,策略共加仓了9次。每一次加仓,止损位都随之提高,有效地保护了交易利润:

当然,这个策略也有其弱点。最主要的是,当市场进入横盘震荡时,它会像其他所有趋势跟踪策略一样亏损。在那种环境下,亏损的原因包括:趋势未能有效启动、趋势持续时间不足以盈利退出,或者仅仅是价格走向了错误的方向。

例如,在下图中,E-mini S&P 500期货横盘震荡了数月。在此期间,SMA交叉金字塔策略连续遭遇了四次亏损:

下方是该策略(基于规则设定一次加仓)的资金曲线图。有几点值得注意:策略的回撤相对于其净利润来说是温和的。另一点是,资金曲线上几次大的跳升表明,该策略的盈利严重依赖于少数几笔大的盈利交易。这不幸地也意味着,错过一两笔关键交易就可能毁掉一整年的业绩。

该策略也存在相对漫长的无所作为时期。我们可以看到,在前50笔左右的交易中,策略的资金曲线基本在盈亏平衡点附近徘徊。如果当时我们就此放弃,认为策略是失败的,那我们就会错过之后的大幅上涨行情。

下方的表格展示了本次回测的结果。这些结果基于一次额外加仓(即在同一方向上最多进行2笔交易)的设定。但需要注意的是,结果是在禁用头寸规模管理功能的情况下得出的。这样做的目的是为了让策略能执行它遇到的每一个信号,从而最大化回测报告中的交易数量。

该策略在E-迷你标普500指数期货和欧洲斯托克50指数期货上均取得了盈利。对于一个未经优化的长期趋势跟踪策略而言,其胜率表现尚可。此外,策略的回撤相对较小——大约仅为策略净利润的三分之一。

更进一步看,该策略的利润是标准版SMA交叉策略(该策略不进行加仓,但其他方面与SMA交叉金字塔策略相同)的三倍。然而,其在E-迷你标普500指数期货上的最大回撤仅增加了$7k。因此,从这个角度看,我们通过承担更大的仓位获得了三倍的利润,而策略风险的增加却微乎其微。

一个值得顾虑的问题是交易数量偏低。每年仅有略超2次的交易,这意味着我们没有足够的数据样本。要真正理解SMA交叉金字塔策略是否有效,我们必须进行更多的测试。

表现指标 E-迷你标普500指数期货 (ES) 欧洲斯托克50指数期货 (FESX)
首次交易 1998-09-07 1998-12-22
最后交易 2018-10-23 2018-11-02
时间框架 日线 日线
净利润 $94,144 €90,330
总利润 $273,550 €179,631
总亏损 -$179,406 -€89,301
最大回撤 $33,215 €31,903
盈利因子 1.525 2.012
总交易数 80 96
胜率 38.75% 47.92%
平均每笔交易 $1,176 €940
平均盈利交易 $8,824 €3,905
平均亏损交易 -$3,661 -€1,786
平均盈亏比 2.41 2.186
支付佣金 $468 €500
每笔订单滑点 2个跳动点 2个跳动点

改进思路与新策略方向

虽然初步的回测是盈利的,但我们仍有一些可以探索的方向,以期获得更好的结果并加深对策略的理解。以下是一些你可能会觉得有价值去进一步探索的思路。

SMA交叉金字塔策略的一个问题是,移动平均线的周期较长,导致策略响应迟缓。虽然50周期的SMA确实能过滤掉市场噪音,但其长度也意味着我们必须等待相当长的时间才能等到离场信号。到那时,我们可能已经回吐了部分利润,或眼看着亏损进一步扩大。或许我们可以使用50/100周期的交叉来入场,但使用一个更短(因此反应更快)的移动平均线来平仓。

柯威尔报告的一项优化研究显示,该策略的最佳表现出现在快线均线为30-40周期、慢线SMA为80-100周期时。我们或许应该进行自己的参数测试,以找出最优的参数组合,特别是因为柯威尔的测试发生在十多年前,市场环境已大不相同。

与大多数趋势跟踪策略一样,当市场横盘时,SMA交叉金字塔策略表现不佳。如果我们能过滤掉这些市场环境,策略的表现可能会得到显著提升。我们可以尝试引入成交量过滤器、平均真实波幅(ATR)过滤器、ADX指标或更高时间框架的分析。

该策略的止损使用的是一个固定的价格。虽然这给了头寸足够的浮动空间,但也意味着在止损被触发前,我们可能会回吐相当可观的利润。也许,一个追踪止损能帮助策略更快地了结亏损的头寸,并且不会放弃那么多利润。

当我们用额外的交易规则和过滤器来扩展策略时,一个好的做法是暂时禁用金字塔加仓和头寸规模管理功能。这样,我们就能更清晰地看到这些改动对策略表现的影响,而不受加仓行为的干扰。

我们也可以通过承担更少的风险来使策略变得更好。例如,通过设置最大持仓规模来防止交易过大,或者通过设置最大连续亏损天数来防止长期的资金回撤。

与SMA交叉金字塔策略相似的TradingView策略,可以参考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)

// --- 移动平均线输入项 ---
fastLen = input.int(50, title="快线SMA周期")
slowLen = input.int(100, title="慢线SMA周期")

// --- 止损输入项 ---
atrLen       = input.int(10, title="ATR周期")
longStpMult  = input.float(4, title="多头止损倍数")
shortStpMult = input.float(4, title="空头止损倍数")

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

// --- 金字塔加仓设置 ---
longThres       = input.float(1, title="多头加仓利润阈值%")
shortThres      = input.float(1, title="空头加仓利润阈值%")
maxLongEntries  = input.int(2, title="最大多头入场次数")
maxShortEntries = input.int(2, title="最大空头入场次数")

// --- 回测时间范围设置 ---
endMonth = input.int(11, title="结束月份", minval=1, maxval=12)
endYear  = input.int(2018, title="结束年份", minval=1950, maxval=2030)

// 步骤2. 计算策略所需指标值
// 计算移动平均线和ATR
fastMA   = ta.sma(close, fastLen)
slowMA   = ta.sma(close, slowLen)
atrValue = ta.atr(atrLen)

// 确定回测窗口
tradeWindow = time <= timestamp(endYear, endMonth, 1, 0, 0)

// 确定头寸规模 (双重风控:风险百分比 vs 最大敞口)
riskEquity = maxRisk * strategy.equity
riskLong   = (longStpMult * atrValue) * syminfo.pointvalue
riskShort  = (shortStpMult * atrValue) * syminfo.pointvalue

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

posSizeLong  = usePosSize ? math.min(math.floor(riskEquity / riskLong), maxPos) : 1
posSizeShort = usePosSize ? math.min(math.floor(riskEquity / riskShort), maxPos) : 1

// 计算交易的浮动盈亏百分比 (无持仓时为NaN)
tradeProfit = ((close - strategy.position_avg_price) /
     strategy.position_avg_price) * 100

// 计算仓位的入场次数
longEntries = 0
longEntries :=
     (strategy.position_size < 1) ? 0 :
     (strategy.position_size > strategy.position_size[1]) ? longEntries[1] + 1 :
     longEntries[1]

shortEntries = 0
shortEntries :=
     (strategy.position_size > -1) ? 0 :
     (strategy.position_size < strategy.position_size[1]) ? shortEntries[1] + 1 :
     shortEntries[1]

// 步骤3. 定义多头交易条件
// 初始入场
enterLong = ta.crossover(fastMA, slowMA) and
     strategy.position_size < 1 and
     tradeWindow
// 加仓
extraLong = strategy.position_size > 0 and
     longEntries < maxLongEntries and
     tradeProfit / longEntries > longThres and
     tradeWindow

// 计算多头止损并在有初始或加仓信号时更新止损价
longStop = 0.0
longStop := enterLong or extraLong ?
     close - (atrValue * longStpMult) :
     longStop[1]

// 步骤4. 定义空头交易条件
// 初始入场
enterShort = ta.crossunder(fastMA, slowMA) and
     strategy.position_size > -1 and
     tradeWindow
// 加仓
extraShort = strategy.position_size < 0 and
     shortEntries < maxShortEntries and
     tradeProfit / shortEntries < shortThres * -1 and
     tradeWindow

// 计算空头止损并在有初始或加仓信号时更新止损价
shortStop = 0.0
shortStop := enterShort or extraShort ?
     close + (atrValue * shortStpMult) :
     shortStop[1]

// 步骤5. 在图表上输出数据
// 绘制移动平均线
plot(fastMA, color=color.orange, title="快线SMA")
plot(slowMA, color=color.teal, title="慢线SMA", linewidth=2)
// 绘制止损水平
plot(strategy.position_size > 0 ? longStop : na, color=color.green, 
     style=plot.style_cross, linewidth=2, title="多头止损价")
plot(strategy.position_size < 0 ? shortStop : na, color=color.red, 
     style=plot.style_cross, linewidth=2, title="空头止损价")

// 步骤6. 提交入场订单
// 发送初始入场订单
if enterLong
    strategy.entry("EL", strategy.long, qty=posSizeLong)
if enterShort
    strategy.entry("ES", strategy.short, qty=posSizeShort)
// 发送额外加仓订单
if extraLong
    strategy.order("EL Extra", strategy.long, qty=posSizeLong)
if extraShort
    strategy.order("ES Extra", strategy.short, qty=posSizeShort)

// 步骤7. 提交出场订单
// ATR止损
if strategy.position_size > 0
    strategy.exit("XL", stop=longStop)
if strategy.position_size < 0
    strategy.exit("XS", stop=shortStop)
// 在回测窗口结束时平掉所有仓位
if not tradeWindow
    strategy.close_all()
赞(0)
未经允许不得转载:图道交易 » Pine Script(259):趋势形态入场与金字塔加仓策略
分享到

评论 抢沙发

登录

找回密码

注册