Auf GitHub ansehen

Laptrend_1 Strategy

Overview

Laptrend_1 reproduces the logic of the MetaTrader expert advisor Laptrend_1.mq4. The strategy blends a multi-timeframe LabTrend channel filter, Fisher Transform momentum confirmation and an ADX trend strength check on 15-minute candles. Orders are opened only when the higher-timeframe (H1) and signal timeframe (M15) LabTrend directions agree, the Fisher transform confirms the move and the ADX shows a strengthening trend. Positions are closed when the momentum reverses, the LabTrend direction changes, or the market transitions into a flat regime where ADX and the DI components converge.

Trading Logic

  • Primary data – 15-minute candles drive entries/exits while 1-hour candles feed the long-term LabTrend filter.
  • LabTrend channel – The code recreates the LabTrend1_v2.1 indicator by building Donchian-style channels over the last ChannelLength bars and narrowing them with the RiskFactor. A close above the upper band marks a bullish trend; a close below the lower band marks a bearish trend. The M15 and H1 trends must align to open trades.
  • Fisher Transform – A custom Fisher Transform (Fisher_Yur4ik) tracks momentum on the M15 timeframe. Crosses through zero flip the bullish/bearish bias, while traversing ±0.25 produces exit signals.
  • ADX filter – The 15-minute Average Directional Index must rise and the dominant DI component has to agree with the proposed trade. When ADX, +DI and –DI fall within Delta points of each other, the strategy treats the market as flat, resets the momentum flags and liquidates open positions.
  • Position management – New positions close any opposite exposure and trade a configurable volume. Exits are triggered by LabTrend reversals, Fisher exits or a flat market condition.

Risk Management

  • Stop Loss / Take Profit – Configurable in instrument points (MetaTrader “pips”). They are evaluated against candle highs/lows to mimic protective orders from the original EA.
  • Trailing Stop – Once the price moves in the trade’s favour, a trailing stop tracks the close at a distance equal to TrailingStopPoints. Crossing the trailing level triggers an immediate market exit.
  • Volume – All orders use the fixed Volume parameter (lots).

Parameters

  • Volume – Order size in lots. Default 1.
  • AdxPeriod – ADX smoothing period. Default 14.
  • FisherLength – Window for the Fisher transform. Default 10.
  • ChannelLength – Bars used for the LabTrend channel. Default 9.
  • RiskFactor – LabTrend channel narrowing factor (original indicator range 1..10). Default 3.
  • Delta – Maximum difference between ADX and DI values before the market is labelled flat. Default 7.
  • StopLossPoints – Stop loss distance in points. Default 100.
  • TakeProfitPoints – Take profit distance in points. Default 40.
  • TrailingStopPoints – Trailing stop distance in points. Default 100.
  • SignalCandleType – Candle series for signal calculations (default M15).
  • TrendCandleType – Candle series for the higher-timeframe LabTrend filter (default H1).

Notes

  • The original MQL implementation worked on every incoming tick; this port processes completed M15 candles, which keeps the logic deterministic while still respecting the indicator calculations.
  • Stop loss, take profit and trailing exits are executed with market orders when the candle’s high/low breaches the configured thresholds. This mirrors the behaviour of MetaTrader protective orders without maintaining explicit stop/limit orders.
  • Ensure that the data source supplies both the 15-minute and 1-hour candle series defined in the parameters before starting the strategy.
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>
/// Conversion of the Laptrend_1 MetaTrader expert advisor.
/// Combines LabTrend channel direction, Fisher transform momentum and ADX filter on multiple timeframes.
/// </summary>
public class Laptrend1Strategy : Strategy
{
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<int> _fisherLength;
	private readonly StrategyParam<int> _channelLength;
	private readonly StrategyParam<decimal> _risk;
	private readonly StrategyParam<decimal> _delta;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<DataType> _signalCandleType;
	private readonly StrategyParam<DataType> _trendCandleType;

	private AverageDirectionalIndex _adx = null!;
	private FisherYur4ikIndicator _fisher = null!;
	private readonly LabTrendState _signalTrend = new();
	private readonly LabTrendState _trendTrend = new();
	private readonly Queue<decimal> _fisherHistory = new();

	private bool _fisherBullish;
	private bool _fisherBearish;
	private bool _fisherExitLong;
	private bool _fisherExitShort;

