This strategy is a direct port of the MetaTrader 4 "Channels" expert advisor included in Gordago's public library. It combines a very fast exponential moving average (EMA) with three EMA-based envelopes to detect moments when price escapes from compressed zones. Once a single position is open the strategy relies on stop orders and optional trailing stops to manage exits, just like the original MQL implementation.
Trading logic
The strategy subscribes to hourly candles by default and calculates:
A fast EMA (length 2) using candle close prices.
A second fast EMA (length 2) using candle open prices, required by the short entry rules of the expert advisor.
A slow EMA (length 220) on closes that serves as the base for three envelope deviations: ±1.0%, ±0.7% and ±0.3%.
A long position is opened when the close-based fast EMA satisfies any of the six historical cross checks:
It crosses upward through the outer 1% lower envelope.
It crosses upward through the 0.7% lower envelope.
It spends two consecutive bars below the 0.3% lower envelope (oversold condition).
It crosses upward through the slow EMA itself.
It crosses upward through the 0.3% upper envelope.
It crosses upward through the 0.7% upper envelope.
A short position is opened when the open-based fast EMA triggers any of the symmetric short rules:
It crosses downward through the outer 1% upper envelope.
It crosses downward through the 0.7% upper envelope.
It crosses downward through the 0.3% upper envelope.
It crosses downward through the slow EMA.
It crosses downward through the 0.3% lower envelope.
It crosses downward through the 0.7% lower envelope.
Only one market position can exist at a time. A new signal is ignored while a trade is active, matching the behaviour of the MetaTrader expert.
Risk management
Individual stop-loss and take-profit distances can be configured for long and short trades. When set to zero those protective orders are skipped, which replicates the disabled-by-default state from the original source.
Optional trailing stops tighten the protective order once price moves in favour of the position by more than the trailing distance measured in points.
All protective orders are cancelled automatically when the position is flattened or the strategy stops.
Parameters
Name
Description
Candle Type
Timeframe used for price analysis (default: 1 hour).
Volume
Order size used for all entries.
Fast EMA / Slow EMA
Periods for the fast and slow EMAs.
Envelope 1%, Envelope 0.7%, Envelope 0.3%
Percentage width of the three envelope bands.
Buy Stop-Loss, Sell Stop-Loss
Distance in points between the entry price and the initial stop-loss for long or short trades.
Buy Take-Profit, Sell Take-Profit
Distance in points for the optional fixed take-profit levels.
Buy Trailing, Sell Trailing
Trailing stop distance in points for long or short positions.
Use Trading Hours
Enables the time window filter.
From Hour, To Hour
Inclusive hour-of-day boundaries for opening new positions. The window wraps around midnight if From is greater than To.
Usage notes
Because the stop distances are defined in points they are multiplied by the security PriceStep internally. Make sure this step matches the instrument used for trading.
The fast EMA length is intentionally very short to mirror the MT4 expert. Increasing it will dramatically change signal frequency.
The original advisor also allowed account whitelisting and sound alerts. Those were omitted as they are platform specific and do not affect order logic.
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Channels strategy - EMA envelope breakout.
/// Buys when price breaks above fast EMA + offset, sells when it breaks below.
/// Uses slow EMA as trend filter.
/// </summary>
public class ChannelsStrategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevFast;
private decimal _prevSlow;
private bool _hasPrev;
public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public ChannelsStrategy()
{
_fastPeriod = Param(nameof(FastPeriod), 10)
.SetDisplay("Fast EMA", "Fast EMA period", "Indicators");
_slowPeriod = Param(nameof(SlowPeriod), 30)
.SetDisplay("Slow EMA", "Slow EMA period", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
protected override void OnReseted() { base.OnReseted(); _prevFast = 0m; _prevSlow = 0m; _hasPrev = false; }
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_hasPrev = false;
var fast = new ExponentialMovingAverage { Length = FastPeriod };
var slow = new ExponentialMovingAverage { Length = SlowPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(fast, slow, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow)
{
if (candle.State != CandleStates.Finished)
return;
if (!_hasPrev)
{
_prevFast = fast;
_prevSlow = slow;
_hasPrev = true;
return;
}
var close = candle.ClosePrice;
// Buy: price crosses above fast EMA, fast EMA above slow EMA (uptrend)
if (close > fast && fast > slow && _prevFast <= _prevSlow && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
}
// Sell: price crosses below fast EMA, fast EMA below slow EMA (downtrend)
else if (close < fast && fast < slow && _prevFast >= _prevSlow && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
}
// Exit long when fast crosses below slow
else if (Position > 0 && fast < slow && _prevFast >= _prevSlow)
{
SellMarket();
}
// Exit short when fast crosses above slow
else if (Position < 0 && fast > slow && _prevFast <= _prevSlow)
{
BuyMarket();
}
_prevFast = fast;
_prevSlow = slow;
}
}
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
class channels_strategy(Strategy):
def __init__(self):
super(channels_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 10) \
.SetDisplay("Fast EMA", "Fast EMA period", "Indicators")
self._slow_period = self.Param("SlowPeriod", 30) \
.SetDisplay("Slow EMA", "Slow EMA period", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle timeframe", "General")
self._prev_fast = 0.0
self._prev_slow = 0.0
self._has_prev = False
@property
def fast_period(self):
return self._fast_period.Value
@property
def slow_period(self):
return self._slow_period.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(channels_strategy, self).OnReseted()
self._prev_fast = 0.0
self._prev_slow = 0.0
self._has_prev = False
def OnStarted2(self, time):
super(channels_strategy, self).OnStarted2(time)
self._has_prev = False
fast = ExponentialMovingAverage()
fast.Length = self.fast_period
slow = ExponentialMovingAverage()
slow.Length = self.slow_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(fast, slow, self.process_candle).Start()
def process_candle(self, candle, fast, slow):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast)
slow_val = float(slow)
if not self._has_prev:
self._prev_fast = fast_val
self._prev_slow = slow_val
self._has_prev = True
return
close = float(candle.ClosePrice)
if close > fast_val and fast_val > slow_val and self._prev_fast <= self._prev_slow and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif close < fast_val and fast_val < slow_val and self._prev_fast >= self._prev_slow and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
elif self.Position > 0 and fast_val < slow_val and self._prev_fast >= self._prev_slow:
self.SellMarket()
elif self.Position < 0 and fast_val > slow_val and self._prev_fast <= self._prev_slow:
self.BuyMarket()
self._prev_fast = fast_val
self._prev_slow = slow_val
def CreateClone(self):
return channels_strategy()