Ver no GitHub

Estratégia CH2010 Structure de Rompimento Multi-Período

Esta estratégia replica o comportamento do especialista original ch2010structure.mq5 rastreando múltiplos pares forex em dois períodos. Cada instrumento monitora a vela diária para determinar um viés direcional e depois observa velas de 30 minutos em busca de rompimentos além do intervalo diário anterior. Posições de mercado são abertas quando o rompimento se alinha com a tendência diária e fechadas usando níveis protetores de stop-loss e take-profit.

Lógica principal

  1. Detecção de viés diário

    • A estratégia subscreve às velas diárias para USDCHF, GBPUSD, AUDUSD, USDJPY e EURGBP.
    • Quando uma vela diária termina, a relação fechamento/abertura define o viés: de alta, de baixa ou neutro.
    • O máximo, mínimo e fechamento diário são armazenados junto com a data da sessão para que a lógica intradia possa confirmar que está operando a mesma sessão.
  2. Execução de rompimentos intradia

    • As velas de 30 minutos são avaliadas assim que fecham.
    • Se o fechamento estiver acima do máximo diário anterior mais um buffer configurável e o viés não for de baixa, um trade comprado é acionado.
    • Se o fechamento estiver abaixo do mínimo diário anterior menos o buffer e o viés não for de alta, um trade vendido é acionado.
    • Apenas um rompimento comprado e um vendido pode ser ativado por instrumento a cada dia para evitar operar excessivamente.
  3. Gerenciamento de risco inspirado nas funções helper originais

    • Os volumes são limitados entre MinTradeVolume e MaxTradeVolume e a posição agregada em todos os instrumentos é restringida por MaxAggregateVolume.
    • Cada posição preenchida calcula imediatamente os níveis absolutos de stop-loss e take-profit usando offsets percentuais a partir do preço de entrada.
    • As posições são fechadas por ordens de mercado assim que o stop ou alvo é atingido; ordens de saída repetidas são evitadas pelo flag ExitInProgress.
  4. Rastreamento de estado

    • Para cada instrumento, a estratégia rastreia seus próprios níveis diários, última posição conhecida, lado de entrada, ordens de saída e flags de rompimento em um InstrumentContext.
    • Isso permite o fluxo de trabalho multi-símbolo sem ter que manter coleções personalizadas fora da classe de contexto.

Parâmetros da estratégia

Parâmetro Descrição
TradeVolume Volume base usado para novas entradas, sujeito aos limites de volume.
MinTradeVolume e MaxTradeVolume Limites que espelham o filtro de risco original do MQL.
MaxAggregateVolume Soma máxima de posições absolutas em todos os pares operados.
StopLossPercent Offset do stop de proteção em porcentagem a partir do preço de entrada detectado.
TakeProfitPercent Offset do take-profit em porcentagem a partir do preço de entrada detectado.
BreakoutBufferPercent Porcentagem do intervalo diário anterior adicionada aos gatilhos de rompimento.
DailyCandleType DataType usado para solicitar as velas de período superior.
IntradayCandleType DataType usado para solicitar as velas de período de execução.
UsdChfSecurity .. EurGbpSecurity Objetos de instrumento para os cinco símbolos forex monitorados por padrão.

Dados necessários

  • Velas diárias para cada símbolo configurado (padrão: período de 1 dia).
  • Velas intradia (padrão: 30 minutos) para os mesmos símbolos.
  • Roteamento de ordens em tempo real para enviar ordens de mercado para cada instrumento.

Notas de uso

  1. Configurar os cinco parâmetros de instrumento antes de iniciar a estratégia. Podem ser substituídos por outros instrumentos se desejado.
  2. Definir o portfólio e conector como em outras estratégias StockSharp.
  3. Opcionalmente ajustar o buffer de rompimento ou parâmetros de risco para refletir as especificações de contrato do broker alvo.
  4. Iniciar a estratégia. Ela subscreverá automaticamente a ambos os fluxos de velas para cada instrumento, registrará a estrutura diária e aguardará rompimentos intradia válidos.
  5. Monitorar o log para entradas como Daily candle captured e Enter Buy para verificar o fluxo de decisões.

Diferenças vs. o Especialista MQL original

  • Ordens pendentes são substituídas por ordens de mercado imediatas assim que a condição de rompimento é observada. Isso mantém a lógica compatível com a API de alto nível do StockSharp enquanto preserva a ideia de limitar a exposição e reagir apenas uma vez por direção a cada dia.
  • As restrições de volume do helper DebugOrderSend foram adaptadas em parâmetros que limitam os tamanhos de trades individuais e a exposição total.
  • Registro extensivo é adicionado para mostrar níveis diários, razões de entrada e gatilhos de saída em comentários em inglês para facilitar a depuração no StockSharp.

