GitHub で見る

保留中のトレッドグリッド戦略

概要

保留中のトレッド グリッド戦略は、MetaTrader 4 エキスパート アドバイザー Pending_tread.mq4 の忠実な StockSharp 移植です。オリジナルの EA は、未決注文の 2 つのはしごを常に再構築します。1 つは市場の上に、もう 1 つは下にあります。各ラダーは買い注文または売り注文を使用するように構成でき、間隔はピップで定義されます。 StockSharp 実装は、追加のインジケーターやコレクションを導入することなく、高レベルの API を通じて同じ動作を再現します。

取引ロジック

  1. 買値/売値ベースのメンテナンス – この戦略はレベル 1 相場 (SubscribeLevel1) をサブスクライブし、最新の買値と売値を維持します。新しいデータが到着するたびに、メンテナンス ルーチンが (構成可能なスロットルを使用して) 実行され、既存の未決注文と構成されたグリッド サイズが比較されます。
  2. 市場を上回るラダーAboveMarketSide に応じて、アルゴリズムは市場より PipStep ピッップずつ上に買い逆指値注文または売り指値注文を出します。新しい注文はそれぞれ、TakeProfitPips ピップ分オフセットされた独自のテイクプロフィットレベルを受け取ります。
  3. 市場下限ラダーBelowMarketSide パラメーターは、市場の下に積み上げられた買い指値注文と売りストップ注文のどちらかを選択します。同じピップ間隔とテイクプロフィットロジックが適用されます。
  4. ストップレベル ガードMinStopDistancePoints パラメータは、MetaTrader MODE_STOPLEVEL チェックをエミュレートします。価格と関連する買値/売値アンカー間の距離が指定された制限よりも小さい場合、注文はスキップされます。
  5. スロットルThrottleSeconds は、TRADE_CONTEXT_BUSY エラーを回避した元の 5 秒スロットルを反映しています。到着するティック数に関係なく、その間隔中に実行されるメンテナンス サイクルは 1 つだけです。

すべてのピップベースの入力(PipStepTakeProfitPips)は、商品 PriceStep および Decimals を使用して絶対価格オフセットに変換されます。 5 桁の引用符は、MetaTrader の「調整ポイント」ロジックに一致するように、自動的にステップを 10 倍します。

パラメーター

パラメータ デフォルト 説明
OrderVolume 0.01 すべての未決注文を発注するときに使用されるボリューム。登録前の楽器の音量ステップに四捨五入されます。
PipStep 12 ラダー内の連続する注文間の間隔。ピップ単位で表されます。
TakeProfitPips 10 すべての未決注文のテイクプロフィットを配置するために使用されるピップ単位の距離。
OrdersPerSide 10 市場上および市場下で維持されるアクティブな注文の最大数。
AboveMarketSide 購入する 市場上で使用される注文タイプ。 Buy は買いの逆指値注文を作成し、Sell は売りの指値注文を作成します。
BelowMarketSide 売る 市場の下で使用される注文タイプ。 Buy は買い指値注文を作成し、Sell は売り逆指値注文を作成します。
MinStopDistancePoints 0 買値/売値と未決価格との間に許容される最小距離 (生のポイント単位)。必要に応じて、これをブローカー MODE_STOPLEVEL に設定します。
ThrottleSeconds 5 グリッドメンテナンスサイクル間のクールダウン期間。
SlippagePoints 3 ドキュメントの同等性のために保存されています。 StockSharp の未決注文ではこの値は使用されません。

実装メモ

  • StockSharp の高レベル ヘルパー (SubscribeLevel1BuyLimitSellLimitBuyStopSellStop) のみを使用します。
  • 価格は Security.ShrinkPrice を通じて正規化され、ブローカーがティックに合わせた有効な値を受け取ることができます。
  • 各注文が送信される前に、VolumeStepMinVolume、および MaxVolume を考慮して音量が調整されます。
  • すべての診断メッセージは、AddInfoLog / AddWarningLog を介してルーティングされ、MetaTrader スクリプトの詳細出力をミラーリングします。
  • Python の実装は、要求に応じて意図的に省略されています。

使用のヒント

  1. 流動的な商品とポートフォリオを割り当てて、戦略を開始します。保留中のはしごは、最初のレベル 1 アップデートの直後に表示されます。
  2. OrdersPerSide を増やす場合は注意が必要です。ラングを追加するたびに、ブローカー側で別のライブ未決注文が発生します。
  3. オリジナルの EA を正確に模倣するには、デフォルトのスロットルを 5 秒のままにして、ブローカーの停止レベル要件を使用して MinStopDistancePoints を構成します。
  4. StockSharp はネット ポジションを処理することに注意してください。反対側のラダーが同時にトリガーされた場合、結果として生じる約定はヘッジされたサブポジションを作成するのではなく、部分的に相互に相殺されます。
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Pending grid strategy converted from the MetaTrader 4 expert advisor "Pending_tread".
/// Maintains two independent ladders of limit orders above and below the market with configurable direction and spacing.
/// When price reaches a grid level, a market order is placed in the configured direction.
/// </summary>
public class PendingTreadStrategy : Strategy
{
	private readonly StrategyParam<decimal> _pipStep;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _ordersPerSide;
	private readonly StrategyParam<Sides> _aboveMarketSide;
	private readonly StrategyParam<Sides> _belowMarketSide;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pipSize;
	private decimal _anchorPrice;
	private bool _initialized;
	private readonly List<decimal> _triggeredLevelsAbove = new();
	private readonly List<decimal> _triggeredLevelsBelow = new();
	private decimal _entryPrice;

