View on GitHub

Regularities of Exchange Rates Strategy

This StockSharp strategy is a faithful C# conversion of the MetaTrader 4 expert advisor Strategy_of_Regularities_of_Exchange_Rates.mq4. The system was designed as a daily breakout straddle: it brackets the market with stop orders when a specific hour arrives and keeps those orders active until the nightly closing hour. Any filled position is supervised by both a broker-side stop-loss and an intraday take-profit watchdog so that trades do not linger beyond the defined trading session.

Unlike indicator-driven systems, the logic focuses solely on time and distance. When the schedule says the market should be ready, the strategy measures a fixed offset in broker points (pips) from the current bid and ask and places a pair of symmetrical stop orders. The code automatically adapts the point calculation to symbols with 3- or 5-digit quotes, matching the behaviour of the original MQL version.

Trading Logic

  1. Opening hour – once a finished candle reports OpeningHour, the strategy cancels any leftover pending orders and submits a buy stop above the current ask and a sell stop below the current bid. The distance is EntryOffsetPoints * point, where the point value is derived from the instrument PriceStep and adjusted for fractional quotes.
  2. Protective orders – immediately after start-up the strategy enables StartProtection with the configured StopLossPoints. Any executed trade therefore receives a broker-side stop-loss identical to the original EA.
  3. Take profit supervision – on every completed candle the algorithm checks whether the current profit exceeds TakeProfitPoints * point. If so, it closes the position at market. This mirrors the original OrderClose loop that exited when profit reached the threshold.
  4. Closing hour – when the clock reaches ClosingHour, the strategy forcefully closes any open positions and cancels the stop orders, ensuring the book is flat for the next session.
  5. Daily reset – a new batch of pending orders is sent only once per trading day, preventing duplicates while still respecting the original intent of a single setup per session.

Parameters

Parameter Default Description
OpeningHour 9 Hour (0–23) when the pair of stop orders is placed.
ClosingHour 2 Hour (0–23) when pending orders are removed and any open trades are flattened.
EntryOffsetPoints 20 Distance in broker points from the current bid/ask to the stop orders.
TakeProfitPoints 20 Profit target in broker points that triggers a market exit. Set to 0 to disable the manual take profit.
StopLossPoints 500 Distance in broker points for the protective stop attached via StartProtection.
OrderVolume 0.1 Volume of each stop order.
CandleType 30 minute time frame Candle series used to evaluate the schedule. Any timeframe ≤ 1 hour keeps the behaviour consistent with the MQL script.

Conversion Notes

  • The original expert advisor worked on tick events and referenced Hour() directly. In StockSharp the strategy listens to finished candles and uses their opening hour, which preserves the once-per-hour logic while staying within the repository guidelines about candle states.
  • Pending orders are normalised with Security.ShrinkPrice so that the generated prices always match the instrument tick size.
  • Stop management delegates to StartProtection, recreating the platform-generated stop-loss that MetaTrader attached during OrderSend.
  • The code tracks the last trading date to avoid resubmitting the same bracket multiple times within the same day, something that could happen on sub-hour timeframes in the original EA.
  • Extensive inline comments clarify each step of the workflow for future maintenance or experimentation.
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>
/// Time-based breakout strategy converted from the "Strategy of Regularities of Exchange Rates" MQL expert advisor.
/// At a scheduled hour captures reference price, then enters on breakout above/below offset levels.
/// Exits at a closing hour or on take-profit/stop-loss hit.
/// </summary>
public class RegularitiesOfExchangeRatesStrategy : Strategy
{
	private readonly StrategyParam<int> _openingHour;
	private readonly StrategyParam<int> _closingHour;
	private readonly StrategyParam<decimal> _entryOffsetPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _dummySma;
	private decimal _pointSize;
	private DateTime? _lastEntryDate;
	private decimal _referencePrice;
	private decimal _entryPrice;
	private bool _waitingForBreakout;

