Ver no GitHub

Estratégia Carbophos Grid

Visão Geral

A Estratégia Carbophos Grid é uma conversão direta do assessor especialista MetaTrader 5 "Carbophos". Ela mantém continuamente uma escada simétrica de ordens limit de compra e venda ao redor dos preços bid/ask atuais. A estratégia monitora o lucro flutuante agregado de toda a grade e fecha toda a exposição assim que o alvo de lucro desejado ou o drawdown máximo tolerado é atingido. Depois que a posição é aplainada e não restam ordens pendentes, a escada é reconstruída automaticamente.

Lógica de Trading

  1. Quando a estratégia inicia e não há ordens ativas nem posições abertas, ela calcula o espaçamento da grade em unidades de preço com base no passo configurado em pips e a precisão de preço do instrumento. Cinco (configurável) ordens sell limit são colocadas acima do melhor bid e o mesmo número de ordens buy limit são colocadas abaixo do melhor ask.
  2. Se alguma ordem for preenchida, a posição resultante é monitorada tick a tick usando dados de Level1. O PnL flutuante é calculado a partir da diferença entre o preço de saída atual (bid para posições compradas, ask para posições vendidas) e o preço médio de entrada ponderado por volume.
  3. Uma vez que o lucro flutuante excede o alvo configurado, ou a perda flutuante viola o limiar de proteção, a estratégia envia uma ordem de mercado para fechar a posição aberta e cancela todas as ordens limit restantes. O indicador de estado é limpo para que a escada seja reconstruída na próxima atualização de preço.
  4. Se todas as ordens forem preenchidas mas a posição líquida retornar a zero (por exemplo, porque o mercado se reverte através da grade), a próxima atualização de Level1 aciona uma nova colocação de escada.

Parâmetros

Parâmetro Descrição
ProfitTarget Lucro flutuante (dinheiro) que desencadeia o fechamento de toda a grade.
MaxLoss Perda flutuante (dinheiro) que força uma saída de emergência.
StepPips Distância entre níveis consecutivos da grade expressa em pips. Convertida internamente em unidades de preço usando o tamanho do tick e a precisão decimal do símbolo.
OrdersPerSide Número de ordens limit colocadas acima e abaixo do preço atual do mercado.
OrderVolume Volume para cada ordem de grade.

Todos os parâmetros suportam intervalos de otimização para simplificar a experimentação no otimizador do StockSharp.

Gestão de Risco e Proteções

A estratégia usa o gancho embutido StartProtection() e aplica níveis monetários duros de stop/profit no nível da estratégia. O cálculo do PnL flutuante depende das configurações PriceStep e StepPrice do instrumento. Quando qualquer um dos limiares é atingido, a estratégia fecha a posição com uma ordem de mercado e cancela cada ordem limit ativa antes de redefinir o indicador interno da grade.

Notas de Conversão

  • O assessor especialista MQL5 original ajustava os valores de pip para símbolos Forex de três e cinco decimais. O port StockSharp replica esse comportamento multiplicando o PriceStep da exchange por 10 quando o instrumento expõe três ou cinco decimais.
  • MetaTrader agrega o lucro de posição, comissão e swap por número mágico. No StockSharp o PnL flutuante é recalculado a partir do preço médio de entrada e o preço bid/ask atual, então o tratamento explícito de comissões não é necessário.
  • A colocação de ordens, cancelamento e gestão de posições são implementados via API Strategy de alto nível (BuyLimit, SellLimit, CancelActiveOrders, BuyMarket, SellMarket) conforme exigido pelas diretrizes do projeto.
  • A grade é atualizada exclusivamente a partir de atualizações de Level1, replicando o comportamento "OnTick" do código original sem introduzir timers personalizados ou coleções.

Uso

  1. Atribua a Security e o Portfolio desejados à instância da estratégia antes de iniciá-la.
  2. Opcionalmente ajuste os parâmetros para corresponder à volatilidade do instrumento alvo e à tolerância ao risco.
  3. Inicie a estratégia. Ela imediatamente subscreve os dados de Level1, constrói a primeira grade assim que os preços bid e ask estiverem disponíveis, e continua gerenciando a exposição automaticamente.
  4. Monitore o registro de mensagens como "Profit target reached" ou "Maximum loss reached" para saber quando a grade foi redefinida.

Certifique-se de que o instrumento selecionado fornece atualizações de Level1 com os melhores preços bid e ask; caso contrário a escada não será construída.

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