GitHub で見る

2つの保留注文を開く戦略

概要

この戦略は、現在のスプレッド周辺にバイストップ注文とセルストップ注文を同時に発注するMetaTraderエキスパートアドバイザーを再現します。単一の銘柄で動作し、StockSharpの高レベルAPIコールを使用してオーダーブックをサブスクライブし、保留注文を管理し、ポートフォリオリスク管理を処理します。保留注文の一方が約定するとすぐに、逆の注文はキャンセルされ、アクティブなポジションはストップロス、テイクプロフィット、トレーリングストップルールで管理されます。

取引ロジック

  1. オーダーブックをサブスクライブし、最良bidとask価格を読み取ります。
  2. オープンポジションまたは有効なエントリー注文がない場合、エントリーボリュームを計算し、2つのストップ注文を発注します:
    • バイストップは ask + EntryOffsetPoints × PriceStep に。
    • セルストップは bid − EntryOffsetPoints × PriceStep に。
  3. ストップ注文が実行されたとき:
    • 逆方向の保留注文をキャンセルします。
    • 実行価格を新しいエントリー価格として保存します。
    • フィルを基準とした価格ステップでの初期ストップロスとテイクプロフィットレベルを計算します。
  4. ポジションがアクティブな間、オーダーブックを監視します:
    • bidがストップロスまたはテイクプロフィットレベルに達したときにロングを閉じます。
    • askがストップロスまたはテイクプロフィットレベルに達したときにショートを閉じます。
    • 価格がトレーリング距離だけトレードの有利な方向に移動した後にトレーリングストップを有効化し、ストップレベルをそれに合わせて移動します。
  5. ポジションが平坦に戻ったとき、内部状態をリセットし、新しいストップ注文のペアを発注します。

保護レベルに触れると成行注文で決済が実行されます。これにより、低レベルの注文変更APIに依存せずにMQL実装に近いロジックを維持します。

資金管理

戦略は固定ボリュームまたはダイナミックなリスクベースのサイジングを使用できます:

  • 固定ボリュームFixedVolumeパラメーターで定義された一定のロットサイズを使用します。
  • 資金管理 – 有効な場合、ポートフォリオの資本、リスクパーセンテージ、価格ステップでのストップロス距離からボリュームを計算します。ボリュームは銘柄のボリュームステップに丸められ、銘柄の最小値と最大値の間でクランプされます。

パラメーター

パラメーター 説明
UseMoneyManagement リスクベースのポジションサイジングを有効にします。デフォルト:true
RiskPercent 資金管理が有効な場合に取引ごとにリスクにさらすポートフォリオ資本のパーセンテージ。デフォルト:2
FixedVolume 資金管理が無効な場合に使用するロットサイズ。デフォルト:1
StopLossPoints エントリー価格からの価格ステップでのストップロス距離。デフォルト:100
TakeProfitPoints エントリー価格からの価格ステップでのテイクプロフィット距離。デフォルト:300
TrailingStopPoints 価格ステップでのトレーリングストップ距離。0の値はトレーリングを無効にします。デフォルト:50
EntryOffsetPoints スプレッドから保留注文を置くために使用する価格ステップの距離。デフォルト:50
SlippagePoints スリッページのために予約された価格ステップでの追加クッション。現在は情報提供のみで直接使用されません。デフォルト:5

