Twenty Pips Price Channel Strategy
Overview
The Twenty Pips Price Channel Strategy is a conversion of the original MetaTrader expert advisor 20 pips that combines a Donchian-style price channel with short-term moving-average filters. The algorithm opens trades only when the current candle opens opposite to the previous one, filters direction with moving averages calculated on typical prices, and manages exits through a fixed twenty-pip target supported by a dynamic channel-based trailing stop.
The StockSharp version keeps the spirit of the original approach while adapting order management to the high-level API. Market orders are used for entries and exits, profit targets are monitored internally, and stop levels are emulated with price-channel conditions.
Trading Logic
Indicator stack
- A one-period simple moving average of the typical price (H+L+C)/3 acts as a fast baseline that mirrors the previous candle's typical price.
- A configurable slow simple moving average (default 20) calculated on closing prices plays the role of the
MA_Low filter from the EA.
- Highest and lowest indicators with the same period as the price channel (default 20) emulate the original custom indicator buffers.
Entry conditions
- Long setup: the previous fast typical price is above the previous slow moving average and the current candle opens below the previous open. After a losing trade the volume is multiplied by the recovery factor (default 2). The entry price is recorded to track profit and loss.
- Short setup: the previous fast typical price is below the previous slow moving average and the current candle opens above the previous open. Volume scaling follows the same recovery logic as for long trades.
Exit management
- A fixed take-profit target equal to
TakeProfitPips multiplied by the instrument price step is placed when the position opens.
- A channel-driven trailing stop mimics the original
OrderModify call. When the previous bar breaks beyond the price channel (two-bar shift from the MT4 logic), the protective stop is moved to the previous extreme minus/plus the trailing offset in pips. If the next candle gaps beyond that extreme, the position exits immediately at the open price.
- Take-profit, trailing stop, and gap exits are all executed through market orders while tracking the actual exit price to update the win/loss flag for the martingale-style scaling.
Martingale recovery
- After every closed losing position, the next entry size is multiplied by
RecoveryMultiplier. Profitable trades reset the flag and revert to the base volume.
Parameters
| Name |
Description |
Default |
CandleType |
Primary timeframe used for calculations. |
1 hour candles |
ChannelPeriod |
Lookback period for the Donchian-style channel. |
20 |
SlowMaPeriod |
Length of the slow moving average filter. |
20 |
TakeProfitPips |
Distance in pips for the fixed profit target. |
20 |
TrailingOffsetPips |
Offset used when tightening the stop to the previous extreme. |
10 |
RecoveryMultiplier |
Volume multiplier applied after a loss. |
2 |
Volume |
Base trading volume before recovery scaling. |
0.1 |
Usage Notes
- The strategy expects
Security.PriceStep to reflect the pip value of the traded instrument. Adjust TakeProfitPips and TrailingOffsetPips if the symbol uses a different pip definition.
- Because StockSharp uses market orders for exits, backtests may show slippage compared to the original MT4 stop and limit orders. The logic still reproduces the same price thresholds.
- The channel values are shifted to emulate the
iCustom(..., shift=2) calls; keep this in mind when modifying the trailing behaviour.
- The recovery multiplier can be set to 1 to disable martingale-style scaling.
using System;
using System.Linq;
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>
/// Converted "20 pips" price channel strategy.
/// Uses Donchian channel breakouts with MA filter, trailing stop, and recovery multiplier.
/// </summary>
public class TwentyPipsPriceChannelStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _channelPeriod;
private readonly StrategyParam<int> _slowMaPeriod;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<decimal> _takeProfit;
private readonly List<decimal> _highs = new();
private readonly List<decimal> _lows = new();
private decimal? _prevChannelUpper;
private decimal? _prevChannelLower;
/// <summary>
/// Candle type to process.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Donchian channel lookback period.
/// </summary>
public int ChannelPeriod
{
get => _channelPeriod.Value;
set => _channelPeriod.Value = value;
}
/// <summary>
/// Slow moving average length.
/// </summary>
public int SlowMaPeriod
{
get => _slowMaPeriod.Value;
set => _slowMaPeriod.Value = value;
}
/// <summary>
/// Stop loss distance in absolute price units.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Take profit distance in absolute price units.
/// </summary>
public decimal TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="TwentyPipsPriceChannelStrategy"/>.
/// </summary>
public TwentyPipsPriceChannelStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary candle type", "General");
_channelPeriod = Param(nameof(ChannelPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Channel Period", "Donchian channel lookback", "Parameters");
_slowMaPeriod = Param(nameof(SlowMaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Slow MA Period", "Slow moving average length", "Parameters");
_stopLoss = Param(nameof(StopLoss), 500m)
.SetNotNegative()
.SetDisplay("Stop Loss", "Stop loss distance", "Risk");
_takeProfit = Param(nameof(TakeProfit), 500m)
.SetNotNegative()
.SetDisplay("Take Profit", "Take profit distance", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return new[] { (Security, CandleType) };
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highs.Clear();
_lows.Clear();
_prevChannelUpper = null;
_prevChannelLower = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
var slowMa = new SMA { Length = SlowMaPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(slowMa, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, slowMa);
DrawOwnTrades(area);
}
// Use StartProtection for SL/TP
var tp = TakeProfit > 0 ? new Unit(TakeProfit, UnitTypes.Absolute) : null;
var sl = StopLoss > 0 ? new Unit(StopLoss, UnitTypes.Absolute) : null;
StartProtection(tp, sl);
base.OnStarted2(time);
}
private void ProcessCandle(ICandleMessage candle, decimal slowMaValue)
{
if (candle.State != CandleStates.Finished)
return;
// Track highs and lows for manual Donchian channel
_highs.Add(candle.HighPrice);
_lows.Add(candle.LowPrice);
while (_highs.Count > ChannelPeriod)
_highs.RemoveAt(0);
while (_lows.Count > ChannelPeriod)
_lows.RemoveAt(0);
if (_highs.Count < ChannelPeriod)
{
_prevChannelUpper = null;
_prevChannelLower = null;
return;
}
var channelUpper = _highs.Max();
var channelLower = _lows.Min();
if (!IsFormedAndOnlineAndAllowTrading())
{
_prevChannelUpper = channelUpper;
_prevChannelLower = channelLower;
return;
}
// Channel breakout with MA filter
if (_prevChannelUpper.HasValue && _prevChannelLower.HasValue)
{
// Breakout above the previous channel high -> buy signal
if (candle.ClosePrice > _prevChannelUpper.Value && candle.ClosePrice > slowMaValue && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(Volume);
}
// Breakout below the previous channel low -> sell signal
else if (candle.ClosePrice < _prevChannelLower.Value && candle.ClosePrice < slowMaValue && Position >= 0)
{
if (Position > 0)
SellMarket(Position);
SellMarket(Volume);
}
}
_prevChannelUpper = channelUpper;
_prevChannelLower = channelLower;
}
}
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, UnitTypes, Unit
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class twenty_pips_price_channel_strategy(Strategy):
"""Donchian channel breakout with SMA filter and StartProtection SL/TP."""
def __init__(self):
super(twenty_pips_price_channel_strategy, self).__init__()
self._channel_period = self.Param("ChannelPeriod", 20).SetGreaterThanZero().SetDisplay("Channel Period", "Donchian channel lookback", "Parameters")
self._slow_ma_period = self.Param("SlowMaPeriod", 20).SetGreaterThanZero().SetDisplay("Slow MA Period", "Slow MA length", "Parameters")
self._sl = self.Param("StopLoss", 500).SetNotNegative().SetDisplay("Stop Loss", "SL distance", "Risk")
self._tp = self.Param("TakeProfit", 500).SetNotNegative().SetDisplay("Take Profit", "TP distance", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).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(twenty_pips_price_channel_strategy, self).OnReseted()
self._highs = []
self._lows = []
self._prev_upper = None
self._prev_lower = None
def OnStarted2(self, time):
super(twenty_pips_price_channel_strategy, self).OnStarted2(time)
self._highs = []
self._lows = []
self._prev_upper = None
self._prev_lower = None
slow_ma = SimpleMovingAverage()
slow_ma.Length = self._slow_ma_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(slow_ma, self.OnProcess).Start()
sl_val = float(self._sl.Value)
tp_val = float(self._tp.Value)
tp = Unit(tp_val, UnitTypes.Absolute) if tp_val > 0 else None
sl = Unit(sl_val, UnitTypes.Absolute) if sl_val > 0 else None
self.StartProtection(tp, sl)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, slow_ma)
self.DrawOwnTrades(area)
def OnProcess(self, candle, slow_ma_val):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
ma_val = float(slow_ma_val)
period = self._channel_period.Value
self._highs.append(high)
self._lows.append(low)
while len(self._highs) > period:
self._highs.pop(0)
while len(self._lows) > period:
self._lows.pop(0)
if len(self._highs) < period:
self._prev_upper = None
self._prev_lower = None
return
ch_upper = max(self._highs)
ch_lower = min(self._lows)
if self._prev_upper is not None and self._prev_lower is not None and self.IsFormedAndOnlineAndAllowTrading():
if close > self._prev_upper and close > ma_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket(abs(self.Position))
self.BuyMarket(self.Volume)
elif close < self._prev_lower and close < ma_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket(self.Position)
self.SellMarket(self.Volume)
self._prev_upper = ch_upper
self._prev_lower = ch_lower
def CreateClone(self):
return twenty_pips_price_channel_strategy()