GitHub で見る

ディーラーの取引 MACD MQL4 戦略

Dealers Trade MACD MQL4 戦略は、MetaTrader 4 の「Dealers Trade v7.74」エキスパート アドバイザーを直接変換したものです。元のシステムのピラミッド型資金管理と MACD スロープ ロジックを維持しながら、ポジション処理を StockSharp のネット口座に適応させます。この戦略は H4/D1 チャートでのスイングトレード向けに設計されており、勢いが MACD のメインラインと一致している限り、継続的にトレンドを追加します。

戦略の仕組み

  • シグナル検出 – この戦略は、設定された時間枠のローソク足をサブスクライブし、古典的な MACD インジケーター (速い EMA、遅い EMA、シグナル EMA) を計算します。前のバーと比較して MACD メイン値の上昇は強気の勢いを示し、値の低下は弱気の勢いを示します。逆張りアプローチが好ましい場合、ReverseCondition パラメータを使用して方向を反転できます。
  • 注文の間隔とスケーリング – 一度に 1 つの方向性バスケットのみがアクティブになります。 MACD が長期トレンドを示している場合、戦略は最初の成行買い注文を開きます。追加購入は、価格が最後のエントリー価格から少なくとも SpacingPips * PriceStep 下がった場合にのみ送信され、MQL スクリプトの「平均化」動作を反映しています。ショートバスケットは、MACD の傾きが負になると対称的に動作します。
  • ロットサイジング – 基本ロットサイズは固定の FixedVolume、または UseRiskSizing が有効な場合はポートフォリオの資本と RiskPercent から導出される値のいずれかです。ミニ アカウントは、元の「アカウントは通常」オプションをエミュレートする IsStandardAccount フラグを通じてサポートされます。同じバスケット内の追加注文はすべて LotMultiplier 倍され、上限は MaxVolume となります。
  • リスク管理 – ハードストップロスとテイクプロフィットレベルは、StopLossPips と TakeProfitPips の距離を使用して各ポジションに関連付けられます。取引で利益が TrailingStopPips + SpacingPips 移動すると、少なくとも TrailingStopPips の利益を維持するためにストップ レベルが厳しくなり、MetaTrader 実装のトレーリング ルールが再現されます。
  • アカウント保護 – オープン取引の数が MaxTrades - OrdersToProtect に達し、未実現利益の合計が SecureProfit を超えると、新しい注文が検討される前に、利益を固定するために最新の取引が終了します。これは、ソース EA の「AccountProtection」ブロックに対応します。

パラメーター

名前 デフォルト 説明
CandleType H4 MACD の計算と信号評価に使用されるタイムフレーム。
FixedVolume 0.1 UseRiskSizing が無効になっている場合の基本ロット サイズ。
UseRiskSizing 本当の バランスに基づいたポジションサイジングを有効にします。
RiskPercent 2 UseRiskSizing が true の場合、ポジションのサイズ設定に使用される資本の割合。
IsStandardAccount 本当の ミニ口座 (ロットを 10 で割ったもの) の場合は false に設定します。
MaxVolume 5 1回の注文で許可される最大量。
LotMultiplier 1.5 バスケット内の追加エントリごとに基本ロットに適用される乗数。
MaxTrades 5 同時にオープンする取引の最大数。
SpacingPips 4 連続するエントリ間の最小ピップ距離。
OrdersToProtect 3 保護ブロックが新しい取引を開始できるようになるまでに保持される注文の数。
AccountProtection 本当の 安全な利益保護ロジックを有効にします。
SecureProfit 50 保護を発動するには未実現利益 (口座通貨) が必要です。
TakeProfitPips 30 取引ごとの利益確定距離 (pips で表されます)。
StopLossPips 90 取引ごとのストップロス距離 (pips で表されます)。
TrailingStopPips 15 アクティブ化後に適用されるトレーリング ストップ距離。
ReverseCondition 偽 MACD の傾きの解釈を反転します。
MacdFast 14 MACD インジケーターの高速な EMA の長さ。
MacdSlow 26 MACD インジケーターの EMA の長さが遅いです。
MacdSignal 1 MACD インジケーターの信号 EMA の長さ。

