GitHub で見る

ナイト・フラット・トレード戦略

ナイト・フラット・トレード戦略はEURUSDのH1ローソク足で夜間の狭いレンジを探す古典的なMQL5エキスパートアドバイザーを再現します。取引日の変わり目の前後の時間帯に注目し、狭いコンソリデーションチャネルの端にリターンする価格を待ち、ブレイクアウトの継続に賭けます。StockSharpバージョンはより優れた設定性のために高レベルのローソク足サブスクリプション、インジケーターバインディング、パラメーターオブジェクトに依存しながらオリジナルのアイデアをそのまま維持しています。

概要

  • 市場とタイムフレーム: EURUSDのH1タイムフレーム向けに設計されていますが、明確に定義された価格ステップを持つ任意のインストゥルメントを使用できます。
  • セッションウィンドウ: エントリーは設定されたOpenHourから始まりOpenHour + 1(取引所時間)に終わる2時間のウィンドウ内のみ許可されます。
  • レンジフィルター: 最後の3つの完成したローソク足の高低スパンがDiffMinPipsDiffMaxPips(価格単位に変換)の間に留まる必要があります。
  • 方向性: 最後の終値がクオリファイングレンジ内のどこに位置するかによって、ロングのみまたはショートのみ。

トレードロジック

  1. レンジバウンダリを計算する

    • 戦略は組み込みのHighestLowestインジケーター(長さ = 3)にバインドして、最後の3つのローソク足にわたる最高値と最安値を取得します。
    • これらの境界間の距離がその後のすべてのチェックに使用される作業レンジです。
  2. エントリー条件

    • ロングセットアップ: アクティブセッション中に、終値がレンジの安値より上だが下の四半期の内側(lowest + range/4)にある場合、戦略はlowest - range/3での初期保護ストップでロングポジションを開きます。
    • ショートセットアップ: 対称的に、終値がレンジの高値より下だが上の四半期の内側(highest - range/4)にある場合、highest + range/3でのストップでショートポジションが開かれます。
  3. エグジット管理

    • ストップロス: ストップは内部でシミュレートされ、次のローソク足が保存された閾値に違反したときに成行エグジットをトリガーします。
    • テイクプロフィット: TakeProfitPips > 0のとき、エントリー価格に対する追加の固定テイクプロフィットレベル(pips)が作成されます。
    • トレーリングストップ: TrailingStopPipsTrailingStepPipsの両方が正の場合、トレードの有利な方向に価格がTrailingStop + TrailingStep pips進んだ後にのみストップが引き締められます。その後の調整はオリジナルのステップワイズトレーリング動作を反映するために追加のTrailingStepPipsの進歩が必要です。
  4. 再エントリー制御

    • アルゴリズムは参照エキスパートアドバイザーのように取引間でシステムをフラットに保つため、新しいシグナルを探す前に常に現在のポジションが完全に閉じられるのを待ちます。

パラメーター

パラメーター 説明 デフォルト値
CandleType サブスクライブするローソク足シリーズ(デフォルトH1)。 1時間ローソク足
TakeProfitPips オプションのpipsでのテイクプロフィット距離。 50
TrailingStopPips pipsでの価格とトレーリングストップ間の距離(0でトレーリング無効)。 15
TrailingStepPips 各トレーリングストップ更新前に必要な追加pips。 5
DiffMinPips 最小許容3ローソク足レンジ(pips)。 18
DiffMaxPips 最大許容3ローソク足レンジ(pips)。 28
OpenHour 取引所時間でのセッション開始時間(OpenHour + 1までエントリー許可)。 0

インジケーター

  • Highest(Length = 3) 最近のレンジ上限を監視するため。
  • Lowest(Length = 3) 最近のレンジ下限を監視するため。

実装上の注意

  • pips変換は、オリジナルのMQ5実装とまったく同様に、3または5桁の小数点を持つ商品に自動的に適応し、報告された価格ステップに10を掛けます。
  • StockSharpはこのサンプルで完成したローソク足で動作するため、ローソク足内のエントリー条件は終値を使用して近似されます。これにより、ソースコードの意図に忠実でありながらロジックを決定論的に保ちます。
  • すべてのリスクパラメーターはStrategyParam<T>オブジェクトを通じて公開されており、UIに表示され、最適化やバッチ実験に対応できます。
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>
/// Night session flat trading strategy that enters near range extremes.
/// </summary>
public class NightFlatTradeStrategy : Strategy
{
	private readonly StrategyParam<int> _rangeLength;

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _diffMinPips;
	private readonly StrategyParam<decimal> _diffMaxPips;
	private readonly StrategyParam<int> _openHour;

	private Highest _highest = null!;
	private Lowest _lowest = null!;

