波动市场网格策略
概述
该策略在 StockSharp 高级 API 上重现 MetaTrader 专家顾问“Gridtrading_at_volatile_market.mq4”。它在更高周期上跟踪 Donchian 通道的触碰,并在交易周期上通过吞没形态确认入场。一旦网格被激活,价格每向不利方向移动多个 ATR,就会按几何级数加仓,并在达到既定利润或触发投资组合回撤限制时平掉全部仓位。
运作方式
- 策略订阅两个级别的 K 线:用户选择的交易周期,以及根据映射自动推导的更高周期(M1→M5→M15→M30→H1→H4→D1)。
- 在高周期上计算以下指标:
ATR(20)用于确定网格级距;SMA(SlowMaLength)与 RSI 一起进行趋势过滤;DonchianChannels(20)提供动态支撑与阻力。
- 在交易周期上,策略保存最近两根已完成的 K 线以检测多头或空头吞没形态。
- 当前一根 K 线触及 Donchian 下轨、形成多头吞没并且 RSI 表示超卖 (
RSI < 35且收盘价高于高周期 SMA) 时开启多头网格;空头网格在上轨、RSI > 65的镜像条件下触发。 - 首笔市价单成交后,策略记录初始价格。如果价格相对仓位不利移动
2 * ATR(按当前网格层级计算),则以GridMultiplier的倍率加码下一单。 - 满足以下任一条件时取消所有挂单并平掉仓位:
- 总盈亏(含已实现与浮动)超过
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();
}
}
import clr
from collections import deque
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class grid_trading_at_volatile_market_strategy(Strategy):
"""
Grid Trading at Volatile Market: RSI + SMA trend filter for initial entry
then manages a grid of averaging orders based on distance threshold.
"""
def __init__(self):
super(grid_trading_at_volatile_market_strategy, self).__init__()
self._rsi_period = self.Param("RsiPeriod", 14) \
.SetDisplay("RSI Period", "RSI period for entry signals", "Indicators")
self._sma_period = self.Param("SmaPeriod", 50) \
.SetDisplay("SMA Period", "SMA period for trend filter", "Indicators")
self._max_grid_levels = self.Param("MaxGridLevels", 3) \
.SetDisplay("Max Grid Levels", "Maximum averaging levels", "Grid")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe for signal detection", "General")
self._sma_queue = deque()
self._sma_sum = 0.0
self._grid_direction = None # 'buy' or 'sell'
self._grid_level = 0
self._last_entry_price = 0.0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(grid_trading_at_volatile_market_strategy, self).OnReseted()
self._sma_queue = deque()
self._sma_sum = 0.0
self._grid_direction = None
self._grid_level = 0
self._last_entry_price = 0.0
def OnStarted2(self, time):
super(grid_trading_at_volatile_market_strategy, self).OnStarted2(time)
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(rsi, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, rsi)
self.DrawOwnTrades(area)
def _process_candle(self, candle, rsi_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
rsi = float(rsi_val)
self._sma_queue.append(close)
self._sma_sum += close
sma_period = self._sma_period.Value
while len(self._sma_queue) > sma_period:
self._sma_sum -= self._sma_queue.popleft()
if len(self._sma_queue) < sma_period:
return
sma_value = self._sma_sum / len(self._sma_queue)
if self._grid_direction is None:
if rsi < 35 and close < sma_value:
self.BuyMarket()
self._grid_direction = 'buy'
self._grid_level = 1
self._last_entry_price = close
elif rsi > 65 and close > sma_value:
self.SellMarket()
self._grid_direction = 'sell'
self._grid_level = 1
self._last_entry_price = close
else:
distance_threshold = self._last_entry_price * 0.005
if self._grid_direction == 'buy':
if self._grid_level < self._max_grid_levels.Value and close < self._last_entry_price - distance_threshold:
self.BuyMarket()
self._grid_level += 1
self._last_entry_price = close
elif close > sma_value and rsi > 50:
if self.Position > 0:
self.SellMarket()
self._grid_direction = None
self._grid_level = 0
elif self._grid_direction == 'sell':
if self._grid_level < self._max_grid_levels.Value and close > self._last_entry_price + distance_threshold:
self.SellMarket()
self._grid_level += 1
self._last_entry_price = close
elif close < sma_value and rsi < 50:
if self.Position < 0:
self.BuyMarket()
self._grid_direction = None
self._grid_level = 0
def CreateClone(self):
return grid_trading_at_volatile_market_strategy()