三均线通道策略
概述
三均线通道策略 将 MetaTrader 专家顾问 3MaCross_EA 转换为 StockSharp 的高级 API。策略跟踪三条可配置的移动平均线,当较快的均线向上或向下穿越较慢的均线时开仓。可选的 Donchian 价格通道用于管理离场,对应原脚本使用的 “Price Channel” 指标。
交易逻辑
- 做多入场:当快速和中速均线同时收盘在慢速均线上方,并且其中任意一条均线在当前柱完成向上穿越慢速均线时触发。
- 做空入场:当快速和中速均线同时收盘在慢速均线下方,并且其中任意一条均线在当前柱完成向下穿越慢速均线时触发。
- 离场条件:
- 出现反向交叉信号。
- 可选 Donchian 通道止损:多头在价格跌破下轨时平仓,空头在价格突破上轨时平仓。
- 可选固定止盈/止损,按照绝对价格距离计算。
策略仅在柱线收盘后做出决策,与原 EA 的 TradeAtCloseBar 行为一致。一次只持有一个方向的头寸,若出现反向信号,先平掉现有头寸再开新仓。
参数
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
FastLength |
int |
2 |
快速均线的周期。 |
MediumLength |
int |
4 |
中速均线的周期。 |
SlowLength |
int |
30 |
慢速均线的周期。 |
ChannelLength |
int |
15 |
Donchian 通道的窗口长度。 |
FastType |
MovingAverageTypeEnum |
EMA |
快速均线采用的算法(SMA、EMA、SMMA、WMA)。 |
MediumType |
MovingAverageTypeEnum |
EMA |
中速均线采用的算法。 |
SlowType |
MovingAverageTypeEnum |
EMA |
慢速均线采用的算法。 |
TakeProfit |
decimal |
0 |
绝对价格单位的止盈距离,0 表示禁用。 |
StopLoss |
decimal |
0 |
绝对价格单位的止损距离,0 表示禁用。 |
UseChannelStop |
bool |
true |
是否启用 Donchian 通道离场。 |
CandleType |
DataType |
TimeSpan.FromMinutes(1).TimeFrame() |
用于计算的蜡烛类型。 |
说明
- 所有均线均使用收盘价,可分别配置以对应原 EA 的
FasterMode、MediumMode与SlowerMode。 TakeProfit与StopLoss为绝对价差(例如外汇五位报价中0.0010约等于 10 点),在柱线收盘时进行判断。- 开启
UseChannelStop时,策略复刻原脚本依赖Price Channel指标的自动止损逻辑。 - 策略会在图表上绘制三条均线、Donchian 通道与交易标记,便于核对信号。
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>
/// Three moving average crossover strategy with channel-based exits.
/// Opens a long position when the fast and medium moving averages cross above the slow moving average and a short position when they cross below.
/// Optionally uses a Donchian channel to set trailing exit levels and supports fixed take-profit and stop-loss distances.
/// </summary>
public class ThreeMaCrossChannelStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _mediumLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<int> _channelLength;
private readonly StrategyParam<MovingAverageTypes> _fastType;
private readonly StrategyParam<MovingAverageTypes> _mediumType;
private readonly StrategyParam<MovingAverageTypes> _slowType;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<bool> _useChannelStop;
private readonly StrategyParam<DataType> _candleType;
private DecimalLengthIndicator _fastMa = null!;
private DecimalLengthIndicator _mediumMa = null!;
private DecimalLengthIndicator _slowMa = null!;
private DonchianChannels _channel = null!;
private bool? _prevFastAboveSlow;
private bool? _prevMediumAboveSlow;
private decimal? _entryPrice;
/// <summary>
/// Length of the fast moving average.
/// </summary>
public int FastLength
{
get => _fastLength.Value;
set => _fastLength.Value = value;
}
/// <summary>
/// Length of the medium moving average.
/// </summary>
public int MediumLength
{
get => _mediumLength.Value;
set => _mediumLength.Value = value;
}
/// <summary>
/// Length of the slow moving average.
/// </summary>
public int SlowLength
{
get => _slowLength.Value;
set => _slowLength.Value = value;
}
/// <summary>
/// Period used for the Donchian channel.
/// </summary>
public int ChannelLength
{
get => _channelLength.Value;
set => _channelLength.Value = value;
}
/// <summary>
/// Moving-average type applied to the fast average.
/// </summary>
public MovingAverageTypes FastType
{
get => _fastType.Value;
set => _fastType.Value = value;
}
/// <summary>
/// Moving-average type applied to the medium average.
/// </summary>
public MovingAverageTypes MediumType
{
get => _mediumType.Value;
set => _mediumType.Value = value;
}
/// <summary>
/// Moving-average type applied to the slow average.
/// </summary>
public MovingAverageTypes SlowType
{
get => _slowType.Value;
set => _slowType.Value = value;
}
/// <summary>
/// Take-profit distance measured in price units.
/// </summary>
public decimal TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
/// <summary>
/// Stop-loss distance measured in price units.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Enables exits based on Donchian channel boundaries.
/// </summary>
public bool UseChannelStop
{
get => _useChannelStop.Value;
set => _useChannelStop.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ThreeMaCrossChannelStrategy"/> class.
/// </summary>
public ThreeMaCrossChannelStrategy()
{
_fastLength = Param(nameof(FastLength), 2)
.SetGreaterThanZero()
.SetDisplay("Fast MA", "Length of the fast moving average", "Moving Averages");
_mediumLength = Param(nameof(MediumLength), 4)
.SetGreaterThanZero()
.SetDisplay("Medium MA", "Length of the medium moving average", "Moving Averages");
_slowLength = Param(nameof(SlowLength), 30)
.SetGreaterThanZero()
.SetDisplay("Slow MA", "Length of the slow moving average", "Moving Averages");
_channelLength = Param(nameof(ChannelLength), 15)
.SetGreaterThanZero()
.SetDisplay("Channel", "Donchian channel lookback period", "Risk Management");
_fastType = Param(nameof(FastType), MovingAverageTypes.EMA)
.SetDisplay("Fast MA Type", "Algorithm used for the fast average", "Moving Averages");
_mediumType = Param(nameof(MediumType), MovingAverageTypes.EMA)
.SetDisplay("Medium MA Type", "Algorithm used for the medium average", "Moving Averages");
_slowType = Param(nameof(SlowType), MovingAverageTypes.EMA)
.SetDisplay("Slow MA Type", "Algorithm used for the slow average", "Moving Averages");
_takeProfit = Param(nameof(TakeProfit), 0m)
.SetDisplay("Take Profit", "Distance to close profitable trades", "Risk Management")
.SetOptimize(0m, 3m, 0.1m);
_stopLoss = Param(nameof(StopLoss), 0m)
.SetDisplay("Stop Loss", "Distance to limit losses", "Risk Management")
.SetOptimize(0m, 3m, 0.1m);
_useChannelStop = Param(nameof(UseChannelStop), true)
.SetDisplay("Channel Exit", "Use Donchian channel boundaries for exits", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used by the strategy", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevFastAboveSlow = null;
_prevMediumAboveSlow = null;
_entryPrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastMa = CreateMovingAverage(FastType, FastLength);
_mediumMa = CreateMovingAverage(MediumType, MediumLength);
_slowMa = CreateMovingAverage(SlowType, SlowLength);
_channel = new DonchianChannels { Length = ChannelLength };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_fastMa, _mediumMa, _slowMa, _channel, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _fastMa);
DrawIndicator(area, _mediumMa);
DrawIndicator(area, _slowMa);
DrawIndicator(area, _channel);
DrawOwnTrades(area);
}
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue fastVal, IIndicatorValue mediumVal, IIndicatorValue slowVal, IIndicatorValue channelVal)
{
if (candle.State != CandleStates.Finished)
return;
if (!_fastMa.IsFormed || !_mediumMa.IsFormed || !_slowMa.IsFormed)
return;
var fastValue = fastVal.ToDecimal();
var mediumValue = mediumVal.ToDecimal();
var slowValue = slowVal.ToDecimal();
decimal upperBand = 0m, lowerBand = 0m;
if (channelVal is IDonchianChannelsValue dcVal)
{
upperBand = dcVal.UpperBand ?? 0m;
lowerBand = dcVal.LowerBand ?? 0m;
}
var fastAbove = fastValue > slowValue;
var mediumAbove = mediumValue > slowValue;
var fastCrossUp = _prevFastAboveSlow.HasValue && !_prevFastAboveSlow.Value && fastAbove;
var fastCrossDown = _prevFastAboveSlow.HasValue && _prevFastAboveSlow.Value && !fastAbove;
var mediumCrossUp = _prevMediumAboveSlow.HasValue && !_prevMediumAboveSlow.Value && mediumAbove;
var mediumCrossDown = _prevMediumAboveSlow.HasValue && _prevMediumAboveSlow.Value && !mediumAbove;
_prevFastAboveSlow = fastAbove;
_prevMediumAboveSlow = mediumAbove;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var buySignal = fastAbove && mediumAbove && (fastCrossUp || mediumCrossUp);
var sellSignal = !fastAbove && !mediumAbove && (fastCrossDown || mediumCrossDown);
if (Position > 0)
{
var shouldExit = sellSignal;
if (!shouldExit && _entryPrice.HasValue)
{
if (TakeProfit > 0m && candle.ClosePrice - _entryPrice.Value >= TakeProfit)
shouldExit = true;
if (StopLoss > 0m && _entryPrice.Value - candle.ClosePrice >= StopLoss)
shouldExit = true;
}
if (!shouldExit && UseChannelStop && candle.ClosePrice <= lowerBand)
shouldExit = true;
if (shouldExit)
{
SellMarket(Position);
_entryPrice = null;
return;
}
}
else if (Position < 0)
{
var shouldExit = buySignal;
if (!shouldExit && _entryPrice.HasValue)
{
if (TakeProfit > 0m && _entryPrice.Value - candle.ClosePrice >= TakeProfit)
shouldExit = true;
if (StopLoss > 0m && candle.ClosePrice - _entryPrice.Value >= StopLoss)
shouldExit = true;
}
if (!shouldExit && UseChannelStop && candle.ClosePrice >= upperBand)
shouldExit = true;
if (shouldExit)
{
BuyMarket(Math.Abs(Position));
_entryPrice = null;
return;
}
}
if (buySignal && Position <= 0)
{
var volume = Volume + Math.Abs(Position);
BuyMarket(volume);
_entryPrice = candle.ClosePrice;
}
else if (sellSignal && Position >= 0)
{
var volume = Volume + Math.Abs(Position);
SellMarket(volume);
_entryPrice = candle.ClosePrice;
}
}
private static DecimalLengthIndicator CreateMovingAverage(MovingAverageTypes type, int length)
{
return type switch
{
MovingAverageTypes.SMA => new SMA { Length = length },
MovingAverageTypes.EMA => new EMA { Length = length },
MovingAverageTypes.SMMA => new SmoothedMovingAverage { Length = length },
MovingAverageTypes.WMA => new WeightedMovingAverage { Length = length },
_ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unsupported moving average type."),
};
}
/// <summary>
/// Moving-average algorithms supported by the strategy.
/// Matches the modes available in the original MetaTrader script.
/// </summary>
public enum MovingAverageTypes
{
/// <summary>
/// Simple moving average.
/// </summary>
SMA,
/// <summary>
/// Exponential moving average.
/// </summary>
EMA,
/// <summary>
/// Smoothed moving average.
/// </summary>
SMMA,
/// <summary>
/// Linear weighted moving average.
/// </summary>
WMA,
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import (
SimpleMovingAverage,
ExponentialMovingAverage,
SmoothedMovingAverage,
WeightedMovingAverage,
DonchianChannels,
)
class three_ma_cross_channel_strategy(Strategy):
def __init__(self):
super(three_ma_cross_channel_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 2) \
.SetDisplay("Fast MA", "Length of the fast moving average", "Moving Averages")
self._medium_length = self.Param("MediumLength", 4) \
.SetDisplay("Medium MA", "Length of the medium moving average", "Moving Averages")
self._slow_length = self.Param("SlowLength", 30) \
.SetDisplay("Slow MA", "Length of the slow moving average", "Moving Averages")
self._channel_length = self.Param("ChannelLength", 15) \
.SetDisplay("Channel", "Donchian channel lookback period", "Risk Management")
self._take_profit = self.Param("TakeProfit", 0.0) \
.SetDisplay("Take Profit", "Distance to close profitable trades", "Risk Management")
self._stop_loss = self.Param("StopLoss", 0.0) \
.SetDisplay("Stop Loss", "Distance to limit losses", "Risk Management")
self._use_channel_stop = self.Param("UseChannelStop", True) \
.SetDisplay("Channel Exit", "Use Donchian channel boundaries for exits", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Type of candles used by the strategy", "General")
self._prev_fast_above_slow = None
self._prev_medium_above_slow = None
self._entry_price = None
@property
def FastLength(self):
return self._fast_length.Value
@property
def MediumLength(self):
return self._medium_length.Value
@property
def SlowLength(self):
return self._slow_length.Value
@property
def ChannelLength(self):
return self._channel_length.Value
@property
def TakeProfit(self):
return self._take_profit.Value
@property
def StopLoss(self):
return self._stop_loss.Value
@property
def UseChannelStop(self):
return self._use_channel_stop.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(three_ma_cross_channel_strategy, self).OnStarted2(time)
self._fast_ma = ExponentialMovingAverage()
self._fast_ma.Length = self.FastLength
self._medium_ma = ExponentialMovingAverage()
self._medium_ma.Length = self.MediumLength
self._slow_ma = ExponentialMovingAverage()
self._slow_ma.Length = self.SlowLength
self._channel = DonchianChannels()
self._channel.Length = self.ChannelLength
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.BindEx(self._fast_ma, self._medium_ma, self._slow_ma, self._channel, self.ProcessCandle) \
.Start()
self.StartProtection(None, None)
def ProcessCandle(self, candle, fast_val, medium_val, slow_val, channel_val):
if candle.State != CandleStates.Finished:
return
if not self._fast_ma.IsFormed or not self._medium_ma.IsFormed or not self._slow_ma.IsFormed:
return
fast_value = float(fast_val)
medium_value = float(medium_val)
slow_value = float(slow_val)
upper_band = 0.0
lower_band = 0.0
if hasattr(channel_val, 'UpperBand') and channel_val.UpperBand is not None:
upper_band = float(channel_val.UpperBand)
if hasattr(channel_val, 'LowerBand') and channel_val.LowerBand is not None:
lower_band = float(channel_val.LowerBand)
fast_above = fast_value > slow_value
medium_above = medium_value > slow_value
fast_cross_up = self._prev_fast_above_slow is not None and not self._prev_fast_above_slow and fast_above
fast_cross_down = self._prev_fast_above_slow is not None and self._prev_fast_above_slow and not fast_above
medium_cross_up = self._prev_medium_above_slow is not None and not self._prev_medium_above_slow and medium_above
medium_cross_down = self._prev_medium_above_slow is not None and self._prev_medium_above_slow and not medium_above
self._prev_fast_above_slow = fast_above
self._prev_medium_above_slow = medium_above
buy_signal = fast_above and medium_above and (fast_cross_up or medium_cross_up)
sell_signal = not fast_above and not medium_above and (fast_cross_down or medium_cross_down)
close = float(candle.ClosePrice)
tp = float(self.TakeProfit)
sl = float(self.StopLoss)
if self.Position > 0:
should_exit = sell_signal
if not should_exit and self._entry_price is not None:
if tp > 0 and close - self._entry_price >= tp:
should_exit = True
if sl > 0 and self._entry_price - close >= sl:
should_exit = True
if not should_exit and self.UseChannelStop and close <= lower_band:
should_exit = True
if should_exit:
self.SellMarket(self.Position)
self._entry_price = None
return
elif self.Position < 0:
should_exit = buy_signal
if not should_exit and self._entry_price is not None:
if tp > 0 and self._entry_price - close >= tp:
should_exit = True
if sl > 0 and close - self._entry_price >= sl:
should_exit = True
if not should_exit and self.UseChannelStop and close >= upper_band:
should_exit = True
if should_exit:
self.BuyMarket(abs(self.Position))
self._entry_price = None
return
if buy_signal and self.Position <= 0:
volume = self.Volume + abs(self.Position)
self.BuyMarket(volume)
self._entry_price = close
elif sell_signal and self.Position >= 0:
volume = self.Volume + abs(self.Position)
self.SellMarket(volume)
self._entry_price = close
def OnReseted(self):
super(three_ma_cross_channel_strategy, self).OnReseted()
self._prev_fast_above_slow = None
self._prev_medium_above_slow = None
self._entry_price = None
def CreateClone(self):
return three_ma_cross_channel_strategy()