GitHub で見る

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

概要

パーセンテージクロスオーバーチャネル戦略は、MetaTrader 5のエキスパートアドバイザー Percentage_Crossover_Channel_EA に由来します。高速移動平均を中心に構築されたカスタムチャネルに依存し、バンドタッチまたは中央線クロスオーバーに反応します。このStockSharp実装は、ハイレベルAPIを使用して完了済みローソク足を処理しながら同じロジックに従います。

チャネルの構築

基礎となるインジケーターは、選択された価格 (デフォルトでは終値) を中心に動的チャネルを構築します:

  1. 設定された Applied Price モードを使用してベース価格を計算する。
  2. 1期間の単純移動平均を適用して短期参照価格を得る。
  3. Percent パラメーターを使用して2つの境界を計算する (例:50 → ±0.5%)。
  4. 新しい境界内に前の中央線を制限して現在の中央値を得る。
  5. 上下のバンドは制限された中央値に ±パーセント係数を掛けたものです。

この再帰により、価格が強いトレンド中にチャネルが遅れる一方、価格が統合するときには密接なエンベロープを維持できます。

トレーディングロジック

2つの異なるシグナルモードが利用可能です:

  • バンドタッチモード (デフォルト):
    • 前のローソク足の安値が下限バンドを上回っており、最後に完了したローソク足がそれに触れるか貫通する場合にロングエントリー。
    • 前のローソク足の高値が上限バンドを下回っており、最後に完了したローソク足がそれに触れるか貫通する場合にショートエントリー。
  • 中央線クロスオーバーモード (TradeOnMiddleCross = true):
    • 価格が中央線を上から下へクロスするときにロングエントリー。
    • 価格が中央線を下から上へクロスするときにショートエントリー。

ReverseSignals フラグはロングとショートのルールを入れ替えます。戦略は常に既存のポジションを閉じて反転させ、設定された OrderVolume と現在のポジションの絶対値の合計のボリュームで単一の成行注文を送信します。

リスク管理

オプションの保護レベルは元のMT5のストップロスとテイクプロフィット設定をエミュレートします:

  • StopLossPoints – 推定エントリー価格から差し引かれる (ロング) またはそれに追加される (ショート) 価格ステップでの距離。
  • TakeProfitPoints – エントリー価格に追加される (ロング) またはそこから差し引かれる (ショート) 価格ステップでの距離。

いずれかのパラメーターがゼロの場合、対応する保護は無効になります。ストップは、ローソク足の高値と安値を保存されたレベルと比較することで、完了した各ローソク足で評価されます。トレーリングロジックは適用されません。

パラメーター

パラメーター 説明
CandleType サブスクライブするローソク足データタイプ (デフォルトは15分の時間軸)。
Percent 価格のパーセントでのチャネル幅 (±パーセント/100係数に変換)。
PriceMode チャネルに使用する適用価格。オプション:Close、Open、High、Low、Median (H+L)/2、Typical (H+L+C)/3、Weighted (H+L+2C)/4、Average (O+H+L+C)/4。
TradeOnMiddleCross バンドタッチロジックと中央線クロスオーバーロジックを切り替える。
ReverseSignals ロングとショートの条件を反転させる。
StopLossPoints 証券の価格ステップで表現された保護ストップ距離。
TakeProfitPoints 証券の価格ステップで表現された利益目標距離。
OrderVolume 成行エントリーのベースボリューム。戦略は1つのトランザクションで反転するために絶対オープンポジションを追加します。

