GitHub で見る

価格極値戦略

概要

価格極値戦略は、MetaTraderのエキスパートアドバイザー Price_Extreme_Strategy をStockSharpの高レベルAPIを使って再現したものです。システムは、設定可能な数の完了済みローソク足にわたる最高値と最低値から導出されるスライドチャネルを監視します。選択された参照ローソク足が上限を上回るか下限を下回って終値を付けると、ブレイクアウトシグナルが生成されます。このロジックはオプションで反転させ、ブレイクアウト条件を逆張りエントリーに変換することができます。

この実装はイベント駆動型のトレーディングワークフローを維持します。注文は各完了済みローソク足のクローズ直後に送信され、次のバーの始値ティックで反応した元のMQLアルゴリズムの動作と一致します。

インジケーターロジック

価格極値チャネルは、StockSharpの Highest および Lowest インジケーターを使用して、完了済みローソク足ごとに再構築されます:

  • Highest は直近 N 本のローソク足の最高値を追跡します。
  • Lowest は直近 N 本のローソク足の最安値を追跡します。

これらのバッファは、元のエキスパートアドバイザーに同梱されているカスタムスタディ Price_Extreme_Indicator をエミュレートします。インジケーターの長さは Level Length パラメーターで設定します。

別のパラメーター Signal Shift は、ブレイクアウト条件の評価に使用される終値済みローソク足を定義します。シフト値 1 は「直近の終値済みローソク足を使用する」(デフォルト) を意味します。より大きな値は、古いバーを参照することで追加確認を待つことができます。

トレーディングルール

  1. 完了済みローソク足ごとに上限・下限チャネル値を再計算する。
  2. 内部履歴バッファから Signal Shift で指定されたローソク足を取得する。
  3. 方向性の意図を生成する:
    • 上方ブレイクアウト: ローソク足の終値が上限チャネル値を上回る。
    • 下方ブレイクアウト: ローソク足の終値が下限チャネル値を下回る。
  4. Reverse Signals によるオプションの反転を適用する:
    • 無効の場合、ブレイクアウト方向に取引する (上方ブレイクアウトで買い、下方ブレイクアウトで売り)。
    • 有効の場合、反応を入れ替える (上方ブレイクアウトで売り、下方ブレイクアウトで買い)。
  5. 注文送信前に Enable Long および Enable Short の許可を確認する。
  6. 新しいトレードを開く前に反対ポジションを自動的に閉じ、常に1つのネットポジションのみが存在するようにする。

リスク管理

この戦略はMQLバージョンのポイントベースのコントロールを反映したストップロスとテイクプロフィットの処理を提供します:

  • Stop LossTake Profit は価格ステップ (Security.PriceStep) で表現されます。
  • ネットポジションサイズが変化するたびに目標価格が再計算されます。
  • 完了済みローソク足が保護レベルを越えた場合 (ロングのストップ以下の安値、ショートのストップ以上の高値など)、ポジションは成行注文で決済され、保護目標はクリアされます。
  • StartProtection()OnStarted 中に有効化され、StockSharpの組み込みセーフガードを活用します。

パラメーター

パラメーター 説明 デフォルト グループ
LevelLength 極値チャネル計算時に考慮する完了済みローソク足の数。 5 Indicator
SignalShift ブレイクアウト検証に使用する終値済みローソク足のインデックス (1 = 直近の終値済みローソク足)。 1 Indicator
EnableLong true の場合、買いを許可する。 true Trading
EnableShort true の場合、売りを許可する。 true Trading
ReverseSignals ブレイクアウトの反応を反転させる (下落で買い、ブレイクアウトで売り)。 false Trading
OrderVolume 各成行注文で送信するボリューム。ゼロより大きくなければならない。 1 Trading
StopLossPoints 価格ステップで測定したストップロスの距離。0 はストップを無効にする。 0 Risk
TakeProfitPoints 価格ステップで測定したテイクプロフィットの距離。0 は目標を無効にする。 0 Risk
CandleType データ購読の主要な時間軸。 5分ローソク足 Data

すべてのパラメーターはUIメタデータを持つ StrategyParam<T> を使用しており、Designerから最適化または変更できます。

使用ガイドライン

  1. 戦略を銘柄にアタッチし、元のMetaTrader設定で使用したタイムフレームに合わせて Candle Type を設定する。
  2. より広いまたは狭い価格極値チャネルが必要な場合は Level Length を調整する。
  3. ブレイクアウト評価前に何本の終値済みローソク足を待つかを制御するために Signal Shift を設定する。
  4. Enable LongEnable ShortReverse Signals を使用して、希望するトレード方向を選択する。
  5. リスク設定に合わせて Order VolumeStop LossTake Profit を定義する。両方の保護値は価格ステップで動作することに注意すること。
  6. 戦略を起動する。チャートエリアが利用可能な場合、ローソク足、インジケーターバンド、約定済みトレードが自動的にプロットされます。

追加メモ

  • この戦略は意図的に単一のネットポジションで動作し、新しいトレードに入る前に反対側をフラット化するMQLエキスパートのヘッジロジックを反映しています。
  • 保護ストップと目標は完了済みローソク足で評価されます。ライブトレードでは、これは元のスクリプトが使用するサーバー側の保護注文を近似します。
  • 要望に応じてPythonポートは含まれていません。
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>
/// Breakout strategy based on the Price Extreme indicator.
/// </summary>
public class PriceExtremeStrategy : Strategy
{
	private readonly StrategyParam<int> _levelLength;
	private readonly StrategyParam<int> _signalShift;
	private readonly StrategyParam<bool> _enableLong;
	private readonly StrategyParam<bool> _enableShort;
	private readonly StrategyParam<bool> _reverseSignals;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<ICandleMessage> _history = new();
	private readonly List<decimal> _highs = new();
	private readonly List<decimal> _lows = new();
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal _prevPosition;
	private decimal _entryPrice;
	private decimal _prevUpper;
	private decimal _prevLower;

