Auf GitHub ansehen

Carbophos Grid Strategie

Übersicht

Die Carbophos Grid Strategie ist eine direkte Konvertierung des MetaTrader 5 Expert Advisors "Carbophos". Sie pflegt kontinuierlich eine symmetrische Leiter von Kauf- und Verkauf-Limit-Orders rund um die aktuellen Bid/Ask-Preise. Die Strategie überwacht den aggregierten Floating-Profit des gesamten Grids und schließt alle Positionen, sobald entweder das gewünschte Gewinnziel oder der maximal tolerierte Drawdown erreicht wird. Nachdem die Position abgeflacht wurde und keine Pending Orders mehr vorhanden sind, wird die Leiter automatisch neu aufgebaut.

Handelslogik

  1. Wenn die Strategie startet und keine aktiven Orders oder offenen Positionen vorhanden sind, berechnet sie den Grid-Abstand in Kurseinheiten basierend auf dem konfigurierten Schritt in Pips und der Kurspräzision des Instruments. Fünf (konfigurierbar) Sell-Limit-Orders werden über dem besten Bid und die gleiche Anzahl von Buy-Limit-Orders unter dem besten Ask platziert.
  2. Wenn eine Order gefüllt wird, wird die resultierende Position tick-by-tick mit Level1-Daten überwacht. Der Floating-PnL wird aus der Differenz zwischen dem aktuellen Exit-Preis (Bid für Long-Positionen, Ask für Short-Positionen) und dem volumengewichteten durchschnittlichen Eintrittspreis berechnet.
  3. Sobald der Floating-Profit das konfigurierte Ziel überschreitet, oder der Floating-Verlust den Schutzschwellenwert verletzt, gibt die Strategie eine Marktorder aus, um die offene Position zu schließen, und storniert alle verbleibenden Limit-Orders. Die Statuskennung wird gelöscht, damit die Leiter bei der nächsten Preisaktualisierung neu aufgebaut wird.
  4. Wenn alle Orders gefüllt werden, aber die Nettoposition auf null zurückkehrt (zum Beispiel weil der Markt durch das Grid umkehrt), löst das nächste Level1-Update eine neue Leiterplatzierung aus.

Parameter

Parameter Beschreibung
ProfitTarget Floating-Profit (Geld), der das Schließen des gesamten Grids auslöst.
MaxLoss Floating-Verlust (Geld), der einen Notausstieg erzwingt.
StepPips Abstand zwischen aufeinanderfolgenden Grid-Levels in Pips. Wird intern in Kurseinheiten umgerechnet unter Verwendung der Tick-Größe und der Dezimalpräzision des Symbols.
OrdersPerSide Anzahl der Limit-Orders, die über und unter dem aktuellen Marktpreis platziert werden.
OrderVolume Volumen für jede Grid-Order.

Alle Parameter unterstützen Optimierungsbereiche zur Vereinfachung der Experimente im StockSharp-Optimierer.

Risikomanagement und Schutz

Die Strategie verwendet den eingebauten StartProtection()-Hook und wendet harte monetäre Stop/Profit-Levels auf Strategieebene an. Die Floating-PnL-Berechnung hängt von den PriceStep- und StepPrice-Einstellungen des Instruments ab. Wenn ein Schwellenwert erreicht wird, schließt die Strategie die Position mit einer Marktorder und storniert alle aktiven Limit-Orders, bevor die interne Grid-Kennung zurückgesetzt wird.

Konvertierungshinweise

  • Der ursprüngliche MQL5-Expert Advisor passte Pip-Werte für Drei- und Fünf-Dezimal-Forex-Symbole an. Der StockSharp-Port repliziert dieses Verhalten, indem der PriceStep des Exchanges mit 10 multipliziert wird, wenn das Instrument drei oder fünf Dezimalstellen hat.
  • MetaTrader aggregiert Positionsgewinn, Provision und Swap pro Magic Number. In StockSharp wird der Floating-PnL aus dem gewichteten durchschnittlichen Eintrittspreis und dem aktuellen Bid/Ask-Preis neu berechnet, sodass explizite Provisionsbehandlung nicht erforderlich ist.
  • Orderplatzierung, -stornierung und Positionsmanagement werden über die High-Level-Strategy-API implementiert (BuyLimit, SellLimit, CancelActiveOrders, BuyMarket, SellMarket).
  • Das Grid wird ausschließlich aus Level1-Updates aktualisiert und repliziert das "OnTick"-Verhalten des Originalcodes ohne Einführung benutzerdefinierter Timer oder Sammlungen.