	private decimal? _previousAdx;
	private decimal _pointValue;

	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;
	private int _lastPositionSign;
	private bool _pendingClose;
	private int _candleIndex;
	private int _lastCloseCandle;


	/// <summary>
	/// ADX calculation period.
	/// </summary>
	public int AdxPeriod
	{
		get => _adxPeriod.Value;
		set => _adxPeriod.Value = value;
	}

	/// <summary>
	/// Fisher transform length.
	/// </summary>
	public int FisherLength
	{
		get => _fisherLength.Value;
		set => _fisherLength.Value = value;
	}

	/// <summary>
	/// LabTrend channel lookback.
	/// </summary>
	public int ChannelLength
	{
		get => _channelLength.Value;
		set => _channelLength.Value = value;
	}

	/// <summary>
	/// LabTrend risk factor (1..10 in the original code).
	/// </summary>
	public decimal RiskFactor
	{
		get => _risk.Value;
		set => _risk.Value = value;
	}

	/// <summary>
	/// Maximum distance between ADX and DI values before the market is considered flat.
	/// </summary>
	public decimal Delta
	{
		get => _delta.Value;
		set => _delta.Value = value;
	}

	/// <summary>
	/// Stop loss in points (MetaTrader style).
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit in points (MetaTrader style).
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop in points (MetaTrader style).
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Candle type used for signal calculations (default 15 minutes).
	/// </summary>
	public DataType SignalCandleType
	{
		get => _signalCandleType.Value;
		set => _signalCandleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe candle type for the LabTrend filter (default 1 hour).
	/// </summary>
	public DataType TrendCandleType
	{
		get => _trendCandleType.Value;
		set => _trendCandleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="Laptrend1Strategy"/>.
	/// </summary>
	public Laptrend1Strategy()
	{

		_adxPeriod = Param(nameof(AdxPeriod), 14)
		.SetGreaterThanZero()
		.SetDisplay("ADX Period", "Average Directional Index length", "Indicators");

		_fisherLength = Param(nameof(FisherLength), 10)
		.SetGreaterThanZero()
		.SetDisplay("Fisher Length", "Fisher transform window", "Indicators");

		_channelLength = Param(nameof(ChannelLength), 9)
		.SetGreaterThanZero()
		.SetDisplay("Channel Length", "LabTrend channel lookback", "Indicators");

		_risk = Param(nameof(RiskFactor), 3m)
		.SetGreaterThanZero()
		.SetDisplay("Risk Factor", "LabTrend risk factor", "Indicators");

		_delta = Param(nameof(Delta), 7m)
		.SetGreaterThanZero()
		.SetDisplay("ADX Delta", "Maximum spread between ADX and DI before flat exit", "Filters");

		_stopLossPoints = Param(nameof(StopLossPoints), 100m)
		.SetNotNegative()
		.SetDisplay("Stop Loss", "Stop loss distance in points", "Risk Management");

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

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 100m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop", "Trailing stop distance in points", "Risk Management");

		_signalCandleType = Param(nameof(SignalCandleType), TimeSpan.FromMinutes(15).TimeFrame())
		.SetDisplay("Signal Candle", "Primary timeframe for signals", "General");

		_trendCandleType = Param(nameof(TrendCandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Trend Candle", "Higher timeframe for LabTrend filter", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, SignalCandleType);
		yield return (Security, TrendCandleType);
	}

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

		_adx = null!;
		_fisher = null!;
		_signalTrend.Reset();
		_trendTrend.Reset();
		_fisherHistory.Clear();
		_fisherBullish = false;
		_fisherBearish = false;
		_fisherExitLong = false;
		_fisherExitShort = false;
		_previousAdx = null;
		_pointValue = 0m;
		ResetLongState();
		ResetShortState();
		_lastPositionSign = 0;
		_longEntryPrice = null;
		_shortEntryPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_pendingClose = false;
		_candleIndex = 0;
		_lastCloseCandle = 0;
	}

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

		_pointValue = Security?.PriceStep ?? 0m;
		if (_pointValue <= 0m)
			_pointValue = 1m;

		_fisherHistory.Clear();
		_fisherBullish = false;
		_fisherBearish = false;
		_fisherExitLong = false;
		_fisherExitShort = false;
		_previousAdx = null;
		ResetLongState();
		ResetShortState();
		_lastPositionSign = Math.Sign(Position);
		_pendingClose = false;
		_candleIndex = 0;
		_lastCloseCandle = -20;

		_fisher = new FisherYur4ikIndicator
		{
			Length = FisherLength
		};

		_adx = new AverageDirectionalIndex
		{
			Length = AdxPeriod
		};

		var signalSubscription = SubscribeCandles(SignalCandleType);
		signalSubscription.BindEx(_fisher, _adx, ProcessSignalCandle).Start();

		var trendSubscription = SubscribeCandles(TrendCandleType);
		trendSubscription.Bind(ProcessTrendCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, signalSubscription);
			DrawIndicator(area, _fisher);
			DrawOwnTrades(area);
		}
	}

	private void ProcessSignalCandle(ICandleMessage candle, IIndicatorValue fisherValue, IIndicatorValue adxValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_candleIndex++;

		// Update LabTrend state on the signal timeframe.
		_signalTrend.Process(candle, ChannelLength, RiskFactor);

		// Keep Fisher state in sync whenever a final value is available.
		if (fisherValue.IsFinal && _fisher.IsFormed)
		{
			var fisher = fisherValue.GetValue<decimal>();
			UpdateFisherFlags(fisher);
		}

		var canTrade = IsFormedAndOnlineAndAllowTrading();

		if (!adxValue.IsFinal || !_adx.IsFormed || adxValue is not AverageDirectionalIndexValue adxData ||
			adxData.MovingAverage is not decimal adxCurrent ||
			adxData.Dx.Plus is not decimal plusDi ||
			adxData.Dx.Minus is not decimal minusDi)
		{
			return;
		}

		var previousAdx = _previousAdx;
		_previousAdx = adxCurrent;

		var adxRising = previousAdx.HasValue && adxCurrent > previousAdx.Value;
		var bullDirectional = plusDi > minusDi;
		var bearDirectional = minusDi > plusDi;

		var flat = Math.Abs(plusDi - minusDi) < Delta &&
			Math.Abs(adxCurrent - plusDi) < Delta &&
			Math.Abs(adxCurrent - minusDi) < Delta;

		if (flat && canTrade)
		{
			// Reset momentum flags and close any open trades in ranging conditions.
			_fisherBullish = false;
			_fisherBearish = false;
			_fisherExitLong = false;
			_fisherExitShort = false;

			if (Position > 0)
			{
				_lastCloseCandle = _candleIndex;
				SellMarket(Position);
			}
			else if (Position < 0)
			{
				_lastCloseCandle = _candleIndex;
				BuyMarket(Math.Abs(Position));
			}

			return;
		}

		if (canTrade)
		{
			const int CooldownCandles = 20;
			var cooldownOk = (_candleIndex - _lastCloseCandle) >= CooldownCandles;

			if (Position == 0)
			{
				// Only enter when flat, cooldown has elapsed, and ADX is strong enough
				var adxStrong = adxCurrent >= 20m;
				if (cooldownOk && adxStrong && _trendTrend.IsUpTrend && _signalTrend.IsUpTrend && _fisherBullish && bullDirectional && adxRising)
				{
					_fisherBullish = false;
					BuyMarket(Volume);
					return;
				}
				else if (cooldownOk && adxStrong && _trendTrend.IsDownTrend && _signalTrend.IsDownTrend && _fisherBearish && bearDirectional && adxRising)
				{
					_fisherBearish = false;
					SellMarket(Volume);
					return;
				}
			}
			else if (Position > 0)
			{
				if (_fisherExitLong)
				{
					_fisherExitLong = false;
					_lastCloseCandle = _candleIndex;
					SellMarket(Position);
					return;
				}
			}
			else // Position < 0
			{
				if (_fisherExitShort)
				{
					_fisherExitShort = false;
					_lastCloseCandle = _candleIndex;
					BuyMarket(Math.Abs(Position));
					return;
				}
			}
		}

		ManagePosition(candle, canTrade);
	}

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

		// Track the higher timeframe trend for directional filtering.
		_trendTrend.Process(candle, ChannelLength, RiskFactor);
	}

