Auf GitHub ansehen

CH2010 Structure Multi-Zeitrahmen-Ausbruchsstrategie

Diese Strategie repliziert das Verhalten des ursprünglichen ch2010structure.mq5-Experten, indem sie mehrere Forex-Paare auf zwei Zeitrahmen verfolgt. Jedes Instrument überwacht die Tageskerze, um einen Richtungsbias zu bestimmen, und beobachtet dann 30-Minuten-Kerzen auf der Suche nach Ausbrüchen jenseits des vorherigen Tagesbereichs. Marktpositionen werden eröffnet, wenn der Ausbruch mit dem Tagestrend übereinstimmt, und mit schützenden Stop-Loss- und Take-Profit-Niveaus geschlossen.

Kernlogik

  1. Tägliche Bias-Erkennung

    • Die Strategie abonniert Tageskerzen für USDCHF, GBPUSD, AUDUSD, USDJPY und EURGBP.
    • Wenn eine Tageskerze endet, definiert die Schluss/Eröffnungs-Beziehung den Bias: bullisch, bärisch oder neutral.
    • Das tägliche Hoch, Tief und Schluss werden zusammen mit dem Sessionsdatum gespeichert, damit die Intraday-Logik bestätigen kann, dass die gleiche Session gehandelt wird.
  2. Intraday-Ausbruchsausführung

    • 30-Minuten-Kerzen werden ausgewertet, sobald sie schließen.
    • Wenn der Schluss über dem vorherigen Tageshoch plus einem konfigurierbaren Puffer liegt und der Bias nicht bärisch ist, wird ein Long-Trade ausgelöst.
    • Wenn der Schluss unter dem vorherigen Tagestief minus dem Puffer liegt und der Bias nicht bullisch ist, wird ein Short-Trade ausgelöst.
    • Pro Instrument und Tag kann nur ein Long- und ein Short-Ausbruch aktiviert werden, um übermäßiges Trading zu vermeiden.
  3. Risikomanagement inspiriert von den ursprünglichen Helper-Funktionen

    • Volumina werden zwischen MinTradeVolume und MaxTradeVolume begrenzt und die aggregierte Position über alle Instrumente wird durch MaxAggregateVolume eingeschränkt.
    • Jede gefüllte Position berechnet sofort absolute Stop-Loss- und Take-Profit-Niveaus unter Verwendung prozentualer Offsets vom Einstiegspreis.
    • Positionen werden über Marktorders geschlossen, sobald der Stop oder das Ziel erreicht wird; wiederholte Exit-Orders werden durch den ExitInProgress-Flag verhindert.
  4. Zustandsverfolgung

    • Für jedes Instrument verfolgt die Strategie seine eigenen Tagesniveaus, letzte bekannte Position, Einstiegsseite, Exit-Orders und Ausbruchs-Flags in einem InstrumentContext.
    • Dies ermöglicht den Multi-Symbol-Workflow, ohne benutzerdefinierte Sammlungen außerhalb der Kontextklasse pflegen zu müssen.

Strategie-Parameter

Parameter Beschreibung
TradeVolume Basis-Volumen für neue Einstiege, unterliegt den Volumengrenzen.
MinTradeVolume und MaxTradeVolume Grenzen, die den ursprünglichen MQL-Risikofilter spiegeln.
MaxAggregateVolume Maximale Summe absoluter Positionen über alle gehandelten Paare.
StopLossPercent Schutzstopp-Offset in Prozent vom erkannten Einstiegspreis.
TakeProfitPercent Take-Profit-Offset in Prozent vom erkannten Einstiegspreis.
BreakoutBufferPercent Prozentsatz des vorherigen Tagesbereichs, der zu Ausbruchsauslösern hinzugefügt wird.
DailyCandleType DataType zum Anfordern der höheren Zeitrahmen-Kerzen.
IntradayCandleType DataType zum Anfordern der Ausführungszeitrahmen-Kerzen.
UsdChfSecurity .. EurGbpSecurity Wertpapier-Objekte für die standardmäßig überwachten fünf Forex-Symbole.

Erforderliche Daten

  • Tageskerzen für jedes konfigurierte Symbol (Standard: 1-Tages-Zeitrahmen).
  • Intraday-Kerzen (Standard: 30 Minuten) für dieselben Symbole.
  • Echtzeit-Order-Routing zum Einreichen von Marktorders für jedes Wertpapier.

Verwendungshinweise

  1. Die fünf Wertpapier-Parameter vor dem Start der Strategie konfigurieren. Sie können bei Bedarf durch andere Instrumente ersetzt werden.
  2. Portfolio und Connector wie in anderen StockSharp-Strategien einrichten.
  3. Optional den Ausbruchspuffer oder Risikoparameter anpassen, um die Kontraktspezifikationen des Ziel-Brokers zu berücksichtigen.
  4. Die Strategie starten. Sie wird automatisch beide Kerzenströme für jedes Instrument abonnieren, die Tagesstruktur protokollieren und auf gültige Intraday-Ausbrüche warten.
  5. Das Log auf Einträge wie Daily candle captured und Enter Buy überwachen, um den Entscheidungsfluss zu überprüfen.

Unterschiede vs. dem ursprünglichen MQL-Experten

  • Ausstehende Orders werden durch sofortige Marktorders ersetzt, sobald die Ausbruchsbedingung beobachtet wird. Dies hält die Logik kompatibel mit der StockSharp High-Level-API und bewahrt die Idee, die Exposition zu begrenzen und nur einmal pro Richtung täglich zu reagieren.
  • Volumenbeschränkungen aus dem DebugOrderSend-Helper wurden in Parameter angepasst, die einzelne Trade-Größen und die Gesamtexposition begrenzen.
  • Umfangreiche Protokollierung wurde hinzugefügt, um Tagesniveaus, Einstiegsgründe und Exit-Auslöser in englischen Kommentaren für einfacheres Debugging in StockSharp anzuzeigen.

Haftungsausschluss

Dieses Beispiel dient zu Bildungszwecken. Parameter und Wertpapiere sollten überprüft und angepasst werden, bevor die Strategie im Produktionshandel eingesetzt wird.

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