Auf GitHub ansehen

Amstell Grid-Manager-Strategie

High-Level-Port des MetaTrader-Experten "exp_Amstell-SL", der ein bidirektionales Averaging-Grid betreibt. Die Strategie verfolgt den zuletzt ausgeführten Preis auf jeder Seite und gibt zusätzliche Marktaufträge aus, wenn der Preis weit genug driftet, während der offene Batch liquidiert wird, sobald eine feste Take-Profit- oder Stop-Loss-Distanz erreicht ist. Die Implementierung verwendet StockSharps Kerzensubskriptionen und High-Level-Order-Helfer, sodass sie in jede Umgebung eingesteckt werden kann, die Kerzendaten für ein einzelnes Wertpapier bereitstellt.

Die übersetzte Logik ist leicht an StockSharps Netto-Portfolio-Modell angepasst: Long- und Short-Grids werden weiterhin separat verwaltet, aber nicht gleichzeitig gehalten. Das Long-Grid ist aktiv, solange die Nettoposition nicht negativ ist, und das Short-Grid übernimmt erst, nachdem das gesamte Long-Engagement abgebaut wurde.

Wie es funktioniert

Marktdaten und Ausführungsfluss

  • Abonniert den konfigurierten CandleType (Standard: 1-Minuten-Zeitrahmen-Kerzen) und verarbeitet nur abgeschlossene Kerzen.
  • Berechnet Pip-basierte Offsets vom PriceStep des Wertpapiers. Wenn der Schritt 3 oder 5 Dezimalstellen hat, wird er mit 10 multipliziert, um die MetaTrader 3/5-Digit-Pip-Anpassung nachzuahmen.
  • Alle Trades werden durch BuyMarket/SellMarket-Helfer platziert; es werden keine ausstehenden Aufträge verwendet.

Long-Seiten-Management

  • Öffnet die erste Long-Position (OrderVolume), sobald kein bestehendes Long-Engagement vorhanden ist und die Strategie nicht dabei ist, Shorts zu schließen.
  • Verfolgt den zuletzt ausgeführten Long-Preis und den volumengewichteten durchschnittlichen Einstandspreis für den aktiven Long-Batch.
  • Platziert zusätzliche Long-Aufträge der Größe OrderVolume, wenn der Schlusskurs um mindestens BuyDistancePips (in Preiseinheiten umgerechnet) unter dem letzten Long-Fill gefallen ist.

Short-Seiten-Management

  • Sobald der Long-Batch vollständig geschlossen ist und die Nettoposition nicht positiv ist, erlaubt die Strategie Short-Einstiege.
  • Platziert die anfängliche Short-Order, wenn keine Short-Exposition vorhanden ist; weitere Shorts werden eröffnet, nachdem der Preis um BuyDistancePips * SellDistanceMultiplier über den vorherigen Short-Fill gestiegen ist.
  • Verwaltet den zuletzt ausgeführten Short-Preis und den volumengewichteten durchschnittlichen Einstandspreis für den aktiven Short-Batch.

Ausstiegsregeln

  • Berechnet für jede Richtung den nicht realisierten Gewinn relativ zum durchschnittlichen Fill.
  • Schließt den gesamten Long-Batch mit einem Marktverkauf, wenn der Gewinn TakeProfitPips Pips erreicht oder der Drawdown StopLossPips Pips erreicht.
  • Schließt den gesamten Short-Batch mit einem Marktkauf, wenn der Gewinn TakeProfitPips Pips erreicht oder die adverse Bewegung StopLossPips Pips erreicht.
  • Nach der Liquidation werden alle zwischengespeicherten Preise und Volumen zurückgesetzt, damit ein neues Grid mit der nächsten Kerze beginnen kann.

Unterschiede zum originalen MQL-Experten

  • Die StockSharp-Version arbeitet auf Kerzenabschlüssen anstatt auf einzelnen Ticks.
  • Long- und Short-Grids werden sequentiell statt gleichzeitig ausgeführt, was dem Standard-Netting-Modus von StockSharp entspricht.
  • Alle Schutzabstände werden gegen den gemittelten Einstandspreis statt gegen jedes einzelne Ticket geprüft, was das aggregierte Netto-Positionsverhalten widerspiegelt.

Parameter