	private void ManagePosition(ICandleMessage candle, bool canTrade)
	{
		var position = Position;
		var positionSign = Math.Sign(position);

		if (!canTrade)
		{
			_lastPositionSign = positionSign;
			return;
		}

		// If we submitted a close order, wait until position is actually flat.
		if (_pendingClose)
		{
			if (positionSign == 0)
				_pendingClose = false;
			else
			{
				_lastPositionSign = positionSign;
				return;
			}
		}

		var step = _pointValue > 0m ? _pointValue : 1m;
		var stopOffset = StopLossPoints > 0m ? StopLossPoints * step : 0m;
		var takeOffset = TakeProfitPoints > 0m ? TakeProfitPoints * step : 0m;
		var trailingOffset = TrailingStopPoints > 0m ? TrailingStopPoints * step : 0m;

		if (positionSign > 0)
		{
			// Capture the entry price when switching from short or flat to long.
			if (_lastPositionSign <= 0)
			{
				_longEntryPrice = candle.ClosePrice;
				_longTrailingStop = trailingOffset > 0m ? candle.ClosePrice - trailingOffset : null;
				ResetShortState();
			}

			if (_longEntryPrice.HasValue)
			{
				var entry = _longEntryPrice.Value;
				var volume = position;

				if (stopOffset > 0m && candle.LowPrice <= entry - stopOffset)
				{
					SellMarket(volume);
					ResetLongState();
					_lastPositionSign = 0;
					_pendingClose = true;
					_lastCloseCandle = _candleIndex;
					return;
				}

				if (takeOffset > 0m && candle.HighPrice >= entry + takeOffset)
				{
					SellMarket(volume);
					ResetLongState();
					_lastPositionSign = 0;
					_pendingClose = true;
					_lastCloseCandle = _candleIndex;
					return;
				}

				if (trailingOffset > 0m)
				{
					var candidate = candle.ClosePrice - trailingOffset;
					if (!_longTrailingStop.HasValue || candidate > _longTrailingStop.Value)
						_longTrailingStop = candidate;

					if (_longTrailingStop.HasValue && candle.LowPrice <= _longTrailingStop.Value)
					{
						SellMarket(volume);
						ResetLongState();
						_lastPositionSign = 0;
						_pendingClose = true;
						return;
					}
				}
			}
		}
		else if (positionSign < 0)
		{
			// Capture the entry price when switching from long or flat to short.
			if (_lastPositionSign >= 0)
			{
				_shortEntryPrice = candle.ClosePrice;
				_shortTrailingStop = trailingOffset > 0m ? candle.ClosePrice + trailingOffset : null;
				ResetLongState();
			}

			if (_shortEntryPrice.HasValue)
			{
				var entry = _shortEntryPrice.Value;
				var volume = Math.Abs(position);

				if (stopOffset > 0m && candle.HighPrice >= entry + stopOffset)
				{
					BuyMarket(volume);
					ResetShortState();
					_lastPositionSign = 0;
					_pendingClose = true;
					_lastCloseCandle = _candleIndex;
					return;
				}

				if (takeOffset > 0m && candle.LowPrice <= entry - takeOffset)
				{
					BuyMarket(volume);
					ResetShortState();
					_lastPositionSign = 0;
					_pendingClose = true;
					_lastCloseCandle = _candleIndex;
					return;
				}

				if (trailingOffset > 0m)
				{
					var candidate = candle.ClosePrice + trailingOffset;
					if (!_shortTrailingStop.HasValue || candidate < _shortTrailingStop.Value)
						_shortTrailingStop = candidate;

					if (_shortTrailingStop.HasValue && candle.HighPrice >= _shortTrailingStop.Value)
					{
						BuyMarket(volume);
						ResetShortState();
						_lastPositionSign = 0;
						_pendingClose = true;
						return;
					}
				}
			}
		}
		else
		{
			ResetLongState();
			ResetShortState();
		}

		_lastPositionSign = positionSign;
	}

