GitHub で見る

パーセンテージ・クロスオーバー・チャンネル・システム戦略

この戦略はMetaTraderのエキスパートアドバイザー Exp_PercentageCrossoverChannel_System の直接ポートです。カスタムの「Percentage Crossover Channel」との価格の相互作用を追跡し、ローソク足が以前のブレイクアウト後にチャンネルに戻ってきたときに反応します。コードはStockSharpの高レベルAPIで書き直され、オリジナルのシグナルフローが保持されています。

トレードロジック

  1. インジケーターの構築

    • Percentage Crossover Channelは、価格に近く留まるが固定パーセンテージ(Percent)より速くドリフトできない適応型中線を構築します。
    • 上部と下部のバンドは同じパーセンテージ距離を使用して中線から導出されます。
    • 完成した各ローソク足は Shift 本前のチャンネルとの関係に応じて色付けされます:
      • 3 / 4:上部バンドより上でのクローズ(それぞれ弱気/強気のローソク足ボディ)。
      • 0 / 1:下部バンドより下でのクローズ(弱気/強気のボディ)。
      • 2:ローソク足がチャンネル内で終了。
  2. エントリーとエグジットのルール

    • 最後の SignalBar ローソク足とその直前のものを評価します(MQLの CopyBuffer 呼び出しを反映)。
    • 強気シーケンス (olderColor > 2):市場が最近チャンネルより上でクローズしました。最新のローソク足が内部に戻った場合(recentColor < 3)戦略は:
      • SellPositionsClose が有効な場合、アクティブなショートを閉じます。
      • 取引が開かれておらず BuyPositionsOpen が有効な場合、ロングポジションを開きます。
    • 弱気シーケンス (olderColor < 2):市場が最近チャンネルより下でクローズしました。最新のローソク足が内部に戻った場合(recentColor > 1)戦略は:
      • BuyPositionsClose が有効な場合、ロングを閉じます。
      • 取引がアクティブでなく SellPositionsOpen が有効な場合、ショートを開きます。
    • ロジックはしたがって、ブレイクアウト方向にコミットする前にブレイクアウトとそれに続くチャンネルへの再エントリーを待ちます。
  3. リスク管理

    • オプションのストップロスとテイクプロフィットは価格ステップで表され、ローソク足の高値/安値で評価されます。
    • 保護的な注文がトリガーされると、戦略は市場を離れ、同じバーに対する新しいエントリーを無視します。これはブローカー側のストップが最初に取引を閉じるMQLの動作を模倣します。

パラメーター

名前 説明
Percent チャンネル幅(パーセント)。MQLインジケーターの入力に対応します。
Shift ブレイクアウトと歴史的なバンドを比較するために使用するバーの数。
SignalBar シグナル評価のためのオフセット(バー単位)。値1はオリジナルEAのデフォルトと同様に「前のバー」を意味します。
BuyPositionsOpen / SellPositionsOpen 対応する方向への取引の開始を有効または無効にします。
BuyPositionsClose / SellPositionsClose 新しいシグナル時に反対ポジションを強制閉鎖することを有効または無効にします。
StopLoss Security.PriceStep の倍数で表されるストップロス距離。ゼロに設定すると無効になります。
TakeProfit 価格ステップ単位のテイクプロフィット距離。ゼロに設定すると無効になります。
CandleType ローソク足サブスクリプションの時間軸。PERIOD_H4 を反映するために4時間足がデフォルトです。

実装に関する注意事項

  • StockSharpはネイティブのPercentage Crossover Channelを提供していないため、インジケーターロジックはインラインで実装されています。中線計算、バンド導出、色割り当てはMQLソースアルゴリズムをステップバイステップで再現します。
  • ポジション管理はオリジナルのヘルパー関数(BuyPositionOpenSellPositionOpenなど)に従い、新しいものを開く前に反対の取引を閉じて、反対のポジションがまだある場合はエントリーをスキップします。
  • オリジナルのincludeファイルからのマネーマネジメント、偏差処理、マージンモード固有のロットサイジングは複製されていません。StockSharpユーザーは標準の Strategy プロパティまたはホスティング環境を通じて戦略ボリュームを設定する必要があります。
  • ストップロス/テイクプロフィットの値はMetaTraderの入力がポイントで指定されているため、価格ステップとして解釈されます。接続した銘柄が有効な PriceStep を公開していることを確認してください。

