在 GitHub 上查看

波动市场网格策略

概述

该策略在 StockSharp 高级 API 上重现 MetaTrader 专家顾问“Gridtrading_at_volatile_market.mq4”。它在更高周期上跟踪 Donchian 通道的触碰,并在交易周期上通过吞没形态确认入场。一旦网格被激活,价格每向不利方向移动多个 ATR,就会按几何级数加仓,并在达到既定利润或触发投资组合回撤限制时平掉全部仓位。

运作方式

  1. 策略订阅两个级别的 K 线:用户选择的交易周期,以及根据映射自动推导的更高周期(M1→M5→M15→M30→H1→H4→D1)。
  2. 在高周期上计算以下指标:
    • ATR(20) 用于确定网格级距;
    • SMA(SlowMaLength) 与 RSI 一起进行趋势过滤;
    • DonchianChannels(20) 提供动态支撑与阻力。
  3. 在交易周期上,策略保存最近两根已完成的 K 线以检测多头或空头吞没形态。
  4. 当前一根 K 线触及 Donchian 下轨、形成多头吞没并且 RSI 表示超卖 (RSI < 35 且收盘价高于高周期 SMA) 时开启多头网格;空头网格在上轨、RSI > 65 的镜像条件下触发。
  5. 首笔市价单成交后,策略记录初始价格。如果价格相对仓位不利移动 2 * ATR(按当前网格层级计算),则以 GridMultiplier 的倍率加码下一单。
  6. 满足以下任一条件时取消所有挂单并平掉仓位:
    • 总盈亏(含已实现与浮动)超过 TakeProfitFactor * 网格总持仓量
    • 回撤低于 -MaxDrawdownFraction * 投资组合初始价值

参数

  • TakeProfitFactor:网格总仓位需要达到的利润倍数(默认 0.1)。
  • SlowMaLength:高周期 SMA 的周期数(默认 50)。
  • GridMultiplier:每层加仓的几何倍率(默认 1.5)。
  • BaseOrderVolume:网格首单的交易量(默认 0.1)。
  • MaxDrawdownFraction:允许的最大回撤占初始资产的比例(默认 0.8)。
  • CandleType:交易周期;高周期会自动匹配。

说明

  • 仅处理已收盘的 K 线,避免信号重绘。
  • 策略使用买一/卖一报价估算浮动盈亏;若仅有最新成交价则精度会降低。
  • 当缺少投资组合信息时,回撤保护会被跳过,网格将持续运行直到达到利润目标或手动干预。
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();
	}
}