View on GitHub

The Horizontal Line Levels strategy emulates the MetaTrader 5 expert advisor of the same name. It continuously rebuilds two price levels around the current quote and notifies the user once the market crosses them. The implementation relies on Level1 (bid/ask) market data, mimicking the original OnTick/OnTimer workflow without submitting any orders.

Core Idea

  1. Subscribe to Level1 data and cache the latest best bid and best ask prices.
  2. Convert the MetaTrader point distance to the StockSharp price scale.
  3. Offset the best ask upward and the best bid downward by the configured distance, creating two virtual horizontal lines.
  4. Periodically check (via an internal timer) whether the bid or ask crosses those reference levels and log alerts in the strategy journal.

Parameters

Name Default Description
TimerPeriodMinutes 1 Minutes between two consecutive timer checks. Must stay positive.
OffsetPoints 50 Distance in MetaTrader points applied above the ask and below the bid when constructing the lines.

Behavior Details

  • Data subscription: GetWorkingSecurities registers a Level1 stream so the strategy receives bid/ask updates even without candles.
  • Initialization: The first time both best bid and best ask are available, RecalculateLevels stores the current upper and lower horizontal levels.
  • Timer: Each timer tick recreates missing levels (if initialization happened before quotes were ready) and emits log messages once the market breaches either bound.
  • MetaTrader point translation: The helper EnsurePointSize converts MetaTrader "points" into absolute price increments using the Security.PriceStep. The same technique is used in other converted strategies to maintain numeric compatibility.
  • No trading: The strategy never sends orders; it only produces alerts through AddInfoLog. This matches the original expert which displayed pop-up alerts when the price touched either line.
  • Stop/Reset: Stopping the strategy cancels the timer and clears all cached values so the next run starts from a clean state.

Typical Usage

  1. Attach the strategy to the desired instrument and set TimerPeriodMinutes and OffsetPoints in the Designer UI.
  2. Start the strategy. Once a full quote snapshot arrives, a log entry such as Horizontal levels updated. Upper: 1.12345, Lower: 1.12245. confirms the calculated thresholds.
  3. Watch the log window. When the ask rallies above the upper level (or the bid drops below the lower level) the strategy prints the corresponding alert message.
  4. If you change the offset or restart the strategy, the levels are recomputed using the new parameters.

Classification

  • Category: Utilities / Alerts
  • Trading Direction: None
  • Execution Style: Event-driven monitoring
  • Data Requirements: Level1 bid/ask
  • Complexity: Basic
  • Recommended Timeframe: Any (purely quote-driven)
  • Risk Management: Not applicable (no positions opened)

This conversion keeps the alert-centric behavior of the MetaTrader original while leveraging StockSharp abstractions such as strategy timers and Level1 subscriptions.

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;

public class HorizontalLineLevelsStrategy : 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 HorizontalLineLevelsStrategy()
	{
		_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;
	}
}