GitHub で見る

月曜日の典型的なブレイクアウト戦略

概要

月曜日の典型的なブレイクアウト戦略 は、MetaTrader エキスパート アドバイザー yi1ywioff50qr6 (リポジトリ ID 8187) の C# ポートです。オリジナルのロボットは時間足のローソク足を監視し、毎週月曜日に新しいセッションが前の足の典型的な価格 (high + low + close) / 3 を上回ったときにロングポジションをオープンします。この実装は、StockSharp の高レベル戦略フレームワーク内のエントリー ロジックを再現し、ポジションのサイジングとリスク管理のための詳細な構成パラメーターを追加します。

取引ロジック

  1. この戦略は、設定されたローソク足シリーズ (デフォルトでは時間ごと) をサブスクライブします。
  2. 完成した各キャンドルの開始時に、次のことがチェックされます。
    • ろうそくは月曜日のものです。
    • キャンドルの開始時間は、設定された Open Hour パラメーター (デフォルトは 09:00) と一致します。
    • 未決済のポジションやアクティブな注文は存在しません。
    • ローソク足の始値は、前のバーの通常の価格よりも高くなります。
  3. すべての条件が満たされると、ストラテジーは資金管理ブロックによって計算された数量で成行買い注文を送信します。保護的なストップロスとテイクプロフィットディスタンスは、StartProtection を通じて適用されます。

この戦略はショート ポジションをオープンすることはなく、対象となる月曜日のローソク足ごとに 1 回の取引のみを行います。

パラメーター

パラメータ 説明 デフォルト
FixedVolume エントリーのロットサイズ。株式スケーリング テーブルを有効にするには、0 に設定します。 0.1
OpenHour 月曜日のシグナルが評価される取引セッション時間 (0 ~ 23)。 9
StopLossPoints 保護停止の価格ポイントの距離。 0 は停止を無効にします。 50
TakeProfitPoints 利益目標の価格点の距離。 0 はターゲットを無効にします。 20
InitialEquity 株式ベースのロットスケーリングを有効にする株式しきい値。 600
EquityStep 取引規模を拡大するには資本の増加が必要です。 300
InitialStepVolume 資本が少なくとも InitialEquity の場合に使用されるロットサイズ。 0.4
VolumeStep EquityStep に達するごとに追加のロット サイズが追加されます。 0.2
CandleType 戦略を推進するローソク足のデータ型 (デフォルトでは時間単位)。 1 hour time-frame

資金管理

  • FixedVolume がゼロより大きい場合、戦略は常に固定ロット サイズを使用します。
  • FixedVolume がゼロに等しい場合、戦略はポートフォリオの資本を検査します。
    • 資本が InitialEquity を下回る場合、商品の最小ロットが使用されます。
    • それ以外の場合、ボリュームは InitialStepVolume から始まり、追加の株式が EquityStep ごとに VolumeStep ずつ増加します。
    • 最終的なボリュームは、楽器の最小値とステップの制約に合わせて調整されます。

リスク管理

StartProtectionOnStarted 中にアクティブ化されます。ストップロスとテイクプロフィットの距離は、商品 PriceStep を使用してポイントから価格オフセットに自動的に変換されます。そのコンポーネントを無効にするには、いずれかの距離をゼロに設定します。

使用上の注意

  • オリジナルの EA は時間足のローソク足用に設計されています。より低い時間枠では、同じ時間の複数の月曜日のローソク足が生成される可能性があります。ポートはキャンドルごとに 1 つのエントリーの動作を維持し、ポジションがオープンしている間は追加のシグナルを無視します。
  • 動的サイジング ブロックが有効になっている場合は、ポートフォリオ情報 (Portfolio.CurrentValue) が利用可能であることを確認してください。
  • この戦略には、成行注文を実行するためのレベル 1 データと、設定された CandleType の対応するキャンドル サブスクリプションが必要です。

変換メモ

  • MQL のマジックナンバー フィルタリングは、StockSharp の位置と順序のチェック (Position および ActiveOrders) に置き換えられます。
  • 時間比較では、ローソク足の始値からの DateTimeOffset.ToLocalTime() を利用して、チャートの時間と一致させます。
  • 保護注文は、手動注文ではなく、高レベルの StartProtection ヘルパーによって処理されます。
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;

using StockSharp.Algo;
using StockSharp.Algo.Candles;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Port of the MetaTrader strategy yi1ywioff50qr6 (ID 8187).
/// Buys on Monday when the hourly open breaks above the prior bar's typical price.
/// Applies equity-based position sizing when the fixed lot is disabled.
/// </summary>
public class MondayTypicalBreakoutStrategy : Strategy
{
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<int> _openHour;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<decimal> _initialEquity;
	private readonly StrategyParam<decimal> _equityStep;
	private readonly StrategyParam<decimal> _initialStepVolume;
	private readonly StrategyParam<decimal> _volumeStep;
	private readonly StrategyParam<DataType> _candleType;

	private ICandleMessage _previousCandle;
	private DateTimeOffset? _lastSignalTime;
	private decimal _priceStep;

	/// <summary>
	/// Initializes parameters to mirror the MQL expert defaults.
	/// </summary>
	public MondayTypicalBreakoutStrategy()
	{
		_fixedVolume = Param(nameof(FixedVolume), 0.1m)
		.SetNotNegative()
		.SetDisplay("Fixed Volume", "Lot size used for entries (set to 0 to enable equity scaling)", "Risk");

		_openHour = Param(nameof(OpenHour), 9)
		.SetRange(0, 23)
		.SetDisplay("Open Hour", "Hour of the session to evaluate Monday breakout entries", "Session");

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

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 20)
		.SetNotNegative()
		.SetDisplay("Take Profit (points)", "Profit target distance expressed in price points", "Risk");