Isenção de responsabilidade

Este exemplo é destinado a propósitos educacionais. Parâmetros e instrumentos devem ser revisados e ajustados antes de usar a estratégia em trading de produção.

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>
/// Multi-currency breakout strategy converted from the original CH2010 structure expert.
/// Watches daily candles to define trend bias and 30-minute candles for entries and exits.
/// </summary>
public class Ch2010StructureStrategy : Strategy
{
	private readonly StrategyParam<Security> _usdChf;
	private readonly StrategyParam<Security> _gbpUsd;
	private readonly StrategyParam<Security> _audUsd;
	private readonly StrategyParam<Security> _usdJpy;
	private readonly StrategyParam<Security> _eurGbp;
	
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<decimal> _minTradeVolume;
	private readonly StrategyParam<decimal> _maxTradeVolume;
	private readonly StrategyParam<decimal> _maxAggregateVolume;
	private readonly StrategyParam<decimal> _stopLossPercent;
	private readonly StrategyParam<decimal> _takeProfitPercent;
	private readonly StrategyParam<decimal> _breakoutBufferPercent;
	private readonly StrategyParam<DataType> _dailyCandleType;
	private readonly StrategyParam<DataType> _intradayCandleType;
	
	private readonly List<InstrumentContext> _contexts = new();

	private static readonly (string Alias, Func<Ch2010StructureStrategy, Security> Getter)[] _instrumentSlots =
	[
		("USDCHF", s => s.UsdChfSecurity),
		("GBPUSD", s => s.GbpUsdSecurity),
		("AUDUSD", s => s.AudUsdSecurity),
		("USDJPY", s => s.UsdJpySecurity),
		("EURGBP", s => s.EurGbpSecurity),
	];
	
	/// <summary>
	/// Initializes a new instance of the <see cref="Ch2010StructureStrategy"/> class.
	/// </summary>
	public Ch2010StructureStrategy()
	{
		_usdChf = Param<Security>(nameof(UsdChfSecurity), null);
		_usdChf.SetDisplay("USD/CHF", "USDCHF symbol to trade", "Instruments");
		_gbpUsd = Param<Security>(nameof(GbpUsdSecurity), null);
		_gbpUsd.SetDisplay("GBP/USD", "GBPUSD symbol to trade", "Instruments");
		_audUsd = Param<Security>(nameof(AudUsdSecurity), null);
		_audUsd.SetDisplay("AUD/USD", "AUDUSD symbol to trade", "Instruments");
		_usdJpy = Param<Security>(nameof(UsdJpySecurity), null);
		_usdJpy.SetDisplay("USD/JPY", "USDJPY symbol to trade", "Instruments");
		_eurGbp = Param<Security>(nameof(EurGbpSecurity), null);
		_eurGbp.SetDisplay("EUR/GBP", "EURGBP symbol to trade", "Instruments");
		
		_tradeVolume = Param(nameof(TradeVolume), 1m);
		_tradeVolume.SetGreaterThanZero();
		_tradeVolume.SetDisplay("Trade Volume", "Nominal volume used for entries", "Risk");
		_minTradeVolume = Param(nameof(MinTradeVolume), 0.1m);
		_minTradeVolume.SetGreaterThanZero();
		_minTradeVolume.SetDisplay("Minimum Volume", "Lower bound that mirrors the MQL expert", "Risk");
		_maxTradeVolume = Param(nameof(MaxTradeVolume), 5m);
		_maxTradeVolume.SetGreaterThanZero();
		_maxTradeVolume.SetDisplay("Maximum Volume", "Upper bound for a single position", "Risk");
		_maxAggregateVolume = Param(nameof(MaxAggregateVolume), 15m);
		_maxAggregateVolume.SetGreaterThanZero();
		_maxAggregateVolume.SetDisplay("Aggregate Volume", "Cap across all instruments", "Risk");
		_stopLossPercent = Param(nameof(StopLossPercent), 1.5m);
		_stopLossPercent.SetGreaterThanZero();
		_stopLossPercent.SetDisplay("Stop Loss %", "Protective stop percentage", "Risk");
		_takeProfitPercent = Param(nameof(TakeProfitPercent), 3m);
		_takeProfitPercent.SetGreaterThanZero();
		_takeProfitPercent.SetDisplay("Take Profit %", "Profit target percentage", "Risk");
		_breakoutBufferPercent = Param(nameof(BreakoutBufferPercent), 10m);
		_breakoutBufferPercent.SetGreaterThanZero();
		_breakoutBufferPercent.SetDisplay("Buffer %", "Percentage of daily range added above/below breakout", "Logic");
		
		_dailyCandleType = Param(nameof(DailyCandleType), TimeSpan.FromMinutes(5).TimeFrame());
		_dailyCandleType.SetDisplay("Daily Candle", "Time frame used for the daily bias", "Data");
		_intradayCandleType = Param(nameof(IntradayCandleType), TimeSpan.FromMinutes(30).TimeFrame());
		_intradayCandleType.SetDisplay("Intraday Candle", "Time frame used for intraday execution", "Data");
	}
	
