The Flat Channel Strategy is a C# translation of the MetaTrader 5 expert advisor Flat Channel (barabashkakvn's edition). It keeps the original workflow: a smoothed standard deviation highlights volatility squeezes, the highest and lowest prices inside the squeeze define a horizontal channel, and pending stop orders are placed just outside of that range. When the market breaks out the strategy joins the move with predefined stop-loss and take-profit levels and can optionally trail the stop as the position gains profit.
How it works
Volatility squeeze detection – A StandardDeviation indicator with length StdDevPeriod is smoothed by a short SimpleMovingAverage of SmoothingLength. Whenever the smoothed series prints FlatBars consecutive non-increasing values the market is treated as flat and the order flags are re-armed.
Channel construction – Once a flat is confirmed, the strategy requests the highest high and lowest low over the last max(ChannelLookback, FlatBars + 1) candles using the built-in Highest/Lowest indicators. The channel height is filtered by ChannelMinPips/ChannelMaxPips after converting pips into price units through PipSize (or the detected tick size when the parameter is left at zero).
Pending orders – If the current position is flat and trading is allowed, the strategy submits a buy stop at high + IndentPips and a sell stop at low − IndentPips. Each order remembers the protective levels that were calculated at submission time.
Breakout execution – When a pending order fills, the opposite pending order is cancelled automatically. The filled price becomes the entry anchor for trailing-stop logic and the memorised stop-loss / take-profit distances are activated.
Position management – The active position is supervised on every completed candle. If price touches the stop-loss or take-profit level the strategy issues a market exit. When TrailingStopPips is greater than zero the stop is pulled forward once the close price moves at least TrailingStopPips + TrailingStepPips away from the fill price.
Session filter – When UseTradingHours is enabled the breakout logic only runs between StartHour (inclusive) and EndHour (exclusive). Overnight sessions are supported by allowing StartHour > EndHour.
Risk management
Dynamic or fixed protection – Set StopLossPips / TakeProfitPips to positive values to use fixed distances (in pips). Keeping them at zero switches to dynamic sizing based on the channel height and the DynamicStopMultiplier / DynamicTakeMultiplier coefficients.
Trailing stop – Enable TrailingStopPips to follow the move once the trade is in profit. The trailing logic respects TrailingStepPips to avoid micro adjustments.
Position cap – MaxPositions limits the aggregated exposure to MaxPositions × TradeVolume. If that threshold is reached no new pending orders are submitted until the exposure decreases.
Directional filters – UseBuy and UseSell allow the strategy to operate in breakout-only, breakdown-only or bi-directional modes.
Parameters
Parameter
Default
Description
TradeVolume
1
Volume submitted with every pending order.
PipSize
0.0001
Manual pip size override. Leave at zero to use the security tick size (with automatic 3/5-digit adjustment).
StdDevPeriod
46
Lookback for the base StandardDeviation.
SmoothingLength
3
Moving average length applied to the volatility series.
FlatBars
3
Number of consecutive non-increasing smoothed volatility values required to re-arm breakout orders.
ChannelLookback
5
Candles used to measure the highest high and lowest low once a flat is detected. Automatically compared with FlatBars + 1.
ChannelMinPips
15
Minimum channel height (in pips). Set to 0 to disable the lower bound.
ChannelMaxPips
105
Maximum channel height (in pips). Set to 0 to disable the upper bound.
DynamicStopMultiplier
1
Channel-height multiplier used for dynamic stop-loss calculation when StopLossPips = 0.
DynamicTakeMultiplier
1
Channel-height multiplier used for dynamic take-profit calculation when TakeProfitPips = 0.
StopLossPips
0
Fixed stop-loss distance in pips. Overrides the dynamic formula when positive.
TakeProfitPips
0
Fixed take-profit distance in pips. Overrides the dynamic formula when positive.
IndentPips
0
Additional offset (in pips) added beyond the channel boundaries for pending orders.
TrailingStopPips
5
Trailing stop distance in pips. Set to 0 to disable trailing.
TrailingStepPips
5
Minimum step (in pips) required to move the trailing stop.
UseBuy
true
Enable long (buy stop) breakouts.
UseSell
true
Enable short (sell stop) breakouts.
MaxPositions
5
Maximum number of base volumes allowed in the aggregated position.
UseTradingHours
true
Enable the trading session filter.
StartHour
0
Session start hour (inclusive).
EndHour
23
Session end hour (exclusive).
CandleType
H1
Candle series used for calculations (defaults to 1-hour time frame).
Notes
The strategy operates exclusively on completed candles via the high-level SubscribeCandles().Bind(...) API, matching the deterministic behaviour expected from the original EA.
Protective prices are normalised through Security.ShrinkPrice to respect exchange tick sizes.
When both pending orders are active and one of them fills, the opposite order is cancelled immediately so that only one breakout position can be open at a time.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Flat Channel Breakout strategy using EMA crossover.
/// Buys when fast EMA crosses above slow EMA, sells on reverse.
/// </summary>
public class FlatChannelBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private ExponentialMovingAverage _fast;
private ExponentialMovingAverage _slow;
private decimal _prevFast;
private decimal _prevSlow;
private decimal _entryPrice;
private int _cooldown;
public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
public FlatChannelBreakoutStrategy()
{
_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
protected override void OnReseted()
{
base.OnReseted();
_fast = null; _slow = null;
_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fast = new ExponentialMovingAverage { Length = FastPeriod };
_slow = new ExponentialMovingAverage { Length = SlowPeriod };
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(_fast, _slow, ProcessCandle);
subscription.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished) return;
if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }
var close = candle.ClosePrice;
var step = Security?.PriceStep ?? 1m;
if (Position > 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
}
else if (Position < 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
}
if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }
_prevFast = fastValue; _prevSlow = slowValue;
}
}
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 flat_channel_breakout_strategy(Strategy):
def __init__(self):
super(flat_channel_breakout_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 14) \
.SetDisplay("Fast Period", "Fast MA period", "Indicator")
self._slow_period = self.Param("SlowPeriod", 50) \
.SetDisplay("Slow Period", "Slow MA period", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", 200) \
.SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 400) \
.SetDisplay("Take Profit", "Take-profit in price steps", "Risk")
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
@property
def fast_period(self):
return self._fast_period.Value
@property
def slow_period(self):
return self._slow_period.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
def OnReseted(self):
super(flat_channel_breakout_strategy, self).OnReseted()
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(flat_channel_breakout_strategy, self).OnStarted2(time)
self._fast = ExponentialMovingAverage()
self._fast.Length = self.fast_period
self._slow = ExponentialMovingAverage()
self._slow.Length = self.slow_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(5)))
subscription.Bind(self._fast, self._slow, self._process_candle)
subscription.Start()
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
slow_val = float(slow_value)
if not self._fast.IsFormed or not self._slow.IsFormed:
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_fast = fast_val
self._prev_slow = slow_val
return
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if self.Position > 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close <= self._entry_price - self.stop_loss_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close >= self._entry_price + self.take_profit_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
elif self.Position < 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close >= self._entry_price + self.stop_loss_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close <= self._entry_price - self.take_profit_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._prev_fast <= self._prev_slow and fast_val > slow_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 100
elif self._prev_fast >= self._prev_slow and fast_val < slow_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
def CreateClone(self):
return flat_channel_breakout_strategy()