GitHub で見る

フォーストレンド戦略

概要

  • MQL/18817にあるMetaTrader 5エキスパートアドバイザーExp_ForceTrend.mq5の変換版。
  • 独自のForceTrendオシレーターを使用して、強気モメンタムと弱気モメンタムの間の移行を検出する。
  • シリーズへの直接アクセスではなく、ローソク足サブスクリプションと組み込みインジケーターに依存して、StockSharpの高レベルAPIでロジックを実装する。

ForceTrendインジケーター

  • インジケーターはLength本のローソク足を遡り、そのウィンドウ内の最高値と最安値の距離を測定する。
  • 現在のローソク足の中間価格はその範囲内で正規化され、2回平滑化される:
    • 第1段階では係数0.660.67を使用して中間的なforce値を生成する。
    • 第2段階では対数変換と半減期平滑化を組み合わせて最終的なForceTrend値を取得する。
  • ゼロを超える値は強気(元々は青で表示)として扱われ、ゼロ未満の値は弱気(マゼンタで表示)となる。

パラメーター

  • Length – ForceTrendルックバックウィンドウのサイズ。正のままでなければならない。
  • SignalBar – シグナルをシフトする完了済みローソク足の数。0は最新の確定バーに反応し、1は1本余分に待つことでMT5のデフォルト設定を模倣し、大きな値はさらに実行を遅らせる。
  • EnableLongEntry – 無効にすると、戦略は強気の移行でロングポジションを開かない。
  • EnableShortEntry – 無効にすると、戦略は弱気の移行でショートポジションを開かない。
  • EnableLongExit – 強気シグナルが既存のショートポジションをクローズできるかを制御する。
  • EnableShortExit – 弱気シグナルが既存のロングポジションをクローズできるかを制御する。
  • CandleType – インジケーター計算に使用されるローソク足の時間軸。

取引ルール

  1. ForceTrendの出力は離散的な方向(+10-1)に変換される。
  2. 方向は固定長の履歴に保存され、戦略がSignalBarオフセットのバーを直前のバーと比較できるようにする。
  3. 強気シグナル(direction > 0)がトリガーする:
    • EnableShortExittrueの場合、オープンしているショートポジションをクローズする。
    • 前の方向が強気でなくEnableLongEntrytrueの場合、ロングポジションをオープンまたは反転する(成行注文サイズVolume + |Position|)。
  4. 弱気シグナル(direction < 0)はEnableLongExit/EnableShortEntryが有効な場合、ロングポジションに対して対称的な動作をトリガーする。
  5. 中立のForceTrend読み取りは最後の既知の方向を継承し、システムがフラット状態間で振動しないようにする。
  6. 注文は戦略が完全に形成され、オンラインであり、StockSharpランタイムによって取引が許可されている場合にのみ送信される。

実装上の注意事項

  • ローソク足はSubscribeCandles(CandleType)を通じて受信され、インジケーター処理はProcessCandleコールバックで実行される。
  • 最高値と最安値はStockSharpのHighestLowestインジケーターを通じて取得され、手動のバッファ管理やLINQ操作が不要になる。
  • 方向履歴はSignalBarに応じてサイズが決まる小さな固定配列に保存され、各ティックのコレクションを再作成せずにオリジナルのMT5動作を再現する。
  • ポジションの反転は、目的のエクスポージャーと絶対的な現在のポジションの合計に等しいボリュームを持つ単一の成行注文を使用し、MQLバージョンのBuyPositionOpen/SellPositionOpenヘルパーをエミュレートする。
  • エキスパートアドバイザーの資金管理パラメーター(ロットサイジング、ポイントでのストップロスとテイクプロフィット)は意図的に省略されている。StockSharp戦略はユーザー設定のVolumeとオプションの外部保護モジュールに依存する。
  • ブールスイッチはMT5の入力(BuyPosOpenSellPosOpenBuyPosCloseSellPosClose)を反映する。

使用上のヒント

  • 注文サイズを制御するために、戦略を開始する前にVolumeプロパティを設定する。
  • MT5テスト中に使用した時間軸に合致するローソク足タイプを選択する(デフォルトは4時間足)。
  • ストップロスまたはテイクプロフィットの自動化が必要な場合は、StockSharpのリスク/保護コンポーネントと組み合わせる。

ファイル

  • 戦略実装: CS/ForceTrendStrategy.cs
  • オリジナルMQLファイル: MQL/18817/mql5/Experts/Exp_ForceTrend.mq5 および MQL/18817/mql5/Indicators/ForceTrend.mq5
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Force Trend strategy that mirrors the original MT5 expert advisor logic.
/// It reacts to ForceTrend indicator color changes to switch between long and short positions.
/// </summary>
public class ForceTrendStrategy : Strategy
{
	private readonly StrategyParam<int> _length;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<bool> _enableLongEntry;
	private readonly StrategyParam<bool> _enableShortEntry;
	private readonly StrategyParam<bool> _enableLongExit;
	private readonly StrategyParam<bool> _enableShortExit;
	private readonly StrategyParam<DataType> _candleType;

