GitHub で見る

Laptrend_1 戦略

概要

Laptrend_1 は、MetaTrader エキスパート アドバイザ Laptrend_1.mq4 のロジックを再現します。この戦略は、マルチタイムフレームの LabTrend チャネル フィルター、Fisher Transform のモメンタム確認、および 15 分足のローソク足の ADX トレンドの強さのチェックを組み合わせています。注文は、より高いタイムフレーム (H1) とシグナル タイムフレーム (M15) の LabTrend の方向が一致し、フィッシャー変換が動きを確認し、ADX が強化傾向を示している場合にのみオープンされます。勢いが逆転するか、LabTrend の方向が変わるか、市場が ADX と DI コンポーネントが収束するフラットな状況に移行すると、ポジションは閉じられます。

取引ロジック

  • 主要データ – 15 分足のローソク足がエントリー/エグジットを駆動し、1 時間足のローソク足が長期 LabTrend フィルターに供給されます。
  • LabTrend チャネル – このコードは、最後の ChannelLength バー上に Donchian スタイルのチャネルを構築し、それを RiskFactor で狭めることにより、LabTrend1_v2.1 インジケーターを再作成します。終値が上部バンドを上回った場合は強気傾向を示します。下限バンドを下回る終値は弱気トレンドを示します。 M15 と H1 のトレンドは、オープントレードに合わせて調整する必要があります。
  • フィッシャー変換 – カスタムのフィッシャー変換 (Fisher_Yur4ik) は、M15 タイムフレームの勢いを追跡します。ゼロを通過すると強気/弱気バイアスが反転し、±0.25 を横切ると出口シグナルが生成されます。
  • ADX フィルター – 15 分間の平均方向性指数が上昇し、支配的な DI コンポーネントが提案された取引と一致する必要があります。 ADX、+DI、および –DI が互いに Delta ポイント以内にある場合、この戦略は市場をフラットとして扱い、モメンタム フラグをリセットし、オープン ポジションを清算します。
  • ポジション管理 – 新しいポジションは反対のエクスポージャーを決済し、設定可能な量を取引します。イグジットは、LabTrend の反転、フィッシャーのイグジット、または市場の横ばい状況によって引き起こされます。

リスク管理

  • ストップロス/テイクプロフィット – 商品ポイント(MetaTrader「pips」)で設定可能。これらは、元の EA からの保護命令を模倣するために、ローソク足の高値/安値に対して評価されます。
  • トレーリング ストップ – 価格が取引に有利に動くと、トレーリング ストップは TrailingStopPoints に等しい距離で終値を追跡します。トレーリングレベルを超えると、即座に市場から撤退します。
  • ボリューム – すべての注文は固定の Volume パラメータ (ロット) を使用します。

パラメーター

  • Volume – ロット単位の注文サイズ。デフォルトは 1。
  • AdxPeriod – ADX 平滑化期間。デフォルトは 14。
  • FisherLength – フィッシャー変換のウィンドウ。デフォルトは 10。
  • ChannelLength – LabTrend チャネルに使用されるバー。デフォルトは9。
  • RiskFactor – LabTrend チャネルの絞り込み係数 (元のインジケーター範囲 1..10)。デフォルトは 3。
  • Delta – 市場が横ばいとみなされるまでの、ADX と DI 値の間の最大差。デフォルトは7。
  • StopLossPoints – ポイント単位のストップロス距離。デフォルトは 100。
  • TakeProfitPoints – ポイント単位の利食い距離。デフォルトは 40。
  • TrailingStopPoints – トレーリング ストップの距離 (ポイント単位)。デフォルトは 100。
  • SignalCandleType – 信号計算用のキャンドル シリーズ (デフォルトは M15)。
  • TrendCandleType – より高いタイムフレームの LabTrend フィルターのローソク足シリーズ (デフォルトは H1)。

注意事項

  • 元の MQL 実装は、受信ティックごとに動作しました。このポートは完了した M15 キャンドルを処理し、インジケーターの計算を尊重しながらロジックの決定性を保ちます。
  • ストップロス、テイクプロフィット、およびトレーリングエグジットは、ローソク足の高値/安値が設定されたしきい値を超えたときに成行注文で実行されます。これは、明示的なストップ/リミット注文を維持せずに、MetaTrader の保護注文の動作を反映します。
  • 戦略を開始する前に、データ ソースがパラメーターで定義された 15 分足と 1 時間足のローソク足シリーズの両方を提供していることを確認してください。
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;
		}
	}
}