Auf GitHub ansehen

CMO Duplex Strategy

The strategy is a StockSharp port of the MetaTrader 5 expert Exp_CMO_Duplex.mq5. It splits the logic into two independent legs (long and short) that both react to zero-line crossovers of the Chande Momentum Oscillator (CMO). Each leg can consume its own candle series, period and signal offset, which makes it possible to run asymmetric configurations on the same instrument.

How it works

  • The strategy subscribes to one or two candle feeds depending on whether the long and short legs use the same DataType.
  • Every leg owns its own CMO indicator instance. The indicator is evaluated on finished candles only.
  • The SignalBar setting defines how many completed candles back in history should be used for the crossover logic. A value of 0 means "use the most recent closed bar", 1 uses the previous bar, 2 uses the bar before that, and so on.
  • Long leg: when the selected CMO value crosses from above zero to zero or below, the strategy enters (or flips into) a long position if long entries are allowed. Long exits are triggered when the older value of the CMO is below zero or when the stop loss / take profit levels are touched.
  • Short leg: mirrors the long logic. A crossover from below zero to zero or above opens (or flips into) a short position and the opposite sign of the CMO value or the configured stops flat the position.
  • Position flips reuse Volume plus any opposite exposure, therefore a single market order both closes the previous position and opens the new one.
  • StartProtection() is enabled on launch, so the built-in StockSharp risk controls remain active.

Parameters

Parameter Description
LongCandleType Candle type used by the long leg.
LongCmoPeriod Period of the CMO indicator on the long side.
LongSignalBar Number of closed bars between the current time and the bar analysed for signals (0 = latest closed bar).
EnableLongEntries Allows or blocks opening new long positions.
EnableLongExits Allows or blocks closing long positions on oscillator signals.
LongStopLossPoints Stop-loss distance in price steps for long trades (0 disables the stop).
LongTakeProfitPoints Take-profit distance in price steps for long trades (0 disables the target).
ShortCandleType Candle type used by the short leg.
ShortCmoPeriod Period of the CMO indicator on the short side.
ShortSignalBar Number of closed bars between the current time and the bar analysed for short signals.
EnableShortEntries Allows or blocks opening new short positions.
EnableShortExits Allows or blocks closing short positions on oscillator signals.
ShortStopLossPoints Stop-loss distance in price steps for short trades (0 disables the stop).
ShortTakeProfitPoints Take-profit distance in price steps for short trades (0 disables the target).

The base Strategy.Volume property controls the default order size. When the strategy needs to flip direction it sends a market order whose volume equals Volume + |Position|, which closes the old exposure and opens the new one in a single transaction.

Risk management

  • Stop-loss and take-profit levels are evaluated on every finished candle. For long positions the stop is placed below the entry and the target above it; for short positions the levels are mirrored.
  • A stop or target triggers an immediate market order to flat the position. The same exit routine also runs when the respective oscillator value keeps the wrong sign (below zero for longs, above zero for shorts).
  • Setting the distance to zero disables the corresponding protection and leaves the leg managed purely by the oscillator logic.

Usage notes

  • The strategy works best on instruments where the CMO tends to revert after touching the zero line. Contrarian entries are deliberately delayed by the SignalBar offset to match the original expert.
  • Long and short legs can share the same candle feed or operate on different timeframes. If both use the same DataType, the strategy reuses a single subscription for better performance.
  • Because the strategy operates on completed candles, it is recommended to supply a continuous candle stream (for example via a historical backtest or real-time feed) to avoid missing signals.
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);
	}
}