View on GitHub

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

  1. 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.
  2. 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.
  3. 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.
  4. 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;
	}
}