GitHub で見る

E-フライデーセッション戦略

概要

E フライデー セッション戦略は、金曜日のみ取引する古典的な MetaTrader エキスパート アドバイザーを再現しています。前回の日足のローソク足を観察し、金曜日のセッション開始時に設定された時間にポジションをオープンします。方向は逆張りです。前日の終値が始値 (弱気のローソク足) を下回った場合、戦略は買いです。前日の終値が始値(強気のローソク足)を上回った場合、戦略は売りになります。ポジションは日中に管理され、設定可能な時間の経過後に自動的にクローズするか、保護ストップによってクローズすることができます。

取引ルール

  1. 毎日のローソク足 (デフォルト: 1 日) を収集して、前日の始値と終値を取得します。
  2. 金曜日に、日中のローソク足 (デフォルト: 1 分) を監視して、設定されたエントリー時間を検出します。
  3. 入場時間の最初のローソク足で:
    • 前日が弱気だった場合はロングにします。
    • 前日が強気だった場合はショートする。
    • 前日が童子(始値と終値が等しい)だった場合は取引をスキップします。
  4. オプションで、設定された終了時間に達するとポジションを自動的にクローズします。
  5. 利益のアクティベーションやトレーリング ステップのしきい値を含む、オリジナルの Expert Advisor を模倣するストップロス、テイクプロフィット、およびオプションのトレーリング ストップ ロジックを使用してエグジットを管理します。

実装メモ

  • 毎日のコンテキストと日中のタイミングの両方に、StockSharp の高レベルのキャンドル サブスクリプションを使用します。
  • 証券の価格ステップを使用して、ポイントベースのリスク管理を MQL バージョンから絶対価格オフセットに変換します。
  • コード内でトレーリング ストップを維持し、終了したローソク足ごとにトレーリング ストップを更新し、価格の極端な値を突破したときにポジションを閉じます。
  • 毎日の状態を追跡することで、金曜日に 1 回の取引のみを保証します。
  • ロングエントリーとショートエントリーの両方をサポートし、ストラテジーインスタンスごとに単一のシンボルを取引することで元のマジックナンバーゲーティングを尊重します。

パラメーター

名前 説明 デフォルト
Volume ロット/契約における取引サイズ。 0.1
StopLossPoints 価格ステップのストップロス距離 (0 無効)。 75
TakeProfitPoints 価格ステップでの利食い距離 (0 無効)。 0
HourOpen ポジションをオープンする時刻 (0 ~ 23)。 7
UseClosePositions 終了時間後の自動閉鎖を有効にします。 true
HourClose 有効な場合、ポジションを決済する時刻 (0 ~ 23)。 19
UseTrailing トレーリングストップ調整を有効にします。 true
ProfitTrailing トレーリングがアクティブになる前に、利益がトレーリング距離を超える必要があります。 true
TrailingStopPoints 価格ステップでのトレーリングストップ距離。 60
TrailingStepPoints トレーリングストップを強める前に追加ポイントが必要。 5
IntradayCandleType 日中のタイミングのローソク足タイプ (デフォルトは 1 分ローソク足)。 TimeSpan.FromMinutes(1)
DailyCandleType 毎日のセンチメント検出用のローソクのタイプ (デフォルトは 1 日のローソク)。 TimeSpan.FromDays(1)

使用のヒント

  • 金曜日のエントリー時間が希望する市場の開始時間と一致するように商品の取引セッションを調整します。
  • ストップロスとトレーリングの値を設定するときは、シンボルの価格ステップで使用されるのと同じ「ポイント」で表現して、MetaTrader の動作を再現します。
  • この戦略は金曜日に 1 回の取引を行うように設計されています。複数のシンボルを取引するには、シンボルごとに個別の戦略インスタンスを実行します。

オリジナルの EA との違い

  • 意思決定にはローソク足の終値データが使用されますが、ティックごとの元のポーリング価格が使用されます。
  • プロテクティブ・エグジットは、ローソク足がインターバル内にストップまたはターゲットレベルに触れたことを示したときに、成行注文を通じて実行されます。
  • 戦略パラメータは、StockSharp の StrategyParam システムを通じて公開され、最適化と UI バインディングをサポートします。