	/// <summary>
	/// USDCHF security parameter.
	/// </summary>
	public Security UsdChfSecurity
	{
		get => _usdChf.Value;
		set => _usdChf.Value = value;
	}
	
	/// <summary>
	/// GBPUSD security parameter.
	/// </summary>
	public Security GbpUsdSecurity
	{
		get => _gbpUsd.Value;
		set => _gbpUsd.Value = value;
	}
	
	/// <summary>
	/// AUDUSD security parameter.
	/// </summary>
	public Security AudUsdSecurity
	{
		get => _audUsd.Value;
		set => _audUsd.Value = value;
	}
	
	/// <summary>
	/// USDJPY security parameter.
	/// </summary>
	public Security UsdJpySecurity
	{
		get => _usdJpy.Value;
		set => _usdJpy.Value = value;
	}
	
	/// <summary>
	/// EURGBP security parameter.
	/// </summary>
	public Security EurGbpSecurity
	{
		get => _eurGbp.Value;
		set => _eurGbp.Value = value;
	}
	
	/// <summary>
	/// Nominal trade volume.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}
	
	/// <summary>
	/// Minimum allowed volume.
	/// </summary>
	public decimal MinTradeVolume
	{
		get => _minTradeVolume.Value;
		set => _minTradeVolume.Value = value;
	}
	
	/// <summary>
	/// Maximum allowed volume for a single position.
	/// </summary>
	public decimal MaxTradeVolume
	{
		get => _maxTradeVolume.Value;
		set => _maxTradeVolume.Value = value;
	}
	
	/// <summary>
	/// Maximum combined exposure across all instruments.
	/// </summary>
	public decimal MaxAggregateVolume
	{
		get => _maxAggregateVolume.Value;
		set => _maxAggregateVolume.Value = value;
	}
	
