GitHub で見る

ベクターバスケットトレンド戦略(MT4ポート)

このフォルダには、MetaTrader 4 Expert Advisor Vector の StockSharp の高レベル API ポートが含まれています (元のスクリプト: MQL/8305/Vector.mq4)。この戦略は、最大 4 つの主要外国為替ペア (EURUSD (プライマリー)、GBPUSD、USDCHF、および USDJPY) を調整し、共有の平滑移動平均の調整が表示されると、それらを同じ方向に取引します。この変換では、中心となる Vector のアイデアを維持しながら、慣用的な StockSharp パターンに適応させます。

取引ロジック

  1. 平滑移動平均 (SMMA) – 各商品は、設定可能な取引時間枠 (デフォルトでは 15 分) の中央値に基づいて計算された高速 (3 期間) および低速 (7 期間) の SMMA を追跡します。
  2. ベクトル トレンド フィルター – すべての高速/低速ペアの差が合計されます。プラスの合計はバスケット全体で同期した強気の勢いを示し、マイナスの合計は全体的な弱気の圧力を示します。
  3. エントリールール – この戦略は、次の場合にのみ成行注文でポジションをオープンまたは反転します。
    • バスケットのトレンドはポジティブであり、商品の高速 SMMA は低速 SMMA (ロングエントリー) を上回っています。
    • バスケットのトレンドはネガティブで、ファースト SMMA はスロー SMMA (ショート エントリー) を下回っています。
  4. H4 レンジからのピップターゲット – 各商品について、個別の 4 時間のローソク足サブスクリプションで以前のレンジを測定します。その範囲の 5 分の 1 (13 ピップに上限) がポジションごとの利益目標となり、MT4 コードからの固定ピップの出口を反映しています。
  5. グローバル株式ガード – パーセンテージベースの利益とドローダウンのしきい値(元の PrcProfit および PrcLose 入力から取得)は、トリガーされるとすべてのオープンポジションをクローズします。

オリジナルの EA との主な違い

  • StockSharp の 高レベルのローソク足サブスクリプションとインジケーター バインディングは、MT4 (SubscribeCandles().Bind(...)) にある低レベルのポーリングを置き換えます。
  • このポートは オプションのセカンダリ商品をサポートしています。メイン証券のみを取引するには、GBPUSD / USDCHF / USDJPY スロットを空のままにしておきます。
  • MT4 アカウントの証拠金に関連付けられた動的なロットサイジングは、各証券の VolumeStepMinVolume、および MaxVolume に正規化されたクリーンな BaseVolume パラメータに置き換えられました。
  • 取引管理は、OnNewMyTrade コールバックを通じてエントリー価格を保存し、許可されていないインジケーター値の直接検索を回避します。

パラメーター

名前 デフォルト 説明
CandleType TimeSpan.FromMinutes(15) SMMA の計算とエントリのチェックに使用される時間枠。
RangeCandleType TimeSpan.FromHours(4) 適応型 pip ターゲットを導出するために使用されるより高いタイムフレーム。
SecondSecurity null オプションの GBPUSD スロット (開始前に Security を設定)。
ThirdSecurity null オプションのUSDCHFスロット。
FourthSecurity null オプションのUSDJPYスロット。
BaseVolume 1 取引限度に対して正規化された、注文ごとの要求された取引量。
TakeProfitPercent 0.5 ポートフォリオ全体のエグジットを引き起こすグローバル株式利益(%)。
MaxDrawdownPercent 30 すべてのポジションがクローズされるまでの最大許容資本ドローダウン (%)。

使用上の注意

  • 戦略を開始する前に、パラメータによって参照されるすべての証券に同じコネクタとポートフォリオを割り当てます。
  • データ ソースがすべての商品の取引時間枠とレンジ時間枠の両方を提供していることを確認してください。
  • オプションの証券が提供されていない場合、ベクトル計算は利用可能な商品に自動的に適応します。
  • 元の MT4 の動作と一致するように、成行注文では常に終了が発生します。

