View on GitHub

Gazonkos Rollback Strategy

Overview

The Gazonkos Rollback Strategy is a conversion of the original gazonkos MetaTrader 5 expert advisor. The approach trades the EUR/USD hourly chart and looks for strong momentum between two historical closes. After detecting that momentum, it waits for a pullback of a predefined size and then enters in the direction of the initial move. The StockSharp implementation keeps the same staged state machine as the source code while using the high-level API with candle subscriptions and protective orders.

Trading Logic

  1. Eligibility check – only one position per hour is allowed. If another trade was opened during the same clock hour, or if the configured number of simultaneous trades is already running, the strategy waits.
  2. Momentum detection – compares the closing prices of two past candles (SecondShift minus FirstShift). If the difference exceeds Delta, the strategy records the intended direction (long if the newer close is higher, short otherwise).
  3. Rollback tracking – from the moment the momentum appears, the code monitors the highest high (for long setups) or lowest low (for short setups) reached during that hour. When price pulls back by at least Rollback, the setup becomes eligible for execution. If the hour changes before the pullback happens, the signal is discarded.
  4. Order execution – once the rollback condition is met, the strategy places a market order with fixed take profit and stop loss distances. Position sizing is controlled through the TradeVolume parameter, and the built-in StartProtection helper manages the protective orders.

This sequence closely mirrors the MT5 version that used STATE and Trade variables to coordinate the workflow.

Risk Management

  • StartProtection configures fixed take profit and stop loss distances in absolute price units, similar to how the expert attached TP/SL to each order.
  • ActiveTrades limits the maximum total exposure by comparing the absolute position value to the product of the configured volume and allowed trade count.
  • The combination of hourly gating and rollback confirmation reduces over-trading during sideways conditions.

Parameters

Name Default Description
TakeProfit 0.0016 Absolute distance (in price units) for the take profit. Matches 16 points on a 5-digit EUR/USD quote.
Rollback 0.0016 Required pullback from the extreme reached after the momentum signal.
StopLoss 0.0040 Absolute distance for the protective stop loss. Equivalent to 40 points on EUR/USD.
Delta 0.0040 Minimum difference between the two historical closes that defines a strong move.
TradeVolume 0.1 Default order volume passed to BuyMarket() and SellMarket().
FirstShift 3 Older bar index (number of candles back) used for the closing price comparison.
SecondShift 2 Newer bar index used in the closing price comparison.
ActiveTrades 1 Maximum number of simultaneous trades. Set to zero to disable the limit.
CandleType 1 hour time frame Candle series used for analysis; defaults to hourly candles like the source EA.

Notes

  • The strategy works with any instrument that has a reasonable tick size; adjust Delta, Rollback, TakeProfit, and StopLoss to match the instrument's point value.
  • All inline comments are written in English as required by the project guidelines.
  • No Python port is provided for this strategy yet.
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>
/// Momentum breakout with rollback confirmation inspired by the gazonkos MT5 expert.
/// The strategy waits for a spread between two historical closes, then joins the trend after a pullback.
/// </summary>
public class GazonkosStrategy : Strategy
{
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _rollback;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _delta;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _firstShift;
	private readonly StrategyParam<int> _secondShift;
	private readonly StrategyParam<int> _activeTrades;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closeHistory = new();

	private int _state;
	private int _tradeDirection;
	private decimal _maxPrice;
	private decimal _minPrice;
	private bool _canTrade;
	private int _lastTradeHour;
	private int _lastSignalHour;
	private int _maxHistory;