Parameter Standard Optimierungsbereich Beschreibung
OrderVolume 0.01 0.010.10 (Schritt 0.01) Mit jeder Grid-Order eingereichte Menge. Muss positiv sein.
TakeProfitPips 30 10150 (Schritt 10) Gewinnziel für den aktiven Batch in Pips.
StopLossPips 30 10150 (Schritt 10) Maximale adverse Bewegung vor dem Aufgeben des Batches.
BuyDistancePips 10 560 (Schritt 5) Minimaler Rückgang vom letzten Long-Fill zum Hinzufügen eines weiteren Kaufs. Muss kleiner als TP und SL sein.
SellDistanceMultiplier 10 215 (Schritt 1) Multiplikator für die Long-Distanz beim Abstands-Management von Short-Einstiegen.
CandleType 1-Minuten-Zeitrahmen Kerzenserie für die Signalgenerierung.

Implementierungshinweise

  • BuyDistancePips muss strikt kleiner als TakeProfitPips und StopLossPips sein; die Strategie wirft ansonsten beim Start eine Ausnahme und reproduziert damit die MetaTrader-Validierung.
  • Die Pip-Größe wird vom PriceStep des Wertpapiers abgeleitet. Passen Sie die Parameter an, wenn das Instrument eine nicht standardmäßige Tick-Größe verwendet.
  • Der gesamte interne Zustand wird in OnReseted gelöscht, sodass die Strategie ohne verbleibende Grid-Daten neu gestartet werden kann.
  • Keine Farbanpassung oder manuelle Indikatorregistrierung wird verwendet, was den High-Level-API-Richtlinien in diesem Repository entspricht.
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>
/// Amstell averaging grid strategy that opens new entries when price drifts away
/// from the last fill and closes exposure once profit or loss thresholds are reached.
/// </summary>
public class AmstellGridManagerStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _buyDistancePips;
	private readonly StrategyParam<decimal> _sellDistanceMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _longVolume;
	private decimal _shortVolume;
	private decimal? _averageLongPrice;
	private decimal? _averageShortPrice;
	private decimal? _lastBuyPrice;
	private decimal? _lastSellPrice;
	private decimal _pipValue;
	private decimal _takeProfitOffset;
	private decimal _stopLossOffset;
	private decimal _buyDistanceOffset;
	private decimal _sellDistanceOffset;
	private bool _closingLong;
	private bool _closingShort;

	/// <summary>
	/// Quantity per market order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Profit target in pips for each grid leg.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Maximum tolerated loss in pips for each grid leg.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Price distance in pips required to add another long position.
	/// </summary>
	public int BuyDistancePips
	{
		get => _buyDistancePips.Value;
		set => _buyDistancePips.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the long distance when stacking short entries.
	/// </summary>
	public decimal SellDistanceMultiplier
	{
		get => _sellDistanceMultiplier.Value;
		set => _sellDistanceMultiplier.Value = value;
	}

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

	/// <summary>
	/// Initializes the strategy parameters.
	/// </summary>
	public AmstellGridManagerStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Quantity submitted with each grid order", "Trading")
			
			.SetOptimize(0.01m, 0.1m, 0.01m);

		_takeProfitPips = Param(nameof(TakeProfitPips), 30)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
			
			.SetOptimize(10, 150, 10);

		_stopLossPips = Param(nameof(StopLossPips), 30)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
			
			.SetOptimize(10, 150, 10);

		_buyDistancePips = Param(nameof(BuyDistancePips), 10)
			.SetGreaterThanZero()
			.SetDisplay("Buy Distance (pips)", "Distance before adding another long", "Entries")
			
			.SetOptimize(5, 60, 5);

		_sellDistanceMultiplier = Param(nameof(SellDistanceMultiplier), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Sell Distance Multiplier", "Multiplier applied to long distance when adding shorts", "Entries")
			
			.SetOptimize(2m, 15m, 1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for processing", "General");
	}

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

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

		_longVolume = 0m;
		_shortVolume = 0m;
		_averageLongPrice = null;
		_averageShortPrice = null;
		_lastBuyPrice = null;
		_lastSellPrice = null;
		_pipValue = 0m;
		_takeProfitOffset = 0m;
		_stopLossOffset = 0m;
		_buyDistanceOffset = 0m;
		_sellDistanceOffset = 0m;
		_closingLong = false;
		_closingShort = false;
	}

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

		if (BuyDistancePips >= TakeProfitPips || BuyDistancePips >= StopLossPips)
			throw new InvalidOperationException("Buy distance must be less than take profit and stop loss distances.");

		UpdatePriceOffsets();

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

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

		// no indicators bound via .Bind()

		var close = candle.ClosePrice;

		if (!_closingLong && _longVolume > 0m && _averageLongPrice is decimal longAvg)
		{
			var profit = close - longAvg;
			if (profit >= _takeProfitOffset || -profit >= _stopLossOffset)
			{
				SellMarket();
				_closingLong = true;
				return;
			}
		}

		if (!_closingShort && _shortVolume > 0m && _averageShortPrice is decimal shortAvg)
		{
			var profit = shortAvg - close;
			if (profit >= _takeProfitOffset || -profit >= _stopLossOffset)
			{
				BuyMarket();
				_closingShort = true;
				return;
			}
		}

		var openedLong = false;

		if (!_closingLong && Position >= 0m)
		{
			if (_longVolume <= 0m)
			{
				BuyMarket();
				openedLong = true;
			}
			else if (_lastBuyPrice is decimal lastBuy && lastBuy - close >= _buyDistanceOffset)
			{
				BuyMarket();
				openedLong = true;
			}
		}

		if (openedLong)
			return;

		if (!_closingShort && Position <= 0m)
		{
			if (_shortVolume <= 0m)
			{
				SellMarket();
			}
			else if (_lastSellPrice is decimal lastSell && close - lastSell >= _sellDistanceOffset)
			{
				SellMarket();
			}
		}
	}

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

		if (trade.Order == null)
			return;

		var tradeVolume = trade.Trade.Volume;
		var price = trade.Trade.Price;

		if (trade.Order.Side == Sides.Buy)
		{
			if (_shortVolume > 0m)
			{
				var closingVolume = Math.Min(tradeVolume, _shortVolume);
				_shortVolume -= closingVolume;
				tradeVolume -= closingVolume;
				if (_shortVolume <= 0m)
				{
					_shortVolume = 0m;
					_averageShortPrice = null;
					_lastSellPrice = null;
				}
			}

			if (tradeVolume > 0m)
			{
				var newVolume = _longVolume + tradeVolume;
				var totalCost = (_averageLongPrice ?? 0m) * _longVolume + price * tradeVolume;
				_longVolume = newVolume;
				_averageLongPrice = totalCost / newVolume;
				_lastBuyPrice = price;
				_closingLong = false;
			}
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			if (_longVolume > 0m)
			{
				var closingVolume = Math.Min(tradeVolume, _longVolume);
				_longVolume -= closingVolume;
				tradeVolume -= closingVolume;
				if (_longVolume <= 0m)
				{
					_longVolume = 0m;
					_averageLongPrice = null;
					_lastBuyPrice = null;
				}
			}

			if (tradeVolume > 0m)
			{
				var newVolume = _shortVolume + tradeVolume;
				var totalCost = (_averageShortPrice ?? 0m) * _shortVolume + price * tradeVolume;
				_shortVolume = newVolume;
				_averageShortPrice = totalCost / newVolume;
				_lastSellPrice = price;
				_closingShort = false;
			}
		}

		if (_longVolume <= 0m && Position <= 0m)
			_closingLong = false;

		if (_shortVolume <= 0m && Position >= 0m)
			_closingShort = false;

		if (Position == 0m)
		{
			_longVolume = 0m;
			_shortVolume = 0m;
			_averageLongPrice = null;
			_averageShortPrice = null;
			_lastBuyPrice = null;
			_lastSellPrice = null;
			_closingLong = false;
			_closingShort = false;
		}
	}

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

		var decimals = GetDecimalPlaces(step);
		_pipValue = decimals == 3 || decimals == 5 ? step * 10m : step;

		_takeProfitOffset = TakeProfitPips * _pipValue;
		_stopLossOffset = StopLossPips * _pipValue;
		_buyDistanceOffset = BuyDistancePips * _pipValue;
		_sellDistanceOffset = _buyDistanceOffset * SellDistanceMultiplier;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		if (value == 0m)
			return 0;

		var bits = decimal.GetBits(value);
		return (bits[3] >> 16) & 0x7F;
	}
}