GitHub で見る

MAグリッド戦略

概要

この戦略は、MetaTrader 5 Expert Advisor MAGrid.mq5 の C# ポートです。指数移動平均 (EMA) の周囲でヘッジされた買いポジションと売りポジションのグリッドを維持します。アイデアは、EMA アンカーの周りでグリッドのバランスを保つことです。価格が EMA の上下の事前定義された距離ステップを横切ると、ストラテジーはグリッドの反対側から 1 つのポジションを閉じ、ブレイクアウトの方向に新しいポジションを開きます。これにより、バスケットは常に移動平均を中心に再配置されます。

オリジナルソース

  • MQL リポジトリ フォルダ: MQL/38303
  • 元のファイル: MAGrid.mq5
  • プラットフォーム: MetaTrader 5 (ヘッジモード)

取引ロジック

  1. EMA アンカー

    • EMA 期間は構成可能です (デフォルトは 48)。
    • EMA は、選択したローソク足シリーズに基づいて計算されます。
    • グリッド レベルは、EMA の上下の Distance パラメータの倍数として計算されます。
  2. グリッドの初期化

    • 有効グリッド サイズは、EMA の両側をミラーリングするために強制的に均等になります。
    • 現在のグリッド インデックスは、最後の終値を EMA ベースのレベルと比較することによって決定されます。
    • 買いと売りの成行注文の対称バスケットが開かれ、ポジションの半分が EMA より下に位置し、残りの半分がそれより上に位置します。
  3. 系統メンテナンス

    • 価格が次に上のグリッド レベルを超えて終了した場合、戦略は次のようになります。
      • グリッドインデックスをインクリメントします。
      • エクスポージャが残っている場合は、ロング注文を 1 つクローズします。
      • 新しいショートオーダーをオープンして、グリッドの上半分を拡張します。
    • 価格が次に低いグリッド レベルを下回って終了した場合、戦略は次のようになります。
      • グリッドインデックスをデクリメントします。
      • エクスポージャーが残っている場合は、1 つのショート注文をクローズします。
      • 新しいロング注文をオープンして、グリッドの下半分を再構築します。
    • グリッドの片側の露出がなくなると、新しい注文がオープンされるまで、対応するトリガーは無効になります。
  4. 注文の処理

    • 注文は単純な内部マップを通じて追跡され、開始約定と終了約定を区別します。
    • この戦略では、ロング バスケットとショート バスケットに対して個別のエクスポージャー カウンタを保存します。これは、StockSharp のネット ポジション モデルを使用する際の、MQL バージョンのヘッジ動作を反映しています。

パラメーター

名前 デフォルト 説明
MaPeriod 48 EMA の期間がアンカー レベルに使用されます。
GridAmount 6 グリッドステップの数。自動的に偶数値に切り上げられます。
Distance 0.005 グリッド レベル間の相対間隔 (例: 0.005 = 0.5%)。
OrderVolume 0.1 各成行注文で発注された数量。
CandleType 毎日の時間枠 EMA を計算し、シグナルを評価するために使用されるローソク足シリーズ。

リスク管理

  • この戦略にはストップロスやテイクプロフィットのルールは実装されていません。リスクはグリッドのステップ数と注文量によって制御されます。
  • グリッドはロングエクスポージャーとショートエクスポージャーの両方を維持するため、ポートフォリオの価値は比較的安定した状態を維持できますが、マージンの使用量はグリッドのサイズと距離に応じて増加します。
  • 戦略またはポートフォリオのレベルでポートフォリオのリスク管理 (最大ドローダウン、資本の使用量) を使用することを検討してください。

変換メモ

  • C# 実装では、長時間露光と短時間露光を個別に追跡することで、ヘッジ ロジックを再現します。
  • わかりやすくするために、MQL からのアカウント依存のボリューム計算は、構成可能な OrderVolume パラメーターに置き換えられました。
  • Candle サブスクリプションは、プロジェクト ガイドラインに従って、SubscribeCandles().Bind(...) を使用する StockSharp の高レベルの API に依存します。
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>
/// Moving average grid strategy converted from the MetaTrader MAGrid expert.
/// It manages a symmetric basket of long and short orders around an EMA-based anchor level.
/// </summary>
public class MaGridStrategy : Strategy
{
	private readonly StrategyParam<decimal> _volumeTolerance;

	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _gridAmount;
	private readonly StrategyParam<decimal> _distance;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Dictionary<Order, OrderIntents> _orderIntents = new();

