GitHub で見る

CMO デュプレックス戦略

この戦略はMetaTrader 5エキスパートExp_CMO_Duplex.mq5のStockSharp移植版です。ロジックを2つの独立した レグ(ロングとショート)に分割し、どちらもChande Momentumオシレーター(CMO)のゼロライン クロスオーバーに反応します。各レグは独自のローソク足シリーズ、期間、シグナルオフセットを使用でき、 同じインスツルメントで非対称な設定を実行することが可能です。

仕組み

  • 戦略はロングとショートのレグが同じDataTypeを使用するかどうかに応じて、1つまたは2つのローソク足フィードをサブスクライブします。
  • 各レグは独自のCMOインジケーターインスタンスを持ちます。インジケーターは完成したローソク足のみで評価されます。
  • SignalBar設定は、クロスオーバーロジックに使用するべき履歴の完成したローソク足の数を定義します。値0は 「最新のクローズドバーを使用」を意味し、1は前のバーを使用し、2はその前のバーを使用し、以下同様です。
  • ロングレグ: 選択されたCMO値がゼロより上からゼロまたは以下にクロスすると、ロングエントリーが許可されている場合に ロングポジションにエントリー(またはフリップ)します。古いCMO値がゼロ以下の場合、またはストップロス/テイクプロフィットレベルが タッチされるとロング決済が発動します。
  • ショートレグ: ロングロジックを反映します。ゼロより下からゼロまたは上へのクロスオーバーがショートポジションをオープン (またはフリップ)し、CMO値の逆符号または設定されたストップがポジションをフラットにします。
  • ポジションフリップはVolumeプラス任意の反対エクスポージャーを再利用するため、単一の市場注文で以前のポジションを閉じて 新しいポジションを開きます。
  • StartProtection()は起動時に有効になり、StockSharpの組み込みリスクコントロールがアクティブのまま維持されます。

パラメーター

パラメーター 説明
LongCandleType ロングレグが使用するローソク足タイプ。
LongCmoPeriod ロング側のCMOインジケーターの期間。
LongSignalBar シグナルに分析されるバーと現在時刻の間のクローズドバー数(0 = 最新クローズドバー)。
EnableLongEntries 新しいロングポジションのオープンを許可または禁止します。
EnableLongExits オシレーターシグナルでのロングポジションのクローズを許可または禁止します。
LongStopLossPoints ロングトレードの価格ステップ単位でのストップロス距離(0でストップ無効)。
LongTakeProfitPoints ロングトレードの価格ステップ単位でのテイクプロフィット距離(0でターゲット無効)。
ShortCandleType ショートレグが使用するローソク足タイプ。
ShortCmoPeriod ショート側のCMOインジケーターの期間。
ShortSignalBar ショートシグナルに分析されるバーと現在時刻の間のクローズドバー数。
EnableShortEntries 新しいショートポジションのオープンを許可または禁止します。
EnableShortExits オシレーターシグナルでのショートポジションのクローズを許可または禁止します。
ShortStopLossPoints ショートトレードの価格ステップ単位でのストップロス距離(0でストップ無効)。
ShortTakeProfitPoints ショートトレードの価格ステップ単位でのテイクプロフィット距離(0でターゲット無効)。

ベースのStrategy.Volumeプロパティはデフォルトの注文サイズを制御します。戦略が方向を切り替える必要がある場合、 Volume + |Position|に等しいボリュームの市場注文を送信し、これにより古いエクスポージャーを閉じて単一トランザクションで 新しいポジションをオープンします。

リスク管理

  • ストップロスとテイクプロフィットレベルはすべての完成したローソク足で評価されます。ロングポジションではストップはエントリーより下に 配置され、ターゲットは上に;ショートポジションではレベルが反転します。
  • ストップまたはターゲットはポジションをフラットにするための即時市場注文を発動します。それぞれの オシレーター値が誤った符号(ロングではゼロ以下、ショートではゼロ以上)を維持する場合も同じ決済ルーチンが実行されます。
  • 距離をゼロに設定すると対応する保護が無効になり、レグは純粋にオシレーターロジックによって管理されます。

使用上の注意

  • 戦略はCMOがゼロラインを触れた後に反転する傾向があるインスツルメントで最も効果的です。逆張りエントリーは 元のエキスパートに合わせてSignalBarオフセットによって意図的に遅延されます。
  • ロングとショートのレグは同じローソク足フィードを共有するか、異なる時間軸で動作できます。両方が同じDataTypeを使用する場合、 戦略はより良いパフォーマンスのために単一のサブスクリプションを再利用します。
  • 戦略は完成したローソク足で動作するため、シグナルの欠損を避けるために連続したローソク足ストリーム(例えば ヒストリカルバックテストまたはリアルタイムフィード)を提供することが推奨されます。
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);
	}
}