ファイル

  • CS/VectorStrategy.cs – StockSharp の高レベルのガイドラインに従った C# 実装。
  • README.mdREADME_ru.mdREADME_zh.md – 英語、ロシア語、中国語による戦略ドキュメント。
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>
/// Port of the MetaTrader 4 expert advisor "Vector".
/// The strategy trades up to four correlated forex pairs using smoothed moving averages.
/// </summary>
public class VectorBasketTrendStrategy : Strategy
{
	private readonly StrategyParam<Security> _secondSecurity;
	private readonly StrategyParam<Security> _thirdSecurity;
	private readonly StrategyParam<Security> _fourthSecurity;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<DataType> _rangeCandleType;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _takeProfitPercent;
	private readonly StrategyParam<decimal> _maxDrawdownPercent;

	private readonly Dictionary<Security, InstrumentContext> _contexts = new();
	private readonly Dictionary<Security, decimal> _lastPositions = new();

	private decimal _initialBalance;
	private bool _profitTargetTriggered;
	private bool _drawdownTriggered;

	/// <summary>
	/// Secondary instrument representing GBPUSD in the original script.
	/// </summary>
	public Security SecondSecurity
	{
		get => _secondSecurity.Value;
		set => _secondSecurity.Value = value;
	}

	/// <summary>
	/// Third instrument representing USDCHF in the original script.
	/// </summary>
	public Security ThirdSecurity
	{
		get => _thirdSecurity.Value;
		set => _thirdSecurity.Value = value;
	}

	/// <summary>
	/// Fourth instrument representing USDJPY in the original script.
	/// </summary>
	public Security FourthSecurity
	{
		get => _fourthSecurity.Value;
		set => _fourthSecurity.Value = value;
	}

	/// <summary>
	/// Trading candle type used to compute the smoothed moving averages.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe candle type that defines the profit target distance.
	/// </summary>
	public DataType RangeCandleType
	{
		get => _rangeCandleType.Value;
		set => _rangeCandleType.Value = value;
	}

	/// <summary>
	/// Base volume requested for each trade.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Percentage profit target that closes every open position.
	/// </summary>
	public decimal TakeProfitPercent
	{
		get => _takeProfitPercent.Value;
		set => _takeProfitPercent.Value = value;
	}

	/// <summary>
	/// Maximum tolerated equity drawdown expressed in percent.
	/// </summary>
	public decimal MaxDrawdownPercent
	{
		get => _maxDrawdownPercent.Value;
		set => _maxDrawdownPercent.Value = value;
	}

	/// <summary>
/// Initializes a new instance of the <see cref="VectorBasketTrendStrategy"/> class.
/// </summary>
public VectorBasketTrendStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for MA calculation", "General");

		_rangeCandleType = Param(nameof(RangeCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Range Candle Type", "Higher timeframe for pip target", "General");

		_secondSecurity = Param<Security>(nameof(SecondSecurity))
			.SetDisplay("Second Security", "Optional correlated instrument", "General");

		_thirdSecurity = Param<Security>(nameof(ThirdSecurity))
			.SetDisplay("Third Security", "Optional correlated instrument", "General");

		_fourthSecurity = Param<Security>(nameof(FourthSecurity))
			.SetDisplay("Fourth Security", "Optional correlated instrument", "General");

		_baseVolume = Param(nameof(BaseVolume), 1m)
			.SetDisplay("Base Volume", "Requested volume for each trade", "Trading");

		_takeProfitPercent = Param(nameof(TakeProfitPercent), 0.5m)
			.SetDisplay("Account Take Profit %", "Equity gain that forces a global exit", "Risk");

		_maxDrawdownPercent = Param(nameof(MaxDrawdownPercent), 30m)
			.SetDisplay("Max Drawdown %", "Equity loss that forces a global exit", "Risk");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		foreach (var security in EnumerateSecurities())
		{
			yield return (security, CandleType);
			yield return (security, RangeCandleType);
		}
	}

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

		_contexts.Clear();
		_lastPositions.Clear();
		_initialBalance = 0m;
		_profitTargetTriggered = false;
		_drawdownTriggered = false;
	}

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

		if (Security == null)
			throw new InvalidOperationException("Primary security is not specified.");

