Auf GitHub ansehen

CMO Duplex Strategie

Die Strategie ist eine StockSharp-Portierung des MetaTrader 5-Experten Exp_CMO_Duplex.mq5. Sie teilt die Logik in zwei unabhängige Legs (Long und Short) auf, die beide auf Nulllinien-Kreuzungen des Chande Momentum Oscillators (CMO) reagieren. Jedes Leg kann seine eigene Kerzenserie, Periode und Signal-Offset verbrauchen, was es ermöglicht, asymmetrische Konfigurationen auf demselben Instrument zu betreiben.

Funktionsweise

  • Die Strategie abonniert einen oder zwei Kerzen-Feeds, je nachdem ob die Long- und Short-Legs dasselbe DataType verwenden.
  • Jedes Leg besitzt seine eigene CMO-Indikatorinstanz. Der Indikator wird nur auf fertigen Kerzen ausgewertet.
  • Die SignalBar-Einstellung definiert, wie viele abgeschlossene Kerzen im Verlauf für die Kreuzungslogik verwendet werden sollen. Ein Wert von 0 bedeutet «die aktuellste geschlossene Bar verwenden», 1 verwendet die vorherige Bar, 2 verwendet die davor, und so weiter.
  • Long-Leg: Wenn der ausgewählte CMO-Wert von über null auf null oder darunter kreuzt, tritt die Strategie in (oder dreht in) eine Long- Position, wenn Long-Einstiege erlaubt sind. Long-Ausstiege werden ausgelöst, wenn der ältere CMO-Wert unter null liegt oder wenn Stop-Loss-/Take-Profit-Level berührt werden.
  • Short-Leg: Spiegelt die Long-Logik. Ein Kreuz von unter null auf null oder darüber öffnet (oder dreht in) eine Short-Position und das entgegengesetzte Vorzeichen des CMO-Werts oder die konfigurierten Stops schließen die Position.
  • Positionsdrehungen verwenden Volume plus jede gegensätzliche Exposition, sodass eine einzelne Marktorder die vorherige Position schließt und die neue öffnet.
  • StartProtection() ist beim Start aktiviert, sodass die eingebauten StockSharp-Risikokontrollen aktiv bleiben.

Parameter

Parameter Beschreibung
LongCandleType Kerzentyp, der vom Long-Leg verwendet wird.
LongCmoPeriod Periode des CMO-Indikators auf der Long-Seite.
LongSignalBar Anzahl der geschlossenen Bars zwischen der aktuellen Zeit und der für Signale analysierten Bar (0 = aktuellste geschlossene Bar).
EnableLongEntries Erlaubt oder blockiert das Öffnen neuer Long-Positionen.
EnableLongExits Erlaubt oder blockiert das Schließen von Long-Positionen auf Oszillatorsignale.
LongStopLossPoints Stop-Loss-Abstand in Preisschritten für Long-Trades (0 deaktiviert den Stop).
LongTakeProfitPoints Take-Profit-Abstand in Preisschritten für Long-Trades (0 deaktiviert das Ziel).
ShortCandleType Kerzentyp, der vom Short-Leg verwendet wird.
ShortCmoPeriod Periode des CMO-Indikators auf der Short-Seite.
ShortSignalBar Anzahl der geschlossenen Bars zwischen der aktuellen Zeit und der für Short-Signale analysierten Bar.
EnableShortEntries Erlaubt oder blockiert das Öffnen neuer Short-Positionen.
EnableShortExits Erlaubt oder blockiert das Schließen von Short-Positionen auf Oszillatorsignale.
ShortStopLossPoints Stop-Loss-Abstand in Preisschritten für Short-Trades (0 deaktiviert den Stop).
ShortTakeProfitPoints Take-Profit-Abstand in Preisschritten für Short-Trades (0 deaktiviert das Ziel).