		_initialEquity = Param(nameof(InitialEquity), 600m)
		.SetGreaterThanZero()
		.SetDisplay("Initial Equity", "Account equity threshold that triggers the first scaling tier", "Money Management");

		_equityStep = Param(nameof(EquityStep), 300m)
		.SetGreaterThanZero()
		.SetDisplay("Equity Step", "Incremental equity required to raise the position size", "Money Management");

		_initialStepVolume = Param(nameof(InitialStepVolume), 0.4m)
		.SetGreaterThanZero()
		.SetDisplay("Initial Step Volume", "Lot size used once the equity threshold is met", "Money Management");

		_volumeStep = Param(nameof(VolumeStep), 0.2m)
		.SetNotNegative()
		.SetDisplay("Volume Step", "Additional lot size added for each equity step", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe used for detecting the Monday breakout", "General");
	}

	/// <summary>
	/// Fixed lot size used for entries (set to zero to enable scaling).
	/// </summary>
	public decimal FixedVolume
	{
		get => _fixedVolume.Value;
		set => _fixedVolume.Value = value;
	}

	/// <summary>
	/// Hour (0-23) when Monday entries are evaluated.
	/// </summary>
	public int OpenHour
	{
		get => _openHour.Value;
		set => _openHour.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in price points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in price points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Minimum equity required before the scaling table becomes active.
	/// </summary>
	public decimal InitialEquity
	{
		get => _initialEquity.Value;
		set => _initialEquity.Value = value;
	}

	/// <summary>
	/// Equity increment that increases the trade size by <see cref="VolumeStep"/>.
	/// </summary>
	public decimal EquityStep
	{
		get => _equityStep.Value;
		set => _equityStep.Value = value;
	}

	/// <summary>
	/// Volume applied when the first equity threshold is met.
	/// </summary>
	public decimal InitialStepVolume
	{
		get => _initialStepVolume.Value;
		set => _initialStepVolume.Value = value;
	}

	/// <summary>
	/// Additional volume added for each equity tier.
	/// </summary>
	public decimal VolumeStep
	{
		get => _volumeStep.Value;
		set => _volumeStep.Value = value;
	}

	/// <summary>
	/// Candle type used for detecting breakout conditions.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_previousCandle = null;
		_lastSignalTime = null;
		_priceStep = 0m;
	}

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

		_priceStep = Security?.PriceStep ?? 0m;
		if (_priceStep <= 0m)
		_priceStep = 0.0001m;

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

		if (TakeProfitPoints > 0 || StopLossPoints > 0)
		{
			var takeDistance = TakeProfitPoints > 0
			? new Unit(TakeProfitPoints * _priceStep, UnitTypes.Absolute)
			: new Unit(0m);
			var stopDistance = StopLossPoints > 0
			? new Unit(StopLossPoints * _priceStep, UnitTypes.Absolute)
			: new Unit(0m);

			StartProtection(takeProfit: takeDistance, stopLoss: stopDistance);
		}
	}

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

		var previous = _previousCandle;
		_previousCandle = candle;

		if (previous is null)
		return;

		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		if (Position != 0m)
		return;

		var candleTime = candle.OpenTime.ToLocalTime();
		if (candleTime.DayOfWeek != DayOfWeek.Monday)
		return;

		if (candleTime.Hour != OpenHour)
		return;

		if (_lastSignalTime is DateTimeOffset last && last == candle.OpenTime)
		return;

		var typicalPrice = (previous.HighPrice + previous.LowPrice + previous.ClosePrice) / 3m;
		if (candle.OpenPrice <= typicalPrice)
		return;

		var volume = CalculateOrderVolume();
		volume = AlignVolume(volume);

		if (volume <= 0m)
		return;

		BuyMarket(volume);

		_lastSignalTime = candle.OpenTime;
	}

	private decimal CalculateOrderVolume()
	{
		var fixedVolume = FixedVolume;
		if (fixedVolume > 0m)
		return fixedVolume;

		var security = Security;
		var portfolio = Portfolio;

		var minVolume = security?.MinVolume ?? 0m;
		if (minVolume <= 0m)
		minVolume = 0.01m;

		var equity = portfolio?.CurrentValue ?? portfolio?.BeginValue ?? 0m;
		if (equity <= 0m)
		return minVolume;

		if (equity < InitialEquity)
		return minVolume;

		if (EquityStep <= 0m)
		return InitialStepVolume;

		var stepsDecimal = (equity - InitialEquity) / EquityStep;
		if (stepsDecimal < 0m)
		stepsDecimal = 0m;

		var steps = (int)Math.Floor(stepsDecimal);
		var dynamicVolume = InitialStepVolume + VolumeStep * steps;

		if (dynamicVolume < minVolume)
		dynamicVolume = minVolume;

		return dynamicVolume;
	}

	private decimal AlignVolume(decimal volume)
	{
		var security = Security;
		if (security == null)
		return volume;

		var minVolume = security.MinVolume ?? 0m;
		var maxVolume = security.MaxVolume ?? 0m;
		var step = security.VolumeStep ?? 0m;

		if (minVolume > 0m && volume < minVolume)
		volume = minVolume;

		if (maxVolume > 0m && volume > maxVolume)
		volume = maxVolume;

		if (step > 0m)
		{
			var steps = Math.Round(volume / step, MidpointRounding.AwayFromZero);
			volume = steps * step;
		}

		return volume;
	}
}