	private void UpdateFisherFlags(decimal value)
	{
		_fisherHistory.Enqueue(value);
		while (_fisherHistory.Count > 3)
			_fisherHistory.Dequeue();

		if (_fisherHistory.Count < 3)
			return;

		var values = _fisherHistory.ToArray();
		var fx0n = values[^1];
		var fx1n = values[^2];
		var fx2n = values[^3];

		var fx0 = (fx0n + fx1n) / 2m;
		var fx1 = (fx1n + fx2n) / 2m;

		if (fx1 < 0m && fx0 > 0m)
		{
			// Fisher crossed above zero -> bullish momentum.
			_fisherBullish = true;
			_fisherBearish = false;
		}
		else if (fx1 > 0m && fx0 < 0m)
		{
			// Fisher crossed below zero -> bearish momentum.
			_fisherBearish = true;
			_fisherBullish = false;
		}

		if (fx1 > 0.25m && fx0 < 0.25m)
		{
			// Fisher dropped back under +0.25 -> exit long.
			_fisherExitLong = true;
			_fisherExitShort = false;
		}
		else if (fx1 < -0.25m && fx0 > -0.25m)
		{
			// Fisher climbed back above -0.25 -> exit short.
			_fisherExitShort = true;
			_fisherExitLong = false;
		}
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longTrailingStop = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortTrailingStop = null;
	}