Die Basis-Eigenschaft Strategy.Volume steuert die Standard-Ordergröße. Wenn die Strategie die Richtung drehen muss, sendet sie eine Marktorder, deren Volumen gleich Volume + |Position| ist, was die alte Exposition schließt und die neue in einer einzigen Transaktion öffnet.

Risikomanagement

  • Stop-Loss- und Take-Profit-Level werden bei jeder fertigen Kerze ausgewertet. Für Long-Positionen wird der Stop unter dem Einstieg platziert und das Ziel darüber; für Short-Positionen werden die Level gespiegelt.
  • Ein Stop oder ein Ziel löst eine sofortige Marktorder zum Schließen der Position aus. Die gleiche Ausstiegsroutine läuft auch, wenn der jeweilige Oszillatorwert das falsche Vorzeichen beibehält (unter null für Longs, über null für Shorts).
  • Die Distanz auf null zu setzen deaktiviert den entsprechenden Schutz und lässt das Leg rein durch die Oszillatorlogik verwalten.

Verwendungshinweise

  • Die Strategie funktioniert am besten auf Instrumenten, bei denen der CMO nach dem Berühren der Nulllinie umkehrt. Konträre Einstiege werden durch den SignalBar-Offset absichtlich verzögert, um dem ursprünglichen Experten zu entsprechen.
  • Long- und Short-Legs können denselben Kerzen-Feed teilen oder auf verschiedenen Zeitrahmen operieren. Wenn beide dasselbe DataType verwenden, verwendet die Strategie ein einzelnes Abonnement für bessere Leistung.
  • Da die Strategie auf abgeschlossenen Kerzen operiert, wird empfohlen, einen kontinuierlichen Kerzenstrom zu liefern (z. B. über einen historischen Backtest oder einen Echtzeit-Feed), um fehlende Signale zu vermeiden.
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>
/// Two-sided strategy built around the Chande Momentum Oscillator zero-line crossings.
/// Long and short legs can use different candle types, periods and signal offsets.
/// </summary>
public class CmoDuplexStrategy : Strategy
{
	private readonly StrategyParam<DataType> _longCandleType;
	private readonly StrategyParam<int> _longCmoPeriod;
	private readonly StrategyParam<int> _longSignalBar;
	private readonly StrategyParam<bool> _enableLongEntries;
	private readonly StrategyParam<bool> _enableLongExits;
	private readonly StrategyParam<int> _longStopLossPoints;
	private readonly StrategyParam<int> _longTakeProfitPoints;

	private readonly StrategyParam<DataType> _shortCandleType;
	private readonly StrategyParam<int> _shortCmoPeriod;
	private readonly StrategyParam<int> _shortSignalBar;
	private readonly StrategyParam<bool> _enableShortEntries;
	private readonly StrategyParam<bool> _enableShortExits;
	private readonly StrategyParam<int> _shortStopLossPoints;
	private readonly StrategyParam<int> _shortTakeProfitPoints;

	private ChandeMomentumOscillator _longCmo;
	private ChandeMomentumOscillator _shortCmo;

	private readonly List<decimal> _longValues = new();
	private readonly List<decimal> _shortValues = new();

	private decimal? _entryPrice;

	public DataType LongCandleType
	{
		get => _longCandleType.Value;
		set => _longCandleType.Value = value;
	}

	public int LongCmoPeriod
	{
		get => _longCmoPeriod.Value;
		set => _longCmoPeriod.Value = value;
	}

	public int LongSignalBar
	{
		get => _longSignalBar.Value;
		set => _longSignalBar.Value = value;
	}

	public bool EnableLongEntries
	{
		get => _enableLongEntries.Value;
		set => _enableLongEntries.Value = value;
	}

	public bool EnableLongExits
	{
		get => _enableLongExits.Value;
		set => _enableLongExits.Value = value;
	}

	public int LongStopLossPoints
	{
		get => _longStopLossPoints.Value;
		set => _longStopLossPoints.Value = value;
	}

