GitHub で見る

パーセンテージ・クロスオーバー戦略

この戦略は、MetaTraderのオリジナルエキスパート Exp_PercentageCrossover の動作を再現します。Percentage Crossoverインジケーターの方向に従って取引します。このインジケーターは、現在の終値を中心とした固定パーセンテージバンド内でのみ動くことができるトレイリング価格ラインを描画します。このラインの傾きが市場の状態を定義し、取引をトリガーします。

コンセプト

  1. 完成した各ローソク足において、インジケーターは前のライン値を保持します。
  2. 終値がトレイリングラインをその前回値より少なくとも percent パーセント上に押し上げると、強気の更新が行われます。
  3. 終値がトレイリングラインをその前回値より同じパーセント下に引き下げると、弱気の更新が行われます。
  4. 終値がバンド内に留まると、ラインは横ばいを維持し最後の色を保持します。

ラインの色はMetaTraderと同様に解釈されます:

  • カラーインデックス 0(青/紫) – ラインが上昇中(強気コンテキスト)。
  • カラーインデックス 1(オレンジ) – ラインが下降中(弱気コンテキスト)。

トレードルール

ロングエントリー

  • BuyPosOpen = true の場合のみ有効。
  • SignalBar で選択されたバーを評価します(1は最後に閉じたバーを意味する)。
  • そのバーが色1から色0に切り替わったときにロングポジションを開きます。

ショートエントリー

  • SellPosOpen = true の場合のみ有効。
  • 同じ SignalBar バーを評価します。
  • バーが色0から色1に切り替わったときにショートポジションを開きます。

ポジション管理

  • BuyPosClose = true の場合、現在のバー(SignalBar オフセットを適用後)が色1のとき、オープンロングポジションはいつでも閉じられます。
  • SellPosClose = true の場合、そのバーが色0のとき、オープンショートポジションはいつでも閉じられます。
  • UseTimeFilter = true で現在時刻が設定された取引ウィンドウ外にある場合、戦略はすぐにアクティブポジションを終了し、市場がウィンドウに再入するまで新しいシグナルを無視します。
  • 注文は BuyMarket()SellMarket() で送信されます。実際の数量は戦略の Volume プロパティから取得されます。

パラメーター

パラメーター 説明 デフォルト
Percent トレイリングラインのパーセンテージバンド。値が高いほどラインの反応が遅くなります。 1
SignalBar 分析する閉じたバー(1 = 最後に閉じたバー)。正の値を維持する必要があります。 1
BuyPosOpen / SellPosOpen それぞれロングまたはショートエントリーを有効にします。 true
BuyPosClose / SellPosClose ロングまたはショートポジションのクローズロジックを有効にします。 true
UseTimeFilter 取引ウィンドウをアクティブ化します。 true
StartHour / StartMinute フィルターがアクティブなときに取引ウィンドウを開く時間と分。 0 / 0
EndHour / EndMinute 取引ウィンドウを閉じる時間と分。 23 / 59
CandleType インジケーターとシグナルに使用するローソク足の時間軸。 4h

注意事項

  • タイムフィルターはオリジナルのエキスパートアドバイザーに厳密に従います。開始時刻が終了時刻より大きい場合、ロジックは一晩ウィンドウを作成しますが、セッションがアクティブになる前に分が StartMinute 以上である必要があります。
  • SignalBar は完成したローソク足のみで評価されます。デフォルトのMetaTrader設定を反映するには 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>
/// Strategy based on the Percentage Crossover indicator.
/// </summary>
public class PercentageCrossoverStrategy : Strategy
{
	private readonly StrategyParam<bool> _buyPosOpen;
	private readonly StrategyParam<bool> _sellPosOpen;
	private readonly StrategyParam<bool> _buyPosClose;
	private readonly StrategyParam<bool> _sellPosClose;
	private readonly StrategyParam<bool> _useTimeFilter;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _startMinute;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<int> _endMinute;
	private readonly StrategyParam<decimal> _percent;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<DataType> _candleType;

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

	private decimal? _previousMiddle;
	private int? _lastColor;

	public bool BuyPosOpen
	{
		get => _buyPosOpen.Value;
		set => _buyPosOpen.Value = value;
	}

	public bool SellPosOpen
	{
		get => _sellPosOpen.Value;
		set => _sellPosOpen.Value = value;
	}

	public bool BuyPosClose
	{
		get => _buyPosClose.Value;
		set => _buyPosClose.Value = value;
	}

	public bool SellPosClose
	{
		get => _sellPosClose.Value;
		set => _sellPosClose.Value = value;
	}

	public bool UseTimeFilter
	{
		get => _useTimeFilter.Value;
		set => _useTimeFilter.Value = value;
	}

	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	public int StartMinute
	{
		get => _startMinute.Value;
		set => _startMinute.Value = value;
	}

	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	public int EndMinute
	{
		get => _endMinute.Value;
		set => _endMinute.Value = value;
	}

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

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

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

