GitHub で見る

T3 MA 方向転換戦略

概要

この戦略は、元のエキスパートアドバイザー T3MA(barabashkakvn's edition) の動作を再現します。エキスパートアドバイザーは「T3MA-ALARM」インジケーターに依存しており、指数平滑を2回適用し、平滑化された線が方向を変えるとシグナルを出します。StockSharpポートは同じコンセプトを維持しており、二重平滑化指数移動平均(EMAのEMA)を作成し、その曲線の傾きが下降から上昇または逆に変わるたびに取引します。

戦略は完成したローソク足のみで動作します。シグナルは、元の InpBarNumber オプションを模倣するため、設定可能な数のバーだけ遅延させることができます(デフォルト遅延:1バー)。注文は成行執行を使用して配置されるため、ポートフォリオは複数の同時ヘッジポジションを積み上げずにロングとショートの露出を切り替えます。

トレードルール

  1. 設定されたローソク足シリーズを購読し、終値のEMAを計算します。最初のEMAの出力の上に2番目のEMAを実行し、インジケーターが使用する平滑化されたシリーズを生成します。
  2. 平滑化されたシリーズの現在の値(オプションで EMA Shift だけ前に移動)を前の値と比較します。シリーズが増加すると傾きは強気、減少すると弱気と見なされます。
  3. 傾きが弱気から強気に変わった場合、買いシグナルをキューに入れます。強気から弱気に変わった場合、売りシグナルをキューに入れます。中立のローソク足は遅延カウンターが正確に保たれるようにゼロシグナルをキューに入れます。
  4. 設定された完了ローソク足数(Signal Delay)が経過した後、キューに入れられたシグナルを実行します。遅延した買いは未決済のショートポジションをクローズし、基本 Trade Volume でロングエントリーします。同様に、遅延した売りはロングポジションをクローズし、ショートエントリーします。
  5. 保護的なストップロスとテイクプロフィット注文が StartProtection を介して初期化されます。両方の距離は価格ステップで表現されるため、選択した銘柄のティックサイズに自動的に適応します。

パラメーター

名前 説明
EMA Length 両方の平滑化パスに使用するEMAの長さ。MetaTrader実装の MAPeriod 入力に対応します。
EMA Shift 傾きを比較する前に平滑化されたEMAがシフトされるバーの数。インジケーターの MAShift に相当します。
Signal Delay シグナルを実行する前に待つ完了したローソク足の数。InpBarNumber を反映しており、1の値は前のバーのシグナルを取引します。
Stop Loss (steps) 価格ステップで測定したストップロス距離。無効にするにはゼロに設定します。
Take Profit (steps) 価格ステップで測定したテイクプロフィット距離。無効にするにはゼロに設定します。
Trade Volume 新規エントリーに使用する基本注文サイズ。ポジションを反転する場合、戦略は現在の絶対ポジションサイズをこの値に加算します。
Candle Type 計算に使用するローソク足データタイプ(デフォルト:5分時間軸)。

リスク管理

  • StartProtection は戦略開始時にストップロスとテイクプロフィットレベルを自動的に登録します。両方のレベルは銘柄のティックサイズに従い、戦略の全期間にわたってアクティブのままです。
  • ポジション転換は成行注文を使用して実行されます。シグナルの方向が現在の露出と一致する場合、追加の取引は発行されず、意図しないピラミッド積みを防ぎます。
  • すべての取引でログが出力され、ソースローソク足から取得した理由と参照価格を追跡します。

MQL5バージョンとの違い

  • MetaTrader 5はヘッジアカウントを必要とし、複数のポジションを積み上げることができました。StockSharpバージョンは単一のネットポジションを維持し、逆シグナルが発動したときにそれを反転します。
  • シグナル処理はローソク足ベースで、すべてのティックではなく完成したローソク足ごとに一度発生します。これはStockSharpの高レベルAPIの中でより自然です。
  • ストップロスとテイクプロフィットの管理は、各注文に手動でSL/TP価格を送信する代わりに StartProtection を介して処理されます。
  • StockSharp環境での読みやすさを高めるため、英語コメント、構造化パラメーター、チャートヘルパーが追加されました。

使用上の注意

  1. 戦略を目的の証券に接続し、ローソク足タイプが元のエキスパートアドバイザーを最適化する際に使用された時間軸と一致することを確認します。
  2. 銘柄のボラティリティに合わせて EMA Length とリスクパラメーターを調整します。遅延が大きいほど(Signal Delay)応答が遅くなり、ノイズをフィルタリングできる場合があります。
  3. 戦略は価格ステップで機能するため、保護注文が意味のある距離に配置されるように証券の PriceStep プロパティが正しく設定されていることを確認します。
namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Strategy that trades when a double-smoothed EMA changes its slope direction.
/// </summary>
public class T3MaDirectionChangeStrategy : Strategy
{
	private readonly StrategyParam<int> _maLength;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<int> _signalBarOffset;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _signalCooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _recentSmoothed = new();
	private readonly Queue<SignalInfo> _pendingSignals = new();
	private ExponentialMovingAverage _emaPrice;
	private ExponentialMovingAverage _emaSmooth;
	private int _previousDirection;
	private int _cooldownRemaining;

	public int MaLength { get => _maLength.Value; set => _maLength.Value = value; }
	public int MaShift { get => _maShift.Value; set => _maShift.Value = value; }
	public int SignalBarOffset { get => _signalBarOffset.Value; set => _signalBarOffset.Value = value; }
	public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public decimal TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
	public decimal TradeVolume { get => _tradeVolume.Value; set => _tradeVolume.Value = value; }
	public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public T3MaDirectionChangeStrategy()
	{
		_maLength = Param(nameof(MaLength), 4)
			.SetGreaterThanZero()
			.SetDisplay("EMA Length", "Length of the EMA used for the double smoothing", "Indicator");

		_maShift = Param(nameof(MaShift), 0)
			.SetNotNegative()
			.SetDisplay("EMA Shift", "Shift applied to the smoothed EMA when evaluating slope changes", "Indicator");

		_signalBarOffset = Param(nameof(SignalBarOffset), 1)
			.SetNotNegative()
			.SetDisplay("Signal Delay", "How many completed candles to wait before acting on a signal", "Trading rules");

		_stopLossPoints = Param(nameof(StopLossPoints), 20m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (steps)", "Stop loss distance expressed in price steps", "Risk management");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 125m)
			.SetNotNegative()
			.SetDisplay("Take Profit (steps)", "Take profit distance expressed in price steps", "Risk management");

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Base volume used for entries", "Trading rules");

		_signalCooldownBars = Param(nameof(SignalCooldownBars), 12)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait after entries and exits", "Trading rules");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_emaPrice = null;
		_emaSmooth = null;
		_recentSmoothed.Clear();
		_pendingSignals.Clear();
		_previousDirection = 0;
		_cooldownRemaining = 0;
		Volume = TradeVolume;
	}

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

		Volume = TradeVolume;
		_emaPrice = new EMA { Length = MaLength };
		_emaSmooth = new EMA { Length = MaLength };
		_recentSmoothed.Clear();
		_pendingSignals.Clear();
		_previousDirection = 0;
		_cooldownRemaining = 0;

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}

		var slUnit = StopLossPoints > 0m ? new Unit(StopLossPoints, UnitTypes.Absolute) : null;
		var tpUnit = TakeProfitPoints > 0m ? new Unit(TakeProfitPoints, UnitTypes.Absolute) : null;
		StartProtection(slUnit, tpUnit);
	}

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

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		var emaPriceValue = _emaPrice.Process(new DecimalIndicatorValue(_emaPrice, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		var emaSmoothValue = _emaSmooth.Process(emaPriceValue);
		if (!emaSmoothValue.IsFormed)
			return;

		AddSmoothedValue(emaSmoothValue.ToDecimal(), MaShift + 2);
		if (_recentSmoothed.Count < MaShift + 2)
		{
			EnqueueSignal(new SignalInfo(0));
			return;
		}

		var currentIndex = _recentSmoothed.Count - 1 - MaShift;
		var previousIndex = _recentSmoothed.Count - 2 - MaShift;
		var current = _recentSmoothed[currentIndex];
		var previous = _recentSmoothed[previousIndex];
		var direction = _previousDirection;

		if (current > previous)
			direction = 1;
		else if (current < previous)
			direction = -1;

		var signal = 0;
		if (_previousDirection == -1 && direction == 1)
			signal = 1;
		else if (_previousDirection == 1 && direction == -1)
			signal = -1;

		_previousDirection = direction;
		EnqueueSignal(new SignalInfo(signal));
	}

	private void AddSmoothedValue(decimal value, int limit)
	{
		_recentSmoothed.Add(value);
		if (_recentSmoothed.Count > limit)
			_recentSmoothed.RemoveAt(0);
	}

	private void EnqueueSignal(SignalInfo signal)
	{
		_pendingSignals.Enqueue(signal);

		while (_pendingSignals.Count > SignalBarOffset)
		{
			var readySignal = _pendingSignals.Dequeue();
			ExecuteSignal(readySignal);
		}
	}

	private void ExecuteSignal(SignalInfo signal)
	{
		if (signal.Direction == 0 || _cooldownRemaining > 0)
			return;

		if (signal.Direction > 0 && Position <= 0)
		{
			var volume = Volume + Math.Abs(Position);
			BuyMarket(volume);
			_cooldownRemaining = SignalCooldownBars;
		}
		else if (signal.Direction < 0 && Position >= 0)
		{
			var volume = Volume + Math.Abs(Position);
			SellMarket(volume);
			_cooldownRemaining = SignalCooldownBars;
		}
	}

	private readonly record struct SignalInfo(int Direction);
}