	private Highest _highest = null!;
	private Lowest _lowest = null!;
	private decimal _previousForceValue;
	private decimal _previousIndicatorValue;
	private int?[] _directionHistory = Array.Empty<int?>();
	private int _historyCount;
	private int? _lastKnownDirection;

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public ForceTrendStrategy()
	{
		_length = Param(nameof(Length), 13)
			.SetDisplay("Length", "ForceTrend lookback length", "Indicator")
			.SetGreaterThanZero()
			;

		_signalBar = Param(nameof(SignalBar), 1)
			.SetDisplay("Signal Bar", "Number of finished bars to shift the signal", "Trading")
			;

		_enableLongEntry = Param(nameof(EnableLongEntry), true)
			.SetDisplay("Enable Long Entry", "Allow opening long positions", "Trading")
			;

		_enableShortEntry = Param(nameof(EnableShortEntry), true)
			.SetDisplay("Enable Short Entry", "Allow opening short positions", "Trading")
			;

		_enableLongExit = Param(nameof(EnableLongExit), true)
			.SetDisplay("Enable Long Exit", "Allow closing long positions", "Trading")
			;

		_enableShortExit = Param(nameof(EnableShortExit), true)
			.SetDisplay("Enable Short Exit", "Allow closing short positions", "Trading")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for ForceTrend calculations", "General");
	}

	/// <summary>
	/// ForceTrend lookback length.
	/// </summary>
	public int Length
	{
		get => _length.Value;
		set => _length.Value = value;
	}

	/// <summary>
	/// Number of finished candles used to shift the trade signal.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	/// <summary>
	/// Enable opening long positions when the ForceTrend becomes bullish.
	/// </summary>
	public bool EnableLongEntry
	{
		get => _enableLongEntry.Value;
		set => _enableLongEntry.Value = value;
	}

	/// <summary>
	/// Enable opening short positions when the ForceTrend becomes bearish.
	/// </summary>
	public bool EnableShortEntry
	{
		get => _enableShortEntry.Value;
		set => _enableShortEntry.Value = value;
	}

	/// <summary>
	/// Enable closing long positions on bearish ForceTrend signals.
	/// </summary>
	public bool EnableLongExit
	{
		get => _enableLongExit.Value;
		set => _enableLongExit.Value = value;
	}

	/// <summary>
	/// Enable closing short positions on bullish ForceTrend signals.
	/// </summary>
	public bool EnableShortExit
	{
		get => _enableShortExit.Value;
		set => _enableShortExit.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

		_previousForceValue = 0m;
		_previousIndicatorValue = 0m;
		_directionHistory = Array.Empty<int?>();
		_historyCount = 0;
		_lastKnownDirection = null;
	}

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

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

		_previousForceValue = 0m;
		_previousIndicatorValue = 0m;
		_historyCount = 0;
		_lastKnownDirection = null;
		_directionHistory = new int?[Math.Max(SignalBar + 2, 2)];

		_highest = new Highest { Length = Length };
		_lowest = new Lowest { Length = Length };

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

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

		var highestValue = _highest.Process(new CandleIndicatorValue(_highest, candle)).ToDecimal();
		var lowestValue = _lowest.Process(new CandleIndicatorValue(_lowest, candle)).ToDecimal();

		if (!_highest.IsFormed || !_lowest.IsFormed)
			return;

		var range = highestValue - lowestValue;
		decimal forceValue;

		if (range != 0m)
		{
			var average = (candle.HighPrice + candle.LowPrice) / 2m;
			var normalized = (average - lowestValue) / range - 0.5m;
			forceValue = 0.66m * normalized + 0.67m * _previousForceValue;
		}
		else
		{
			forceValue = 0.67m * _previousForceValue - 0.33m;
		}

		forceValue = Math.Clamp(forceValue, -0.999m, 0.999m);

		decimal indicatorValue;
		var denominator = 1m - forceValue;

		if (denominator != 0m)
		{
			var ratio = (forceValue + 1m) / denominator;
			indicatorValue = (decimal)(Math.Log((double)ratio) / 2.0) + _previousIndicatorValue / 2m;
		}
		else
		{
			indicatorValue = _previousIndicatorValue / 2m + 0.5m;
		}

		_previousForceValue = forceValue;
		_previousIndicatorValue = indicatorValue;

		var direction = indicatorValue > 0m ? 1 : indicatorValue < 0m ? -1 : _lastKnownDirection ?? 0;
		if (direction != 0)
			_lastKnownDirection = direction;

		AddDirection(direction);

		var currentDirection = GetDirection(SignalBar);
		if (currentDirection is null)
			return;

		var previousDirection = GetDirection(SignalBar + 1);
		var bullish = currentDirection.Value > 0;
		var bearish = currentDirection.Value < 0;
		var bullishFlip = bullish && previousDirection.HasValue && previousDirection.Value <= 0;
		var bearishFlip = bearish && previousDirection.HasValue && previousDirection.Value >= 0;

		// indicators processed manually, no BindEx

		if (bullish)
		{
			var volumeToBuy = 0m;

			if (EnableShortExit && Position < 0m)
				volumeToBuy += Math.Abs(Position);

			if (EnableLongEntry && bullishFlip && Position <= 0m)
				volumeToBuy += Volume;

			if (volumeToBuy > 0m)
				BuyMarket();
		}
		else if (bearish)
		{
			var volumeToSell = 0m;

			if (EnableLongExit && Position > 0m)
				volumeToSell += Math.Abs(Position);

			if (EnableShortEntry && bearishFlip && Position >= 0m)
				volumeToSell += 1m;

			if (volumeToSell > 0m)
				SellMarket();
		}
	}

	private void AddDirection(int direction)
	{
		if (_historyCount < _directionHistory.Length)
		{
			_directionHistory[_historyCount] = direction;
			_historyCount++;
		}
		else
		{
			for (var i = 1; i < _directionHistory.Length; i++)
				_directionHistory[i - 1] = _directionHistory[i];

			_directionHistory[^1] = direction;
		}
	}

	private int? GetDirection(int offset)
	{
		if (offset < 0)
			return null;

		var index = _historyCount - 1 - offset;
		if (index < 0)
			return null;

		return _directionHistory[index];
	}
}