約定判定の補足: 金曜日のエントリーは、始値の時刻が HourOpen に属する最初の確定済み分足で判定します。時刻の最初のティックでの判定ではありません。確定済みの木曜日の日足が必要です。保護決済後も含め、金曜日の新規エントリーは一度だけです。時刻決済、ストップ、利確、トレーリングは確定した分足の高値・安値を使い成行で決済します。トレーリングは指定幅を満たしたときだけ更新し、同一足内の過去の値動きによる約定を仮定しません。

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Takes at most one contrarian position on Friday at the configured hour,
/// using the previous completed daily candle; manages session and price exits.
/// </summary>
public class EFridaySessionStrategy : Strategy
{
	private readonly StrategyParam<int> _hourOpen;
	private readonly StrategyParam<int> _hourClose;
	private readonly StrategyParam<bool> _useClosePositions;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<bool> _useTrailing;
	private readonly StrategyParam<bool> _profitTrailing;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<DataType> _intradayCandleType;
	private readonly StrategyParam<DataType> _dailyCandleType;

	private DateTime? _lastDailyDate;
	private decimal _dailyOpen;
	private decimal _dailyClose;
	private DateTime? _tradedDate;
	private decimal? _entryPrice;
	private decimal? _trailingLevel;
	private decimal _bestPrice;
	private Order _entryOrder;
	private Order _exitOrder;

	public int HourOpen { get => _hourOpen.Value; set => _hourOpen.Value = value; }
	public int HourClose { get => _hourClose.Value; set => _hourClose.Value = value; }
	public bool UseClosePositions { get => _useClosePositions.Value; set => _useClosePositions.Value = value; }
	public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public decimal TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
	public bool UseTrailing { get => _useTrailing.Value; set => _useTrailing.Value = value; }
	public bool ProfitTrailing { get => _profitTrailing.Value; set => _profitTrailing.Value = value; }
	public decimal TrailingStopPoints { get => _trailingStopPoints.Value; set => _trailingStopPoints.Value = value; }
	public decimal TrailingStepPoints { get => _trailingStepPoints.Value; set => _trailingStepPoints.Value = value; }
	public DataType IntradayCandleType { get => _intradayCandleType.Value; set => _intradayCandleType.Value = value; }
	public DataType DailyCandleType { get => _dailyCandleType.Value; set => _dailyCandleType.Value = value; }