注意事項と制限事項

  • StockSharp 戦略は証券ごとにネット ポジションを管理するため、ヘッジされたロング バスケットとショート バスケットを共存させることはできません。元の EA ではヘッジが可能でしたが、変換により方向が切り替わる前に反対側がクローズされます。
  • 安全な利益ロジックは、商品 PriceStep および StepPrice のメタデータを使用して未実現利益を計算します。この情報のない商品は単位通貨ステップで名目ピップ値 0.0001 にフォールバックするため、それに応じてしきい値を調整します。
  • リスクベースのサイジングには、正の StopLossPips 値が必要です。ストップ距離がゼロの場合、計算されたリスク量は不定となり、戦略は取引をスキップします。
  • この戦略は閉じたローソク足でのみ機能します。 MetaTrader のバー内 MACD の動きに依存したシグナルは、この実装の後半でバーとして表示される場合がありますが、バックテストでは動作が大幅に安定しています。
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>
/// Dealers Trade MACD strategy converted from the original MQL4 version (Dealers Trade v7.74).
/// </summary>
public class DealersTradeMacdMql4Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<bool> _useRiskSizing;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<bool> _isStandardAccount;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _lotMultiplier;
	private readonly StrategyParam<int> _maxTrades;
	private readonly StrategyParam<int> _spacingPips;
	private readonly StrategyParam<int> _ordersToProtect;
	private readonly StrategyParam<bool> _accountProtection;
	private readonly StrategyParam<decimal> _secureProfit;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<bool> _reverseCondition;
	private readonly StrategyParam<int> _macdFast;
	private readonly StrategyParam<int> _macdSlow;
	private readonly StrategyParam<int> _macdSignal;

	private MovingAverageConvergenceDivergence _macd;
	private List<PositionState> _positions;
	private decimal? _previousMacd;
	private decimal _pipSize;
	private decimal _stepValue;
	private int _cooldown;

	/// <summary>
	/// Initializes a new instance of <see cref="DealersTradeMacdMql4Strategy"/>.
	/// </summary>
	public DealersTradeMacdMql4Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for signals", "General");

		_fixedVolume = Param(nameof(FixedVolume), 0.1m)
			.SetDisplay("Fixed Volume", "Lot size when risk sizing is disabled", "Risk");

		_useRiskSizing = Param(nameof(UseRiskSizing), true)
			.SetDisplay("Use Risk Sizing", "Enable balance based money management", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 2m)
			.SetDisplay("Risk Percent", "Percentage of equity used when sizing dynamically", "Risk");

		_isStandardAccount = Param(nameof(IsStandardAccount), true)
			.SetDisplay("Standard Account", "True for standard (1.0 lot) accounts, false for mini", "Risk");

		_maxVolume = Param(nameof(MaxVolume), 5m)
			.SetDisplay("Max Volume", "Upper cap for any single order", "Risk")
			.SetGreaterThanZero();

		_lotMultiplier = Param(nameof(LotMultiplier), 1.5m)
			.SetDisplay("Lot Multiplier", "Multiplier applied to subsequent entries", "Money Management")
			.SetGreaterThanZero();

		_maxTrades = Param(nameof(MaxTrades), 1)
			.SetDisplay("Max Trades", "Maximum simultaneous positions", "Money Management")
			.SetGreaterThanZero();

		_spacingPips = Param(nameof(SpacingPips), 200)
			.SetDisplay("Spacing (pips)", "Minimum price movement before adding", "Money Management")
			.SetNotNegative();

		_ordersToProtect = Param(nameof(OrdersToProtect), 3)
			.SetDisplay("Orders To Protect", "Number of trades kept when protection triggers", "Money Management")
			.SetNotNegative();

		_accountProtection = Param(nameof(AccountProtection), true)
			.SetDisplay("Account Protection", "Close last trade once secure profit is reached", "Money Management");

		_secureProfit = Param(nameof(SecureProfit), 50m)
			.SetDisplay("Secure Profit", "Currency profit required to lock gains", "Money Management")
			.SetNotNegative();

		_takeProfitPips = Param(nameof(TakeProfitPips), 200)
			.SetDisplay("Take Profit (pips)", "Take profit distance from entry", "Risk")
			.SetNotNegative();

		_stopLossPips = Param(nameof(StopLossPips), 500)
			.SetDisplay("Stop Loss (pips)", "Initial stop loss distance", "Risk")
			.SetNotNegative();

		_trailingStopPips = Param(nameof(TrailingStopPips), 100)
			.SetDisplay("Trailing Stop (pips)", "Trailing distance applied after activation", "Risk")
			.SetNotNegative();

		_reverseCondition = Param(nameof(ReverseCondition), false)
			.SetDisplay("Reverse Condition", "Invert MACD slope interpretation", "General");

		_macdFast = Param(nameof(MacdFast), 14)
			.SetDisplay("MACD Fast", "Fast EMA length", "Indicators")
			.SetGreaterThanZero();

		_macdSlow = Param(nameof(MacdSlow), 26)
			.SetDisplay("MACD Slow", "Slow EMA length", "Indicators")
			.SetGreaterThanZero();

		_macdSignal = Param(nameof(MacdSignal), 1)
			.SetDisplay("MACD Signal", "Signal EMA length", "Indicators")
			.SetGreaterThanZero();
	}

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

	/// <summary>
	/// Fixed order volume in lots.
	/// </summary>
	public decimal FixedVolume
	{
		get => _fixedVolume.Value;
		set => _fixedVolume.Value = value;
	}

	/// <summary>
	/// Enables balance based position sizing.
	/// </summary>
	public bool UseRiskSizing
	{
		get => _useRiskSizing.Value;
		set => _useRiskSizing.Value = value;
	}

	/// <summary>
	/// Risk percentage applied when <see cref="UseRiskSizing"/> is true.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Indicates whether the account uses standard lot sizes.
	/// </summary>
	public bool IsStandardAccount
	{
		get => _isStandardAccount.Value;
		set => _isStandardAccount.Value = value;
	}

	/// <summary>
	/// Maximum volume allowed for a single order.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the base size for subsequent entries.
	/// </summary>
	public decimal LotMultiplier
	{
		get => _lotMultiplier.Value;
		set => _lotMultiplier.Value = value;
	}

	/// <summary>
	/// Maximum number of simultaneously open trades.
	/// </summary>
	public int MaxTrades
	{
		get => _maxTrades.Value;
		set => _maxTrades.Value = value;
	}

	/// <summary>
	/// Minimum spacing between entries expressed in pips.
	/// </summary>
	public int SpacingPips
	{
		get => _spacingPips.Value;
		set => _spacingPips.Value = value;
	}

	/// <summary>
	/// Number of orders that should remain protected before adding new exposure.
	/// </summary>
	public int OrdersToProtect
	{
		get => _ordersToProtect.Value;
		set => _ordersToProtect.Value = value;
	}

	/// <summary>
	/// Enables the secure profit exit block.
	/// </summary>
	public bool AccountProtection
	{
		get => _accountProtection.Value;
		set => _accountProtection.Value = value;
	}

	/// <summary>
	/// Profit target used by the protection block.
	/// </summary>
	public decimal SecureProfit
	{
		get => _secureProfit.Value;
		set => _secureProfit.Value = value;
	}

	/// <summary>
	/// Take profit distance in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop loss distance in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Inverts the MACD slope interpretation.
	/// </summary>
	public bool ReverseCondition
	{
		get => _reverseCondition.Value;
		set => _reverseCondition.Value = value;
	}

	/// <summary>
	/// Fast EMA period for the MACD indicator.
	/// </summary>
	public int MacdFast
	{
		get => _macdFast.Value;
		set => _macdFast.Value = value;
	}

	/// <summary>
	/// Slow EMA period for the MACD indicator.
	/// </summary>
	public int MacdSlow
	{
		get => _macdSlow.Value;
		set => _macdSlow.Value = value;
	}

	/// <summary>
	/// Signal EMA period for the MACD indicator.
	/// </summary>
	public int MacdSignal
	{
		get => _macdSignal.Value;
		set => _macdSignal.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_macd = null;
		_positions = null;
		_previousMacd = null;
		_pipSize = 0;
		_stepValue = 0;
		_cooldown = 0;
	}

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

		_positions = new List<PositionState>();

		_macd = new MovingAverageConvergenceDivergence(
			new ExponentialMovingAverage { Length = MacdSlow },
			new ExponentialMovingAverage { Length = MacdFast }
		);

		_pipSize = GetPriceStep();
		_stepValue = Security?.PriceStep ?? 0m;
		if (_stepValue <= 0m)
			_stepValue = 1m;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_macd, ProcessCandle)
			.Start();
	}

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

		if (!macdResult.IsFinal || !_macd.IsFormed)
			return;

		var macdValue = macdResult.GetValue<decimal>();

		UpdateTrailingAndStops(candle);

		if (_cooldown > 0)
		{
			_cooldown--;
			_previousMacd = macdValue;
			return;
		}

		var openTrades = _positions.Count;
		var allowNewTrade = openTrades < MaxTrades;

		if (_previousMacd is null)
		{
			_previousMacd = macdValue;
			return;
		}

		var direction = Math.Sign(macdValue - _previousMacd.Value);
		if (ReverseCondition)
			direction = -direction;

		if (AccountProtection && openTrades >= Math.Max(1, MaxTrades - OrdersToProtect))
		{
			var totalProfit = CalculateTotalProfit(candle.ClosePrice);
			if (totalProfit >= SecureProfit)
			{
				CloseLastPosition();
				_cooldown = 3;
				_previousMacd = macdValue;
				return;
			}
		}

		if (allowNewTrade && direction > 0)
			TryOpen(Sides.Buy, candle);
		else if (allowNewTrade && direction < 0)
			TryOpen(Sides.Sell, candle);

		_previousMacd = macdValue;
	}

	private void TryOpen(Sides side, ICandleMessage candle)
	{
		var price = candle.ClosePrice;
		var spacing = SpacingPips * _pipSize;

		if (side == Sides.Buy)
		{
			var reference = GetReferencePrice(Sides.Buy);
			if (reference != 0m && reference - price < spacing)
				return;
		}
		else
		{
			var reference = GetReferencePrice(Sides.Sell);
			if (reference != 0m && price - reference < spacing)
				return;
		}

		var volume = CalculateVolume();
		if (volume <= 0m)
			return;

		var sameSideCount = CountPositions(side);
		if (sameSideCount > 0)
		{
			volume *= Pow(LotMultiplier, sameSideCount);
		}

		volume = NormalizeVolume(Math.Min(volume, MaxVolume));
		if (volume <= 0m)
			return;

		var stopDistance = StopLossPips * _pipSize;
		var takeDistance = TakeProfitPips * _pipSize;

		if (side == Sides.Buy)
			BuyMarket();
		else
			SellMarket();

		var state = new PositionState
		{
			Side = side,
			Volume = volume,
			EntryPrice = price,
			StopPrice = stopDistance > 0m ? (side == Sides.Buy ? price - stopDistance : price + stopDistance) : (decimal?)null,
			TakeProfitPrice = takeDistance > 0m ? (side == Sides.Buy ? price + takeDistance : price - takeDistance) : (decimal?)null
		};

		_positions.Add(state);
		_cooldown = 3;
	}

	private void UpdateTrailingAndStops(ICandleMessage candle)
	{
		var trailingDistance = TrailingStopPips * _pipSize;
		var activationDistance = (TrailingStopPips + SpacingPips) * _pipSize;

		for (var i = _positions.Count - 1; i >= 0; i--)
		{
			var state = _positions[i];

			if (state.Side == Sides.Buy)
			{
				if (state.TakeProfitPrice is decimal tp && candle.HighPrice >= tp)
				{
					SellMarket();
					_positions.RemoveAt(i);
					_cooldown = 3;
					continue;
				}

				if (state.StopPrice is decimal sl && candle.LowPrice <= sl)
				{
					SellMarket();
					_positions.RemoveAt(i);
					_cooldown = 3;
					continue;
				}

				if (TrailingStopPips > 0 && candle.ClosePrice - state.EntryPrice >= activationDistance)
				{
					var candidate = candle.ClosePrice - trailingDistance;
					if (state.StopPrice is null || state.StopPrice < candidate)
						state.StopPrice = candidate;
				}
			}
			else
			{
				if (state.TakeProfitPrice is decimal tp && candle.LowPrice <= tp)
				{
					BuyMarket();
					_positions.RemoveAt(i);
					_cooldown = 3;
					continue;
				}

				if (state.StopPrice is decimal sl && candle.HighPrice >= sl)
				{
					BuyMarket();
					_positions.RemoveAt(i);
					_cooldown = 3;
					continue;
				}

				if (TrailingStopPips > 0 && state.EntryPrice - candle.ClosePrice >= activationDistance)
				{
					var candidate = candle.ClosePrice + trailingDistance;
					if (state.StopPrice is null || state.StopPrice > candidate)
						state.StopPrice = candidate;
				}
			}
		}
	}

	private decimal CalculateVolume()
	{
		decimal baseVolume;

		if (UseRiskSizing)
		{
			if (Portfolio is null)
				return 0m;

			var balance = Portfolio.CurrentValue ?? 0m;
			if (balance <= 0m)
				return 0m;

			var rawLots = Math.Ceiling(balance * (RiskPercent / 100m) / 10000m);
			if (!IsStandardAccount)
				rawLots /= 10m;

			baseVolume = rawLots;
		}
		else
		{
			baseVolume = FixedVolume;
		}

		return baseVolume;
	}

	private decimal CalculateTotalProfit(decimal currentPrice)
	{
		decimal profit = 0m;

		foreach (var state in _positions)
		{
			var priceDifference = state.Side == Sides.Buy
				? currentPrice - state.EntryPrice
				: state.EntryPrice - currentPrice;

			var steps = _pipSize > 0m ? priceDifference / _pipSize : priceDifference;
			profit += steps * _stepValue * state.Volume;
		}

		return profit;
	}

	private void CloseLastPosition()
	{
		if (_positions.Count == 0)
			return;

		var index = _positions.Count - 1;
		var state = _positions[index];

		if (state.Side == Sides.Buy)
			SellMarket();
		else
			BuyMarket();

		_positions.RemoveAt(index);
	}

	private decimal GetReferencePrice(Sides side)
	{
		for (var i = _positions.Count - 1; i >= 0; i--)
		{
			var state = _positions[i];
			if (state.Side == side)
				return state.EntryPrice;
		}

		return 0m;
	}

	private int CountPositions(Sides side)
	{
		var count = 0;
		for (var i = 0; i < _positions.Count; i++)
		{
			if (_positions[i].Side == side)
				count++;
		}

		return count;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			var steps = Math.Floor(volume / step);
			if (steps <= 0)
				steps = 1;
			volume = steps * step;
		}
		else
		{
			volume = Math.Round(volume, 1, MidpointRounding.AwayFromZero);
			if (volume <= 0m)
				volume = 0.1m;
		}

		return volume;
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step > 0m)
			return step;

		var decimals = Security?.Decimals ?? 0;
		if (decimals > 0)
			return (decimal)Math.Pow(10, -decimals);

		return 0.0001m;
	}

	private static decimal Pow(decimal value, int power)
	{
		if (power <= 0)
			return 1m;

		return (decimal)Math.Pow((double)value, power);
	}

	private sealed class PositionState
	{
		public Sides Side { get; set; }
		public decimal Volume { get; set; }
		public decimal EntryPrice { get; set; }
		public decimal? StopPrice { get; set; }
		public decimal? TakeProfitPrice { get; set; }
	}
}