	public int OpeningHour
	{
		get => _openingHour.Value;
		set => _openingHour.Value = value;
	}

	public int ClosingHour
	{
		get => _closingHour.Value;
		set => _closingHour.Value = value;
	}

	public decimal EntryOffsetPoints
	{
		get => _entryOffsetPoints.Value;
		set => _entryOffsetPoints.Value = value;
	}

	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public RegularitiesOfExchangeRatesStrategy()
	{
		_openingHour = Param(nameof(OpeningHour), 9)
			.SetDisplay("Opening Hour", "Hour (0-23) when breakout levels are set", "Schedule")
			.SetRange(0, 23);

		_closingHour = Param(nameof(ClosingHour), 2)
			.SetDisplay("Closing Hour", "Hour (0-23) when the strategy exits", "Schedule")
			.SetRange(0, 23);

		_entryOffsetPoints = Param(nameof(EntryOffsetPoints), 20m)
			.SetDisplay("Entry Offset (points)", "Distance from reference price for breakout", "Orders")
			.SetGreaterThanZero();

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 20m)
			.SetDisplay("Take Profit (points)", "Profit target distance in points", "Risk")
			.SetNotNegative();

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
			.SetDisplay("Stop Loss (points)", "Stop-loss distance in points", "Risk")
			.SetGreaterThanZero();

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to evaluate trading hours", "General");
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_pointSize = 0m;
		_lastEntryDate = null;
		_referencePrice = 0m;
		_entryPrice = 0m;
		_waitingForBreakout = false;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_pointSize = Security?.PriceStep ?? 0.01m;
		if (_pointSize <= 0m)
			_pointSize = 0.01m;

		_dummySma = new SimpleMovingAverage { Length = 2 };

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

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

		var hour = candle.OpenTime.Hour;
		var close = candle.ClosePrice;
		var high = candle.HighPrice;
		var low = candle.LowPrice;

		// At closing hour: flatten position and cancel breakout watch
		if (hour == ClosingHour)
		{
			if (Position > 0)
				SellMarket(Position);
			else if (Position < 0)
				BuyMarket(-Position);

			_waitingForBreakout = false;
			_entryPrice = 0m;
		}

		// Manage take-profit and stop-loss for existing position
		if (Position != 0 && _entryPrice > 0m)
		{
			var tp = TakeProfitPoints * _pointSize;
			var sl = StopLossPoints * _pointSize;

			if (Position > 0)
			{
				if ((tp > 0m && close - _entryPrice >= tp) || (sl > 0m && _entryPrice - close >= sl))
				{
					SellMarket(Position);
					_entryPrice = 0m;
					_waitingForBreakout = false;
				}
			}
			else if (Position < 0)
			{
				if ((tp > 0m && _entryPrice - close >= tp) || (sl > 0m && close - _entryPrice >= sl))
				{
					BuyMarket(-Position);
					_entryPrice = 0m;
					_waitingForBreakout = false;
				}
			}
		}

		// At opening hour: set reference price for breakout
		if (hour == OpeningHour && Position == 0)
		{
			var date = candle.OpenTime.Date;
			if (!_lastEntryDate.HasValue || _lastEntryDate.Value != date)
			{
				_referencePrice = close;
				_waitingForBreakout = true;
				_lastEntryDate = date;
			}
		}

		// Check for breakout entry
		if (_waitingForBreakout && Position == 0 && _referencePrice > 0m)
		{
			var offset = EntryOffsetPoints * _pointSize;
			var buyLevel = _referencePrice + offset;
			var sellLevel = _referencePrice - offset;

			if (high >= buyLevel)
			{
				BuyMarket();
				_entryPrice = close;
				_waitingForBreakout = false;
			}
			else if (low <= sellLevel)
			{
				SellMarket();
				_entryPrice = close;
				_waitingForBreakout = false;
			}
		}
	}
}