	public PercentageCrossoverStrategy()
	{
		_buyPosOpen = Param(nameof(BuyPosOpen), true)
			.SetDisplay("Enable Buy Entries", "Allow opening long positions", "General");

		_sellPosOpen = Param(nameof(SellPosOpen), true)
			.SetDisplay("Enable Sell Entries", "Allow opening short positions", "General");

		_buyPosClose = Param(nameof(BuyPosClose), true)
			.SetDisplay("Enable Buy Exits", "Allow closing long positions", "General");

		_sellPosClose = Param(nameof(SellPosClose), true)
			.SetDisplay("Enable Sell Exits", "Allow closing short positions", "General");

		_useTimeFilter = Param(nameof(UseTimeFilter), true)
			.SetDisplay("Use Time Filter", "Restrict trading to specific hours", "Time Filter");

		_startHour = Param(nameof(StartHour), 0)
			.SetDisplay("Start Hour", "Trading window start hour", "Time Filter");

		_startMinute = Param(nameof(StartMinute), 0)
			.SetDisplay("Start Minute", "Trading window start minute", "Time Filter");

		_endHour = Param(nameof(EndHour), 23)
			.SetDisplay("End Hour", "Trading window end hour", "Time Filter");

		_endMinute = Param(nameof(EndMinute), 59)
			.SetDisplay("End Minute", "Trading window end minute", "Time Filter");

		_percent = Param(nameof(Percent), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Percent", "Percentage offset for the indicator", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
			.SetGreaterThanZero()
			.SetDisplay("Signal Bar", "Closed bars to look back for the signal", "Indicator");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for signal candles", "Data");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_colorHistory.Clear();
		_previousMiddle = null;
		_lastColor = null;
	}

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

		_colorHistory.Clear();
		_previousMiddle = null;
		_lastColor = null;

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

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var close = candle.ClosePrice;
		var percentFactor = Percent / 100m;

		if (_previousMiddle is null)
		{
			_previousMiddle = close;
			_lastColor = 0;
			_colorHistory.Clear();
			_colorHistory.Add(0);
			return;
		}

		var previousMiddle = _previousMiddle.Value;
		var lowerBoundary = close * (1 - percentFactor);
		var upperBoundary = close * (1 + percentFactor);

		var middle = previousMiddle;

		if (lowerBoundary > previousMiddle)
			middle = lowerBoundary;
		else if (upperBoundary < previousMiddle)
			middle = upperBoundary;

		var color = _lastColor ?? 0;

		if (middle > previousMiddle)
			color = 0;
		else if (middle < previousMiddle)
			color = 1;

		_previousMiddle = middle;
		_lastColor = color;

		_colorHistory.Add(color);
		var maxSize = Math.Max(SignalBar + 2, 4);
		while (_colorHistory.Count > maxSize)
		{
			try { _colorHistory.RemoveAt(0); }
			catch { break; }
		}

		var currentIndex = _colorHistory.Count - SignalBar;
		if (currentIndex <= 0)
			return;

		var previousIndex = currentIndex - 1;
		if (previousIndex < 0)
			return;

		var currentColor = _colorHistory[currentIndex];
		var previousColor = _colorHistory[previousIndex];

		var buyOpen = BuyPosOpen && currentColor == 0 && previousColor == 1;
		var sellOpen = SellPosOpen && currentColor == 1 && previousColor == 0;
		var buyClose = BuyPosClose && currentColor == 1;
		var sellClose = SellPosClose && currentColor == 0;

		var inTradingWindow = !UseTimeFilter || IsTradingTime(candle.CloseTime);

		if (UseTimeFilter && !inTradingWindow)
		{
			if (Position > 0)
				SellMarket();
			else if (Position < 0)
				BuyMarket();

			return;
		}

		if (buyClose && Position > 0)
			SellMarket();

		if (sellClose && Position < 0)
			BuyMarket();

		if (!inTradingWindow)
			return;

		if (buyOpen && Position <= 0)
			BuyMarket();
		else if (sellOpen && Position >= 0)
			SellMarket();
	}

	private bool IsTradingTime(DateTimeOffset time)
	{
		var hour = time.Hour;
		var minute = time.Minute;

		if (StartHour < EndHour)
		{
			if (hour == StartHour && minute >= StartMinute)
				return true;

			if (hour > StartHour && hour < EndHour)
				return true;

			if (hour > StartHour && hour == EndHour && minute < EndMinute)
				return true;

			return false;
		}

		if (StartHour == EndHour)
		{
			return hour == StartHour && minute >= StartMinute && minute < EndMinute;
		}

		if (hour >= StartHour && minute >= StartMinute)
			return true;

		if (hour < EndHour)
			return true;

		if (hour == EndHour && minute < EndMinute)
			return true;

		return false;
	}
}