	private ExponentialMovingAverage _ema;
	private int _effectiveGridAmount;
	private int _currentGrid;
	private decimal _nextGridPrice;
	private decimal _lastGridPrice;
	private bool _isGridInitialized;
	private decimal _longExposure;
	private decimal _shortExposure;

	private enum OrderIntents
	{
		OpenLong,
		OpenShort,
		CloseLong,
		CloseShort
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="MaGridStrategy"/> class.
	/// </summary>
	public MaGridStrategy()
	{
		_volumeTolerance = Param(nameof(VolumeTolerance), 0.0000001m)
			.SetNotNegative()
			.SetDisplay("Volume Tolerance", "Small tolerance applied when balancing grid exposure.", "Risk");

		_maPeriod = Param(nameof(MaPeriod), 48)
		.SetRange(5, 400)
		.SetDisplay("MA Period", "Exponential moving average length", "Grid")
		;

		_gridAmount = Param(nameof(GridAmount), 6)
		.SetRange(2, 40)
		.SetDisplay("Grid Amount", "Number of grid steps (will be forced to an even value)", "Grid")
		;

		_distance = Param(nameof(Distance), 0.005m)
		.SetGreaterThanZero()
		.SetDisplay("Distance", "Relative spacing between grid levels", "Grid")
		;

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume per grid order", "Risk")
		;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Primary candle type used by the strategy", "Data");
	}

	/// <summary>
	/// Small tolerance used when comparing accumulated exposure.
	/// </summary>
	public decimal VolumeTolerance
	{
		get => _volumeTolerance.Value;
		set => _volumeTolerance.Value = value;
	}

	/// <summary>
	/// EMA period used for the anchor level.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Total number of grid steps that will be mirrored around the EMA.
	/// </summary>
	public int GridAmount
	{
		get => _gridAmount.Value;
		set => _gridAmount.Value = value;
	}

	/// <summary>
	/// Relative distance between consecutive grid levels.
	/// </summary>
	public decimal Distance
	{
		get => _distance.Value;
		set => _distance.Value = value;
	}

	/// <summary>
	/// Volume submitted with each market order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

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

		_orderIntents.Clear();
		_ema = null;
		_effectiveGridAmount = 0;
		_currentGrid = 0;
		_nextGridPrice = 0m;
		_lastGridPrice = 0m;
		_isGridInitialized = false;
		_longExposure = 0m;
		_shortExposure = 0m;
	}

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

		_effectiveGridAmount = GetEffectiveGridAmount();
		_currentGrid = 0;
		_nextGridPrice = 0m;
		_lastGridPrice = 0m;
		_isGridInitialized = false;
		_longExposure = 0m;
		_shortExposure = 0m;
		_orderIntents.Clear();

		_ema = new EMA
		{
			Length = MaPeriod
		};

