Ver en GitHub

Estrategia Carbophos Grid

Descripción general

La Estrategia Carbophos Grid es una conversión directa del asesor experto de MetaTrader 5 "Carbophos". Mantiene continuamente una escalera simétrica de órdenes límite de compra y venta alrededor de los precios bid/ask actuales. La estrategia monitorea el beneficio flotante agregado de toda la cuadrícula y cierra toda la exposición una vez que se alcanza el objetivo de beneficio deseado o el drawdown máximo tolerado. Después de que la posición se aplana y no quedan órdenes activas, la escalera se reconstruye automáticamente.

Lógica de Trading

  1. Cuando la estrategia inicia y no hay órdenes activas ni posiciones abiertas, calcula el espaciado de la cuadrícula en unidades de precio basadas en el paso configurado en pips y la precisión de precio del instrumento. Se colocan cinco (configurable) órdenes sell limit por encima del mejor bid y el mismo número de órdenes buy limit por debajo del mejor ask.
  2. Si alguna orden se llena, la posición resultante se monitorea tick a tick usando datos de Level1. El PnL flotante se calcula a partir de la diferencia entre el precio de salida actual (bid para posiciones largas, ask para posiciones cortas) y el precio de entrada ponderado por volumen.
  3. Una vez que el beneficio flotante supera el objetivo configurado, o la pérdida flotante viola el umbral de protección, la estrategia envía una orden de mercado para cerrar la posición abierta y cancela todas las órdenes límite restantes. El indicador de estado se limpia para que la escalera sea reconstruida en la próxima actualización de precio.
  4. Si todas las órdenes se llenan pero la posición neta vuelve a cero (por ejemplo, porque el mercado se revierte a través de la cuadrícula), la próxima actualización de Level1 activa una nueva colocación de escalera.

Parámetros

Parámetro Descripción
ProfitTarget Beneficio flotante (dinero) que desencadena el cierre de toda la cuadrícula.
MaxLoss Pérdida flotante (dinero) que fuerza una salida de emergencia.
StepPips Distancia entre niveles consecutivos de la cuadrícula expresada en pips. Se convierte internamente a unidades de precio usando el tamaño del tick y la precisión decimal del símbolo.
OrdersPerSide Número de órdenes límite colocadas por encima y por debajo del precio actual del mercado.
OrderVolume Volumen para cada orden de cuadrícula.

Todos los parámetros admiten rangos de optimización para simplificar la experimentación en el optimizador de StockSharp.

Gestión de Riesgos y Protecciones

La estrategia usa el gancho incorporado StartProtection() y aplica niveles monetarios duros de stop/profit en el nivel de la estrategia. El cálculo del PnL flotante depende de los ajustes PriceStep y StepPrice del instrumento. Cuando se alcanza cualquiera de los umbrales, la estrategia cierra la posición con una orden de mercado y cancela cada orden límite activa antes de restablecer el indicador interno de cuadrícula.

Notas de Conversión

  • El asesor experto MQL5 original ajustaba los valores de pip para los símbolos Forex de tres y cinco decimales. El port de StockSharp replica este comportamiento multiplicando el PriceStep del exchange por 10 cuando el instrumento expone tres o cinco decimales.
  • MetaTrader agrega el beneficio de posición, la comisión y el swap por número mágico. En StockSharp el PnL flotante se recalcula desde el precio de entrada ponderado y el precio bid/ask actual, por lo que no se requiere manejo explícito de comisiones.
  • La colocación de órdenes, la cancelación y la gestión de posiciones se implementan a través del API Strategy de alto nivel (BuyLimit, SellLimit, CancelActiveOrders, BuyMarket, SellMarket) según lo requerido por las directrices del proyecto.
  • La cuadrícula se actualiza exclusivamente desde las actualizaciones de Level1, replicando el comportamiento "OnTick" del código original sin introducir temporizadores personalizados o colecciones.

Uso

  1. Asigne el Security y Portfolio deseados a la instancia de la estrategia antes de iniciarla.
  2. Opcionalmente ajuste los parámetros para coincidir con la volatilidad del instrumento objetivo y la tolerancia al riesgo.
  3. Inicie la estrategia. Inmediatamente se suscribe a los datos de Level1, construye la primera cuadrícula una vez que tanto los precios bid como ask están disponibles, y continúa gestionando la exposición automáticamente.
  4. Monitoree el registro de mensajes como "Profit target reached" o "Maximum loss reached" para saber cuándo se ha restablecido la cuadrícula.

Asegúrese de que el instrumento seleccionado proporcione actualizaciones de Level1 con los mejores precios bid y ask; de lo contrario la escalera no se construirá.

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