実装メモ

  • 注文はローソク足が終了した後のみ発行されます。これは前のバーのデータを使って次のバーの開始時に行動したMT5エキスパートを反映しています。
  • チャネルインジケーターは履歴コレクションを保存せずに戦略内で再作成され、スカラー状態変数に依存しています。
  • 保護ストップとターゲットは手動で確認され、MT5からのプラットフォーム固有の注文処理を再現します。
  • 選択したインストゥルメントが有効な PriceStep を公開していることを確認してください。そうでない場合、ストップロスとテイクプロフィットの距離は無視されます。
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 strategy converted from MetaTrader 5.
/// </summary>
public class PercentageCrossoverChannelStrategy : Strategy
{
	public enum PercentageChannelPriceModes
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted,
		Average
	}

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _percent;
	private readonly StrategyParam<PercentageChannelPriceModes> _priceMode;
	private readonly StrategyParam<bool> _tradeOnMiddleCross;
	private readonly StrategyParam<bool> _reverseSignals;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<decimal> _orderVolume;

	// Cached indicator values for the previous two finished candles.
	private decimal? _prevUpper;
	private decimal? _prevMiddle;
	private decimal? _prevLower;
	private decimal? _prevPrevUpper;
	private decimal? _prevPrevMiddle;
	private decimal? _prevPrevLower;

	// Stored price data for signal evaluation.
	private decimal? _prevClose;
	private decimal? _prevHigh;
	private decimal? _prevLow;
	private decimal? _prevPrevClose;
	private decimal? _prevPrevHigh;
	private decimal? _prevPrevLow;

	// Internal state of the channel middle line recursion.
	private decimal _lastMiddle;
	private bool _hasIndicatorState;

	// Protective levels that mimic MT5 stop loss and take profit requests.
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal _entryPrice;

	public PercentageCrossoverChannelStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for processing", "General");

		_percent = Param(nameof(Percent), 1m)
			.SetDisplay("Percent", "Channel width percent", "Channel")
			
			.SetGreaterThanZero();

		_priceMode = Param(nameof(PriceMode), PercentageChannelPriceModes.Close)
			.SetDisplay("Applied Price", "Price source for channel calculations", "Channel");

		_tradeOnMiddleCross = Param(nameof(TradeOnMiddleCross), false)
			.SetDisplay("Trade Middle Cross", "Use middle line crossovers instead of band touches", "Signals");

		_reverseSignals = Param(nameof(ReverseSignals), false)
			.SetDisplay("Reverse Signals", "Invert long and short logic", "Signals");

		_stopLossPoints = Param(nameof(StopLossPoints), 0)
			.SetDisplay("Stop Loss (points)", "Protective stop distance in points", "Risk")
			.SetNotNegative();

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0)
			.SetDisplay("Take Profit (points)", "Target profit distance in points", "Risk")
			.SetNotNegative();

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetDisplay("Order Volume", "Base volume for market entries", "Trading")
			.SetGreaterThanZero();
	}

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

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

	public PercentageChannelPriceModes PriceMode
	{
		get => _priceMode.Value;
		set => _priceMode.Value = value;
	}

	public bool TradeOnMiddleCross
	{
		get => _tradeOnMiddleCross.Value;
		set => _tradeOnMiddleCross.Value = value;
	}

	public bool ReverseSignals
	{
		get => _reverseSignals.Value;
		set => _reverseSignals.Value = value;
	}

	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

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

		_prevUpper = null;
		_prevMiddle = null;
		_prevLower = null;
		_prevPrevUpper = null;
		_prevPrevMiddle = null;
		_prevPrevLower = null;

		_prevClose = null;
		_prevHigh = null;
		_prevLow = null;
		_prevPrevClose = null;
		_prevPrevHigh = null;
		_prevPrevLow = null;

		_lastMiddle = 0m;
		_hasIndicatorState = false;

		ResetProtection();
	}

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

		Volume = OrderVolume;

		// Subscribe to candle updates that will drive the high level logic.
		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Work only with completed candles to stay consistent with the MT5 implementation.
		if (candle.State != CandleStates.Finished)
			return;

		var exitTriggered = CheckProtection(candle);

		if (!exitTriggered)
			TryEnterPositions(candle);

		UpdateChannelState(candle);
	}

	private void TryEnterPositions(ICandleMessage candle)
	{
		// Wait until the channel has valid values for two completed candles.
		if (!_prevLower.HasValue || !_prevPrevLower.HasValue)
			return;

		if (!_prevClose.HasValue || !_prevPrevClose.HasValue || !_prevHigh.HasValue || !_prevPrevHigh.HasValue || !_prevLow.HasValue || !_prevPrevLow.HasValue)
			return;

		var openLong = false;
		var openShort = false;

		if (TradeOnMiddleCross)
		{
			// Evaluate crossovers of the price and the middle channel line.
			var crossDown = _prevPrevClose.Value > _prevPrevMiddle.Value && _prevClose.Value < _prevMiddle.Value;
			var crossUp = _prevPrevClose.Value < _prevPrevMiddle.Value && _prevClose.Value > _prevMiddle.Value;

			if (!ReverseSignals)
			{
				if (crossDown)
					openLong = true;

				if (crossUp)
					openShort = true;
			}
			else
			{
				if (crossDown)
					openShort = true;

				if (crossUp)
					openLong = true;
			}
		}
		else
		{
			// Default mode trades touches of the outer channel boundaries.
			var touchLower = _prevPrevLow.Value > _prevPrevLower.Value && _prevLow.Value <= _prevLower.Value;
			var touchUpper = _prevPrevHigh.Value < _prevPrevUpper.Value && _prevHigh.Value >= _prevUpper.Value;

			if (!ReverseSignals)
			{
				if (touchLower)
					openLong = true;

				if (touchUpper)
					openShort = true;
			}
			else
			{
				if (touchLower)
					openShort = true;

				if (touchUpper)
					openLong = true;
			}
		}

		if (openLong)
		{
			EnterLong(candle);
		}
		else if (openShort)
		{
			EnterShort(candle);
		}
	}

	private void EnterLong(ICandleMessage candle)
	{
		// Combine base order volume with the size required to flatten shorts.
		var volume = OrderVolume + (Position < 0 ? Math.Abs(Position) : 0m);
		if (volume <= 0m)
			return;

		BuyMarket(volume);

		_entryPrice = candle.OpenPrice;
		_stopPrice = CalculateStopPrice(Sides.Buy, _entryPrice);
		_takePrice = CalculateTakePrice(Sides.Buy, _entryPrice);
	}

	private void EnterShort(ICandleMessage candle)
	{
		// Combine base order volume with the size required to flatten longs.
		var volume = OrderVolume + (Position > 0 ? Position : 0m);
		if (volume <= 0m)
			return;

		SellMarket(volume);

		_entryPrice = candle.OpenPrice;
		_stopPrice = CalculateStopPrice(Sides.Sell, _entryPrice);
		_takePrice = CalculateTakePrice(Sides.Sell, _entryPrice);
	}

	private bool CheckProtection(ICandleMessage candle)
	{
		// Emulate MT5 protective stop and take profit that were attached to market orders.
		if (Position > 0)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetProtection();
				return true;
			}

			if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetProtection();
				return true;
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetProtection();
				return true;
			}

			if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetProtection();
				return true;
			}
		}
		else
		{
			ResetProtection();
		}

		return false;
	}

	private void UpdateChannelState(ICandleMessage candle)
	{
		// Recreate the Percentage Crossover Channel middle line recursion.
		var percent = Percent <= 0m ? 0.001m : Percent;
		var plusFactor = 1m + percent / 100m;
		var minusFactor = 1m - percent / 100m;
		var price = GetAppliedPrice(candle);

		decimal currentMiddle;
		if (!_hasIndicatorState)
		{
			currentMiddle = price;
			_hasIndicatorState = true;
		}
		else
		{
			var lowerBound = price * minusFactor;
			var upperBound = price * plusFactor;
			var previousMiddle = _lastMiddle;

			currentMiddle = previousMiddle;

			if (lowerBound > previousMiddle)
				currentMiddle = lowerBound;
			else if (upperBound < previousMiddle)
				currentMiddle = upperBound;
		}

		var currentUpper = currentMiddle * plusFactor;
		var currentLower = currentMiddle * minusFactor;

		if (_prevUpper.HasValue)
		{
			_prevPrevUpper = _prevUpper;
			_prevPrevMiddle = _prevMiddle;
			_prevPrevLower = _prevLower;
			_prevPrevClose = _prevClose;
			_prevPrevHigh = _prevHigh;
			_prevPrevLow = _prevLow;
		}

		_prevUpper = currentUpper;
		_prevMiddle = currentMiddle;
		_prevLower = currentLower;
		_prevClose = candle.ClosePrice;
		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
		_lastMiddle = currentMiddle;
	}

	private decimal GetAppliedPrice(ICandleMessage candle)
	{
		// Convert the selected price mode into a candle value.
		return PriceMode switch
		{
			PercentageChannelPriceModes.Open => candle.OpenPrice,
			PercentageChannelPriceModes.High => candle.HighPrice,
			PercentageChannelPriceModes.Low => candle.LowPrice,
			PercentageChannelPriceModes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			PercentageChannelPriceModes.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			PercentageChannelPriceModes.Weighted => (candle.HighPrice + candle.LowPrice + (2m * candle.ClosePrice)) / 4m,
			PercentageChannelPriceModes.Average => (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m,
			_ => candle.ClosePrice,
		};
	}

	private decimal? CalculateStopPrice(Sides side, decimal entryPrice)
	{
		if (StopLossPoints <= 0)
			return null;

		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return null;

		var offset = StopLossPoints * step;
		return side == Sides.Buy ? entryPrice - offset : entryPrice + offset;
	}

	private decimal? CalculateTakePrice(Sides side, decimal entryPrice)
	{
		if (TakeProfitPoints <= 0)
			return null;

		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return null;

		var offset = TakeProfitPoints * step;
		return side == Sides.Buy ? entryPrice + offset : entryPrice - offset;
	}

	private void ResetProtection()
	{
		_stopPrice = null;
		_takePrice = null;
		_entryPrice = 0m;
	}
}