Verwendung

  1. Weisen Sie der Strategieinstanz die gewünschte Security und das gewünschte Portfolio zu, bevor Sie sie starten.
  2. Passen Sie optional die Parameter an die Volatilität des Zielinstruments und die Risikobereitschaft an.
  3. Starten Sie die Strategie. Sie abonniert sofort Level1-Daten, baut das erste Grid auf, sobald sowohl Bid- als auch Ask-Preise verfügbar sind, und verwaltet die Positionen automatisch weiter.
  4. Überwachen Sie das Log auf Meldungen wie "Profit target reached" oder "Maximum loss reached", um zu wissen, wann das Grid zurückgesetzt wurde.

Stellen Sie sicher, dass das ausgewählte Instrument Level1-Updates mit den besten Bid- und Ask-Preisen liefert; andernfalls wird die Leiter nicht aufgebaut.

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 converted from the Carbophos MetaTrader 5 expert advisor.
/// Simulates symmetric grid levels and manages profit and loss on the aggregated position.
/// </summary>
public class CarbophosGridStrategy : Strategy
{
	private readonly StrategyParam<decimal> _profitTarget;
	private readonly StrategyParam<decimal> _maxLoss;
	private readonly StrategyParam<int> _stepPips;
	private readonly StrategyParam<int> _ordersPerSide;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _entryPrice;
	private decimal _gridCenterPrice;
	private bool _gridPlaced;
	private int _cooldownRemaining;

	private readonly List<decimal> _buyLevels = new();
	private readonly List<decimal> _sellLevels = new();

	/// <summary>
	/// Floating profit level (in absolute price * volume) that triggers closing of all positions.
	/// </summary>
	public decimal ProfitTarget
	{
		get => _profitTarget.Value;
		set => _profitTarget.Value = value;
	}

	/// <summary>
	/// Maximum allowed floating loss before the grid is closed.
	/// </summary>
	public decimal MaxLoss
	{
		get => _maxLoss.Value;
		set => _maxLoss.Value = value;
	}

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

	/// <summary>
	/// Number of limit orders to place above and below the market price.
	/// </summary>
	public int OrdersPerSide
	{
		get => _ordersPerSide.Value;
		set => _ordersPerSide.Value = value;
	}

