Auf GitHub ansehen

Amstell Grid-Strategie

Die Amstell Grid-Strategie ist ein C#-Port des MetaTrader 5 Expert Advisors exp_Amstell.mq5. Sie erstellt ein symmetrisches Kauf/Verkauf-Grid und wendet einen virtuellen Take Profit auf einzelne Einstiege an. Die Konvertierung folgt den StockSharp High-Level-API-Richtlinien und ersetzt die Tick-Verarbeitung durch Kerzenverarbeitung, während die ursprüngliche Idee intakt bleibt.

So funktioniert es

  1. Initialisierung

    • Die Strategie abonniert den konfigurierten Kerzentyp und startet den Positionsschutz.
    • Eine angepasste Pip-Größe wird aus dem PriceStep des Wertpapiers und der Dezimalpräzision berechnet. Fünfstellige und dreistellige Symbole erhalten automatisch einen 10-fachen Multiplikator, was der MT5-Implementierung entspricht.
  2. Erster Trade

    • Wenn sowohl der letzte erfasste Kauf- als auch der Verkaufspreis leer sind (erster Start), wird sofort eine Markt-Kauforder gesendet. Dies startet das Grid genau wie der ursprüngliche Expert Advisor.
  3. Grid-Erweiterung

    • Eine neue Kauf-Order wird ausgegeben, sobald der aktuelle Schlusskurs mindestens StepPips unter dem letzten erfassten Kaufpreis liegt.
    • Eine neue Verkauf-Order wird ausgegeben, sobald der Preis mindestens StepPips über dem letzten erfassten Verkaufspreis liegt.
    • Die Strategie verfolgt intern separate Long- und Short-Stapel, sodass abwechselnde Orders auch auf einem Netting-Konto koexistieren können. Entgegengesetzte Orders reduzieren zuerst den anderen Stapel, bevor neue Exposition hinzugefügt wird, was das Hedging-Verhalten der MT5-Version reproduziert.
  4. Virtueller Take Profit

    • Jede offene Long-Position wird unabhängig überwacht. Wenn der Preis um TakeProfitPips steigt, wird eine Markt-Verkaufsorder nur für das Volumen dieser Position gesendet.
    • Jede offene Short-Position wird ähnlich in die entgegengesetzte Richtung behandelt. Der Take Profit ist "virtuell", weil Positionen programmatisch ohne Verwendung von broker-seitigen TP-Orders geschlossen werden.
    • Nachdem eine Richtung vollständig geschlossen wurde, während die entgegengesetzte Seite noch existiert, wird der entsprechende letzte Deal-Preis gelöscht, damit die nächste Order in dieser Richtung sofort auslösen kann, genau wie im ursprünglichen Code.
  5. Statusverfolgung

    • Der OnOwnTradeReceived-Handler baut die Long/Short-Stapel aus ausgeführten Trades neu auf und ermöglicht einen eleganten Umgang mit Teilfüllungen und Reversierungen.
    • Letzte Kauf/Verkauf-Preise bleiben gecacht, wenn beide Seiten flach sind, sodass das Grid auf den erforderlichen Schritt wartet, bevor es nach einem vollständigen Reset wieder einsteigt.

Parameter

Parameter Standard Beschreibung
Volume 0.1 Ordergröße für jede Marktorder in beide Richtungen.
TakeProfitPips 50 Abstand in Pips, der gewonnen werden muss, bevor eine einzelne Position geschlossen wird.
StepPips 15 Abstand in Pips zwischen aufeinanderfolgenden Grid-Orders in dieselbe Richtung.
CandleType 1 Minute Kerzendatenquelle zur Annäherung der tick-basierten Logik.

Alle pip-basierten Einstellungen berücksichtigen den Preisschritt und die Präzision des Instruments. Zum Beispiel entspricht bei EURUSD (5 Stellen) StepPips = 15 einem Wert von 0,0015.

