Bands 策略
概述
该策略把 MetaTrader 5 智能交易系统 Bands.mq5 迁移到了 StockSharp 的高级 API。系统等待一根已经收盘的 K 线从布林带 外部重新收回到通道内部,且只有在唐奇安通道的斜率在可配置的周期内保持稳定时才会开仓。平均真实波幅(ATR)的倍数 用于重建原策略的止损与止盈距离,同时引入线性回归诊断模块,每累计 100 笔成交就输出一次权益曲线的决定系数 R-squa red,与 MQL 版本的调试信息保持一致。
交易逻辑
- 订阅单一时间框架的蜡烛数据,并计算布林带、唐奇安通道以及与原策略一致周期的 ATR。
- 当没有持仓时,检查上一根完成的蜡烛:
- 如果开盘价低于布林带下轨且收盘价重新站上轨道,同时唐奇安下轨在
ConfirmationPeriod根 K 线内未下跌,则开多。 - 如果开盘价高于布林带上轨且收盘价跌回通道,同时唐奇安上轨在
ConfirmationPeriod根 K 线内未上升,则开空。
- 如果开盘价低于布林带下轨且收盘价重新站上轨道,同时唐奇安下轨在
- 当存在持仓时,如果上一根 K 线的收盘价突破了相应方向的唐奇安边界,或当前蜡烛的最高/最低价触发 ATR 倍数保护, 则立即平仓。
- 每当收到自己的成交回报,就记录当前投资组合权益,并在每累计 100 笔成交后打印一次回归的 R-squared。若斜率为负, 输出同样为负值以贴合原始 EA 的处理方式。
风险管理
- 所有入场都以
TradeVolume指定的净数量发送市价单。 - 止损与止盈不通过挂单设置,而是根据蜡烛的最高价和最低价与 ATR 倍数进行比对,在代码中自行触发。
- 当保护条件满足时,以市价单立即平掉全部仓位,并清空当前的保护价格。
参数
| 参数 | 说明 |
|---|---|
TradeVolume |
每次下单的净手数。 |
CandleType |
用于计算所有指标的蜡烛类型 / 时间框架。 |
BollingerPeriod |
布林带的计算周期。 |
BollingerDeviation |
布林带的标准差倍数。 |
DonchianPeriod |
唐奇安通道的长度,用作趋势过滤。 |
ConfirmationPeriod |
要求唐奇安斜率保持不变的最小连续根数。 |
AtrPeriod |
ATR 的计算周期。 |
StopAtrMultiplier |
止损所使用的 ATR 倍数。 |
TakeAtrMultiplier |
止盈所使用的 ATR 倍数。 |
说明
- 唐奇安斜率的判定通过递增计数器实现,无需复制整段指标缓冲区,既符合项目约束也能复现原策略逻辑。
- 所有代码注释和日志信息均为英文,以符合仓库的统一要求。
- 原 MQL 脚本中的资金管理与保证金校验函数未复刻,StockSharp 版本改由
TradeVolume参数直接控制仓位规模。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Bollinger Bands breakout strategy confirmed by Donchian channel slope and ATR-based risk management.
/// </summary>
public class BandsStrategy : Strategy
{
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerDeviation;
private readonly StrategyParam<int> _donchianPeriod;
private readonly StrategyParam<int> _confirmationPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _stopAtrMultiplier;
private readonly StrategyParam<decimal> _takeAtrMultiplier;
private decimal? _prevOpen;
private decimal? _prevClose;
private decimal? _prevLowerBand;
private decimal? _prevUpperBand;
private decimal? _prevDonchLower;
private decimal? _prevDonchUpper;
private decimal? _prevAtr;
private int _lowerTrendLength;
private int _upperTrendLength;
private decimal? _stopLossPrice;
private decimal? _takeProfitPrice;
private int _equitySamples;
private decimal _sumIndices;
private decimal _sumEquity;
private decimal _sumIndexEquity;
private decimal _sumIndexSquared;
private decimal _sumEquitySquared;
/// <summary>
/// Initializes a new instance of <see cref="BandsStrategy"/>.
/// </summary>
public BandsStrategy()
{
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Volume", "Net volume in lots sent with every order", "Trading")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Time frame used for indicator calculations", "Market Data");
_bollingerPeriod = Param(nameof(BollingerPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("Bollinger Period", "Number of candles used for the Bollinger Bands", "Indicators")
;
_bollingerDeviation = Param(nameof(BollingerDeviation), 1m)
.SetGreaterThanZero()
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for the Bollinger Bands", "Indicators")
;
_donchianPeriod = Param(nameof(DonchianPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("Donchian Period", "Donchian Channel length used as trend filter", "Indicators")
;
_confirmationPeriod = Param(nameof(ConfirmationPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("Slope Confirmation", "Minimum number of bars that must keep the Donchian slope intact", "Indicators")
;
_atrPeriod = Param(nameof(AtrPeriod), 21)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "Length of the Average True Range used for stops", "Indicators")
;
_stopAtrMultiplier = Param(nameof(StopAtrMultiplier), 4m)
.SetGreaterThanZero()
.SetDisplay("Stop ATR Multiplier", "How many ATRs below/above the entry to place the stop", "Risk")
;
_takeAtrMultiplier = Param(nameof(TakeAtrMultiplier), 4m)
.SetGreaterThanZero()
.SetDisplay("Take ATR Multiplier", "How many ATRs below/above the entry to place the target", "Risk")
;
}
/// <summary>
/// Trade volume in lots.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Candle type used for analysis.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Period of the Bollinger Bands.
/// </summary>
public int BollingerPeriod
{
get => _bollingerPeriod.Value;
set => _bollingerPeriod.Value = value;
}
/// <summary>
/// Deviation multiplier of the Bollinger Bands.
/// </summary>
public decimal BollingerDeviation
{
get => _bollingerDeviation.Value;
set => _bollingerDeviation.Value = value;
}
/// <summary>
/// Period of the Donchian Channel.
/// </summary>
public int DonchianPeriod
{
get => _donchianPeriod.Value;
set => _donchianPeriod.Value = value;
}
/// <summary>
/// Number of consecutive bars required to confirm the Donchian slope.
/// </summary>
public int ConfirmationPeriod
{
get => _confirmationPeriod.Value;
set => _confirmationPeriod.Value = value;
}
/// <summary>
/// Period of the Average True Range indicator.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in ATR multiples.
/// </summary>
public decimal StopAtrMultiplier
{
get => _stopAtrMultiplier.Value;
set => _stopAtrMultiplier.Value = value;
}
/// <summary>
/// Take-profit distance expressed in ATR multiples.
/// </summary>
public decimal TakeAtrMultiplier
{
get => _takeAtrMultiplier.Value;
set => _takeAtrMultiplier.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevOpen = null;
_prevClose = null;
_prevLowerBand = null;
_prevUpperBand = null;
_prevDonchLower = null;
_prevDonchUpper = null;
_prevAtr = null;
_lowerTrendLength = 0;
_upperTrendLength = 0;
_stopLossPrice = null;
_takeProfitPrice = null;
_equitySamples = 0;
_sumIndices = 0m;
_sumEquity = 0m;
_sumIndexEquity = 0m;
_sumIndexSquared = 0m;
_sumEquitySquared = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var bollinger = new BollingerBands
{
Length = BollingerPeriod,
Width = BollingerDeviation
};
var atr = new AverageTrueRange
{
Length = AtrPeriod
};
var donchian = new DonchianChannels
{
Length = DonchianPeriod
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(bollinger, atr, donchian, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue bollingerVal, IIndicatorValue atrVal, IIndicatorValue donchianVal)
{
if (candle.State != CandleStates.Finished)
return;
if (!bollingerVal.IsFormed || !atrVal.IsFormed || !donchianVal.IsFormed)
return;
var bollingerComplex = (ComplexIndicatorValue<BollingerBands>)bollingerVal;
var middle = bollingerComplex.InnerValues.ElementAt(0).Value.GetValue<decimal>();
var upper = bollingerComplex.InnerValues.ElementAt(1).Value.GetValue<decimal>();
var lower = bollingerComplex.InnerValues.ElementAt(2).Value.GetValue<decimal>();
var atrValue = atrVal.GetValue<decimal>();
var donchianComplex = (ComplexIndicatorValue<DonchianChannels>)donchianVal;
var donchUpper = donchianComplex.InnerValues.ElementAt(0).Value.GetValue<decimal>();
var donchLower = donchianComplex.InnerValues.ElementAt(1).Value.GetValue<decimal>();
var lowerTrendLength = CalculateLowerTrendLength(donchLower);
var upperTrendLength = CalculateUpperTrendLength(donchUpper);
if (!_prevOpen.HasValue)
{
CachePreviousValues(candle, lower, upper, donchLower, donchUpper, atrValue, lowerTrendLength, upperTrendLength);
return;
}
var previousOpen = _prevOpen.Value;
var previousClose = _prevClose!.Value;
var previousLowerBand = _prevLowerBand!.Value;
var previousUpperBand = _prevUpperBand!.Value;
var previousDonchLower = _prevDonchLower!.Value;
var previousDonchUpper = _prevDonchUpper!.Value;
var atrForStops = _prevAtr ?? atrValue;
if (Position == 0m)
{
if (previousOpen < previousLowerBand && previousClose > previousLowerBand && lowerTrendLength > ConfirmationPeriod)
{
OpenLong(candle.ClosePrice, atrForStops);
}
else if (previousOpen > previousUpperBand && previousClose < previousUpperBand && upperTrendLength > ConfirmationPeriod)
{
OpenShort(candle.ClosePrice, atrForStops);
}
}
else if (Position > 0m)
{
var exitVolume = Position;
var stopTriggered = _stopLossPrice is decimal stop && candle.LowPrice <= stop;
var takeTriggered = _takeProfitPrice is decimal take && candle.HighPrice >= take;
if (stopTriggered || takeTriggered || previousClose > previousDonchUpper || previousClose < previousDonchLower)
{
SellMarket(exitVolume);
ClearProtection();
}
}
else if (Position < 0m)
{
var exitVolume = Math.Abs(Position);
var stopTriggered = _stopLossPrice is decimal stop && candle.HighPrice >= stop;
var takeTriggered = _takeProfitPrice is decimal take && candle.LowPrice <= take;
if (stopTriggered || takeTriggered || previousClose < previousDonchLower || previousClose > previousDonchUpper)
{
BuyMarket(exitVolume);
ClearProtection();
}
}
CachePreviousValues(candle, lower, upper, donchLower, donchUpper, atrValue, lowerTrendLength, upperTrendLength);
}
private int CalculateLowerTrendLength(decimal currentLower)
{
if (_prevDonchLower is decimal prevLower)
{
return currentLower >= prevLower ? _lowerTrendLength + 1 : 1;
}
return 1;
}
private int CalculateUpperTrendLength(decimal currentUpper)
{
if (_prevDonchUpper is decimal prevUpper)
{
return currentUpper <= prevUpper ? _upperTrendLength + 1 : 1;
}
return 1;
}
private void CachePreviousValues(ICandleMessage candle, decimal lower, decimal upper, decimal donchLower, decimal donchUpper, decimal atrValue, int lowerTrendLength, int upperTrendLength)
{
_prevOpen = candle.OpenPrice;
_prevClose = candle.ClosePrice;
_prevLowerBand = lower;
_prevUpperBand = upper;
_prevDonchLower = donchLower;
_prevDonchUpper = donchUpper;
_prevAtr = atrValue;
_lowerTrendLength = lowerTrendLength;
_upperTrendLength = upperTrendLength;
}
private void OpenLong(decimal entryPrice, decimal atrValue)
{
var volume = TradeVolume;
if (volume <= 0m)
return;
BuyMarket(volume);
AssignProtection(entryPrice, atrValue, true);
}
private void OpenShort(decimal entryPrice, decimal atrValue)
{
var volume = TradeVolume;
if (volume <= 0m)
return;
SellMarket(volume);
AssignProtection(entryPrice, atrValue, false);
}
private void AssignProtection(decimal entryPrice, decimal atrValue, bool isLong)
{
if (atrValue <= 0m)
{
ClearProtection();
return;
}
var stopDistance = atrValue * StopAtrMultiplier;
var takeDistance = atrValue * TakeAtrMultiplier;
if (isLong)
{
_stopLossPrice = entryPrice - stopDistance;
_takeProfitPrice = entryPrice + takeDistance;
}
else
{
_stopLossPrice = entryPrice + stopDistance;
_takeProfitPrice = entryPrice - takeDistance;
}
}
private void ClearProtection()
{
_stopLossPrice = null;
_takeProfitPrice = null;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
var portfolio = Portfolio;
if (portfolio == null)
return;
UpdateEquityStatistics(portfolio.CurrentValue ?? 0m);
}
private void UpdateEquityStatistics(decimal equity)
{
var index = (decimal)_equitySamples;
_sumIndices += index;
_sumEquity += equity;
_sumIndexEquity += index * equity;
_sumIndexSquared += index * index;
_sumEquitySquared += equity * equity;
_equitySamples++;
if (_equitySamples % 100 != 0)
return;
var n = (decimal)_equitySamples;
if (n <= 1m)
return;
var denominator = n * _sumIndexSquared - _sumIndices * _sumIndices;
if (denominator == 0m)
return;
var slope = (n * _sumIndexEquity - _sumIndices * _sumEquity) / denominator;
var mean = _sumEquity / n;
var ssTotal = _sumEquitySquared - n * mean * mean;
if (ssTotal == 0m)
{
LogInfo("Equity R-squared: 1.0000");
return;
}
var regressionComponent = slope * (_sumIndexEquity - (_sumIndices / n) * _sumEquity);
var rSquared = regressionComponent / ssTotal;
if (slope < 0m)
rSquared = -rSquared;
LogInfo($"Equity R-squared: {rSquared:F4}");
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import BollingerBands, AverageTrueRange, DonchianChannels
from StockSharp.Algo.Strategies import Strategy
class bands_strategy(Strategy):
"""Bollinger Bands breakout confirmed by Donchian channel slope and ATR-based stops."""
def __init__(self):
super(bands_strategy, self).__init__()
self._trade_volume = self.Param("TradeVolume", 0.1) \
.SetGreaterThanZero() \
.SetDisplay("Volume", "Net volume in lots sent with every order", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Time frame used for indicator calculations", "Market Data")
self._bollinger_period = self.Param("BollingerPeriod", 100) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Period", "Number of candles used for Bollinger Bands", "Indicators")
self._bollinger_deviation = self.Param("BollingerDeviation", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier", "Indicators")
self._donchian_period = self.Param("DonchianPeriod", 100) \
.SetGreaterThanZero() \
.SetDisplay("Donchian Period", "Donchian Channel length", "Indicators")
self._confirmation_period = self.Param("ConfirmationPeriod", 100) \
.SetGreaterThanZero() \
.SetDisplay("Slope Confirmation", "Min bars for Donchian slope confirmation", "Indicators")
self._atr_period = self.Param("AtrPeriod", 21) \
.SetGreaterThanZero() \
.SetDisplay("ATR Period", "Length of Average True Range", "Indicators")
self._stop_atr_multiplier = self.Param("StopAtrMultiplier", 4.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop ATR Multiplier", "ATRs for stop placement", "Risk")
self._take_atr_multiplier = self.Param("TakeAtrMultiplier", 4.0) \
.SetGreaterThanZero() \
.SetDisplay("Take ATR Multiplier", "ATRs for target placement", "Risk")
self._prev_open = None
self._prev_close = None
self._prev_lower_band = None
self._prev_upper_band = None
self._prev_donch_lower = None
self._prev_donch_upper = None
self._prev_atr = None
self._lower_trend_length = 0
self._upper_trend_length = 0
self._stop_loss_price = None
self._take_profit_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def TradeVolume(self):
return self._trade_volume.Value
@property
def BollingerPeriod(self):
return self._bollinger_period.Value
@property
def BollingerDeviation(self):
return self._bollinger_deviation.Value
@property
def DonchianPeriod(self):
return self._donchian_period.Value
@property
def ConfirmationPeriod(self):
return self._confirmation_period.Value
@property
def AtrPeriod(self):
return self._atr_period.Value
@property
def StopAtrMultiplier(self):
return self._stop_atr_multiplier.Value
@property
def TakeAtrMultiplier(self):
return self._take_atr_multiplier.Value
def OnReseted(self):
super(bands_strategy, self).OnReseted()
self._prev_open = None
self._prev_close = None
self._prev_lower_band = None
self._prev_upper_band = None
self._prev_donch_lower = None
self._prev_donch_upper = None
self._prev_atr = None
self._lower_trend_length = 0
self._upper_trend_length = 0
self._stop_loss_price = None
self._take_profit_price = None
def OnStarted2(self, time):
super(bands_strategy, self).OnStarted2(time)
bollinger = BollingerBands()
bollinger.Length = self.BollingerPeriod
bollinger.Width = self.BollingerDeviation
atr = AverageTrueRange()
atr.Length = self.AtrPeriod
donchian = DonchianChannels()
donchian.Length = self.DonchianPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(bollinger, atr, donchian, self._process_candle).Start()
def _process_candle(self, candle, bb_val, atr_val, donch_val):
if candle.State != CandleStates.Finished:
return
if not bb_val.IsFormed or not atr_val.IsFormed or not donch_val.IsFormed:
return
upper_bb = bb_val.UpBand
lower_bb = bb_val.LowBand
atr_v = float(atr_val)
donch_upper = donch_val.UpperBand
donch_lower = donch_val.LowerBand
if upper_bb is None or lower_bb is None or donch_upper is None or donch_lower is None:
return
upper = float(upper_bb)
lower = float(lower_bb)
d_upper = float(donch_upper)
d_lower = float(donch_lower)
lower_trend = self._calc_lower_trend(d_lower)
upper_trend = self._calc_upper_trend(d_upper)
if self._prev_open is None:
self._cache_values(candle, lower, upper, d_lower, d_upper, atr_v, lower_trend, upper_trend)
return
prev_open = self._prev_open
prev_close = self._prev_close
prev_lower_band = self._prev_lower_band
prev_upper_band = self._prev_upper_band
prev_donch_lower = self._prev_donch_lower
prev_donch_upper = self._prev_donch_upper
atr_for_stops = self._prev_atr if self._prev_atr is not None else atr_v
if self.Position == 0:
if prev_open < prev_lower_band and prev_close > prev_lower_band and lower_trend > self.ConfirmationPeriod:
self._open_long(float(candle.ClosePrice), atr_for_stops)
elif prev_open > prev_upper_band and prev_close < prev_upper_band and upper_trend > self.ConfirmationPeriod:
self._open_short(float(candle.ClosePrice), atr_for_stops)
elif self.Position > 0:
stop_hit = self._stop_loss_price is not None and float(candle.LowPrice) <= self._stop_loss_price
take_hit = self._take_profit_price is not None and float(candle.HighPrice) >= self._take_profit_price
if stop_hit or take_hit or prev_close > prev_donch_upper or prev_close < prev_donch_lower:
self.SellMarket(self.Position)
self._clear_protection()
elif self.Position < 0:
stop_hit = self._stop_loss_price is not None and float(candle.HighPrice) >= self._stop_loss_price
take_hit = self._take_profit_price is not None and float(candle.LowPrice) <= self._take_profit_price
if stop_hit or take_hit or prev_close < prev_donch_lower or prev_close > prev_donch_upper:
self.BuyMarket(abs(self.Position))
self._clear_protection()
self._cache_values(candle, lower, upper, d_lower, d_upper, atr_v, lower_trend, upper_trend)
def _calc_lower_trend(self, current_lower):
if self._prev_donch_lower is not None:
return self._lower_trend_length + 1 if current_lower >= self._prev_donch_lower else 1
return 1
def _calc_upper_trend(self, current_upper):
if self._prev_donch_upper is not None:
return self._upper_trend_length + 1 if current_upper <= self._prev_donch_upper else 1
return 1
def _cache_values(self, candle, lower, upper, d_lower, d_upper, atr_v, lower_trend, upper_trend):
self._prev_open = float(candle.OpenPrice)
self._prev_close = float(candle.ClosePrice)
self._prev_lower_band = lower
self._prev_upper_band = upper
self._prev_donch_lower = d_lower
self._prev_donch_upper = d_upper
self._prev_atr = atr_v
self._lower_trend_length = lower_trend
self._upper_trend_length = upper_trend
def _open_long(self, entry_price, atr_v):
vol = float(self.TradeVolume)
if vol <= 0:
return
self.BuyMarket(vol)
self._assign_protection(entry_price, atr_v, True)
def _open_short(self, entry_price, atr_v):
vol = float(self.TradeVolume)
if vol <= 0:
return
self.SellMarket(vol)
self._assign_protection(entry_price, atr_v, False)
def _assign_protection(self, entry_price, atr_v, is_long):
if atr_v <= 0:
self._clear_protection()
return
stop_dist = atr_v * float(self.StopAtrMultiplier)
take_dist = atr_v * float(self.TakeAtrMultiplier)
if is_long:
self._stop_loss_price = entry_price - stop_dist
self._take_profit_price = entry_price + take_dist
else:
self._stop_loss_price = entry_price + stop_dist
self._take_profit_price = entry_price - take_dist
def _clear_protection(self):
self._stop_loss_price = None
self._take_profit_price = None
def CreateClone(self):
return bands_strategy()