	public EFridaySessionStrategy()
	{
		Volume = 0.1m;
		_hourOpen = Param(nameof(HourOpen), 7).SetRange(0, 23);
		_hourClose = Param(nameof(HourClose), 19).SetRange(0, 23);
		_useClosePositions = Param(nameof(UseClosePositions), true);
		_stopLossPoints = Param(nameof(StopLossPoints), 75m).SetNotNegative();
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0m).SetNotNegative();
		_useTrailing = Param(nameof(UseTrailing), true);
		_profitTrailing = Param(nameof(ProfitTrailing), true);
		_trailingStopPoints = Param(nameof(TrailingStopPoints), 60m).SetNotNegative();
		_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m).SetNotNegative();
		_intradayCandleType = Param(nameof(IntradayCandleType), TimeSpan.FromMinutes(1).TimeFrame());
		_dailyCandleType = Param(nameof(DailyCandleType), TimeSpan.FromDays(1).TimeFrame());
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, IntradayCandleType), (Security, DailyCandleType)];

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

	private void ClearState()
	{
		_lastDailyDate = null;
		_dailyOpen = 0m;
		_dailyClose = 0m;
		_tradedDate = null;
		_entryPrice = null;
		_trailingLevel = null;
		_bestPrice = 0m;
		_entryOrder = null;
		_exitOrder = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		ClearState();
		if ((Security?.PriceStep ?? 0m) <= 0m)
			throw new InvalidOperationException("E-Friday requires a positive PriceStep.");
		SubscribeCandles(DailyCandleType).Bind(ProcessDailyCandle).Start();
		SubscribeCandles(IntradayCandleType).Bind(ProcessIntradayCandle).Start();
	}

	private void ProcessDailyCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;
		_lastDailyDate = candle.OpenTime.Date;
		_dailyOpen = candle.OpenPrice;
		_dailyClose = candle.ClosePrice;
	}

	private static bool IsPending(Order order) => order is not null &&
		order.State is not (OrderStates.Done or OrderStates.Failed);

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

		if (Position != 0m && !IsPending(_exitOrder))
		{
			if (UseClosePositions && candle.OpenTime.Hour >= HourClose)
			{
				ExitPosition("EFriday session close");
				return;
			}
			if (HandleRiskExits(candle))
				return;
		}

		var day = candle.OpenTime.Date;
		if (candle.OpenTime.DayOfWeek != DayOfWeek.Friday ||
			candle.OpenTime.Hour != HourOpen || _tradedDate == day ||
			_lastDailyDate != day.AddDays(-1) ||
			Position != 0m || IsPending(_entryOrder) || IsPending(_exitOrder))
			return;

		if (_dailyClose == _dailyOpen)
			return;

		var side = _dailyClose < _dailyOpen ? Sides.Buy : Sides.Sell;
		var volume = Volume;
		if (Security?.VolumeStep is decimal step && step > 0m)
			volume = decimal.Floor(volume / step) * step;
		if (Security?.MinVolume is decimal min && min > 0m)
			volume = Math.Max(volume, min);
		if (Security?.MaxVolume is decimal max && max > 0m)
			volume = Math.Min(volume, max);
		if (volume <= 0m)
			return;

		_entryOrder = Market(side, volume, "EFriday entry");
		_tradedDate = day;
	}

	private bool HandleRiskExits(ICandleMessage candle)
	{
		if (_entryPrice is not decimal entry)
			return false;
		var point = Security?.PriceStep ?? 0m;
		var longPosition = Position > 0m;

		var stop = StopLossPoints * point;
		var take = TakeProfitPoints * point;
		if (stop > 0m && (longPosition ? candle.LowPrice <= entry - stop :
			candle.HighPrice >= entry + stop))
		{
			ExitPosition("EFriday stop loss");
			return true;
		}
		if (take > 0m && (longPosition ? candle.HighPrice >= entry + take :
			candle.LowPrice <= entry - take))
		{
			ExitPosition("EFriday take profit");
			return true;
		}

		// Check the previously established trailing level before using the
		// latest bar's extremes: intrabar high/low order is not available.
		if (_trailingLevel is decimal oldStop &&
			(longPosition ? candle.LowPrice <= oldStop : candle.HighPrice >= oldStop))
		{
			ExitPosition("EFriday trailing stop");
			return true;
		}

		if (!UseTrailing || TrailingStopPoints <= 0m)
			return false;

		var distance = TrailingStopPoints * point;
		var improvement = TrailingStepPoints * point;
		_bestPrice = longPosition ? Math.Max(_bestPrice, candle.HighPrice) :
			Math.Min(_bestPrice, candle.LowPrice);
		var profit = longPosition ? _bestPrice - entry : entry - _bestPrice;
		if (ProfitTrailing && profit < distance)
			return false;

		var next = longPosition ? _bestPrice - distance : _bestPrice + distance;
		if (_trailingLevel is not decimal current ||
			(longPosition ? next >= current + improvement && next > current :
				next <= current - improvement && next < current))
			_trailingLevel = next;

		return false;
	}

	private Order Market(Sides side, decimal volume, string comment)
	{
		var order = new Order
		{
			Security = Security,
			Portfolio = Portfolio,
			Type = OrderTypes.Market,
			Side = side,
			Volume = volume,
			Comment = comment,
		};
		if (comment == "EFriday entry")
			_entryOrder = order;
		else
			_exitOrder = order;
		RegisterOrder(order);
		return order;
	}

	private void ExitPosition(string comment)
	{
		if (Position == 0m || IsPending(_exitOrder))
			return;
		_exitOrder = Market(Position > 0m ? Sides.Sell : Sides.Buy, Math.Abs(Position), comment);
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade?.Order == _entryOrder && trade.Trade is not null)
		{
			_entryPrice = trade.Trade.Price;
			_bestPrice = _entryPrice.Value;
			_trailingLevel = null;
		}
		else if (Position == 0m)
		{
			_entryPrice = null;
			_trailingLevel = null;
			_bestPrice = 0m;
			_exitOrder = null;
		}
	}
}