GitHub で見る

階段戦略

階段戦略は、元の MetaTrader 専門家の行動を再現します。まず、現在の売り値を中心に対称的な逆指値注文を発注し、その後、最新の約定を中心にグリッドを継続的に再構築します。利益は、ソース スクリプトとまったく同様に、出来高による重み付けを行わずに、価格ステップ (pips) で累積されます。利益目標が達成されると、この戦略は成行注文に従ってすべてのポジションを清算し、保留中のストップをすべて削除し、グリッドをリセットします。

取引ロジック

  1. オープンなポジションがない場合は、現在の売り値の上下 ChannelSteps / 2 価格ステップの距離に買いストップと売りストップを配置します。
  2. 最初の逆指値注文が約定した後、約定価格付近でグリッドを再準備します。
    • アクティブなストップ注文が 2 つ未満の場合は、古いストップ注文をキャンセルします。
    • 現在の入札価格が最後のエントリーからチャネル距離の半分以内にある限り、最新の約定から離れた新しい買いストップと売りストップ ChannelSteps を配置します。
    • AddLots が有効な場合、各約定後に未決注文の数量が基本ロット分増加します。
  3. MT4 バージョンで使用されるヘッジ バスケットを再現するために、すべてのロングおよびショート エントリを含む 2 つの実行リストを維持します。
  4. ロングの最良の買い値とショートの最良の売り値を使用して、完成したすべてのローソク足のバスケットの未実現利益を計算します。距離は、元のポイント計算を反映して、商品価格ステップによって正規化されます。
  5. いずれかのしきい値を超えた場合に完全清算をトリガーします。
    • ProfitSteps – 現在のシンボルのみによってもたらされる利益。
    • CommonProfitSteps – バスケット全体の利益。
  6. 清算では、ロングエクスポージャーとショートエクスポージャーを個別に閉じるために成行注文を送信します。保留中のストップ注文は、バスケットがフラットになるとキャンセルされます。

注意: 元のエキスパートは未決注文を登録するときにストップロスレベルを付けました。 StockSharp は、高レベルの API による注文ごとの保護レベルをサポートしていません。そのため、ポートは、上記の利益ベースのロジックのみを通じて取引をクローズします。

パラメーター

パラメータ 説明 デフォルト
ChannelSteps 対称ストップ注文間の距離 (最小価格ステップ)。 1000
ProfitSteps ローカル バスケットを閉じるために必要な利益しきい値 (段階単位)。 1500
CommonProfitSteps 完全な清算を強制するグローバル利益のしきい値 (段階的)。 1000
AddLots 有効にすると、各約定後に次の未決注文の数量が基本ロット分増加します。 true
BaseVolume ストップ注文の最初のペアに使用されるボリューム。 0.1m
CandleType キャンドルのサブスクリプションと取引管理に使用される時間枠。 1 minute

実装メモ

  • StockSharp の高レベルの API を SubscribeCandles() および Bind() とともに使用して、完成したキャンドルのみを処理します。
  • OnOwnTradeReceived 内の個々のエントリを追跡することで、利益計算で MQL バージョンのヘッジ ロジックを模倣できるようになります。
  • 利益しきい値は、MT4 エキスパートがピップを合計する方法と一致し、実行量を乗算せず、純粋な価格ステップ距離に基づいて動作します。
  • すべての逆指値注文は BuyStopSellStop を通じて作成されますが、エグジットは成行注文で実行され、データ プロバイダー間でのロジックの移植性を維持します。
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Stairs grid strategy: places trades at regular ATR-based intervals,
/// adding to position on trending moves, closing on profit target.
/// </summary>
public class StairsStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _atrLength;
	private readonly StrategyParam<decimal> _gridMultiplier;
	private readonly StrategyParam<int> _maxLayers;
	private readonly StrategyParam<decimal> _profitMultiplier;
	private readonly StrategyParam<int> _emaLength;

	private decimal _entryPrice;
	private decimal _lastGridPrice;
	private int _gridCount;
	private decimal _prevEma;

	public StairsStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period for grid step.", "Indicators");

		_gridMultiplier = Param(nameof(GridMultiplier), 1.5m)
			.SetDisplay("Grid Multiplier", "ATR multiplier for grid step.", "Grid");

		_maxLayers = Param(nameof(MaxLayers), 5)
			.SetDisplay("Max Layers", "Maximum grid layers.", "Grid");

		_profitMultiplier = Param(nameof(ProfitMultiplier), 2.0m)
			.SetDisplay("Profit Multiplier", "ATR multiplier for profit target.", "Grid");

		_emaLength = Param(nameof(EmaLength), 20)
			.SetDisplay("EMA Length", "EMA for trend direction.", "Indicators");
	}

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

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

	public decimal GridMultiplier
	{
		get => _gridMultiplier.Value;
		set => _gridMultiplier.Value = value;
	}

	public int MaxLayers
	{
		get => _maxLayers.Value;
		set => _maxLayers.Value = value;
	}

	public decimal ProfitMultiplier
	{
		get => _profitMultiplier.Value;
		set => _profitMultiplier.Value = value;
	}

	public int EmaLength
	{
		get => _emaLength.Value;
		set => _emaLength.Value = value;
	}

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

		_entryPrice = 0;
		_lastGridPrice = 0;
		_gridCount = 0;
		_prevEma = 0;
	}

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

		var atr = new AverageTrueRange { Length = AtrLength };
		var ema = new ExponentialMovingAverage { Length = EmaLength };

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

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

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

		if (atrVal <= 0 || _prevEma == 0)
		{
			_prevEma = emaVal;
			return;
		}

		var close = candle.ClosePrice;
		var gridStep = atrVal * GridMultiplier;
		var profitTarget = atrVal * ProfitMultiplier;

		// Check profit target
		if (Position > 0 && _entryPrice > 0)
		{
			if (close - _entryPrice >= profitTarget || close < emaVal)
			{
				SellMarket();
				_gridCount = 0;
				_entryPrice = 0;
				_lastGridPrice = 0;
			}
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (_entryPrice - close >= profitTarget || close > emaVal)
			{
				BuyMarket();
				_gridCount = 0;
				_entryPrice = 0;
				_lastGridPrice = 0;
			}
		}

		// Grid: add to winning direction
		if (Position > 0 && _lastGridPrice > 0 && _gridCount < MaxLayers)
		{
			if (close - _lastGridPrice >= gridStep)
			{
				BuyMarket();
				_lastGridPrice = close;
				_gridCount++;
			}
		}
		else if (Position < 0 && _lastGridPrice > 0 && _gridCount < MaxLayers)
		{
			if (_lastGridPrice - close >= gridStep)
			{
				SellMarket();
				_lastGridPrice = close;
				_gridCount++;
			}
		}

		// Initial entry based on trend
		if (Position == 0)
		{
			var emaRising = emaVal > _prevEma;
			var emaFalling = emaVal < _prevEma;

			if (emaRising && close > emaVal)
			{
				_entryPrice = close;
				_lastGridPrice = close;
				_gridCount = 0;
				BuyMarket();
			}
			else if (emaFalling && close < emaVal)
			{
				_entryPrice = close;
				_lastGridPrice = close;
				_gridCount = 0;
				SellMarket();
			}
		}

		_prevEma = emaVal;
	}
}