	private sealed class LabTrendState
	{
		private readonly Queue<decimal> _highs = new();
		private readonly Queue<decimal> _lows = new();
		private decimal _trend;

		public bool IsUpTrend => _trend > 0m;
		public bool IsDownTrend => _trend < 0m;

		public void Reset()
		{
			_highs.Clear();
			_lows.Clear();
			_trend = 0m;
		}

		public override bool Equals(object obj)
		{
			if (obj is not LabTrendState other) return false;
			return _trend == other._trend
				&& _highs.Count == other._highs.Count
				&& _lows.Count == other._lows.Count
				&& _highs.SequenceEqual(other._highs)
				&& _lows.SequenceEqual(other._lows);
		}

		public override int GetHashCode() => HashCode.Combine(_trend, _highs.Count, _lows.Count);

		public void Process(ICandleMessage candle, int length, decimal risk)
		{
			var lookback = Math.Max(1, length);

			_highs.Enqueue(candle.HighPrice);
			_lows.Enqueue(candle.LowPrice);

			if (_highs.Count > lookback)
			{
				_highs.Dequeue();
				_lows.Dequeue();
			}

			if (_highs.Count < lookback)
				return;

			var highest = _highs.Max();
			var lowest = _lows.Min();
			var range = highest - lowest;

			if (range <= 0m)
				return;

			var safeRisk = risk;
			if (safeRisk < 0m)
				safeRisk = 0m;
			else if (safeRisk > 33m)
				safeRisk = 33m;

			var coefficient = (33m - safeRisk) / 100m;
			var upper = highest - range * coefficient;
			var lower = lowest + range * coefficient;

			if (_trend <= 0m && candle.ClosePrice > upper)
				_trend = 1m;
			else if (_trend >= 0m && candle.ClosePrice < lower)
				_trend = -1m;
		}
	}

	private sealed class FisherYur4ikIndicator : BaseIndicator
	{
		public int Length { get; set; } = 10;

		private readonly Queue<decimal> _medians = new();
		private decimal _previousValue;
		private decimal _previousFish;

		protected override IIndicatorValue OnProcess(IIndicatorValue input)
		{
			var candle = input.GetValue<ICandleMessage>();
			if (candle == null)
				return new DecimalIndicatorValue(this, 0m, input.Time);

			var length = Math.Max(1, Length);
			var median = (candle.HighPrice + candle.LowPrice) / 2m;

			_medians.Enqueue(median);
			if (_medians.Count > length)
			{
				_medians.Dequeue();
			}

			if (_medians.Count < length)
			{
				IsFormed = false;
				return new DecimalIndicatorValue(this, 0m, input.Time);
			}

			var highest = _medians.Max();
			var lowest = _medians.Min();
			var range = highest - lowest;

			decimal fish;
			if (range == 0m)
			{
				fish = _previousFish;
			}
			else
			{
				var value = 0.66m * ((median - lowest) / range - 0.5m) + 0.67m * _previousValue;
				if (value > 0.999m)
					value = 0.999m;
				else if (value < -0.999m)
					value = -0.999m;

				var ratio = (1m + value) / (1m - value);
				fish = 0.5m * (decimal)Math.Log((double)ratio) + 0.5m * _previousFish;

				_previousValue = value;
				_previousFish = fish;
			}

			IsFormed = _medians.Count >= length;

			return new DecimalIndicatorValue(this, fish, input.Time);
		}

		public override void Reset()
		{
			base.Reset();
			_medians.Clear();
			_previousValue = 0m;
			_previousFish = 0m;
		}
	}
}