GitHub で見る

不安定な市場におけるグリッド取引

概要

この戦略は、StockSharp の高レベルの API を使用して、MetaTrader エキスパート「Gridtrading_at_volatile_market.mq4」を複製します。取引時間枠でパターンを飲み込むエントリーを確認しながら、より高い時間枠で検出された Donchian チャネル境界を中心に取引します。グリッドがアクティブになると、この戦略は、価格がより高い時間枠 ATR の倍数に達したときに平均注文を追加し、ポートフォリオの利益またはドローダウンの目標に達したときに終了します。

仕組み

  1. ユーザーが選択した取引タイムフレームと、そこから自動的に導出されるより高いタイムフレーム (M1→M5→M15→M30→H1→H4→D1) の 2 つのローソク足ストリームが使用されます。
  2. より高い時間枠では、戦略は次のように計算します。
    • ATR(20) はグリッド間隔のサイズを指定します。
    • SMA(SlowMaLength) は、RSI と一緒にトレンドをフィルタリングします。
    • DonchianChannels(20) はサポートレベルとレジスタンスレベルです。
  3. 取引時間枠上で、最後の 2 つの完了したローソク足を追跡して、強気または弱気の巻き込みパターンを検出します。
  4. 長いグリッドは、前のローソク足が Donchian の下側バンドに触れ、強気の巻き込みパターンを形成し、RSI が売られ過ぎの状態を確認したときに始まります (価格が高い時間枠 SMA を上回っている間、RSI < 35)。短いグリッドは、RSI > 65 の上部バンドでこれらのルールを反映します。
  5. 最初の成行注文の後、この戦略はアンカーとして初値を維持します。現在のグリッド ステップで価格がポジションに対して 2 * ATR だけ移動すると、出来高に GridMultiplier を掛けた別の注文が追加されます。
  6. 次のいずれかの場合、グリッドは閉じられ、すべての注文がキャンセルされます。
    • (実現+未実現) 合計損益は TakeProfitFactor * total grid volume を超えます。
    • ドローダウンは -MaxDrawdownFraction * initial portfolio value を下回ります。

パラメーター

  • TakeProfitFactor – グリッドを閉じるために必要な総グリッド量の利益の倍数 (デフォルトは 0.1)。
  • SlowMaLength – フィルタリングに使用される上位タイムフレーム SMA の期間 (デフォルトは 50)。
  • GridMultiplier – 追加の各平均次数に適用される幾何学的係数 (デフォルトは 1.5)。
  • BaseOrderVolume – グリッド内の最初の注文のボリューム (デフォルトは 0.1)。
  • MaxDrawdownFraction – グリッドが強制的に閉じられる前のポートフォリオの初期値に対する最大損失 (デフォルトは 0.8)。
  • CandleType – 取引時間枠。より高い時間枠は自動的に推測されます。

注意事項

  • 再塗装を避けるため、閉じたキャンドルのみが処理されます。
  • この戦略は、オープン損益を評価するために利用可能な買値/売値に依存します。最新の取引価格のみが提供されている場合、近似値の精度が低くなる可能性があります。
  • ポートフォリオ情報が利用できない場合、ドローダウン保護はスキップされ、利益目標が達成されるかポジションが手動でクローズされるまでグリッドを実行できます。
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Simplified from "Grid Trading at Volatile Market" MetaTrader expert.
/// Uses RSI + SMA trend filter for initial entry then manages a simple
/// grid of averaging orders based on ATR distance.
/// </summary>
public class GridTradingAtVolatileMarketStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<int> _smaPeriod;
	private readonly StrategyParam<int> _maxGridLevels;

	private RelativeStrengthIndex _rsi;
	private readonly Queue<decimal> _smaQueue = new();
	private decimal _smaSum;

	private Sides? _gridDirection;
	private int _gridLevel;
	private decimal _lastEntryPrice;

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

	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	public int SmaPeriod
	{
		get => _smaPeriod.Value;
		set => _smaPeriod.Value = value;
	}

	public int MaxGridLevels
	{
		get => _maxGridLevels.Value;
		set => _maxGridLevels.Value = value;
	}

	public GridTradingAtVolatileMarketStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for signal detection", "General");

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "RSI period for entry signals", "Indicators");

		_smaPeriod = Param(nameof(SmaPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("SMA Period", "SMA period for trend filter", "Indicators");

		_maxGridLevels = Param(nameof(MaxGridLevels), 3)
			.SetGreaterThanZero()
			.SetDisplay("Max Grid Levels", "Maximum averaging levels", "Grid");
	}

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

		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
		_smaQueue.Clear();
		_smaSum = 0;
		_gridDirection = null;
		_gridLevel = 0;
		_lastEntryPrice = 0;

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

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

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

		var close = candle.ClosePrice;

		// Manual SMA
		_smaQueue.Enqueue(close);
		_smaSum += close;
		while (_smaQueue.Count > SmaPeriod)
			_smaSum -= _smaQueue.Dequeue();

		if (!_rsi.IsFormed || _smaQueue.Count < SmaPeriod)
			return;

		var smaValue = _smaSum / _smaQueue.Count;

		var volume = Volume;
		if (volume <= 0)
			volume = 1;

		// If no grid active, look for entry signals
		if (_gridDirection is null)
		{
			// Buy: RSI oversold + price below SMA (mean reversion)
			if (rsiValue < 35 && close < smaValue)
			{
				if (Position < 0)
					BuyMarket(Math.Abs(Position));

				BuyMarket(volume);
				_gridDirection = Sides.Buy;
				_gridLevel = 1;
				_lastEntryPrice = close;
			}
			// Sell: RSI overbought + price above SMA
			else if (rsiValue > 65 && close > smaValue)
			{
				if (Position > 0)
					SellMarket(Position);

				SellMarket(volume);
				_gridDirection = Sides.Sell;
				_gridLevel = 1;
				_lastEntryPrice = close;
			}
		}
		else
		{
			// Grid management - add levels on adverse moves
			var distanceThreshold = _lastEntryPrice * 0.005m; // 0.5% grid step

			if (_gridDirection == Sides.Buy)
			{
				// Price moved further down - add to grid
				if (_gridLevel < MaxGridLevels && close < _lastEntryPrice - distanceThreshold)
				{
					BuyMarket(volume);
					_gridLevel++;
					_lastEntryPrice = close;
				}
				// Take profit - price recovered above SMA
				else if (close > smaValue && rsiValue > 50)
				{
					if (Position > 0)
						SellMarket(Position);
					_gridDirection = null;
					_gridLevel = 0;
				}
			}
			else if (_gridDirection == Sides.Sell)
			{
				// Price moved further up - add to grid
				if (_gridLevel < MaxGridLevels && close > _lastEntryPrice + distanceThreshold)
				{
					SellMarket(volume);
					_gridLevel++;
					_lastEntryPrice = close;
				}
				// Take profit - price fell below SMA
				else if (close < smaValue && rsiValue < 50)
				{
					if (Position < 0)
						BuyMarket(Math.Abs(Position));
					_gridDirection = null;
					_gridLevel = 0;
				}
			}
		}
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_rsi = null;
		_smaQueue.Clear();
		_smaSum = 0;
		_gridDirection = null;
		_gridLevel = 0;
		_lastEntryPrice = 0;

		base.OnReseted();
	}
}