	public PendingTreadStrategy()
	{
		_pipStep = Param(nameof(PipStep), 200000m)
			.SetGreaterThanZero()
			.SetDisplay("Grid step (pips)", "Distance between adjacent pending orders expressed in pips", "Trading");

		_takeProfitPips = Param(nameof(TakeProfitPips), 150000m)
			.SetGreaterThanZero()
			.SetDisplay("Take profit (pips)", "Individual take-profit distance assigned to every pending order", "Trading");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order volume", "Volume sent with each pending order", "Trading");

		_ordersPerSide = Param(nameof(OrdersPerSide), 2)
			.SetGreaterThanZero()
			.SetDisplay("Orders per side", "Maximum number of grid levels maintained above and below the anchor", "Trading");

		_aboveMarketSide = Param(nameof(AboveMarketSide), Sides.Buy)
			.SetDisplay("Above market side", "Type of orders triggered above the current price", "Orders");

		_belowMarketSide = Param(nameof(BelowMarketSide), Sides.Sell)
			.SetDisplay("Below market side", "Type of orders triggered below the current price", "Orders");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle type", "Candle timeframe", "General");
	}

	public decimal PipStep
	{
		get => _pipStep.Value;
		set => _pipStep.Value = value;
	}

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	public int OrdersPerSide
	{
		get => _ordersPerSide.Value;
		set => _ordersPerSide.Value = value;
	}

	public Sides AboveMarketSide
	{
		get => _aboveMarketSide.Value;
		set => _aboveMarketSide.Value = value;
	}

	public Sides BelowMarketSide
	{
		get => _belowMarketSide.Value;
		set => _belowMarketSide.Value = value;
	}

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

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

		_pipSize = 0m;
		_anchorPrice = 0m;
		_initialized = false;
		_triggeredLevelsAbove.Clear();
		_triggeredLevelsBelow.Clear();
		_entryPrice = 0m;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_pipSize = GetPipSize();

		this
			.SubscribeCandles(CandleType)
			.Bind(ProcessCandle)
			.Start();
	}

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

		var close = candle.ClosePrice;

		if (!_initialized)
		{
			_anchorPrice = close;
			_initialized = true;
			return;
		}

		var distance = PipStep * _pipSize;
		if (distance <= 0m)
			return;

		var tpOffset = TakeProfitPips * _pipSize;

		// Check above-market grid levels
		for (var i = 1; i <= OrdersPerSide; i++)
		{
			var level = _anchorPrice + distance * i;

			if (_triggeredLevelsAbove.Contains(level))
				continue;

			if (close >= level)
			{
				_triggeredLevelsAbove.Add(level);
				ExecuteGridOrder(AboveMarketSide, close, tpOffset);
				return; // one order per candle
			}
		}

		// Check below-market grid levels
		for (var i = 1; i <= OrdersPerSide; i++)
		{
			var level = _anchorPrice - distance * i;

			if (_triggeredLevelsBelow.Contains(level))
				continue;

			if (close <= level)
			{
				_triggeredLevelsBelow.Add(level);
				ExecuteGridOrder(BelowMarketSide, close, tpOffset);
				return; // one order per candle
			}
		}

		// Check take-profit for existing position
		CheckTakeProfit(close, tpOffset);
	}

	private void ExecuteGridOrder(Sides side, decimal price, decimal tpOffset)
	{
		// Close existing opposite position first
		if (Position != 0)
		{
			if ((Position > 0 && side == Sides.Sell) || (Position < 0 && side == Sides.Buy))
			{
				ClosePosition(side);
			}
		}

		var vol = OrderVolume;

		if (side == Sides.Buy)
		{
			BuyMarket(vol);
			_entryPrice = price;
		}
		else
		{
			SellMarket(vol);
			_entryPrice = price;
		}
	}

	private void ClosePosition(Sides newSide)
	{
		var absPos = Position.Abs();
		if (absPos <= 0)
			return;

		if (Position > 0)
			SellMarket(absPos);
		else
			BuyMarket(absPos);
	}

	private void CheckTakeProfit(decimal close, decimal tpOffset)
	{
		if (Position == 0 || _entryPrice == 0 || tpOffset <= 0)
			return;

		if (Position > 0 && close >= _entryPrice + tpOffset)
		{
			SellMarket(Position.Abs());
			_entryPrice = 0;

			// Reset grid to re-establish levels around current price
			ResetGrid(close);
		}
		else if (Position < 0 && close <= _entryPrice - tpOffset)
		{
			BuyMarket(Position.Abs());
			_entryPrice = 0;

			ResetGrid(close);
		}
	}

	private void ResetGrid(decimal newAnchor)
	{
		_anchorPrice = newAnchor;
		_triggeredLevelsAbove.Clear();
		_triggeredLevelsBelow.Clear();
	}

	private decimal GetPipSize()
	{
		var security = Security;
		if (security == null)
			return 0.01m;

		var step = security.PriceStep ?? 0.01m;
		return step > 0m ? step : 0.01m;
	}
}