使用のヒント

  • MetaTraderと同一の動作を希望する場合は、信頼性の高い4時間足データを持つ楽器に戦略を接続してください。CandleType を調整してイントラデイ操作を実験できます。
  • エントリーロジックは有効な色情報を持つ2本の完成したローソク足を必要とするため、少なくとも Shift + SignalBar + 1 本のバーの履歴でウォームアップさせてください。
  • チャンネルは Percent 入力に敏感です。小さな値は価格に近くフィットして取引頻度を増やし、大きな値はより強いブレイクアウトに集中します。
  • ポートフォリオレベルのリスク管理と組み合わせる場合、この実装は一度に最大1つのポジションを開き、ロング、フラット、またはショート状態の間を切り替えることに注意してください。
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>
/// Percentage Crossover Channel breakout system translated from MQL.
/// </summary>
public class PercentageCrossoverChannelSystemStrategy : Strategy
{
	private readonly StrategyParam<decimal> _percent;
	private readonly StrategyParam<int> _shift;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<bool> _buyOpen;
	private readonly StrategyParam<bool> _sellOpen;
	private readonly StrategyParam<bool> _buyClose;
	private readonly StrategyParam<bool> _sellClose;
	private readonly StrategyParam<int> _stopLoss;
	private readonly StrategyParam<int> _takeProfit;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<int> _colorHistory = new();
	private readonly List<decimal> _upperHistory = new();
	private readonly List<decimal> _lowerHistory = new();

	private decimal _previousMiddle;
	private bool _hasMiddle;
	private decimal? _entryPrice;

	public decimal Percent
	{
		get => _percent.Value;
		set => _percent.Value = value;
	}

	public int Shift
	{
		get => _shift.Value;
		set => _shift.Value = value;
	}

	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	public bool BuyPositionsOpen
	{
		get => _buyOpen.Value;
		set => _buyOpen.Value = value;
	}

	public bool SellPositionsOpen
	{
		get => _sellOpen.Value;
		set => _sellOpen.Value = value;
	}

	public bool BuyPositionsClose
	{
		get => _buyClose.Value;
		set => _buyClose.Value = value;
	}

	public bool SellPositionsClose
	{
		get => _sellClose.Value;
		set => _sellClose.Value = value;
	}

