Auf GitHub ansehen

MA Grid-Strategie

Überblick

Diese Strategie ist eine C#-Portierung des MetaTrader 5 Expert Advisors MAGrid.mq5. Es unterhält ein abgesichertes Raster von Kauf- und Verkaufspositionen um einen exponentiellen gleitenden Durchschnitt (EMA). Die Idee besteht darin, das Gitter um den Anker EMA herum im Gleichgewicht zu halten. Wenn der Preis vordefinierte Abstandsschritte über oder unter EMA überschreitet, schließt die Strategie eine Position auf der gegenüberliegenden Seite des Rasters und eröffnet eine neue Position in Richtung des Ausbruchs. Dadurch wird der Korb immer wieder um den gleitenden Durchschnitt herum zentriert.

Originalquelle

  • MQL Repository-Ordner: MQL/38303
  • Originaldatei: MAGrid.mq5
  • Plattform: MetaTrader 5 (Absicherungsmodus)

Handelslogik

  1. EMA Anker

    • Der Zeitraum EMA ist konfigurierbar (Standard 48).
    • Der EMA wird für die ausgewählte Kerzenserie berechnet.
    • Rasterebenen werden als Vielfache des Parameters Distance über und unter EMA berechnet.
  2. Gitterinitialisierung

    • Die effektive Gittergröße muss gleichmäßig sein, um beide Seiten um EMA zu spiegeln.
    • Der aktuelle Rasterindex wird durch Vergleich des letzten Schlusskurses mit den EMA-basierten Niveaus ermittelt.
    • Ein symmetrischer Korb aus Kauf- und Verkaufsmarktaufträgen wird geöffnet, sodass die Hälfte der Positionen unter dem EMA und die andere Hälfte darüber liegt.
  3. Netzwartung

    • Wenn der Preis über der nächsthöheren Rasterebene schließt, gilt die Strategie:
      • Erhöht den Rasterindex.
      • Schließt eine lange Bestellung, wenn noch Belichtung vorhanden ist.
      • Öffnet einen neuen kurzen Auftrag zur Erweiterung der oberen Hälfte des Rasters.
    • Wenn der Preis unter dem nächstniedrigeren Rasterniveau schließt, gilt die Strategie:
      • Dekrementiert den Rasterindex.
      • Schließt eine Short-Order, wenn noch Engagement vorhanden ist.
      • Öffnet einen neuen Langauftrag zum Wiederaufbau der unteren Hälfte des Rasters.
    • Wenn auf einer Seite des Rasters keine Belichtung mehr vorhanden ist, wird der entsprechende Auslöser deaktiviert, bis neue Aufträge geöffnet werden.
  4. Auftragsabwicklung

    • Aufträge werden über eine einfache interne Karte verfolgt, um zwischen Eröffnungs- und Schlussausführungen zu unterscheiden.
    • Die Strategie speichert separate Exposure-Zähler für die Long- und Short-Körbe. Dies spiegelt das Absicherungsverhalten der MQL-Version wider, während das Nettopositionsmodell von StockSharp verwendet wird.

Parameter

Name Standard Beschreibung
MaPeriod 48 EMA Zeitraum, der für die Ankerebene verwendet wird.
GridAmount 6 Anzahl der Rasterschritte; automatisch auf einen geraden Wert aufgerundet.
Distance 0,005 Relativer Abstand zwischen Rasterebenen (z. B. 0,005 = 0,5 %).
OrderVolume 0,1 Mit jeder Market-Order übermitteltes Volumen.
CandleType Täglicher Zeitrahmen Kerzenserien zur Berechnung des EMA und zur Auswertung von Signalen.

Risikomanagement

  • Die Strategie implementiert keine Stop-Loss- oder Take-Profit-Regeln; Das Risiko wird über die Anzahl der Rasterschritte und das Auftragsvolumen gesteuert.
  • Da das Raster sowohl Long- als auch Short-Engagements aufrechterhält, kann der Portfoliowert relativ stabil bleiben, die Margin-Nutzung nimmt jedoch mit der Rastergröße und -entfernung zu.
  • Erwägen Sie die Verwendung von Portfoliorisikokontrollen (maximaler Drawdown, Kapitalverwendung) auf Strategie- oder Portfolioebene.

Konvertierungshinweise

  • Die C#-Implementierung reproduziert die abgesicherte Logik, indem Long- und Short-Engagement getrennt verfolgt werden.
  • Die kontoabhängige Volumenberechnung von MQL wurde aus Gründen der Übersichtlichkeit durch einen konfigurierbaren OrderVolume-Parameter ersetzt.
  • Candle-Abonnements basieren auf der StockSharp-Hochebene API unter Verwendung von SubscribeCandles().Bind(...) gemäß den Projektrichtlinien.
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);
	}
}