	private decimal _pipSize;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

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

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	public decimal DiffMinPips
	{
		get => _diffMinPips.Value;
		set => _diffMinPips.Value = value;
	}

	public decimal DiffMaxPips
	{
		get => _diffMaxPips.Value;
		set => _diffMaxPips.Value = value;
	}

	public int OpenHour
	{
		get => _openHour.Value;
		set => _openHour.Value = value;
	}

	/// <summary>
	/// Number of candles used to form the overnight range.
	/// </summary>
	public int RangeLength
	{
		get => _rangeLength.Value;
		set => _rangeLength.Value = value;
	}

	public NightFlatTradeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for the setup", "General");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetRange(0m, 500m)
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 15m)
			.SetRange(0m, 200m)
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetRange(0m, 200m)
			.SetDisplay("Trailing Step (pips)", "Extra advance required to shift the trailing stop", "Risk");

		_diffMinPips = Param(nameof(DiffMinPips), 18m)
			.SetGreaterThanZero()
			.SetDisplay("Min Range (pips)", "Minimum three-candle range in pips", "Setup");

		_diffMaxPips = Param(nameof(DiffMaxPips), 28m)
			.SetGreaterThanZero()
			.SetDisplay("Max Range (pips)", "Maximum three-candle range in pips", "Setup");

		_openHour = Param(nameof(OpenHour), 0)
			.SetRange(0, 23)
			.SetDisplay("Open Hour", "Hour (exchange time) when entries become active", "Schedule");

		_rangeLength = Param(nameof(RangeLength), 3)
			.SetGreaterThanZero()
			.SetDisplay("Range Length", "Number of candles composing the range", "Setup");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_highest = null!;
		_lowest = null!;
		_pipSize = 0m;
		_entryPrice = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

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

		_highest = new Highest { Length = RangeLength };
		_lowest = new Lowest { Length = RangeLength };

		var priceStep = Security?.PriceStep ?? 0m;
		var decimals = Security?.Decimals;

		if (priceStep <= 0m)
			priceStep = 0.0001m;

		_pipSize = priceStep;

		if (decimals.HasValue && (decimals.Value == 3 || decimals.Value == 5))
			_pipSize *= 10m;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_highest, _lowest, ProcessCandle)
			.Start();

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

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

		// Manage active trades before scanning for new setups.
		HandleExistingPosition(candle);

		if (Position != 0m)
			return;

		if (_highest == null || _lowest == null)
			return;

		if (!_highest.IsFormed || !_lowest.IsFormed)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var diff = highestValue - lowestValue;
		if (diff <= 0m)
			return;

		var quarter = diff / 4m;
		var closePrice = candle.ClosePrice;

		if (closePrice > lowestValue && closePrice <= lowestValue + quarter)
		{
			BuyMarket();
			_entryPrice = closePrice;
			_stopPrice = lowestValue - diff / 3m;
			_takeProfitPrice = TakeProfitPips > 0m ? closePrice + ToPrice(TakeProfitPips) : null;
			return;
		}

		if (closePrice < highestValue && closePrice >= highestValue - quarter)
		{
			SellMarket();
			_entryPrice = closePrice;
			_stopPrice = highestValue + diff / 3m;
			_takeProfitPrice = TakeProfitPips > 0m ? closePrice - ToPrice(TakeProfitPips) : null;
		}
	}

	private void HandleExistingPosition(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			UpdateTrailingForLong(candle);

			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
		else if (Position < 0m)
		{
			UpdateTrailingForShort(candle);

			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _stopPrice == null)
			return;

		var trailingDistance = ToPrice(TrailingStopPips);
		var stepDistance = ToPrice(TrailingStepPips);

		var advance = candle.HighPrice - _entryPrice;
		if (advance < trailingDistance + stepDistance)
			return;

		var newStop = candle.HighPrice - trailingDistance;

		if (newStop <= _stopPrice.Value || newStop - _stopPrice.Value < stepDistance)
			return;

		// Raise the stop only after price travels an additional step distance.
		_stopPrice = newStop;
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _stopPrice == null)
			return;

		var trailingDistance = ToPrice(TrailingStopPips);
		var stepDistance = ToPrice(TrailingStepPips);

		var advance = _entryPrice - candle.LowPrice;
		if (advance < trailingDistance + stepDistance)
			return;

		var newStop = candle.LowPrice + trailingDistance;

		if (newStop >= _stopPrice.Value || _stopPrice.Value - newStop < stepDistance)
			return;

		// Lower the stop only after price moves the additional step distance in favor of the trade.
		_stopPrice = newStop;
	}

	private decimal ToPrice(decimal pips)
	{
		if (pips <= 0m)
			return 0m;

		var pip = _pipSize > 0m ? _pipSize : 0.0001m;
		return pips * pip;
	}

	private void ResetTradeState()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
	}
}