GitHub で見る

SilverTrend V3戦略 (C#)

概要

SilverTrend V3戦略は、MetaTrader 5のエキスパートアドバイザー「SilverTrend v3」を起源とするモメンタムフォローシステムです。StockSharpへのポートは、高レベル戦略APIに適合させながら元のロジックを再現しています。中心的なアイデアは、SilverTrendチャネル計算を使って強気または弱気のモメンタムを検出し、J_TPOマーケットプロファイルオシレーターで確認し、保護ストップ、トレーリングロジック、および金曜日セッションフィルターで結果ポジションを管理することです。

シグナルエンジン

  1. SilverTrendの方向

    • 動的サポート(smin)とレジスタンス(smax)を計算するために、9バーのスムージングパラメーターを持つ350バーのローリングウィンドウを使用します。
    • 現在の終値がsminを下回ると、システムは弱気レジームにフラグを立て、smaxを上回る終値は強気レジームに切り替わります。
    • 計算は最古のバーから最新のバーへ反復し、元のMQLコードの再帰的な性質を再現します。
  2. J_TPOの確認

    • 価格が短期分布内にどのように集中するかを測定する、元の14期間J_TPOオシレーターを実装しています。
    • オシレーターが正の場合のみロングエントリーを許可し、負の場合のみショートエントリーを許可することで、弱いモメンタムシフトをフィルタリングします。
  3. シグナル変化の検出

    • 新たに計算されたSilverTrendの方向が前回の値と異なる場合にのみトレードが開始され、戦略がノイズではなく真のレジームシフトに反応することを保証します。

トレード管理

  • 成行エントリー – 戦略は設定されたVolumeでトレードします。反対のポジションが開いている場合、1つの成行注文で決済して反転します。
  • 初期ストップロス – オプション。エントリー価格に対する価格ステップで定義されます(インストゥルメントのPriceStepで変換)。
  • テイクプロフィット – オプション。価格ステップで定義され、元の注文変更動作を模倣するためにローソク足の高安値に対して評価されます。
  • トレーリングストップ – 設定されたトレーリング距離だけ価格が有利方向に動いた後にアクティブになります。ロングポジションではストップが上方に引き上げられ、ショートでは下方に引き下げられ、MetaTraderのロジックと一致します。
  • 反対シグナルでの決済 – 前のレジームが反対方向を指している場合、既存のポジションは次のローソク足の終値で清算されます。
  • 金曜日トレードブロック – 週末ギャップを避けるために、元のEAと同様に、金曜日の指定時刻以降は新規ポジションをスキップします。

パラメーター

名前 デフォルト値 説明
TrailingStopPoints 50 価格ステップで測定したトレーリングストップ距離。トレーリングを無効にするにはゼロに設定。
TakeProfitPoints 50 価格ステップでのテイクプロフィット距離。ゼロはターゲットを無効にします。
InitialStopLossPoints 0 価格ステップでの初期保護ストップ。ゼロは初期ストップなしでポジションを残します。
FridayCutoffHour 16 金曜日に新規エントリーが許可されない取引所時刻。終日取引を許可するには0を使用。
CandleType 1時間足 インジケーターに供給するデータシリーズ。サポートされている任意の時間軸を使用できます。
Volume 1ロット 各ポジションのトレードサイズ(StockSharpのVolumeプロパティ)。

すべての距離は実行時にPriceStepで乗算され、銘柄のティックサイズ(3/5桁のフォレックスシンボルを含む)に自動的に適合します。

データと環境要件

  • SilverTrendとJ_TPOの両方のバッファが完全に形成されるよう、ライブシグナルが生成される前に少なくとも360本の完成したローソク足が必要です。
  • SubscribeCandlesによる単一インストゥルメントのトレーディング向けに設計されています。GetWorkingSecuritiesオーバーライドにより、戦略は設定された銘柄と時間軸のみを購読することが保証されます。
  • 起動時に一度StartProtection()を使用して、標準のStockSharpポジション保護サービスを有効にします。

使用上の注意

  • アルゴリズムは主要なフォレックスペアや流動性の高い先物などのトレンドインストゥルメントを想定しています。市場のボラティリティに合わせて時間軸を調整してください。
  • SilverTrendの計算は再帰的なため、履歴ローソク足が不十分な状態で戦略を再起動すると、十分なデータが収集されるまでシグナル形成が遅延します。
  • 高レベルAPI実装はローソク足の高安値を使用して注文管理(ストップロス、テイクプロフィット、トレーリング)をシミュレートします。ライブトレーディングでは、インフラが必要とする場合、実際のストップ/リミット注文とロジックを組み合わせることを検討してください。
  • ポートは内部状態(_previousSignal_entryPrice、トレーリングストップ)を完了したローソク足ごとに正確に1回格納し、元のEAの「1バーあたり1トレード」の動作と一致します。

変換の詳細

  • ネストされた配列J_TPOアルゴリズムを含むSilverTrend v3.mq5の数学的ルーティンを忠実に再現しています。
  • C#のベストプラクティスを適用:パラメーターはStrategyParam<T>を通じて公開され、すべてのコメントは英語で、インデントはリポジトリのガイドラインに従いタブを使用しています。
  • タスク要件に従い、このリリースにはPythonバージョンは含まれていません。
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>
/// SilverTrend v3 momentum strategy ported from MetaTrader 5.
/// </summary>
public class SilverTrendV3Strategy : Strategy
{
	private readonly StrategyParam<int> _countBars;
	private readonly StrategyParam<int> _ssp;
	private readonly StrategyParam<int> _jtpoLength;
	private readonly StrategyParam<int> _historyCapacity;
	private readonly StrategyParam<int> _risk;

	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _initialStopLossPoints;
	private readonly StrategyParam<int> _fridayCutoffHour;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closeHistory = new();
	private readonly List<decimal> _highHistory = new();
	private readonly List<decimal> _lowHistory = new();

	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;
	private decimal _entryPrice;
	private int _previousSignal;
	private decimal _pointValue;

	/// <summary>
	/// Trailing stop distance expressed in price steps.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in price steps.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Initial stop loss distance expressed in price steps.
	/// </summary>
	public decimal InitialStopLossPoints
	{
		get => _initialStopLossPoints.Value;
		set => _initialStopLossPoints.Value = value;
	}

	/// <summary>
	/// Hour after which no new trades are allowed on Friday (exchange time).
	/// </summary>
	public int FridayCutoffHour
	{
		get => _fridayCutoffHour.Value;
		set => _fridayCutoffHour.Value = value;
	}

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

	/// <summary>
	/// Number of bars used in the indicator history.
	/// </summary>
	public int CountBars
	{
		get => _countBars.Value;
		set => _countBars.Value = value;
	}

	/// <summary>
	/// Sliding window length for the signal filter.
	/// </summary>
	public int Ssp
	{
		get => _ssp.Value;
		set => _ssp.Value = value;
	}

	/// <summary>
	/// Length used when smoothing JTPO indicator.
	/// </summary>
	public int JtpoLength
	{
		get => _jtpoLength.Value;
		set => _jtpoLength.Value = value;
	}

	/// <summary>
	/// Maximum number of candles stored in history.
	/// </summary>
	public int HistoryCapacity
	{
		get => _historyCapacity.Value;
		set => _historyCapacity.Value = value;
	}

	/// <summary>
	/// Risk coefficient used in signal calculations.
	/// </summary>
	public int Risk
	{
		get => _risk.Value;
		set => _risk.Value = value;
	}

	/// <summary>
	/// Initialize default parameters.
	/// </summary>
	public SilverTrendV3Strategy()
	{
		_countBars = Param(nameof(CountBars), 150)
			.SetGreaterThanZero()
			.SetDisplay("Count Bars", "Number of candles required before trading", "Indicator");

		_ssp = Param(nameof(Ssp), 9)
			.SetGreaterThanZero()
			.SetDisplay("SSP", "Sliding window length", "Indicator");

		_jtpoLength = Param(nameof(JtpoLength), 14)
			.SetGreaterThanZero()
			.SetDisplay("JTPO Length", "JTPO smoothing length", "Indicator");

		_historyCapacity = Param(nameof(HistoryCapacity), 220)
			.SetGreaterThanZero()
			.SetDisplay("History Capacity", "Maximum stored candles", "Indicator");

		_risk = Param(nameof(Risk), 3)
			.SetGreaterThanZero()
			.SetDisplay("Risk", "Risk coefficient", "Trading");

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 50m)
			.SetDisplay("Trailing Stop", "Trailing distance in price steps", "Risk")
			.SetNotNegative();

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

		_initialStopLossPoints = Param(nameof(InitialStopLossPoints), 0m)
			.SetDisplay("Initial Stop Loss", "Initial stop loss in price steps", "Risk")
			.SetNotNegative();

		_fridayCutoffHour = Param(nameof(FridayCutoffHour), 16)
			.SetDisplay("Friday Cutoff Hour", "Disable new entries after this hour on Friday", "Sessions")
			.SetNotNegative();

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Candle type for signal calculations", "General");

		Volume = 1m;
	}

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

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

		_closeHistory.Clear();
		_highHistory.Clear();
		_lowHistory.Clear();
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_entryPrice = 0m;
		_previousSignal = 0;
		_pointValue = 0m;
	}

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

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

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

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

		// protection handled manually via trailing/TP/SL
	}

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

		UpdateHistory(candle);

		// indicators are processed manually

		if (_closeHistory.Count < CountBars + Ssp + 1)
		{
			return;
		}

		var jtpo = CalculateJtpo(JtpoLength);
		var signal = CalculateSilverTrendSignal();

		var longSignal = _previousSignal != signal && signal > 0 && jtpo > 0m;
		var shortSignal = _previousSignal != signal && signal < 0 && jtpo < 0m;

		var exitLong = _previousSignal < 0;
		var exitShort = _previousSignal > 0;

		ManageOpenPosition(candle, exitLong, exitShort);

		if (Position <= 0 && longSignal && !IsFridayBlocked(candle))
		{
			EnterLong(candle);
		}
		else if (Position >= 0 && shortSignal && !IsFridayBlocked(candle))
		{
			EnterShort(candle);
		}

		_previousSignal = signal;
	}

	private void UpdateHistory(ICandleMessage candle)
	{
		_closeHistory.Add(candle.ClosePrice);
		_highHistory.Add(candle.HighPrice);
		_lowHistory.Add(candle.LowPrice);

		if (_closeHistory.Count > HistoryCapacity)
		{
			_closeHistory.RemoveAt(0);
			_highHistory.RemoveAt(0);
			_lowHistory.RemoveAt(0);
		}
	}

	private void ManageOpenPosition(ICandleMessage candle, bool exitLongSignal, bool exitShortSignal)
	{
		if (Position > 0)
		{
			UpdateLongTrailing(candle);

			var initialStop = InitialStopLossPoints > 0m ? _entryPrice - GetDistance(InitialStopLossPoints) : (decimal?)null;
			var trailingStop = _longTrailingStop;
			var stop = CombineLongStops(initialStop, trailingStop);
			var takeProfit = TakeProfitPoints > 0m ? _entryPrice + GetDistance(TakeProfitPoints) : (decimal?)null;

			if (exitLongSignal ||
				(takeProfit.HasValue && candle.HighPrice >= takeProfit.Value) ||
				(stop.HasValue && candle.LowPrice <= stop.Value))
			{
				SellMarket();
				ResetStops();
			}
		}
		else if (Position < 0)
		{
			UpdateShortTrailing(candle);

			var initialStop = InitialStopLossPoints > 0m ? _entryPrice + GetDistance(InitialStopLossPoints) : (decimal?)null;
			var trailingStop = _shortTrailingStop;
			var stop = CombineShortStops(initialStop, trailingStop);
			var takeProfit = TakeProfitPoints > 0m ? _entryPrice - GetDistance(TakeProfitPoints) : (decimal?)null;

			if (exitShortSignal ||
				(takeProfit.HasValue && candle.LowPrice <= takeProfit.Value) ||
				(stop.HasValue && candle.HighPrice >= stop.Value))
			{
				BuyMarket();
				ResetStops();
			}
		}
		else
		{
			ResetStops();
		}
	}

	private void EnterLong(ICandleMessage candle)
	{
		var volume = Volume;
		if (Position < 0)
		{
			volume += Math.Abs(Position);
		}

		BuyMarket(volume);

		_entryPrice = candle.ClosePrice;
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

	private void EnterShort(ICandleMessage candle)
	{
		var volume = Volume;
		if (Position > 0)
		{
			volume += Position;
		}

		SellMarket(volume);

		_entryPrice = candle.ClosePrice;
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

	private void UpdateLongTrailing(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m)
		{
			return;
		}

		var distance = GetDistance(TrailingStopPoints);
		var trigger = _entryPrice + distance;

		if (candle.ClosePrice > trigger)
		{
			var newStop = candle.ClosePrice - distance;
			if (!_longTrailingStop.HasValue || newStop > _longTrailingStop.Value)
			{
				_longTrailingStop = newStop;
			}
		}
	}

	private void UpdateShortTrailing(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m)
		{
			return;
		}

		var distance = GetDistance(TrailingStopPoints);
		var trigger = _entryPrice - distance;

		if (candle.ClosePrice < trigger)
		{
			var newStop = candle.ClosePrice + distance;
			if (!_shortTrailingStop.HasValue || newStop < _shortTrailingStop.Value)
			{
				_shortTrailingStop = newStop;
			}
		}
	}

	private decimal? CombineLongStops(decimal? initialStop, decimal? trailingStop)
	{
		if (initialStop == null && trailingStop == null)
		{
			return null;
		}

		if (initialStop == null)
		{
			return trailingStop;
		}

		if (trailingStop == null)
		{
			return initialStop;
		}

		return Math.Max(initialStop.Value, trailingStop.Value);
	}

	private decimal? CombineShortStops(decimal? initialStop, decimal? trailingStop)
	{
		if (initialStop == null && trailingStop == null)
		{
			return null;
		}

		if (initialStop == null)
		{
			return trailingStop;
		}

		if (trailingStop == null)
		{
			return initialStop;
		}

		return Math.Min(initialStop.Value, trailingStop.Value);
	}

	private void ResetStops()
	{
		if (Position == 0)
		{
			_entryPrice = 0m;
		}

		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

	private bool IsFridayBlocked(ICandleMessage candle)
	{
		if (FridayCutoffHour <= 0)
		{
			return false;
		}

		var time = candle.OpenTime;
		return time.DayOfWeek == DayOfWeek.Friday && time.Hour > FridayCutoffHour;
	}

	private int CalculateSilverTrendSignal()
	{
		var k = 33 - Risk;
		var uptrend = false;
		var val = 0;

		for (var i = CountBars - Ssp; i >= 0; i--)
		{
			var ssMax = GetHigh(i);
			var ssMin = GetLow(i);

			for (var i2 = i; i2 <= i + Ssp - 1; i2++)
			{
				var priceHigh = GetHigh(i2);
				if (ssMax < priceHigh)
				{
					ssMax = priceHigh;
				}

				var priceLow = GetLow(i2);
				if (ssMin >= priceLow)
				{
					ssMin = priceLow;
				}
			}

			var smin = ssMin + (ssMax - ssMin) * k / 100m;
			var smax = ssMax - (ssMax - ssMin) * k / 100m;

			if (GetClose(i) < smin)
			{
				uptrend = false;
			}

			if (GetClose(i) > smax)
			{
				uptrend = true;
			}

			val = uptrend ? 1 : -1;
		}

		return val;
	}

	private decimal CalculateJtpo(int len)
	{
		if (_closeHistory.Count < 200)
		{
			return 0m;
		}

		decimal f8 = 0m;
		decimal f10 = 0m;
		decimal f18 = 0m;
		decimal f20 = 0m;
		decimal f30 = 0m;
		decimal f40 = 0m;
		decimal k = 0m;
		decimal var14 = 0m;
		decimal var18 = 0m;
		decimal var1C = 0m;
		decimal var20 = 0m;
		decimal var24 = 0m;
		decimal value = 0m;
		var f38 = 0;
		var f48 = 0;
		var arr0 = new decimal[400];
		var arr1 = new decimal[400];
		var arr2 = new decimal[400];
		var arr3 = new decimal[400];

		for (var i = 200 - len - 100; i >= 0; i--)
		{
			var14 = 0m;
			var1C = 0m;

			if (f38 == 0)
			{
				f38 = 1;
				f40 = 0m;
				f30 = len - 1 >= 2 ? len - 1 : 2;
				f48 = (int)f30 + 1;
				f10 = GetClose(i);
				arr0[f38] = f10;
				k = f48;
				f18 = 12m / (k * (k - 1) * (k + 1));
				f20 = (f48 + 1) * 0.5m;
			}
			else
			{
				if (f38 <= f48)
				{
					f38 += 1;
				}
				else
				{
					f38 = f48 + 1;
				}

				f8 = f10;
				f10 = GetClose(i);

				if (f38 > f48)
				{
					for (var var6 = 2; var6 <= f48; var6++)
					{
						arr0[var6 - 1] = arr0[var6];
					}

					arr0[f48] = f10;
				}
				else
				{
					arr0[f38] = f10;
				}

				if (f30 >= f38 && f8 != f10)
				{
					f40 = 1m;
				}

				if (f30 == f38 && f40 == 0m)
				{
					f38 = 0;
				}
			}

			if (f38 >= f48)
			{
				for (var varA = 1; varA <= f48; varA++)
				{
					arr2[varA] = varA;
					arr3[varA] = varA;
					arr1[varA] = arr0[varA];
				}

				for (var varA = 1; varA <= f48 - 1; varA++)
				{
					var24 = arr1[varA];
					var var12 = varA;

					for (var var6 = varA + 1; var6 <= f48; var6++)
					{
						if (arr1[var6] < var24)
						{
							var24 = arr1[var6];
							var12 = var6;
						}
					}

					var20 = arr1[varA];
					arr1[varA] = arr1[var12];
					arr1[var12] = var20;

					var20 = arr2[varA];
					arr2[varA] = arr2[var12];
					arr2[var12] = var20;
				}

				var varIndex = 1;
				while (f48 > varIndex)
				{
					var var6 = varIndex + 1;
					var14 = 1m;
					var1C = arr3[varIndex];

					while (var14 != 0m && var6 < arr3.Length)
					{
						if (arr1[varIndex] != arr1[var6])
						{
							if ((var6 - varIndex) > 1)
							{
								var1C /= (var6 - varIndex);

								for (var varE = varIndex; varE <= var6 - 1; varE++)
								{
									arr3[varE] = var1C;
								}
							}

							var14 = 0m;
						}
						else
						{
							var1C += arr3[var6];
							var6 += 1;

							if (var6 > f48 + 1)
							{
								break;
							}
						}
					}

					varIndex = var6;
				}

				var1C = 0m;
				for (var varA = 1; varA <= f48; varA++)
				{
					var1C += (arr3[varA] - f20) * (arr2[varA] - f20);
				}

				var18 = f18 * var1C;
			}
			else
			{
				var18 = 0m;
			}

			value = var18;

			if (value == 0m)
			{
				value = 0.00001m;
			}
		}

		return value;
	}

	private decimal GetClose(int shift)
	{
		var index = _closeHistory.Count - 1 - shift;
		if (index < 0)
		{
			index = 0;
		}

		return _closeHistory[index];
	}

	private decimal GetHigh(int shift)
	{
		var index = _highHistory.Count - 1 - shift;
		if (index < 0)
		{
			index = 0;
		}

		return _highHistory[index];
	}

	private decimal GetLow(int shift)
	{
		var index = _lowHistory.Count - 1 - shift;
		if (index < 0)
		{
			index = 0;
		}

		return _lowHistory[index];
	}

	private decimal GetDistance(decimal points)
	{
		return points * _pointValue;
	}
}