	/// <summary>
	/// Stop-loss percentage applied to entries.
	/// </summary>
	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}
	
	/// <summary>
	/// Take-profit percentage applied to entries.
	/// </summary>
	public decimal TakeProfitPercent
	{
		get => _takeProfitPercent.Value;
		set => _takeProfitPercent.Value = value;
	}
	
	/// <summary>
	/// Buffer in percent of the daily range used to trigger breakouts.
	/// </summary>
	public decimal BreakoutBufferPercent
	{
		get => _breakoutBufferPercent.Value;
		set => _breakoutBufferPercent.Value = value;
	}
	
	/// <summary>
	/// Daily candle type.
	/// </summary>
	public DataType DailyCandleType
	{
		get => _dailyCandleType.Value;
		set => _dailyCandleType.Value = value;
	}
	
	/// <summary>
	/// Intraday candle type.
	/// </summary>
	public DataType IntradayCandleType
	{
		get => _intradayCandleType.Value;
		set => _intradayCandleType.Value = value;
	}
	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		foreach (var slot in _instrumentSlots)
		{
			var security = slot.Getter(this);

			if (security == null)
				continue;

			yield return (security, DailyCandleType);
			yield return (security, IntradayCandleType);
		}
	}

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

		_contexts.Clear();
	}

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

		_contexts.Clear();

		foreach (var slot in _instrumentSlots)
		{
			var security = slot.Getter(this);

			if (security == null)
				continue;

			var context = new InstrumentContext(slot.Alias, security);
			_contexts.Add(context);

			var dailySubscription = SubscribeCandles(DailyCandleType, true, security);
			dailySubscription.Bind(candle => ProcessDailyCandle(context, candle));
			dailySubscription.Start();

			var intradaySubscription = SubscribeCandles(IntradayCandleType, true, security);
			intradaySubscription.Bind(candle => ProcessIntradayCandle(context, candle));
			intradaySubscription.Start();
		}

		if (_contexts.Count == 0)
		{
			throw new InvalidOperationException("At least one security must be configured.");
		}
	}
	
	private void ProcessDailyCandle(InstrumentContext context, ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		{
			return;
		}
		
		context.DailyDate = candle.OpenTime.Date;
		context.DailyHigh = candle.HighPrice;
		context.DailyLow = candle.LowPrice;
		context.DailyClose = candle.ClosePrice;
		context.HasLevels = true;
		context.LongTriggered = false;
		context.ShortTriggered = false;
		
		if (candle.ClosePrice > candle.OpenPrice)
		{
			context.Bias = BiasDirections.Long;
		}
		else if (candle.ClosePrice < candle.OpenPrice)
		{
			context.Bias = BiasDirections.Short;
		}
		else
		{
			context.Bias = BiasDirections.Neutral;
		}
		
		LogInfo($"[{context.Alias}] Daily candle captured. High={candle.HighPrice} Low={candle.LowPrice} Close={candle.ClosePrice}");
	}
	
	private void ProcessIntradayCandle(InstrumentContext context, ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		{
			return;
		}

		if (!context.HasLevels)
		{
			return;
		}
		
		if (context.DailyDate != candle.OpenTime.Date)
		{
			return;
		}
		
		var security = context.Security;
		
		if (security == null)
		{
			return;
		}
		
		var position = GetPositionValue(security, Portfolio) ?? 0m;
		
		UpdatePositionSnapshot(context, candle.ClosePrice, position);
		
		if (position != 0)
		{
			ManageOpenPosition(context, position, candle.ClosePrice);
			return;
		}
		
		var range = context.DailyHigh - context.DailyLow;
		
		if (range <= 0)
		{
			return;
		}
		
		var buffer = range * (BreakoutBufferPercent / 100m);
		var longTrigger = context.DailyHigh + buffer;
		var shortTrigger = context.DailyLow - buffer;
		
		if (!context.LongTriggered && context.Bias != BiasDirections.Short)
		{
			if (candle.ClosePrice > longTrigger)
			{
				TryEnterPosition(context, Sides.Buy, candle.ClosePrice, "Daily breakout long");
				context.LongTriggered = true;
			}
		}
		
		if (!context.ShortTriggered && context.Bias != BiasDirections.Long)
		{
			if (candle.ClosePrice < shortTrigger)
			{
				TryEnterPosition(context, Sides.Sell, candle.ClosePrice, "Daily breakout short");
				context.ShortTriggered = true;
			}
		}
	}
	private void TryEnterPosition(InstrumentContext context, Sides side, decimal price, string reason)
	{
		if (context.ExitInProgress)
		{
			return;
		}
		
		var security = context.Security;
		
		if (security == null)
		{
			return;
		}
		
		var volume = AdjustVolumeForLimits(TradeVolume);
		
		if (volume <= 0)
		{
			return;
		}
		RegisterOrder(new Order
		{
			Security = security,
			Portfolio = Portfolio,
			Side = side,
			Volume = volume,
			Type = OrderTypes.Market,
			Comment = $"{context.Alias}:{reason}"
		});
		
		context.EntrySide = side;
		context.EntryPrice = price;
		context.StopPrice = null;
		context.TakeProfitPrice = null;
		context.ExitInProgress = false;
		
		LogInfo($"[{context.Alias}] Enter {side} at {price} vol={volume}. Reason={reason}");
	}
	private void ManageOpenPosition(InstrumentContext context, decimal position, decimal closePrice)
	{
		if (context.EntrySide == null)
		{
			return;
		}
		
		var isLong = position > 0;
		
		if (context.StopPrice == null || context.TakeProfitPrice == null)
		{
			var entryPrice = context.EntryPrice ?? closePrice;
			var stopOffset = entryPrice * (StopLossPercent / 100m);
			var takeOffset = entryPrice * (TakeProfitPercent / 100m);
			
			if (isLong)
			{
				context.StopPrice = entryPrice - stopOffset;
				context.TakeProfitPrice = entryPrice + takeOffset;
			}
			else
			{
				context.StopPrice = entryPrice + stopOffset;
				context.TakeProfitPrice = entryPrice - takeOffset;
			}
		}
		if (context.ExitInProgress)
		{
			return;
		}
		
		if (isLong)
		{
			if (context.StopPrice != null && closePrice <= context.StopPrice.Value)
			{
				ExitPosition(context, position, Sides.Sell, $"StopLoss at {context.StopPrice.Value}");
				return;
			}
			
			if (context.TakeProfitPrice != null && closePrice >= context.TakeProfitPrice.Value)
			{
				ExitPosition(context, position, Sides.Sell, $"TakeProfit at {context.TakeProfitPrice.Value}");
			}
		}
		else
		{
			var volume = Math.Abs(position);
			
			if (context.StopPrice != null && closePrice >= context.StopPrice.Value)
			{
				ExitPosition(context, volume, Sides.Buy, $"StopLoss at {context.StopPrice.Value}");
				return;
			}
			
			if (context.TakeProfitPrice != null && closePrice <= context.TakeProfitPrice.Value)
			{
				ExitPosition(context, volume, Sides.Buy, $"TakeProfit at {context.TakeProfitPrice.Value}");
			}
		}
	}
	private void ExitPosition(InstrumentContext context, decimal volume, Sides side, string reason)
	{
		if (volume <= 0)
		{
			return;
		}
		
		var security = context.Security;
		
		if (security == null)
		{
			return;
		}
		
		context.ExitInProgress = true;
		
		RegisterOrder(new Order
		{
			Security = security,
			Portfolio = Portfolio,
			Side = side,
			Volume = volume,
			Type = OrderTypes.Market,
			Comment = $"{context.Alias}:{reason}"
		});
		
		LogInfo($"[{context.Alias}] Exit {side} vol={volume}. Reason={reason}");
	}
	private decimal AdjustVolumeForLimits(decimal desired)
	{
		if (desired <= 0)
		{
			return 0m;
		}
		
		var volume = Math.Min(desired, MaxTradeVolume);
		
		if (volume < MinTradeVolume)
		{
			return 0m;
		}
		
		var totalExposure = 0m;
		
		foreach (var context in _contexts)
		{
			var security = context.Security;
			
			if (security == null)
			{
				continue;
			}
			
			var pos = GetPositionValue(security, Portfolio) ?? 0m;
			totalExposure += Math.Abs(pos);
		}
		
		var remaining = MaxAggregateVolume - totalExposure;
		
		if (remaining <= 0)
		{
			return 0m;
		}
		
		return Math.Min(volume, remaining);
	}
	private void UpdatePositionSnapshot(InstrumentContext context, decimal price, decimal position)
	{
		if (position == context.LastKnownPosition)
		{
			return;
		}
		
		if (position == 0)
		{
			context.ResetPosition();
			return;
		}
		
		context.LastKnownPosition = position;
		context.EntrySide = position > 0 ? Sides.Buy : Sides.Sell;
		context.EntryPrice = price;
		context.ExitInProgress = false;
		
		var stopOffset = price * (StopLossPercent / 100m);
		var takeOffset = price * (TakeProfitPercent / 100m);
		
		if (position > 0)
		{
			context.StopPrice = price - stopOffset;
			context.TakeProfitPrice = price + takeOffset;
		}
		else
		{
			context.StopPrice = price + stopOffset;
			context.TakeProfitPrice = price - takeOffset;
		}
	}
	private enum BiasDirections
	{
		Neutral,
		Long,
		Short
	}
	
	private sealed class InstrumentContext
	{
		public InstrumentContext(string alias, Security security)
		{
			Alias = alias;
			Security = security;
		}

		public string Alias { get; }

		public Security Security { get; }
		
		public DateTime? DailyDate { get; set; }
		
		public decimal DailyHigh { get; set; }
		
		public decimal DailyLow { get; set; }
		
		public decimal DailyClose { get; set; }
		
		public BiasDirections Bias { get; set; }
		
		public bool HasLevels { get; set; }
		
		public bool LongTriggered { get; set; }
		
		public bool ShortTriggered { get; set; }
		
		public decimal LastKnownPosition { get; set; }
		
		public Sides? EntrySide { get; set; }
		
		public decimal? EntryPrice { get; set; }
		
		public decimal? StopPrice { get; set; }
		
		public decimal? TakeProfitPrice { get; set; }
		
		public bool ExitInProgress { get; set; }
		
		public void Reset()
		{
			DailyDate = null;
			DailyHigh = 0m;
			DailyLow = 0m;
			DailyClose = 0m;
			Bias = BiasDirections.Neutral;
			HasLevels = false;
			LongTriggered = false;
			ShortTriggered = false;
			ResetPosition();
		}
		
		public void ResetPosition()
		{
			LastKnownPosition = 0m;
			EntrySide = null;
			EntryPrice = null;
			StopPrice = null;
			TakeProfitPrice = null;
			ExitInProgress = false;
		}
	}
}