		SubscribeCandles(CandleType)
		.Bind(_ema, ProcessCandle)
		.Start();
	}

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

		if (trade?.Order is not { } order || !_orderIntents.TryGetValue(order, out var intent))
		return;

		var volume = trade.Trade.Volume;

		switch (intent)
		{
		case OrderIntents.OpenLong:
		_longExposure += volume;
		break;
		case OrderIntents.OpenShort:
		_shortExposure += volume;
		break;
		case OrderIntents.CloseLong:
		_longExposure = Math.Max(0m, _longExposure - volume);
		break;
		case OrderIntents.CloseShort:
		_shortExposure = Math.Max(0m, _shortExposure - volume);
		break;
		}

		if (order.Balance <= VolumeTolerance || (order.State == OrderStates.Done || order.State == OrderStates.Failed))
		_orderIntents.Remove(order);
	}

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

		if (_ema?.IsFormed != true)
		return;

		CleanupCompletedOrders();

		if (!_isGridInitialized)
		{
		InitializeGrid(candle.ClosePrice, emaValue);
		return;
		}

		UpdateGridLevels(emaValue);

		if (_nextGridPrice > 0m && candle.ClosePrice >= _nextGridPrice && _nextGridPrice < decimal.MaxValue)
		{
		_currentGrid++;
		CloseLongExposure();
		OpenShortExposure();
		UpdateGridLevels(emaValue);
		}
		else if (_lastGridPrice > 0m && candle.ClosePrice <= _lastGridPrice && _lastGridPrice > decimal.MinValue)
		{
		_currentGrid--;
		CloseShortExposure();
		OpenLongExposure();
		UpdateGridLevels(emaValue);
		}
	}

	private int GetEffectiveGridAmount()
	{
		var amount = GridAmount;
		if (amount < 2)
		amount = 2;

		if (amount % 2 != 0)
		amount++;

		return amount;
	}

	private void InitializeGrid(decimal closePrice, decimal ema)
	{
		_isGridInitialized = true;
		_currentGrid = DetermineInitialGrid(closePrice, ema);

		var half = _effectiveGridAmount / 2;
		var buyCount = Math.Max(0, half - _currentGrid);
		var sellCount = Math.Max(0, _effectiveGridAmount - buyCount);

		for (var i = 0; i < buyCount; i++)
		OpenLongExposure();

		for (var i = 0; i < sellCount; i++)
		OpenShortExposure();

		UpdateGridLevels(ema);
	}

	private int DetermineInitialGrid(decimal price, decimal ema)
	{
		var half = _effectiveGridAmount / 2;
		var distance = Distance;

		if (price < ema)
		{
		for (var i = 1; i <= half; i++)
		{
		var level = ema * (1m - distance * i);
		if (price > level)
		return 1 - i;
		}

		return -half;
		}

		for (var i = 1; i <= half; i++)
		{
		var level = ema * (1m + distance * i);
		if (price < level)
		return i - 1;
		}

		return half;
	}

	private void UpdateGridLevels(decimal ema)
	{
		var distance = Distance;

		if (_currentGrid < _effectiveGridAmount - 1)
		_nextGridPrice = ema * (1m + distance * (1m + _currentGrid));
		else
		_nextGridPrice = 0m;

		if (_currentGrid > 1 - _effectiveGridAmount)
		_lastGridPrice = ema * (1m - distance * (1m - _currentGrid));
		else
		_lastGridPrice = 0m;

		if (_longExposure <= VolumeTolerance)
		_nextGridPrice = decimal.MaxValue;

		if (_shortExposure <= VolumeTolerance)
		_lastGridPrice = decimal.MinValue;
	}

	private void OpenLongExposure()
	{
		if (OrderVolume <= 0m)
		return;

		RegisterOrder(BuyMarket(OrderVolume), OrderIntents.OpenLong);
	}

	private void OpenShortExposure()
	{
		if (OrderVolume <= 0m)
		return;

		RegisterOrder(SellMarket(OrderVolume), OrderIntents.OpenShort);
	}

	private void CloseLongExposure()
	{
		if (_longExposure <= VolumeTolerance)
		return;

		var volume = Math.Min(OrderVolume, _longExposure);
		if (volume <= VolumeTolerance)
		return;

		RegisterOrder(SellMarket(volume), OrderIntents.CloseLong);
	}

	private void CloseShortExposure()
	{
		if (_shortExposure <= VolumeTolerance)
		return;

		var volume = Math.Min(OrderVolume, _shortExposure);
		if (volume <= VolumeTolerance)
		return;

		RegisterOrder(BuyMarket(volume), OrderIntents.CloseShort);
	}

	private void RegisterOrder(Order order, OrderIntents intent)
	{
		if (order == null)
		return;

		_orderIntents[order] = intent;
	}

	private void CleanupCompletedOrders()
	{
		if (_orderIntents.Count == 0)
		return;

		List<Order> completed = null;

		foreach (var pair in _orderIntents)
		{
		if (!(pair.Key.State == OrderStates.Done || pair.Key.State == OrderStates.Failed))
		continue;

		completed ??= new List<Order>();
		completed.Add(pair.Key);
		}

		if (completed == null)
		return;

		foreach (var order in completed)
		_orderIntents.Remove(order);
	}
}