	public int StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	public int TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public PercentageCrossoverChannelSystemStrategy()
	{
		_percent = Param(nameof(Percent), 1.0m)
			.SetGreaterThanZero()
			.SetDisplay("Channel Percent", "Percentage width of the channel", "Indicator");

		_shift = Param(nameof(Shift), 1)
			.SetGreaterThanZero()
			.SetDisplay("Shift", "Number of bars used for crossover comparison", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
			.SetGreaterThanZero()
			.SetDisplay("Signal Bar", "Bars back to evaluate indicator colors", "Trading Rules");

		_buyOpen = Param(nameof(BuyPositionsOpen), true)
			.SetDisplay("Enable Long Entries", "Allow long position openings", "Trading Rules");

		_sellOpen = Param(nameof(SellPositionsOpen), true)
			.SetDisplay("Enable Short Entries", "Allow short position openings", "Trading Rules");

		_buyClose = Param(nameof(BuyPositionsClose), true)
			.SetDisplay("Allow Long Exits", "Permit closing long trades on bearish signals", "Trading Rules");

		_sellClose = Param(nameof(SellPositionsClose), true)
			.SetDisplay("Allow Short Exits", "Permit closing short trades on bullish signals", "Trading Rules");

		_stopLoss = Param(nameof(StopLoss), 1000)
			.SetDisplay("Stop Loss (steps)", "Protective stop loss distance in price steps", "Risk Management");

		_takeProfit = Param(nameof(TakeProfit), 2000)
			.SetDisplay("Take Profit (steps)", "Target profit distance in price steps", "Risk Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for analysis", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_colorHistory.Clear();
		_upperHistory.Clear();
		_lowerHistory.Clear();
		_hasMiddle = false;
		_previousMiddle = 0m;
		_entryPrice = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Ignore interim updates; we only react on closed candles.
		if (candle.State != CandleStates.Finished)
			return;

		// Evaluate protective orders before generating new signals.
		var stopTriggered = HandleRisk(candle);

		// Mirror the MQL signal logic using cached indicator colors.
		if (_colorHistory.Count > SignalBar)
		{
			// Equivalent to CopyBuffer(..., SignalBar, 2, ...) from the EA.
			var recentIndex = _colorHistory.Count - SignalBar;
			var olderIndex = recentIndex - 1;

			if (olderIndex >= 0)
			{
				var recentColor = _colorHistory[recentIndex];
				var olderColor = _colorHistory[olderIndex];

				var shouldCloseShort = SellPositionsClose && olderColor > 2;
				var shouldCloseLong = BuyPositionsClose && olderColor < 2;
				var shouldOpenBuy = BuyPositionsOpen && olderColor > 2 && recentColor < 3;
				var shouldOpenSell = SellPositionsOpen && olderColor < 2 && recentColor > 1;

				// Close existing positions according to the original toggles.
				if (shouldCloseLong && Position > 0)
				{
					SellMarket();
					_entryPrice = null;
				}

				if (shouldCloseShort && Position < 0)
				{
					BuyMarket();
					_entryPrice = null;
				}

				// Enter only when we are flat to match the EA behaviour.
				if (!stopTriggered && Position == 0)
				{
					if (shouldOpenBuy)
					{
						BuyMarket();
						_entryPrice = candle.ClosePrice;
					}
					else if (shouldOpenSell)
					{
						SellMarket();
						_entryPrice = candle.ClosePrice;
					}
				}
			}
		}

		// Update indicator state after trading decisions are made.
		var color = CalculateColor(candle);

		_colorHistory.Add(color);
		TrimHistory();
	}

	private bool HandleRisk(ICandleMessage candle)
	{
		// Exit early if there is no stored entry price.
		if (_entryPrice is null)
			return false;

		// Price step is required to translate MQL points into absolute prices.
		if (Security?.PriceStep is not decimal step || step <= 0)
			return false;

		var triggered = false;

		if (Position > 0)
		{
			// Long position risk checks.
			if (StopLoss > 0)
			{
				var stopLevel = _entryPrice.Value - StopLoss * step;
				if (candle.LowPrice <= stopLevel)
				{
					SellMarket();
					_entryPrice = null;
					triggered = true;
				}
			}

			if (!triggered && TakeProfit > 0)
			{
				var takeLevel = _entryPrice.Value + TakeProfit * step;
				if (candle.HighPrice >= takeLevel)
				{
					SellMarket();
					_entryPrice = null;
					triggered = true;
				}
			}
		}
		else if (Position < 0)
		{
			// Short position risk checks.
			if (StopLoss > 0)
			{
				var stopLevel = _entryPrice.Value + StopLoss * step;
				if (candle.HighPrice >= stopLevel)
				{
					BuyMarket();
					_entryPrice = null;
					triggered = true;
				}
			}

			if (!triggered && TakeProfit > 0)
			{
				var takeLevel = _entryPrice.Value - TakeProfit * step;
				if (candle.LowPrice <= takeLevel)
				{
					BuyMarket();
					_entryPrice = null;
					triggered = true;
				}
			}
		}

		// Reset cached entry price once we are flat.
		if (Position == 0)
			_entryPrice = null;

		return triggered;
	}

	private int CalculateColor(ICandleMessage candle)
	{
		// Recreate the Percentage Crossover Channel midline and colour logic.
		var percentFactor = Percent / 100m;
		var plusVar = 1m + percentFactor;
		var minusVar = 1m - percentFactor;
		var close = candle.ClosePrice;

		// Initialise the midline on the very first candle.
		if (!_hasMiddle)
		{
			_previousMiddle = close;
			_hasMiddle = true;
		}

		var middle = _previousMiddle;
		var lowerCandidate = close * minusVar;
		var upperCandidate = close * plusVar;

		// Adjust the midline exactly as in the original indicator.
		if (lowerCandidate > _previousMiddle)
		{
			middle = lowerCandidate;
		}
		else if (upperCandidate < _previousMiddle)
		{
			middle = upperCandidate;
		}

		var upper = middle + middle * percentFactor;
		var lower = middle - middle * percentFactor;

		_previousMiddle = middle;

		var color = 2;

		// Determine candle colour relative to past channel values.
		if (_upperHistory.Count >= Shift)
		{
			var referenceIndex = _upperHistory.Count - Shift;
			var referenceUpper = _upperHistory[referenceIndex];
			var referenceLower = _lowerHistory[referenceIndex];

			if (close > referenceUpper)
			{
				color = candle.OpenPrice <= close ? 4 : 3;
			}
			else if (close < referenceLower)
			{
				color = candle.OpenPrice > close ? 0 : 1;
			}
		}

		// Persist channel history for future signal checks.
		_upperHistory.Add(upper);
		_lowerHistory.Add(lower);

		return color;
	}

	private void TrimHistory()
	{
		// Keep only as much history as needed for Shift and SignalBar lookbacks.
		var maxCapacity = Math.Max(Shift + SignalBar + 5, 16);
		if (_colorHistory.Count <= maxCapacity)
			return;

		var removeCount = _colorHistory.Count - maxCapacity;
		for (var i = 0; i < removeCount; i++)
		{
			try
			{
				_colorHistory.RemoveAt(0);
				_upperHistory.RemoveAt(0);
				_lowerHistory.RemoveAt(0);
			}
			catch { break; }
		}
	}
}