Praktische Hinweise

  • Die Strategie verwendet Kerzenschlusskurse, um die Tick-Level-Vergleiche aus dem MT5-Code zu emulieren. Für Hochfrequenzoperationen verringern Sie den Zeitrahmen.
  • Standardmäßig gibt es keinen Stop-Loss. Wie bei jedem Grid-Ansatz können unkontrollierte Trends große Exposition akkumulieren. Verwenden Sie konservative Volumen und erwägen Sie sitzungsbasierte Überwachung.
  • Da Take Profits virtuell verwaltet werden, werden geschlossene Trades sofort im PnL der Strategie widergespiegelt, ohne sichtbare TP-Orders beim Broker zu platzieren.
  • Die Implementierung lässt gecachte letzte Preise unverändert, nachdem beide Seiten aufgelöst wurden. Dies bewahrt das ursprüngliche Verhalten, bei dem das Grid auf Preisverschiebung wartet, bevor es neu startet.

Dateien

  • CS/AmstellGridStrategy.cs – StockSharp-Strategie-Implementierung mit ausführlichen Inline-Kommentaren.
  • README.md, README_ru.md, README_zh.md – Vollständige Dokumentation auf Englisch, Russisch und Chinesisch.

Dieser Port ist für weitere Anpassungen (z.B. Geldverwaltung, Risikolimits) direkt innerhalb des StockSharp-Ökosystems bereit.

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>
/// Grid strategy that alternates buy and sell entries with a virtual take profit.
/// </summary>
public class AmstellGridStrategy : Strategy
{
	private sealed class PositionEntry
	{
		public PositionEntry(decimal price, decimal volume)
		{
			Price = price;
			Volume = volume;
		}

		public decimal Price { get; set; }

		public decimal Volume { get; set; }

		public bool IsClosing { get; set; }
	}

	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stepPips;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<PositionEntry> _longEntries = new();
	private readonly List<PositionEntry> _shortEntries = new();

	private decimal? _lastBuyPrice;
	private decimal? _lastSellPrice;
	private bool _hasInitialOrder;
	private decimal _pipSize;


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

	/// <summary>
	/// Distance between consecutive entries in pips.
	/// </summary>
	public int StepPips
	{
		get => _stepPips.Value;
		set => _stepPips.Value = value;
	}

	/// <summary>
	/// Candle type used to generate trade decisions.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="AmstellGridStrategy"/> class.
	/// </summary>
	public AmstellGridStrategy()
	{

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Virtual take profit distance", "Risk")
			
			.SetOptimize(10, 150, 10);

		_stepPips = Param(nameof(StepPips), 15)
			.SetGreaterThanZero()
			.SetDisplay("Step (pips)", "Distance between grid entries", "Grid")
			
			.SetOptimize(5, 60, 5);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for signal candles", "General");
	}

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

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