	/// <summary>
	/// Number of candles used to build extreme levels.
	/// </summary>
	public int LevelLength
	{
		get => _levelLength.Value;
		set => _levelLength.Value = value;
	}

	/// <summary>
	/// Shift in candles used for the breakout signal.
	/// </summary>
	public int SignalShift
	{
		get => _signalShift.Value;
		set => _signalShift.Value = value;
	}

	/// <summary>
	/// Enable long trades.
	/// </summary>
	public bool EnableLong
	{
		get => _enableLong.Value;
		set => _enableLong.Value = value;
	}

	/// <summary>
	/// Enable short trades.
	/// </summary>
	public bool EnableShort
	{
		get => _enableShort.Value;
		set => _enableShort.Value = value;
	}

	/// <summary>
	/// Reverse long and short signals.
	/// </summary>
	public bool ReverseSignals
	{
		get => _reverseSignals.Value;
		set => _reverseSignals.Value = value;
	}

	/// <summary>
	/// Order volume in lots.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

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

	/// <summary>
	/// Type of candles used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="PriceExtremeStrategy"/>.
	/// </summary>
	public PriceExtremeStrategy()
	{
		_levelLength = Param(nameof(LevelLength), 20)
			.SetGreaterThanZero()
			.SetDisplay("Level Length", "Number of candles for price extremes", "Indicator")
			
			.SetOptimize(3, 30, 1);

		_signalShift = Param(nameof(SignalShift), 1)
			.SetGreaterThanZero()
			.SetDisplay("Signal Shift", "Closed candles used for breakout", "Indicator");

		_enableLong = Param(nameof(EnableLong), true)
			.SetDisplay("Enable Long", "Allow buying trades", "Trading");

		_enableShort = Param(nameof(EnableShort), true)
			.SetDisplay("Enable Short", "Allow selling trades", "Trading");

		_reverseSignals = Param(nameof(ReverseSignals), false)
			.SetDisplay("Reverse Signals", "Invert breakout direction", "Trading");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume sent with market orders", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 0)
			.SetDisplay("Stop Loss", "Protective stop in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0)
			.SetDisplay("Take Profit", "Profit target in price steps", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe", "Data");
	}

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

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

		_history.Clear();
		_highs.Clear();
		_lows.Clear();
		_prevUpper = 0m;
		_prevLower = 0m;
		_entryPrice = 0m;
		_prevPosition = 0m;
		ResetTargets();
	}

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

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

		StartProtection(
			takeProfit: new Unit(3, UnitTypes.Percent),
			stopLoss: new Unit(2, UnitTypes.Percent));

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

	private bool CanOpenLong => EnableLong && OrderVolume > 0m;
	private bool CanOpenShort => EnableShort && OrderVolume > 0m;

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

		_highs.Add(candle.HighPrice);
		_lows.Add(candle.LowPrice);
		_history.Add(candle);

		var maxHistory = Math.Max(LevelLength + SignalShift + 2, 10);
		if (_history.Count > maxHistory)
		{
			var removeCount = _history.Count - maxHistory;
			_history.RemoveRange(0, removeCount);
			_highs.RemoveRange(0, removeCount);
			_lows.RemoveRange(0, removeCount);
		}

		if (_highs.Count < LevelLength)
			return;

		var upper = decimal.MinValue;
		var lower = decimal.MaxValue;
		for (var i = _highs.Count - LevelLength; i < _highs.Count; i++)
		{
			if (_highs[i] > upper) upper = _highs[i];
			if (_lows[i] < lower) lower = _lows[i];
		}

		if (_history.Count < SignalShift)
			return;

		var signalCandle = _history[_history.Count - SignalShift];

		var breakoutUp = candle.ClosePrice > _prevUpper && _prevUpper > 0;
		var breakoutDown = candle.ClosePrice < _prevLower && _prevLower > 0;

		_prevUpper = upper;
		_prevLower = lower;

		var wantLong = ReverseSignals ? breakoutDown : breakoutUp;
		var wantShort = ReverseSignals ? breakoutUp : breakoutDown;

		if (wantLong && CanOpenLong && Position == 0)
		{
			BuyMarket();
		}
		else if (wantShort && CanOpenShort && Position == 0)
		{
			SellMarket();
		}
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade?.Trade == null) return;
		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;
		if (Position == 0)
			_entryPrice = 0m;
	}

	private void UpdateTargets()
	{
		_stopPrice = null;
		_takePrice = null;

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

		if (Position > 0m)
		{
			if (StopLossPoints > 0)
				_stopPrice = _entryPrice - StopLossPoints * step;

			if (TakeProfitPoints > 0)
				_takePrice = _entryPrice + TakeProfitPoints * step;
		}
		else if (Position < 0m)
		{
			if (StopLossPoints > 0)
				_stopPrice = _entryPrice + StopLossPoints * step;

			if (TakeProfitPoints > 0)
				_takePrice = _entryPrice - TakeProfitPoints * step;
		}
	}

	private void ApplyRiskManagement(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetTargets();
				return;
			}

			if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetTargets();
			}
		}
		else if (Position < 0m)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetTargets();
				return;
			}

			if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetTargets();
			}
		}
	}

	private void ResetTargets()
	{
		_stopPrice = null;
		_takePrice = null;
		_prevPosition = Position;
	}
}