	/// <summary>
	/// Volume for each grid level order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="CarbophosGridStrategy"/>.
	/// </summary>
	public CarbophosGridStrategy()
	{
		_profitTarget = Param(nameof(ProfitTarget), 500m)
			.SetGreaterThanZero()
			.SetDisplay("Profit Target", "Floating profit target in money", "Risk")
			.SetOptimize(100m, 1000m, 50m);

		_maxLoss = Param(nameof(MaxLoss), 100m)
			.SetGreaterThanZero()
			.SetDisplay("Max Loss", "Maximum floating loss before closing", "Risk")
			.SetOptimize(50m, 500m, 25m);

		_stepPips = Param(nameof(StepPips), 2000)
			.SetGreaterThanZero()
			.SetDisplay("Step (pips)", "Distance between grid levels in pips", "Grid")
			.SetOptimize(10, 150, 10);

		_ordersPerSide = Param(nameof(OrdersPerSide), 1)
			.SetGreaterThanZero()
			.SetDisplay("Orders Per Side", "Number of pending orders on each side", "Grid")
			.SetOptimize(1, 10, 1);

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume for each pending order", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");
	}

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

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

		_entryPrice = null;
		_gridCenterPrice = 0m;
		_gridPlaced = false;
		_cooldownRemaining = 0;
		_buyLevels.Clear();
		_sellLevels.Clear();
	}

	/// <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);
		}
	}

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

		var currentPrice = candle.ClosePrice;

		// Check if any grid levels were hit by this candle
		CheckGridFills(candle);

		// Check profit/loss on position
		if (Position != 0 && _entryPrice is decimal entry)
		{
			var floatingPnL = (currentPrice - entry) * Position;

			if (floatingPnL >= ProfitTarget)
			{
				CloseAll("Profit target reached.");
				return;
			}

			if (floatingPnL <= -MaxLoss)
			{
				CloseAll("Maximum loss reached.");
				return;
			}
		}

		// Cooldown after closing
		if (_cooldownRemaining > 0)
		{
			_cooldownRemaining--;
			return;
		}

		// Place grid if none is active
		if (!_gridPlaced || (Position == 0 && _buyLevels.Count == 0 && _sellLevels.Count == 0))
		{
			PlaceGrid(currentPrice);
		}
	}

	private void PlaceGrid(decimal centerPrice)
	{
		_buyLevels.Clear();
		_sellLevels.Clear();

		var stepSize = GetGridStep();
		if (stepSize <= 0m || centerPrice <= 0m)
			return;

		for (var i = 1; i <= OrdersPerSide; i++)
		{
			var offset = stepSize * i;
			var buyPrice = centerPrice - offset;
			var sellPrice = centerPrice + offset;

			if (buyPrice > 0m)
				_buyLevels.Add(buyPrice);

			_sellLevels.Add(sellPrice);
		}

		_gridCenterPrice = centerPrice;
		_gridPlaced = true;
	}

	private void CheckGridFills(ICandleMessage candle)
	{
		// Check buy levels (price goes down to the level)
		for (var i = _buyLevels.Count - 1; i >= 0; i--)
		{
			if (i >= _buyLevels.Count) continue;
			if (candle.LowPrice <= _buyLevels[i])
			{
				var level = _buyLevels[i];
				BuyMarket();
				UpdateEntryPrice(level, OrderVolume, true);
				try { _buyLevels.RemoveAt(i); } catch { }
			}
		}

		// Check sell levels (price goes up to the level)
		for (var i = _sellLevels.Count - 1; i >= 0; i--)
		{
			if (i >= _sellLevels.Count) continue;
			if (candle.HighPrice >= _sellLevels[i])
			{
				var level = _sellLevels[i];
				SellMarket();
				UpdateEntryPrice(level, OrderVolume, false);
				try { _sellLevels.RemoveAt(i); } catch { }
			}
		}
	}

	private void UpdateEntryPrice(decimal fillPrice, decimal volume, bool isBuy)
	{
		if (_entryPrice is not decimal existingEntry || Position == 0)
		{
			_entryPrice = fillPrice;
			return;
		}

		// Weighted average entry price calculation
		var existingPos = Position;
		var newPos = isBuy ? existingPos + volume : existingPos - volume;

		if (newPos == 0)
		{
			_entryPrice = null;
			return;
		}

		// Only update if adding to position in same direction
		if ((isBuy && existingPos > 0) || (!isBuy && existingPos < 0))
		{
			var totalVolume = Math.Abs(existingPos) + volume;
			_entryPrice = (existingEntry * Math.Abs(existingPos) + fillPrice * volume) / totalVolume;
		}
		else
		{
			// Reducing position - keep same entry price
			if (Math.Abs(newPos) > 0)
				_entryPrice = existingEntry;
			else
				_entryPrice = null;
		}
	}

	private void CloseAll(string reason)
	{
		if (Position > 0)
			SellMarket();
		else if (Position < 0)
			BuyMarket();

		_buyLevels.Clear();
		_sellLevels.Clear();
		_gridPlaced = false;
		_entryPrice = null;
		_cooldownRemaining = 10;

		LogInfo(reason);
	}

	private decimal GetGridStep()
	{
		var security = Security;

		var priceStep = security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
			priceStep = 0.01m;

		var decimals = security?.Decimals ?? 2;
		var multiplier = (decimals == 3 || decimals == 5) ? 10m : 1m;
		return StepPips * priceStep * multiplier;
	}
}