		_longEntries.Clear();
		_shortEntries.Clear();
		_lastBuyPrice = null;
		_lastSellPrice = null;
		_hasInitialOrder = false;
		_pipSize = 0m;
	}

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

		_pipSize = CalculatePipSize();

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

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

	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Only react to completed candles to emulate stable tick processing.
		if (candle.State != CandleStates.Finished)
			return;


		var price = candle.ClosePrice;
		var stepDistance = GetStepDistance();
		var takeProfitDistance = GetTakeProfitDistance();

		// Bootstrap the grid exactly like the MQL version.
		if (!_hasInitialOrder && _lastBuyPrice is null && _lastSellPrice is null)
		{
			BuyMarket(Volume);
			_hasInitialOrder = true;
			return;
		}

		// Check whether the grid should add a new long layer.
		if (CanOpenBuy(price, stepDistance))
		{
			BuyMarket(Volume);
			return;
		}

		// Mirror logic for the short side of the grid.
		if (CanOpenSell(price, stepDistance))
		{
			SellMarket(Volume);
			return;
		}

		// No new entries were placed, so check for virtual take-profit exits.
		if (TryClosePositions(price, takeProfitDistance))
			return;
	}

	private bool CanOpenBuy(decimal price, decimal stepDistance)
	{
		if (Volume <= 0)
			return false;

		return !_lastBuyPrice.HasValue || _lastBuyPrice.Value - price >= stepDistance;
	}

	private bool CanOpenSell(decimal price, decimal stepDistance)
	{
		if (Volume <= 0)
			return false;

		return !_lastSellPrice.HasValue || price - _lastSellPrice.Value >= stepDistance;
	}

	private bool TryClosePositions(decimal price, decimal takeProfitDistance)
	{
		if (takeProfitDistance <= 0)
			return false;

		// Evaluate longs first because the original EA does the same.
		foreach (var entry in _longEntries)
		{
			if (entry.IsClosing)
				continue;

			if (price - entry.Price >= takeProfitDistance)
			{
				// Prevent duplicate closing requests until the trade is processed.
				entry.IsClosing = true;
				SellMarket(entry.Volume);
				return true;
			}
		}

		// Short entries use the symmetrical distance check.
		foreach (var entry in _shortEntries)
		{
			if (entry.IsClosing)
				continue;

			if (entry.Price - price >= takeProfitDistance)
			{
				entry.IsClosing = true;
				BuyMarket(entry.Volume);
				return true;
			}
		}

		return false;
	}

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

		if (trade?.Order == null || trade.Order.Security != Security)
			return;

		var volume = trade.Trade.Volume;

		// Feed the executed trade into the synthetic short stack first.
		if (trade.Order.Side == Sides.Buy)
		{
			var remainder = ReduceEntries(_shortEntries, volume);

			if (remainder > 0)
			{
				// Remaining volume becomes a new long layer.
				_longEntries.Add(new PositionEntry(trade.Trade.Price, remainder));
				_lastBuyPrice = trade.Trade.Price;
			}
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			var remainder = ReduceEntries(_longEntries, volume);

			if (remainder > 0)
			{
				// Remaining volume becomes a new short layer.
				_shortEntries.Add(new PositionEntry(trade.Trade.Price, remainder));
				_lastSellPrice = trade.Trade.Price;
			}
		}

		// Recalculate helper state after rebuilding the stacks.
		UpdateLastPrices();
	}

	private decimal ReduceEntries(List<PositionEntry> entries, decimal volume)
	{
		var remaining = volume;

		// Consume volume using a FIFO approach just like MT5 positions.
		while (remaining > 0 && entries.Count > 0)
		{
			var entry = entries[0];
			var used = Math.Min(entry.Volume, remaining);
			entry.Volume -= used;
			remaining -= used;

			if (entry.Volume <= 0)
			{
				// Entry fully closed, remove it from the stack.
				entries.RemoveAt(0);
			}
			else
			{
				// Partial reduction keeps the entry alive; clear closing flag.
				entry.IsClosing = false;
			}
		}

		return remaining;
	}

	private void UpdateLastPrices()
	{
		// If only shorts remain, unlock the buy grid for immediate reuse.
		if (_longEntries.Count == 0 && _shortEntries.Count > 0)
		{
			_lastBuyPrice = null;
		}

		// If only longs remain, clear the last sell price to mimic MT5 logic.
		if (_shortEntries.Count == 0 && _longEntries.Count > 0)
		{
			_lastSellPrice = null;
		}

		// Any surviving entries should be marked as active again.
		for (var i = 0; i < _longEntries.Count; i++)
		{
			_longEntries[i].IsClosing = false;
		}

		for (var i = 0; i < _shortEntries.Count; i++)
		{
			_shortEntries[i].IsClosing = false;
		}
	}

	private decimal GetStepDistance()
	{
		var pip = _pipSize;
		if (pip <= 0)
		{
			// Fallback to the raw price step if the pip size has not been initialized yet.
			pip = Security?.PriceStep ?? 1m;
		}

		return StepPips * pip;
	}

	private decimal GetTakeProfitDistance()
	{
		var pip = _pipSize;
		if (pip <= 0)
		{
			// Same fallback logic as the step distance.
			pip = Security?.PriceStep ?? 1m;
		}

		return TakeProfitPips * pip;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0)
			step = 1m;

		return step;
	}
}