Ver en GitHub

CH2010 Structure Multi-Timeframe Breakout Strategy

This strategy replicates the behaviour of the original ch2010structure.mq5 expert by tracking multiple forex pairs on two timeframes. Each instrument monitors the daily candle to determine a directional bias and then watches 30-minute candles to look for breakouts beyond the prior daily range. Market positions are opened when the breakout aligns with the daily trend and closed using protective stop-loss and take-profit levels.

Core Logic

  1. Daily bias detection

    • The strategy subscribes to daily candles for USDCHF, GBPUSD, AUDUSD, USDJPY and EURGBP.
    • When a daily candle finishes the close/open relationship defines the bias: bullish, bearish or neutral.
    • The daily high, low and close are stored together with the session date so intraday logic can confirm it is trading the same session.
  2. Intraday breakout execution

    • 30-minute candles are evaluated once they close.
    • If the close is above the previous daily high plus a configurable buffer and the bias is not bearish, a long trade is triggered.
    • If the close is below the previous daily low minus the buffer and the bias is not bullish, a short trade is triggered.
    • Only one long and one short breakout can be activated per instrument each day to avoid over-trading.
  3. Risk management inspired by the original helper functions

    • Volumes are clamped between MinTradeVolume and MaxTradeVolume and the aggregated position across all instruments is restricted by MaxAggregateVolume.
    • Each filled position immediately calculates absolute stop-loss and take-profit levels using percentage offsets from the entry price.
    • Positions are closed via market orders as soon as the stop or target is reached; repeated exit orders are prevented by the ExitInProgress flag.
  4. State tracking

    • For every instrument the strategy tracks its own daily levels, last known position, entry side, exit orders and breakout flags in an InstrumentContext.
    • This allows the multi-symbol workflow without having to maintain custom collections outside of the context class.

Strategy Parameters

Parameter Description
TradeVolume Base volume used for new entries, subject to the volume limits.
MinTradeVolume & MaxTradeVolume Bounds that mirror the original MQL risk filter.
MaxAggregateVolume Maximum sum of absolute positions across all traded pairs.
StopLossPercent Protective stop offset in percent from the detected entry price.
TakeProfitPercent Take-profit offset in percent from the detected entry price.
BreakoutBufferPercent Percentage of the prior daily range added to breakout triggers.
DailyCandleType DataType used to request the higher timeframe candles.
IntradayCandleType DataType used to request the execution timeframe candles.
UsdChfSecurity .. EurGbpSecurity Security objects for the five forex symbols monitored by default.

Required Data

  • Daily candles for every configured symbol (default: 1-day time frame).
  • Intraday candles (default: 30-minute) for the same symbols.
  • Real-time order routing to submit market orders for each security.

Usage Notes

  1. Configure the five security parameters before starting the strategy. They can be replaced with other instruments if desired.
  2. Set the portfolio and connector as in other StockSharp strategies.
  3. Optionally adjust the breakout buffer or risk parameters to reflect the target broker's contract specifications.
  4. Start the strategy. It will automatically subscribe to both candle streams for each instrument, log the daily structure and wait for valid intraday breakouts.
  5. Monitor the log for entries such as Daily candle captured and Enter Buy to verify the decision flow.

Differences vs. the Original MQL Expert

  • Pending orders are replaced with immediate market orders once the breakout condition is observed. This keeps the logic compatible with the StockSharp high-level API while preserving the idea of limiting exposure and reacting only once per direction each day.
  • Volume restrictions from the DebugOrderSend helper were adapted into parameters that clamp single trade sizes and total exposure.
  • Extensive logging is added to show daily levels, entry reasons and exit triggers in English comments for easier debugging in StockSharp.

Disclaimer

This example is intended for educational purposes. Parameters and securities should be reviewed and adjusted before using the strategy in production trading.

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