	public int LongTakeProfitPoints
	{
		get => _longTakeProfitPoints.Value;
		set => _longTakeProfitPoints.Value = value;
	}

	public DataType ShortCandleType
	{
		get => _shortCandleType.Value;
		set => _shortCandleType.Value = value;
	}

	public int ShortCmoPeriod
	{
		get => _shortCmoPeriod.Value;
		set => _shortCmoPeriod.Value = value;
	}

	public int ShortSignalBar
	{
		get => _shortSignalBar.Value;
		set => _shortSignalBar.Value = value;
	}

	public bool EnableShortEntries
	{
		get => _enableShortEntries.Value;
		set => _enableShortEntries.Value = value;
	}

	public bool EnableShortExits
	{
		get => _enableShortExits.Value;
		set => _enableShortExits.Value = value;
	}

	public int ShortStopLossPoints
	{
		get => _shortStopLossPoints.Value;
		set => _shortStopLossPoints.Value = value;
	}

	public int ShortTakeProfitPoints
	{
		get => _shortTakeProfitPoints.Value;
		set => _shortTakeProfitPoints.Value = value;
	}

	public CmoDuplexStrategy()
	{
		_longCandleType = Param(nameof(LongCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Long Candle Type", "Candle type for the long leg", "Long Leg");

		_longCmoPeriod = Param(nameof(LongCmoPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("Long CMO Period", "CMO period for the long leg", "Long Leg");

		_longSignalBar = Param(nameof(LongSignalBar), 1)
			.SetNotNegative()
			.SetDisplay("Long Signal Bar", "Offset in bars for long signals", "Long Leg");

		_enableLongEntries = Param(nameof(EnableLongEntries), true)
			.SetDisplay("Enable Long Entries", "Allow opening long trades", "Long Leg");

		_enableLongExits = Param(nameof(EnableLongExits), true)
			.SetDisplay("Enable Long Exits", "Allow closing long trades on signals", "Long Leg");

		_longStopLossPoints = Param(nameof(LongStopLossPoints), 1000)
			.SetNotNegative()
			.SetDisplay("Long Stop Loss", "Stop loss in price steps for longs", "Risk Management");

		_longTakeProfitPoints = Param(nameof(LongTakeProfitPoints), 2000)
			.SetNotNegative()
			.SetDisplay("Long Take Profit", "Take profit in price steps for longs", "Risk Management");

		_shortCandleType = Param(nameof(ShortCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Short Candle Type", "Candle type for the short leg", "Short Leg");

		_shortCmoPeriod = Param(nameof(ShortCmoPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("Short CMO Period", "CMO period for the short leg", "Short Leg");

		_shortSignalBar = Param(nameof(ShortSignalBar), 1)
			.SetNotNegative()
			.SetDisplay("Short Signal Bar", "Offset in bars for short signals", "Short Leg");

		_enableShortEntries = Param(nameof(EnableShortEntries), true)
			.SetDisplay("Enable Short Entries", "Allow opening short trades", "Short Leg");

		_enableShortExits = Param(nameof(EnableShortExits), true)
			.SetDisplay("Enable Short Exits", "Allow closing short trades on signals", "Short Leg");

		_shortStopLossPoints = Param(nameof(ShortStopLossPoints), 1000)
			.SetNotNegative()
			.SetDisplay("Short Stop Loss", "Stop loss in price steps for shorts", "Risk Management");

		_shortTakeProfitPoints = Param(nameof(ShortTakeProfitPoints), 2000)
			.SetNotNegative()
			.SetDisplay("Short Take Profit", "Take profit in price steps for shorts", "Risk Management");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, LongCandleType);

		if (!Equals(LongCandleType, ShortCandleType))
			yield return (Security, ShortCandleType);
	}

	protected override void OnReseted()
	{
		base.OnReseted();

		_longCmo = null;
		_shortCmo = null;
		_entryPrice = null;
		_longValues.Clear();
		_shortValues.Clear();
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_longCmo = new ChandeMomentumOscillator { Length = LongCmoPeriod };
		_shortCmo = new ChandeMomentumOscillator { Length = ShortCmoPeriod };

		var longSubscription = SubscribeCandles(LongCandleType);
		longSubscription.Bind(_longCmo, ProcessLongCandle);

		if (Equals(LongCandleType, ShortCandleType))
		{
			longSubscription.Bind(_shortCmo, ProcessShortCandle).Start();
		}
		else
		{
			longSubscription.Start();
			var shortSubscription = SubscribeCandles(ShortCandleType);
			shortSubscription.Bind(_shortCmo, ProcessShortCandle).Start();
		}

		// no fixed protection needed
	}

	private void ProcessLongCandle(ICandleMessage candle, decimal cmoValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_longCmo == null || !_longCmo.IsFormed)
			return;

		_longValues.Add(cmoValue);
		var shift = Math.Max(1, LongSignalBar);
		TrimBuffer(_longValues, shift + 3);

		if (_longValues.Count < shift + 1)
			return;

		var currentIndex = _longValues.Count - shift;
		var previousIndex = currentIndex - 1;
		if (previousIndex < 0)
			return;

		var current = _longValues[currentIndex];
		var previous = _longValues[previousIndex];

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (Position > 0 && _entryPrice is decimal entryPrice)
		{
			var step = Security?.PriceStep ?? 1m;
			var stopPrice = LongStopLossPoints > 0 ? entryPrice - LongStopLossPoints * step : (decimal?)null;
			var takePrice = LongTakeProfitPoints > 0 ? entryPrice + LongTakeProfitPoints * step : (decimal?)null;
			var exitBySignal = EnableLongExits && previous < 0m;

			if ((takePrice.HasValue && candle.HighPrice >= takePrice.Value) ||
				(stopPrice.HasValue && candle.LowPrice <= stopPrice.Value) ||
				exitBySignal)
			{
				SellMarket();
				_entryPrice = null;
			}
		}

		var crossDown = previous > 0m && current <= 0m;
		if (EnableLongEntries && crossDown && Position <= 0)
		{
			if (true)
			{
				BuyMarket();
				_entryPrice = candle.ClosePrice;
			}
		}
	}

	private void ProcessShortCandle(ICandleMessage candle, decimal cmoValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_shortCmo == null || !_shortCmo.IsFormed)
			return;

		_shortValues.Add(cmoValue);
		var shift = Math.Max(1, ShortSignalBar);
		TrimBuffer(_shortValues, shift + 3);

		if (_shortValues.Count < shift + 1)
			return;

		var currentIndex = _shortValues.Count - shift;
		var previousIndex = currentIndex - 1;
		if (previousIndex < 0)
			return;

		var current = _shortValues[currentIndex];
		var previous = _shortValues[previousIndex];

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (Position < 0 && _entryPrice is decimal entryPrice)
		{
			var step = Security?.PriceStep ?? 1m;
			var stopPrice = ShortStopLossPoints > 0 ? entryPrice + ShortStopLossPoints * step : (decimal?)null;
			var takePrice = ShortTakeProfitPoints > 0 ? entryPrice - ShortTakeProfitPoints * step : (decimal?)null;
			var exitBySignal = EnableShortExits && previous > 0m;

			if ((takePrice.HasValue && candle.LowPrice <= takePrice.Value) ||
				(stopPrice.HasValue && candle.HighPrice >= stopPrice.Value) ||
				exitBySignal)
			{
				BuyMarket();
				_entryPrice = null;
			}
		}

		var crossUp = previous < 0m && current >= 0m;
		if (EnableShortEntries && crossUp && Position >= 0)
		{
			if (true)
			{
				SellMarket();
				_entryPrice = candle.ClosePrice;
			}
		}
	}

	private static void TrimBuffer(List<decimal> values, int maxCount)
	{
		if (values.Count <= maxCount)
			return;

		values.RemoveRange(0, values.Count - maxCount);
	}
}