Tunnel Method EMA 策略
概述
Tunnel Method EMA 策略基于原版 MetaTrader「Tunnel Method」专家顾问,并使用 StockSharp 的高级 API 实现。策略运行在 1 小时 K 线之上,比较以下三条基于收盘价的指数移动平均线(EMA):
- 快速 EMA(12):跟踪最新动量变化。
- 中速 EMA(144):构成“通道”中轨,用于确认做空信号。
- 慢速 EMA(169):定义长周期趋势,用作做多过滤器。
策略在任意时刻仅持有单向仓位(多头或空头),并通过硬性止损、止盈以及移动止损来控制风险。
交易逻辑
多头入场
- 等待 K 线收盘(不在未完成的 K 线上决策)。
- 当快速 EMA(12)从下方上穿慢速 EMA(169)时触发信号。
- 若当前无持仓,则按配置手数提交市价买单。
空头入场
- 等待 K 线收盘。
- 当快速 EMA(12)从上方下穿中速 EMA(144)时触发信号。
- 若当前无持仓,则提交市价卖单。
仓位管理
- 止损:价格相对开仓价逆向运行
StopLossPoints(按合约最小价格步长折算)时平仓。 - 止盈:价格顺向运行
TakeProfitPoints时获利了结。 - 移动止损:当浮盈达到
TrailingTriggerPoints后启动,并以TrailingStopPoints的距离跟随价格。多头跟踪自进场以来的最高价,空头跟踪最低价;一旦价格回落到移动止损位置即平仓。 - 状态重置:每次出场后都会清除内部跟踪变量,避免影响下一笔交易。
默认参数
| 参数 | 默认值 | 说明 |
|---|---|---|
CandleType |
TimeSpan.FromHours(1).TimeFrame() |
使用 1 小时 K 线计算 EMA。 |
FastLength |
12 | 快速 EMA 周期。 |
MediumLength |
144 | 中速 EMA 周期,用于空头确认。 |
SlowLength |
169 | 慢速 EMA 周期,用于多头确认。 |
StopLossPoints |
25 | 止损距离(以价格点计)。 |
TakeProfitPoints |
230 | 止盈距离(以价格点计)。 |
TrailingStopPoints |
35 | 移动止损距离。 |
TrailingTriggerPoints |
20 | 启动移动止损所需的最小浮盈。 |
策略特性
- 类型:趋势跟随型均线交叉策略。
- 标的:适用于提供小时级行情并具备明确价格步长的品种。
- 方向:可做多亦可做空,始终保持单向仓位。
- 时间框架:默认 1 小时,可通过
CandleType参数调整。 - 风险控制:内置硬止损、止盈以及移动止损。
- 数据需求:仅依赖 K 线收盘价,无需额外市场深度或成交数据。
补充说明
- 所有指标均使用 StockSharp 内置 EMA,实现方式符合高阶 API 的使用规范。
- 策略忽略未完成的 K 线,避免重复触发信号或使用部分数据。
- 移动止损价格通过
ShrinkPrice调整到有效的最小价格步长,确保挂单价格有效。 - 默认参数保持与原始 MQL 版本一致,同时支持通过 StockSharp 的参数优化工具进行调优。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Tunnel method strategy that trades EMA crossovers on hourly candles.
/// Long trades are opened when the fast EMA crosses above the slow EMA.
/// Short trades are opened when the fast EMA crosses below the medium EMA.
/// Includes fixed stop-loss, take-profit, and a trailing stop once profit reaches a trigger.
/// </summary>
public class TunnelMethodEmaStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _mediumLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _trailingTriggerPoints;
private bool _hasPreviousValues;
private decimal _previousFast;
private decimal _previousMedium;
private decimal _previousSlow;
private decimal _pointValue;
private decimal _stopLossDistance;
private decimal _takeProfitDistance;
private decimal _trailingStopDistance;
private decimal _trailingTriggerDistance;
private decimal? _entryPrice;
private decimal _highestSinceEntry;
private decimal _lowestSinceEntry;
private decimal? _longTrailingStop;
private decimal? _shortTrailingStop;
/// <summary>
/// Initializes a new instance of the <see cref="TunnelMethodEmaStrategy"/> class.
/// </summary>
public TunnelMethodEmaStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for EMA calculations", "General");
_fastLength = Param(nameof(FastLength), 12)
.SetGreaterThanZero()
.SetDisplay("Fast EMA Length", "Period of the fast EMA", "Indicators")
.SetOptimize(6, 30, 2);
_mediumLength = Param(nameof(MediumLength), 144)
.SetGreaterThanZero()
.SetDisplay("Medium EMA Length", "Period of the medium EMA", "Indicators")
.SetOptimize(72, 200, 8);
_slowLength = Param(nameof(SlowLength), 169)
.SetGreaterThanZero()
.SetDisplay("Slow EMA Length", "Period of the slow EMA", "Indicators")
.SetOptimize(120, 220, 5);
_stopLossPoints = Param(nameof(StopLossPoints), 25m)
.SetNotNegative()
.SetDisplay("Stop Loss (points)", "Protective stop distance expressed in price points", "Risk")
.SetOptimize(10m, 60m, 5m);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 230m)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Profit target distance expressed in price points", "Risk")
.SetOptimize(100m, 400m, 20m);
_trailingStopPoints = Param(nameof(TrailingStopPoints), 35m)
.SetNotNegative()
.SetDisplay("Trailing Stop (points)", "Distance maintained by the trailing stop", "Risk")
.SetOptimize(10m, 80m, 5m);
_trailingTriggerPoints = Param(nameof(TrailingTriggerPoints), 20m)
.SetNotNegative()
.SetDisplay("Trailing Trigger (points)", "Profit required before the trailing stop activates", "Risk")
.SetOptimize(5m, 60m, 5m);
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Fast EMA period length.
/// </summary>
public int FastLength
{
get => _fastLength.Value;
set => _fastLength.Value = value;
}
/// <summary>
/// Medium EMA period length.
/// </summary>
public int MediumLength
{
get => _mediumLength.Value;
set => _mediumLength.Value = value;
}
/// <summary>
/// Slow EMA period length.
/// </summary>
public int SlowLength
{
get => _slowLength.Value;
set => _slowLength.Value = value;
}
/// <summary>
/// Stop-loss distance in price points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance in price points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Trailing stop distance in price points.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Trailing activation threshold in price points.
/// </summary>
public decimal TrailingTriggerPoints
{
get => _trailingTriggerPoints.Value;
set => _trailingTriggerPoints.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_hasPreviousValues = false;
_previousFast = 0m;
_previousMedium = 0m;
_previousSlow = 0m;
_pointValue = 0m;
_stopLossDistance = 0m;
_takeProfitDistance = 0m;
_trailingStopDistance = 0m;
_trailingTriggerDistance = 0m;
_entryPrice = null;
_highestSinceEntry = 0m;
_lowestSinceEntry = 0m;
_longTrailingStop = null;
_shortTrailingStop = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pointValue = GetPointValue();
_stopLossDistance = StopLossPoints * _pointValue;
_takeProfitDistance = TakeProfitPoints * _pointValue;
_trailingStopDistance = TrailingStopPoints * _pointValue;
_trailingTriggerDistance = TrailingTriggerPoints * _pointValue;
var slowEma = new ExponentialMovingAverage { Length = SlowLength };
var mediumEma = new ExponentialMovingAverage { Length = MediumLength };
var fastEma = new ExponentialMovingAverage { Length = FastLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(slowEma, mediumEma, fastEma, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal slowValue, decimal mediumValue, decimal fastValue)
{
if (candle.State != CandleStates.Finished)
// Ignore unfinished candles to work on closed data.
return;
if (!_hasPreviousValues)
{
_previousSlow = slowValue;
_previousMedium = mediumValue;
_previousFast = fastValue;
_hasPreviousValues = true;
return;
}
UpdateRiskDistances();
// Refresh risk distances if the price step changes during runtime.
if (Position == 0)
{
ResetPositionState();
// Clear trailing state while flat to prepare for the next trade.
}
else if (Position > 0)
{
ManageLongPosition(candle);
}
else
{
ManageShortPosition(candle);
}
if (Position == 0)
{
var shouldOpenLong = _previousFast < _previousSlow && fastValue > slowValue;
var shouldOpenShort = _previousFast > _previousMedium && fastValue < mediumValue;
if (shouldOpenLong && Position <= 0)
{
var volume = Volume + Math.Abs(Position);
if (volume > 0)
{
_entryPrice = candle.ClosePrice;
_highestSinceEntry = candle.HighPrice;
_longTrailingStop = null;
// Enter long with current volume when the fast EMA crosses above the slow EMA.
BuyMarket();
}
}
else if (shouldOpenShort && Position >= 0)
{
var volume = Volume + Math.Abs(Position);
if (volume > 0)
{
_entryPrice = candle.ClosePrice;
_lowestSinceEntry = candle.LowPrice;
_shortTrailingStop = null;
// Enter short with current volume when the fast EMA crosses below the medium EMA.
SellMarket();
}
}
}
_previousSlow = slowValue;
_previousMedium = mediumValue;
_previousFast = fastValue;
}
private void ManageLongPosition(ICandleMessage candle)
{
if (_entryPrice is null)
_entryPrice = candle.ClosePrice;
_highestSinceEntry = Math.Max(_highestSinceEntry, candle.HighPrice);
// Track the highest price reached since the long entry.
if (_takeProfitDistance > 0m && candle.HighPrice >= _entryPrice.Value + _takeProfitDistance)
{
SellMarket();
ResetPositionState();
return;
}
if (_stopLossDistance > 0m && candle.LowPrice <= _entryPrice.Value - _stopLossDistance)
{
SellMarket();
ResetPositionState();
return;
}
if (_trailingStopDistance <= 0m || _trailingTriggerDistance <= 0m)
return;
if (_highestSinceEntry - _entryPrice.Value < _trailingTriggerDistance)
return;
var candidate = _highestSinceEntry - _trailingStopDistance;
// Align the trailing stop with the instrument price step.
candidate = ShrinkPrice(candidate);
if (!_longTrailingStop.HasValue || candidate > _longTrailingStop.Value)
_longTrailingStop = candidate;
if (_longTrailingStop.HasValue && candle.LowPrice <= _longTrailingStop.Value)
// Close the long position once price falls to the trailing stop.
{
SellMarket();
ResetPositionState();
}
}
private void ManageShortPosition(ICandleMessage candle)
{
if (_entryPrice is null)
_entryPrice = candle.ClosePrice;
_lowestSinceEntry = _lowestSinceEntry == 0m ? candle.LowPrice : Math.Min(_lowestSinceEntry, candle.LowPrice);
// Track the lowest price reached since the short entry.
if (_takeProfitDistance > 0m && candle.LowPrice <= _entryPrice.Value - _takeProfitDistance)
{
BuyMarket();
ResetPositionState();
return;
}
if (_stopLossDistance > 0m && candle.HighPrice >= _entryPrice.Value + _stopLossDistance)
{
BuyMarket();
ResetPositionState();
return;
}
if (_trailingStopDistance <= 0m || _trailingTriggerDistance <= 0m)
return;
if (_entryPrice.Value - _lowestSinceEntry < _trailingTriggerDistance)
return;
var candidate = _lowestSinceEntry + _trailingStopDistance;
// Align the trailing stop with the instrument price step.
candidate = ShrinkPrice(candidate);
if (!_shortTrailingStop.HasValue || candidate < _shortTrailingStop.Value)
_shortTrailingStop = candidate;
if (_shortTrailingStop.HasValue && candle.HighPrice >= _shortTrailingStop.Value)
// Close the short position once price rises to the trailing stop.
{
BuyMarket();
ResetPositionState();
}
}
private void ResetPositionState()
{
_entryPrice = null;
_highestSinceEntry = 0m;
_lowestSinceEntry = 0m;
_longTrailingStop = null;
_shortTrailingStop = null;
}
private void UpdateRiskDistances()
{
var newPointValue = GetPointValue();
if (newPointValue <= 0m)
return;
if (_pointValue != newPointValue)
{
_pointValue = newPointValue;
_stopLossDistance = StopLossPoints * _pointValue;
_takeProfitDistance = TakeProfitPoints * _pointValue;
_trailingStopDistance = TrailingStopPoints * _pointValue;
_trailingTriggerDistance = TrailingTriggerPoints * _pointValue;
}
}
private decimal GetPointValue()
{
var step = Security?.PriceStep;
if (step is > 0m)
return step.Value;
return 1m;
}
private decimal ShrinkPrice(decimal price)
{
if (_pointValue > 0m)
return Math.Round(price / _pointValue) * _pointValue;
return price;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class tunnel_method_ema_strategy(Strategy):
"""EMA crossover tunnel: long on fast cross above slow, short on fast cross below medium, with SL/TP/trailing."""
def __init__(self):
super(tunnel_method_ema_strategy, self).__init__()
self._fast_len = self.Param("FastLength", 12).SetGreaterThanZero().SetDisplay("Fast EMA", "Fast EMA period", "Indicators")
self._mid_len = self.Param("MediumLength", 144).SetGreaterThanZero().SetDisplay("Medium EMA", "Medium EMA period", "Indicators")
self._slow_len = self.Param("SlowLength", 169).SetGreaterThanZero().SetDisplay("Slow EMA", "Slow EMA period", "Indicators")
self._sl_points = self.Param("StopLossPoints", 25.0).SetNotNegative().SetDisplay("Stop Loss", "SL in points", "Risk")
self._tp_points = self.Param("TakeProfitPoints", 230.0).SetNotNegative().SetDisplay("Take Profit", "TP in points", "Risk")
self._trail_points = self.Param("TrailingStopPoints", 35.0).SetNotNegative().SetDisplay("Trailing Stop", "Trail distance", "Risk")
self._trail_trigger = self.Param("TrailingTriggerPoints", 20.0).SetNotNegative().SetDisplay("Trail Trigger", "Profit to activate trail", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(tunnel_method_ema_strategy, self).OnReseted()
self._prev_fast = 0
self._prev_mid = 0
self._prev_slow = 0
self._has_prev = False
self._entry_price = 0
self._highest = 0
self._lowest = 0
self._long_trail = 0
self._short_trail = 0
def OnStarted2(self, time):
super(tunnel_method_ema_strategy, self).OnStarted2(time)
self._prev_fast = 0
self._prev_mid = 0
self._prev_slow = 0
self._has_prev = False
self._entry_price = 0
self._highest = 0
self._lowest = 0
self._long_trail = 0
self._short_trail = 0
slow = ExponentialMovingAverage()
slow.Length = self._slow_len.Value
mid = ExponentialMovingAverage()
mid.Length = self._mid_len.Value
fast = ExponentialMovingAverage()
fast.Length = self._fast_len.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(slow, mid, fast, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle, slow_val, mid_val, fast_val):
if candle.State != CandleStates.Finished:
return
fast = float(fast_val)
mid = float(mid_val)
slow = float(slow_val)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if not self._has_prev:
self._prev_fast = fast
self._prev_mid = mid
self._prev_slow = slow
self._has_prev = True
return
sl = self._sl_points.Value
tp = self._tp_points.Value
trail = self._trail_points.Value
trigger = self._trail_trigger.Value
# Manage long
if self.Position > 0:
if self._entry_price == 0:
self._entry_price = close
self._highest = max(self._highest, high)
if tp > 0 and high >= self._entry_price + tp:
self.SellMarket()
self._reset_pos()
self._prev_fast = fast
self._prev_mid = mid
self._prev_slow = slow
return
if sl > 0 and low <= self._entry_price - sl:
self.SellMarket()
self._reset_pos()
self._prev_fast = fast
self._prev_mid = mid
self._prev_slow = slow
return
if trail > 0 and trigger > 0 and self._highest - self._entry_price >= trigger:
candidate = self._highest - trail
if self._long_trail == 0 or candidate > self._long_trail:
self._long_trail = candidate
if self._long_trail > 0 and low <= self._long_trail:
self.SellMarket()
self._reset_pos()
self._prev_fast = fast
self._prev_mid = mid
self._prev_slow = slow
return
# Manage short
elif self.Position < 0:
if self._entry_price == 0:
self._entry_price = close
if self._lowest == 0:
self._lowest = low
else:
self._lowest = min(self._lowest, low)
if tp > 0 and low <= self._entry_price - tp:
self.BuyMarket()
self._reset_pos()
self._prev_fast = fast
self._prev_mid = mid
self._prev_slow = slow
return
if sl > 0 and high >= self._entry_price + sl:
self.BuyMarket()
self._reset_pos()
self._prev_fast = fast
self._prev_mid = mid
self._prev_slow = slow
return
if trail > 0 and trigger > 0 and self._entry_price - self._lowest >= trigger:
candidate = self._lowest + trail
if self._short_trail == 0 or candidate < self._short_trail:
self._short_trail = candidate
if self._short_trail > 0 and high >= self._short_trail:
self.BuyMarket()
self._reset_pos()
self._prev_fast = fast
self._prev_mid = mid
self._prev_slow = slow
return
# Entries
if self.Position == 0:
cross_up = self._prev_fast < self._prev_slow and fast > slow
cross_down = self._prev_fast > self._prev_mid and fast < mid
if cross_up:
self.BuyMarket()
self._entry_price = close
self._highest = high
self._long_trail = 0
elif cross_down:
self.SellMarket()
self._entry_price = close
self._lowest = low
self._short_trail = 0
self._prev_fast = fast
self._prev_mid = mid
self._prev_slow = slow
def _reset_pos(self):
self._entry_price = 0
self._highest = 0
self._lowest = 0
self._long_trail = 0
self._short_trail = 0
def CreateClone(self):
return tunnel_method_ema_strategy()