	/// <summary>
	/// Take profit distance expressed in absolute price units.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Rollback distance that confirms the entry.
	/// </summary>
	public decimal Rollback
	{
		get => _rollback.Value;
		set => _rollback.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in absolute price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Minimum difference between historical closes to detect momentum.
	/// </summary>
	public decimal Delta
	{
		get => _delta.Value;
		set => _delta.Value = value;
	}

	/// <summary>
	/// Default volume for market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Older bar shift used in the close difference calculation.
	/// </summary>
	public int FirstShift
	{
		get => _firstShift.Value;
		set => _firstShift.Value = value;
	}

	/// <summary>
	/// Recent bar shift used in the close difference calculation.
	/// </summary>
	public int SecondShift
	{
		get => _secondShift.Value;
		set => _secondShift.Value = value;
	}

	/// <summary>
	/// Maximum simultaneous trades counted in volume units.
	/// </summary>
	public int ActiveTrades
	{
		get => _activeTrades.Value;
		set => _activeTrades.Value = value;
	}

	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public GazonkosStrategy()
	{
		_takeProfit = Param(nameof(TakeProfit), 700m)
			.SetDisplay("Take Profit", "Take profit distance in price units", "Risk Management")
			;

		_rollback = Param(nameof(Rollback), 300m)
			.SetDisplay("Rollback", "Required pullback before entering", "Signals")
			;

		_stopLoss = Param(nameof(StopLoss), 1000m)
			.SetDisplay("Stop Loss", "Stop loss distance in price units", "Risk Management")
			;

		_delta = Param(nameof(Delta), 200m)
			.SetDisplay("Delta", "Minimum difference between closes", "Signals")
			;

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetDisplay("Trade Volume", "Default volume for market orders", "Orders")
			;

		_firstShift = Param(nameof(FirstShift), 3)
			.SetDisplay("First Shift", "Older close shift for the comparison", "Signals")
			;

		_secondShift = Param(nameof(SecondShift), 2)
			.SetDisplay("Second Shift", "Recent close shift for the comparison", "Signals")
			;

		_activeTrades = Param(nameof(ActiveTrades), 1)
			.SetDisplay("Active Trades", "Maximum simultaneous trades", "Risk Management")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Candle series used for signals", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_state = 0;
		_tradeDirection = 0;
		_maxPrice = 0m;
		_minPrice = decimal.MaxValue;
		_canTrade = true;
		_lastTradeHour = -1;
		_lastSignalHour = -1;
		_closeHistory.Clear();
		UpdateHistorySize();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = TradeVolume;
		UpdateHistorySize();

		StartProtection(
			takeProfit: new Unit(TakeProfit, UnitTypes.Absolute),
			stopLoss: new Unit(StopLoss, UnitTypes.Absolute),
			isStopTrailing: false,
			useMarketOrders: true);

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		UpdateHistorySize();
		AddClose(candle.ClosePrice);

		var hour = candle.CloseTime.Hour;

		if (_state == 0)
		{
			// Evaluate if another trade can be started during the current hour.
			_canTrade = true;

			if (_lastTradeHour == hour)
				_canTrade = false;

			if (ActiveTrades > 0 && Volume > 0 && Math.Abs(Position) >= ActiveTrades * Volume)
				_canTrade = false;

			if (_canTrade)
				_state = 1;
		}

		if (_state == 1)
		{
			// Look for momentum using the difference between historical closes.
			if (!TryGetClose(FirstShift, out var closeFirst) || !TryGetClose(SecondShift, out var closeSecond))
				return;

			if (closeSecond - closeFirst > Delta)
			{
				_tradeDirection = 1;
				_maxPrice = candle.ClosePrice;
				_lastSignalHour = hour;
				_state = 2;
			}
			else if (closeFirst - closeSecond > Delta)
			{
				_tradeDirection = -1;
				_minPrice = candle.ClosePrice;
				_lastSignalHour = hour;
				_state = 2;
			}
		}

		if (_state == 2)
		{
			// Wait for a rollback confirmation during the same hour when the signal appeared.
			if (_lastSignalHour != hour)
			{
				ResetToIdle();
				return;
			}

			if (_tradeDirection == 1)
			{
				if (candle.HighPrice > _maxPrice)
					_maxPrice = candle.HighPrice;

				if (candle.LowPrice < _maxPrice - Rollback)
					_state = 3;
			}
			else if (_tradeDirection == -1)
			{
				if (candle.LowPrice < _minPrice)
					_minPrice = candle.LowPrice;

				if (candle.HighPrice > _minPrice + Rollback)
					_state = 3;
			}
		}

		if (_state == 3)
		{
			// Execute the trade after rollback confirmation.
			if (_tradeDirection == 1 && Position <= 0)
			{
				BuyMarket();
				_lastTradeHour = hour;
				ResetToIdle();
			}
			else if (_tradeDirection == -1 && Position >= 0)
			{
				SellMarket();
				_lastTradeHour = hour;
				ResetToIdle();
			}
		}
	}

	private void UpdateHistorySize()
	{
		var required = Math.Max(Math.Max(FirstShift, SecondShift) + 1, 1);

		if (_maxHistory == required)
			return;

		_maxHistory = required;

		if (_closeHistory.Count > _maxHistory)
			_closeHistory.RemoveRange(_maxHistory, _closeHistory.Count - _maxHistory);
	}

	private void AddClose(decimal close)
	{
		_closeHistory.Insert(0, close);

		if (_closeHistory.Count > _maxHistory)
			_closeHistory.RemoveAt(_closeHistory.Count - 1);
	}

	private bool TryGetClose(int shift, out decimal close)
	{
		close = 0m;

		if (shift < 0)
			return false;

		if (_closeHistory.Count <= shift)
			return false;

		close = _closeHistory[shift];
		return true;
	}

	private void ResetToIdle()
	{
		_state = 0;
		_tradeDirection = 0;
		_maxPrice = 0m;
		_minPrice = decimal.MaxValue;
		_canTrade = true;
		_lastSignalHour = -1;
	}
}