注意事項

  • 戦略はオーダーブックフィードに依存します。選択した銘柄に対してマーケットデプスデータが利用可能であることを確認してください。
  • ストップロスとテイクプロフィットの実行は、bid/askがレベルを越えると成行注文を使用し、元のMQLトレーリングストップロジックの動作に対応します。
  • トレーリングストップはエントリーから設定されたトレーリング距離だけ価格が移動した後にのみ開始されます。
  • コードはプロジェクトガイドラインに従ってタブインデント、英語コメント、高レベルStockSharpメソッドを使用します。
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 that simulates placing both buy stop and sell stop orders around the current price.
/// It uses candle-based breakout detection and manages the resulting position
/// with fixed stop loss, take profit and optional trailing stop levels.
/// </summary>
public class OpenTwoPendingOrdersStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _entryOffsetPoints;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _pendingBuyPrice;
	private decimal? _pendingSellPrice;
	private decimal? _entryPrice;
	private decimal? _stopLevel;
	private decimal? _takeLevel;
	private decimal _highestSinceEntry;
	private decimal _lowestSinceEntry;
	private int _cooldown;

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

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

	/// <summary>
	/// Trailing stop distance expressed in price steps.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Distance in price steps used to place the pending entries away from the current price.
	/// </summary>
	public decimal EntryOffsetPoints
	{
		get => _entryOffsetPoints.Value;
		set => _entryOffsetPoints.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of <see cref="OpenTwoPendingOrdersStrategy"/>.
	/// </summary>
	public OpenTwoPendingOrdersStrategy()
	{
		_stopLossPoints = Param(nameof(StopLossPoints), 5000m)
			.SetDisplay("Stop Loss (steps)", "Stop loss distance in price steps", "Risk")
			.SetOptimize(20m, 300m, 20m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 8000m)
			.SetDisplay("Take Profit (steps)", "Take profit distance in price steps", "Risk")
			.SetOptimize(50m, 600m, 50m);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 3000m)
			.SetDisplay("Trailing Stop (steps)", "Trailing stop distance in price steps", "Risk")
			.SetOptimize(10m, 200m, 10m);

		_entryOffsetPoints = Param(nameof(EntryOffsetPoints), 1000m)
			.SetDisplay("Entry Offset (steps)", "Offset from close for pending entries", "Execution")
			.SetOptimize(10m, 150m, 10m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles", "General");
	}

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

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

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

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

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

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

		if (_cooldown > 0)
		{
			_cooldown--;
			return;
		}

		var step = GetStep();

		// Manage existing position
		if (Position != 0 && _entryPrice.HasValue)
		{
			ManagePosition(candle, step);

			// If position was closed, reset and set up new pending entries
			if (Position == 0)
			{
				ResetState();
				_cooldown = 20;
			}
			return;
		}

		// Check pending entries
		if (_pendingBuyPrice.HasValue && _pendingSellPrice.HasValue)
		{
			var buyLevel = _pendingBuyPrice.Value;
			var sellLevel = _pendingSellPrice.Value;

			// Buy stop triggered: price went up to pending buy level
			if (candle.HighPrice >= buyLevel)
			{
				_pendingBuyPrice = null;
				_pendingSellPrice = null;
				BuyMarket();
				InitializePositionLevels(true, buyLevel, step);
				return;
			}

			// Sell stop triggered: price went down to pending sell level
			if (candle.LowPrice <= sellLevel)
			{
				_pendingBuyPrice = null;
				_pendingSellPrice = null;
				SellMarket();
				InitializePositionLevels(false, sellLevel, step);
				return;
			}
		}
		else
		{
			// No pending entries, set up new ones
			SetupPendingEntries(candle.ClosePrice, step);
		}
	}

	private void SetupPendingEntries(decimal currentPrice, decimal step)
	{
		var offset = EntryOffsetPoints * step;
		_pendingBuyPrice = currentPrice + offset;
		_pendingSellPrice = currentPrice - offset;
	}

	private void InitializePositionLevels(bool isLong, decimal entryPrice, decimal step)
	{
		_entryPrice = entryPrice;
		_highestSinceEntry = entryPrice;
		_lowestSinceEntry = entryPrice;

		_stopLevel = StopLossPoints > 0m
			? entryPrice + (isLong ? -StopLossPoints : StopLossPoints) * step
			: null;

		_takeLevel = TakeProfitPoints > 0m
			? entryPrice + (isLong ? TakeProfitPoints : -TakeProfitPoints) * step
			: null;
	}

	private void ManagePosition(ICandleMessage candle, decimal step)
	{
		if (Position > 0)
		{
			_highestSinceEntry = Math.Max(_highestSinceEntry, candle.HighPrice);

			if (_stopLevel.HasValue && candle.LowPrice <= _stopLevel.Value)
			{
				SellMarket();
				return;
			}

			if (_takeLevel.HasValue && candle.HighPrice >= _takeLevel.Value)
			{
				SellMarket();
				return;
			}

			UpdateTrailingStop(true, step);
		}
		else if (Position < 0)
		{
			_lowestSinceEntry = Math.Min(_lowestSinceEntry, candle.LowPrice);

			if (_stopLevel.HasValue && candle.HighPrice >= _stopLevel.Value)
			{
				BuyMarket();
				return;
			}

			if (_takeLevel.HasValue && candle.LowPrice <= _takeLevel.Value)
			{
				BuyMarket();
				return;
			}

			UpdateTrailingStop(false, step);
		}
	}

	private void UpdateTrailingStop(bool isLong, decimal step)
	{
		if (TrailingStopPoints <= 0m || _entryPrice == null)
			return;

		var trailingDistance = TrailingStopPoints * step;
		if (trailingDistance <= 0m)
			return;

		if (isLong)
		{
			if (_highestSinceEntry - _entryPrice.Value >= trailingDistance)
			{
				var desiredStop = _highestSinceEntry - trailingDistance;
				if (_stopLevel == null || desiredStop > _stopLevel.Value)
					_stopLevel = desiredStop;
			}
		}
		else
		{
			if (_entryPrice.Value - _lowestSinceEntry >= trailingDistance)
			{
				var desiredStop = _lowestSinceEntry + trailingDistance;
				if (_stopLevel == null || desiredStop < _stopLevel.Value)
					_stopLevel = desiredStop;
			}
		}
	}

	private void ResetState()
	{
		_pendingBuyPrice = null;
		_pendingSellPrice = null;
		_entryPrice = null;
		_stopLevel = null;
		_takeLevel = null;
		_highestSinceEntry = 0m;
		_lowestSinceEntry = 0m;
		_cooldown = 0;
	}

	private decimal GetStep()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 0.01m;
	}
}