GitHub で見る

ディーラートレード v7.51 RIVOT (C#)

概要

Dealers Trade v7.51 は、もともと MetaTrader 4 エキスパート アドバイザー Dealers_Trade_v_7.51_RIVOT.mq4 として提供されたマーチンゲール スタイルのグリッド戦略です。このポートは、ピボットベースの方向性バイアスから離れて取引するという元のアイデアを維持し、設定可能なピップ距離だけ価格がリトレースするたびに支配的な側にスケールします。 StockSharp の実装では、高レベルの戦略ヘルパーを使用して、ローソク足のサブスクライブ、ピボット ゾーンの計算、ポジションのサイジング、リスク、手仕舞いの管理を行います。

取引ロジック

  1. ピボット フレームワーク

    • この戦略では、完成したローソク足ごとに 2 つの参照価格を構築します。
      • クラシック ピボット (P) = (previous high + previous low + previous close + current open) / 4
      • フローティング ピボット (FLP) = (current high + current low + current close) / 3
    • 現在の足の取引を可能にするには、PFLP の間のピップの差が GapThreshold 以上である必要があります。
  2. 方向性の偏り

    • ローソク足の終値が両方のピボットの上にあり、ギャップ フィルターが満たされると、バイアスは ロング に切り替わります。
    • ローソク足の終値が両方のピボットを下回り、ギャップが確認されると、バイアスはショートに切り替わります。
    • バイアスは、一連のポジションが完全にクローズされるか、一連のポジションが終了した後に反対の状態が現れるまで有効です。
  3. エントリーのスケーリング

    • 一度にアクティブにできる取引は 1 つのシリーズのみです。
    • 最初のエントリはバイアスの直後に続きます。
    • 追加のエントリーは、価格が最新の約定から少なくとも PipDistance ピップだけアクティブ バイアスに対してリトレースした場合にのみオープンされ、元のマーチンゲール平均法をエミュレートします。
    • 新しい注文ごとに前のサイズに VolumeMultiplier が掛けられますが、MaxVolume を超えることはありません。
    • スタックされるエントリの数は MaxTrades によって制限されます。
  4. リスク管理

    • 出来高加重平均エントリーからの StopLoss ピップでのハードストップロスにより、シリーズ全体が終了します。
    • 価格が有利に戻れば、TakeProfit ピップでの固定テイクプロフィットにより利益が確定します。
    • 有効にすると、トレーリングストップは、平均エントリーを超えて TrailingStop ピップを超えるたびに、価格に近づくことで利益を動的にロックします。
  5. リセット条件

    • 完全なエグジット(ストップロス、テイクプロフィット、トレーリングストップ、または手動でのポジションフラット化)により、マーチンゲールカウンターがリセットされ、方向性のバイアスが除去されます。

パラメーター

パラメータ デフォルト 説明
Volume 1 最初のエントリの基本注文サイズ。
MaxTrades 5 シリーズごとの平均エントリの最大数。
PipDistance 4 新しいポジションを追加する前に必要な最小限の逆方向の動き (ピップ単位)。
TakeProfit 15 出来高加重平均エントリーからグリッド全体を利益で閉じるまでの距離。
StopLoss 90 保護的出口をトリガーする平均的な入口からの距離。
TrailingStop 15 価格が有利に推移するとトレーリングストップオフセットが適用されます。末尾を無効にするには、ゼロに設定します。
VolumeMultiplier 1.5 後続の各エントリの注文サイズを増やすために使用される係数。
MaxVolume 5 乗数を適用した後の単一オーダー量の上限。
GapThreshold 7 バイアスを有効にするために必要なクラシック ピボットとフローティング ピボットの間の最小ギャップ (ピップ単位)。
CandleType 15分足のローソク足 計算や意思決定に使用されるローソクの種類。

すべてのパラメータは StrategyParam<T> を通じて設定されるため、StockSharp デザイナーまたはストラテジー ランナー内で最適化できます。

使用上の注意

  • この戦略はローソク足データのみに依存します。直接のティックレベルの買い/売りストリームは必要ありません。データ プロバイダーが選択した CandleType を配信できることを確認してください。
  • StockSharp はデフォルトでポジションを集計するため、実装では MT4 グリッド ブックをエミュレートするために内部出来高加重平均が維持されます。部分的な約定が発生した場合、組み込みのポジション計算機能により値の一貫性が維持されます。
  • グラフのレンダリングでは、利用可能な場合、グラフ領域に 2 本の水平線 (PivotFloatingPivot) が追加されます。
  • 自動逆取引はありません。システムは、バイアス フリップを受け入れる前に、進行中のシリーズが終了するのを待ちます。

MQL バージョンとの違い

  • 元のスクリプトでは、MT4 チャートに複数のラベルとコメントを描画しました。ポートは機能的な取引ロジックのみを保持し、ビジュアルを StockSharp のチャート線に置き換えます。
  • 未決済注文の合計に基づくアカウント保護機能、手動マジックナンバーフィルタリング、シンボル固有の PIP 値テーブルは、StockSharp では必要とされず、省略されました。
  • MetaTrader コードの正確なティック価格 (Ask == tp) での注文の決済は、ローソク足の終値での価格比較で近似されます。
  • 取引管理は、MT4 チケット ループではなく成行注文 (BuyMarket/SellMarket) を使用して実装されます。トレーリングストップとエグジットはローソク足の更新時に発生します。

ベストプラクティス

  • 運用を開始する前に、常に現実的なスプレッド/手数料モデルを使用して紙取引または履歴シミュレーションで戦略をテストしてください。
  • ドローダウンを制御するには、ボラティリティの高い商品の VolumeMultiplier または MaxTrades を下げることを検討してください。
  • 日中商品の場合は、元の設定のデータ粒度に一致するように CandleType を調整します (デフォルトは 15 分ですが、M15 と H1 では EA が頻繁に使用されました)。

ファイル

  • CS/DealersTradeV751RivotStrategy.cs – メインの C# 実装。
  • README_zh.md – 中国語のドキュメント。
  • README_ru.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>
/// Dealers Trade v7.51 strategy ported from MetaTrader 4 implementation.
/// Builds directional bias from classic pivot and floating pivot levels
/// and scales into the bias when price retraces by a fixed pip distance.
/// Applies martingale-style position sizing with configurable stop-loss,
/// take-profit, and trailing-stop management.
/// </summary>
public class DealersTradeV751RivotStrategy : Strategy
{
	private readonly StrategyParam<int> _maxTrades;
	private readonly StrategyParam<decimal> _pipDistance;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _trailingStop;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _gapThreshold;
	private readonly StrategyParam<DataType> _candleType;

	private ICandleMessage _previousCandle;
	private decimal _pivotLevel;
	private decimal _floatingPivot;
	private decimal _gapInPips;
	private decimal _lastEntryPrice;
	private decimal _averageEntryPrice;
	private decimal? _trailingStopLevel;
	private int _direction; // -1 short, 0 neutral, 1 long
	private int _entriesInSeries;

	/// <summary>
	/// Maximum number of entries allowed in one scaling series.
	/// </summary>
	public int MaxTrades
	{
		get => _maxTrades.Value;
		set => _maxTrades.Value = value;
	}

	/// <summary>
	/// Distance in pips between martingale entries.
	/// </summary>
	public decimal PipDistance
	{
		get => _pipDistance.Value;
		set => _pipDistance.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Trailing-stop distance in pips.
	/// </summary>
	public decimal TrailingStop
	{
		get => _trailingStop.Value;
		set => _trailingStop.Value = value;
	}

	/// <summary>
	/// Multiplier applied to volume for each additional entry.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

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

	/// <summary>
	/// Minimum pivot gap in pips required to activate the bias.
	/// </summary>
	public decimal GapThreshold
	{
		get => _gapThreshold.Value;
		set => _gapThreshold.Value = value;
	}

	/// <summary>
	/// Type of candles used for pivot calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public DealersTradeV751RivotStrategy()
	{
		_maxTrades = Param(nameof(MaxTrades), 2)
		.SetGreaterThanZero()
		.SetDisplay("Max Trades", "Maximum number of martingale entries", "Position Sizing")
		
		.SetOptimize(1, 10, 1);

		_pipDistance = Param(nameof(PipDistance), 10m)
		.SetGreaterThanZero()
		.SetDisplay("Pip Distance", "Distance between averaged entries in pips", "Position Sizing")
		
		.SetOptimize(2m, 15m, 1m);

		_takeProfit = Param(nameof(TakeProfit), 15m)
		.SetGreaterThanZero()
		.SetDisplay("Take Profit", "Take-profit distance in pips", "Risk Management")
		
		.SetOptimize(5m, 50m, 5m);

		_stopLoss = Param(nameof(StopLoss), 90m)
		.SetGreaterThanZero()
		.SetDisplay("Stop Loss", "Stop-loss distance in pips", "Risk Management")
		
		.SetOptimize(30m, 200m, 10m);

		_trailingStop = Param(nameof(TrailingStop), 15m)
		.SetGreaterThanZero()
		.SetDisplay("Trailing Stop", "Trailing-stop distance in pips", "Risk Management")
		
		.SetOptimize(5m, 40m, 5m);

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 1.5m)
		.SetGreaterThanZero()
		.SetDisplay("Volume Multiplier", "Multiplier applied after each new entry", "Position Sizing")
		
		.SetOptimize(1.1m, 3m, 0.1m);

		_maxVolume = Param(nameof(MaxVolume), 5m)
		.SetGreaterThanZero()
		.SetDisplay("Max Volume", "Upper limit for single-entry volume", "Position Sizing");

		_gapThreshold = Param(nameof(GapThreshold), 15m)
		.SetGreaterThanZero()
		.SetDisplay("Gap Threshold", "Minimal pivot gap required to enable trading", "Signal")
		
		.SetOptimize(3m, 15m, 1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles used for pivot calculations", "Signal");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		ResetSeries();
		_previousCandle = null;
		_pivotLevel = 0m;
		_floatingPivot = 0m;
		_gapInPips = 0m;
	}

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

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

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

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position == 0m)
		{
			// Reset martingale state once the position is closed externally.
			ResetSeries();
		}
	}

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

		if (_previousCandle == null)
		{
			_previousCandle = candle;
			return;
		}

		UpdatePivots(candle);

		if (Position == 0m && _entriesInSeries > 0)
		{
			// Force reset when no exposure remains but scaling data still exists.
			ResetSeries();
		}

		if (_entriesInSeries > 0)
		{
			ManageRisk(candle.ClosePrice);
		}

		if (_entriesInSeries >= MaxTrades)
		{
			_previousCandle = candle;
			return;
		}

		if (_direction == 0)
		{
			EvaluateDirection(candle);
		}

		TryEnter(candle);

		_previousCandle = candle;
	}

	private void UpdatePivots(ICandleMessage candle)
	{
		var step = GetPriceStep();
		_pivotLevel = (_previousCandle!.HighPrice + _previousCandle.LowPrice + _previousCandle.ClosePrice + candle.OpenPrice) / 4m;
		_floatingPivot = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m;
		_gapInPips = step == 0m ? 0m : Math.Abs(_pivotLevel - _floatingPivot) / step;
	}

	private void EvaluateDirection(ICandleMessage candle)
	{
		var price = candle.ClosePrice;

		if (price > _pivotLevel && price > _floatingPivot && _gapInPips >= GapThreshold)
		{
			_direction = 1;
			LogInfo($"Bias switched to long. Pivot={_pivotLevel:F5}, Floating={_floatingPivot:F5}, Gap={_gapInPips:F2} pips.");
		}
		else if (price < _pivotLevel && price < _floatingPivot && _gapInPips >= GapThreshold)
		{
			_direction = -1;
			LogInfo($"Bias switched to short. Pivot={_pivotLevel:F5}, Floating={_floatingPivot:F5}, Gap={_gapInPips:F2} pips.");
		}
	}

	private void TryEnter(ICandleMessage candle)
	{
		if (_direction == 0)
		return;

		var price = candle.ClosePrice;
		var step = GetPriceStep();
		var distance = PipDistance * step;

		if (_direction > 0)
		{
			if (_entriesInSeries == 0 || (_lastEntryPrice - price) >= distance)
			{
				EnterLong(price);
			}
		}
		else
		{
			if (_entriesInSeries == 0 || (price - _lastEntryPrice) >= distance)
			{
				EnterShort(price);
			}
		}
	}

	private void EnterLong(decimal price)
	{
		var volume = CalculateNextVolume();
		_lastEntryPrice = price;
		_averageEntryPrice = UpdateAveragePrice(price, volume, true);
		_entriesInSeries++;
		LogInfo($"Opening long entry #{_entriesInSeries} at {price:F5} with volume {volume}.");
		BuyMarket(volume);
	}

	private void EnterShort(decimal price)
	{
		var volume = CalculateNextVolume();
		_lastEntryPrice = price;
		_averageEntryPrice = UpdateAveragePrice(price, volume, false);
		_entriesInSeries++;
		LogInfo($"Opening short entry #{_entriesInSeries} at {price:F5} with volume {volume}.");
		SellMarket(volume);
	}

	private decimal CalculateNextVolume()
	{
		var volume = Volume;

		for (var i = 0; i < _entriesInSeries; i++)
		{
			volume *= VolumeMultiplier;
			if (volume >= MaxVolume)
			{
				volume = MaxVolume;
				break;
			}
		}

		var volumeStep = Security?.VolumeStep ?? 0.01m;
		if (volumeStep > 0m)
		{
			volume = Math.Ceiling(volume / volumeStep) * volumeStep;
		}

		return volume;
	}

	private decimal UpdateAveragePrice(decimal price, decimal volume, bool isLong)
	{
		var existingVolume = Math.Abs(Position);
		var side = isLong ? 1m : -1m;

		if (existingVolume <= 0m)
		{
			return price;
		}

		var totalVolume = existingVolume + volume;
		var weightedAverage = ((_averageEntryPrice * existingVolume * side) + (price * volume)) / totalVolume;
		return Math.Abs(weightedAverage);
	}

	private void ManageRisk(decimal price)
	{
		if (_entriesInSeries == 0)
		{
			_trailingStopLevel = null;
			return;
		}

		var step = GetPriceStep();
		var stopDistance = StopLoss * step;
		var takeDistance = TakeProfit * step;
		var trailingDistance = TrailingStop * step;

		if (_direction > 0)
		{
			var lossLevel = _averageEntryPrice - stopDistance;
			var profitLevel = _averageEntryPrice + takeDistance;

			if (price <= lossLevel)
			{
				LogInfo($"Long stop-loss triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
				SellMarket(Math.Abs(Position));
				ResetSeries();
				return;
			}

			if (price >= profitLevel)
			{
				LogInfo($"Long take-profit triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
				SellMarket(Math.Abs(Position));
				ResetSeries();
				return;
			}

			if (TrailingStop > 0m)
			{
				var candidate = price - trailingDistance;
				if (_trailingStopLevel == null || candidate > _trailingStopLevel)
				{
					_trailingStopLevel = candidate;
				}

				if (_trailingStopLevel != null && price <= _trailingStopLevel)
				{
					LogInfo($"Long trailing stop activated at {price:F5}.");
					SellMarket(Math.Abs(Position));
					ResetSeries();
				}
			}
		}
		else if (_direction < 0)
		{
			var lossLevel = _averageEntryPrice + stopDistance;
			var profitLevel = _averageEntryPrice - takeDistance;

			if (price >= lossLevel)
			{
				LogInfo($"Short stop-loss triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
				BuyMarket(Math.Abs(Position));
				ResetSeries();
				return;
			}

			if (price <= profitLevel)
			{
				LogInfo($"Short take-profit triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
				BuyMarket(Math.Abs(Position));
				ResetSeries();
				return;
			}

			if (TrailingStop > 0m)
			{
				var candidate = price + trailingDistance;
				if (_trailingStopLevel == null || candidate < _trailingStopLevel)
				{
					_trailingStopLevel = candidate;
				}

				if (_trailingStopLevel != null && price >= _trailingStopLevel)
				{
					LogInfo($"Short trailing stop activated at {price:F5}.");
					BuyMarket(Math.Abs(Position));
					ResetSeries();
				}
			}
		}
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step == 0m)
		{
			// Fallback to four decimal places when instrument metadata is unknown.
			step = 0.0001m;
		}
		return step;
	}

	private void ResetSeries()
	{
		_direction = 0;
		_entriesInSeries = 0;
		_lastEntryPrice = 0m;
		_averageEntryPrice = 0m;
		_trailingStopLevel = null;
	}
}