		_initialBalance = Portfolio?.CurrentValue ?? 0m;
		CreateContext(Security);
		CreateContext(SecondSecurity);
		CreateContext(ThirdSecurity);
		CreateContext(FourthSecurity);
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		var security = trade.Order.Security;
		if (security == null)
			return;

		if (!_contexts.TryGetValue(security, out var context))
			return;

		var position = GetPositionVolume(security);
		_lastPositions.TryGetValue(security, out var previousPosition);
		var tradeVolume = trade.Trade.Volume;

		if (previousPosition == 0m && position != 0m)
		{
			context.EntryPrice = trade.Trade.Price;
		}
		else if (position == 0m)
		{
			context.EntryPrice = null;
		}
		else if (Math.Sign((double)previousPosition) != Math.Sign((double)position))
		{
			context.EntryPrice = trade.Trade.Price;
		}
		else if (position > 0m && trade.Order.Side == Sides.Buy)
		{
			UpdateAverageEntry(context, previousPosition, position, tradeVolume, trade.Trade.Price);
		}
		else if (position < 0m && trade.Order.Side == Sides.Sell)
		{
			UpdateAverageEntry(context, Math.Abs(previousPosition), Math.Abs(position), tradeVolume, trade.Trade.Price);
		}

		_lastPositions[security] = position;
	}

	private void CreateContext(Security security)
	{
		if (security == null)
			return;

		if (_contexts.ContainsKey(security))
			return;

		var fast = new SmoothedMovingAverage { Length = 3 };
		var slow = new SmoothedMovingAverage { Length = 7 };
		var context = new InstrumentContext(security, fast, slow, GetPipSize(security));
		_contexts.Add(security, context);
		_lastPositions[security] = GetPositionVolume(security);

		var subscription = SubscribeCandles(CandleType, security: security);
		subscription
			.Bind(fast, slow, (candle, fastValue, slowValue) => ProcessInstrumentCandle(context, candle, fastValue, slowValue))
			.Start();

		var rangeSubscription = SubscribeCandles(RangeCandleType, security: security);
		rangeSubscription
			.Bind(candle => ProcessRangeCandle(context, candle))
			.Start();
	}

	private void ProcessInstrumentCandle(InstrumentContext context, ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		context.FastValue = fastValue;
		context.SlowValue = slowValue;
		context.LastClose = candle.ClosePrice;

		EvaluateExits(context);
		EvaluateEntries();
		CheckGlobalRisk();
	}

	private void ProcessRangeCandle(InstrumentContext context, ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var range = candle.HighPrice - candle.LowPrice;
		if (range <= 0m || context.PipSize <= 0m)
		{
			context.TargetDistance = 0m;
			return;
		}

		var pipRange = range / context.PipSize;
		var targetPips = Math.Min(13m, pipRange / 5m);
		context.TargetDistance = targetPips * context.PipSize;
	}

	private void EvaluateEntries()
	{
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var totalTrend = 0m;
		var readyPairs = 0;

		foreach (var context in _contexts.Values)
		{
			if (context.FastValue is decimal fast && context.SlowValue is decimal slow)
			{
				totalTrend += fast - slow;
				readyPairs++;
			}
		}

		if (readyPairs == 0 || totalTrend == 0m)
			return;

		var bullish = totalTrend > 0m;

		foreach (var context in _contexts.Values)
		{
			if (context.FastValue is not decimal fast || context.SlowValue is not decimal slow)
				continue;

			var position = GetPositionVolume(context.Security);
			if (bullish)
			{
				if (fast > slow && position <= 0m)
					TryEnter(context, Sides.Buy, Math.Abs(position));
			}
			else
			{
				if (fast < slow && position >= 0m)
					TryEnter(context, Sides.Sell, Math.Abs(position));
			}
		}
	}

	private void EvaluateExits(InstrumentContext context)
	{
		if (context.EntryPrice is null || context.TargetDistance <= 0m || context.LastClose is null)
			return;

		var position = GetPositionVolume(context.Security);
		if (position == 0m)
			return;

		var entryPrice = context.EntryPrice.Value;
		var lastClose = context.LastClose.Value;
		var profit = lastClose - entryPrice;

		if (position > 0m && profit >= context.TargetDistance)
		{
			SellMarket(position, context.Security);
		}
		else if (position < 0m && -profit >= context.TargetDistance)
		{
			BuyMarket(Math.Abs(position), context.Security);
		}
	}

	private void TryEnter(InstrumentContext context, Sides side, decimal currentPosition)
	{
		var volume = NormalizeVolume(context.Security, BaseVolume);
		if (volume <= 0m)
			return;

		if (side == Sides.Buy)
		{
			if (currentPosition > 0m)
			{
				SellMarket(currentPosition, context.Security);
			}
			BuyMarket(volume, context.Security);
		}
		else
		{
			if (currentPosition > 0m)
			{
				BuyMarket(currentPosition, context.Security);
			}
			SellMarket(volume, context.Security);
		}
	}

	private decimal NormalizeVolume(Security security, decimal requestedVolume)
	{
		var step = security.VolumeStep ?? 0m;
		var min = security.MinVolume ?? 0m;
		var max = security.MaxVolume;
		var volume = requestedVolume;

		if (step > 0m)
			volume = Math.Round(volume / step) * step;

		if (min > 0m && volume < min)
			volume = min;

		if (max != null && volume > max.Value)
			volume = max.Value;

		return volume;
	}

	private void CheckGlobalRisk()
	{
		if (Portfolio?.CurrentValue is not decimal currentValue || _initialBalance <= 0m)
			return;

		var profit = currentValue - _initialBalance;
		var profitThreshold = _initialBalance * (TakeProfitPercent / 100m);
		var lossThreshold = _initialBalance * (MaxDrawdownPercent / 100m);

		if (!_profitTargetTriggered && profitThreshold > 0m && profit >= profitThreshold)
		{
			_profitTargetTriggered = true;
			CloseAllPositions("Global profit target reached");
		}

		if (!_drawdownTriggered && lossThreshold > 0m && -profit >= lossThreshold)
		{
			_drawdownTriggered = true;
			CloseAllPositions("Global drawdown limit reached");
		}
	}

	private void CloseAllPositions(string reason)
	{
		foreach (var context in _contexts.Values)
		{
			var position = GetPositionVolume(context.Security);
			if (position > 0m)
			{
				SellMarket(position, context.Security);
			}
			else if (position < 0m)
			{
				BuyMarket(Math.Abs(position), context.Security);
			}
		}

		LogInfo(reason);
	}

	private void UpdateAverageEntry(InstrumentContext context, decimal previousPosition, decimal currentPosition, decimal tradeVolume, decimal tradePrice)
	{
		if (tradeVolume <= 0m || previousPosition <= 0m || currentPosition <= 0m)
		{
			context.EntryPrice ??= tradePrice;
			return;
		}

		if (context.EntryPrice is decimal existing)
		{
			context.EntryPrice = ((existing * previousPosition) + (tradePrice * tradeVolume)) / currentPosition;
		}
		else
		{
			context.EntryPrice = tradePrice;
		}
	}

	private IEnumerable<Security> EnumerateSecurities()
	{
		if (Security != null)
			yield return Security;
		if (SecondSecurity != null)
			yield return SecondSecurity;
		if (ThirdSecurity != null)
			yield return ThirdSecurity;
		if (FourthSecurity != null)
			yield return FourthSecurity;
	}

	private decimal GetPositionVolume(Security security)
	{
		return GetPositionValue(security, Portfolio) ?? 0m;
	}

	private static decimal GetPipSize(Security security)
	{
		var step = security.PriceStep;
		if (step == null || step == 0m)
			return 0.0001m;

		return step.Value;
	}

	private sealed class InstrumentContext
	{
		public InstrumentContext(Security security, SmoothedMovingAverage fast, SmoothedMovingAverage slow, decimal pipSize)
		{
			Security = security;
			Fast = fast;
			Slow = slow;
			PipSize = pipSize;
		}

		public Security Security { get; }
		public SmoothedMovingAverage Fast { get; }
		public SmoothedMovingAverage Slow { get; }
		public decimal PipSize { get; }
		public decimal? FastValue { get; set; }
		public decimal? SlowValue { get; set; }
		public decimal TargetDistance { get; set; }
		public decimal? EntryPrice { get; set; }
		public decimal? LastClose { get; set; }
	}
}