布林收窄突破
该策略关注布林带极窄时的低波动,一旦价格突破带宽就顺势入场,动量衰竭或出现反向突破时离场。带宽收窄预示即将爆发的波动,进入后依靠ATR止损或带线交叉退出。
测试表明年均收益约为 100%,该策略在外汇市场表现最佳。
详情
- 入场条件: 基于 Bollinger 的信号
- 多空方向: 双向
- 退出条件: 反向信号
- 止损: 无
- 默认值:
BollingerPeriod= 20BollingerDeviation= 2mSqueezeThreshold= 0.1mCandleType= TimeSpan.FromMinutes(5)
- 过滤器:
- 类型: 趋势
- 方向: 双向
- 指标: Bollinger
- 止损: 无
- 复杂度: 基础
- 时间框架: 日内 (5m)
- 季节性: 无
- 神经网络: 无
- 背离: 无
- 风险等级: 中
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>
/// Strategy based on Bollinger Bands squeeze.
/// Trades outside-band crossings after a narrow finished bar, exits at the middle band,
/// and optionally protects actual fills with an entry-frozen ATR distance.
/// </summary>
public class BollingerSqueezeStrategy : Strategy
{
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerDeviation;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _squeezeThreshold;
private readonly StrategyParam<bool> _useAtrStop;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private decimal _prevBandWidth;
private bool _hasPrevValues;
private decimal _prevClose, _prevUpper, _prevLower;
private Order _pendingOrder, _entryOrder;
private Unit _stopDistance;
private decimal _requestedDistance;
private bool _protectionStarted;
/// <summary>
/// Bollinger Bands period.
/// </summary>
public int BollingerPeriod
{
get => _bollingerPeriod.Value;
set => _bollingerPeriod.Value = value;
}
/// <summary>
/// Bollinger Bands deviation multiplier.
/// </summary>
public decimal BollingerDeviation
{
get => _bollingerDeviation.Value;
set => _bollingerDeviation.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public decimal SqueezeThreshold { get => _squeezeThreshold.Value; set => _squeezeThreshold.Value = value; }
public bool UseAtrStop { get => _useAtrStop.Value; set => _useAtrStop.Value = value; }
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
/// <summary>
/// Initializes a new instance of the <see cref="BollingerSqueezeStrategy"/>.
/// </summary>
public BollingerSqueezeStrategy()
{
_bollingerPeriod = Param(nameof(BollingerPeriod), 20)
.SetDisplay("Bollinger Period", "Period for Bollinger Bands", "Indicators")
.SetOptimize(15, 30, 5);
_bollingerDeviation = Param(nameof(BollingerDeviation), 2m)
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_squeezeThreshold = Param(nameof(SqueezeThreshold), 0.1m).SetGreaterThanZero()
.SetDisplay("Squeeze Threshold", "Maximum preceding-bar band-width/middle ratio.", "Signal");
_useAtrStop = Param(nameof(UseAtrStop), false)
.SetDisplay("Use ATR Stop", "Optional actual-fill local ATR protection.", "Protection");
_atrPeriod = Param(nameof(AtrPeriod), 14).SetGreaterThanZero()
.SetDisplay("ATR Period", "Wilder ATR lookback when protection is enabled.", "Protection");
_atrMultiplier = Param(nameof(AtrMultiplier), 2m).SetNotNegative()
.SetDisplay("ATR Multiplier", "Frozen signal ATR distance; zero disables the stop.", "Protection");
OrderRegistering += order => _pendingOrder = order;
Trades.TradeAdded += ProcessEntryFill;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return UseAtrStop ? [(Security, CandleType), (Security, DataType.Level1)] : [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevBandWidth = default;
_hasPrevValues = default;
_prevClose = _prevUpper = _prevLower = 0m;
_pendingOrder = _entryOrder = null;
_stopDistance = null;
_requestedDistance = 0m;
_protectionStarted = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var bb = new BollingerBands
{
Length = BollingerPeriod,
Width = BollingerDeviation
};
var subscription = SubscribeCandles(CandleType);
AverageTrueRange atr = null;
if (UseAtrStop)
{
atr = new AverageTrueRange { Length = AtrPeriod };
subscription.BindEx(bb, atr, (candle, bands, value) => ProcessCandle(candle, bands, value.GetValue<decimal>()), false);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
}
else
subscription.BindEx(bb, (candle, bands) => ProcessCandle(candle, bands, 0m), false);
subscription.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, bb);
if (atr is not null) DrawIndicator(area, atr);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// The high-level handler activates optional native protection before this callback, also between bars.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue bbValue, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var bb = (IBollingerBandsValue)bbValue;
if (bb.UpBand is not decimal upper ||
bb.LowBand is not decimal lower ||
bb.MovingAverage is not decimal middle)
return;
if (middle == 0)
return;
var bandWidth = (upper - lower) / middle;
if (!_hasPrevValues)
{
_hasPrevValues = true;
SavePrevious(candle.ClosePrice, upper, lower, bandWidth);
return;
}
if (_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
{
SavePrevious(candle.ClosePrice, upper, lower, bandWidth);
return;
}
var price = candle.ClosePrice;
var narrow = _prevBandWidth <= SqueezeThreshold;
if (narrow && _prevClose <= _prevUpper && price > upper && Position <= 0m)
Enter(Sides.Buy, atrValue);
else if (narrow && _prevClose >= _prevLower && price < lower && Position >= 0m)
Enter(Sides.Sell, atrValue);
else if (Position > 0m && price <= middle)
SellMarket(Position);
else if (Position < 0m && price >= middle)
BuyMarket(Math.Abs(Position));
SavePrevious(price, upper, lower, bandWidth);
}
private void SavePrevious(decimal close, decimal upper, decimal lower, decimal width)
{
_prevClose = close;
_prevUpper = upper;
_prevLower = lower;
_prevBandWidth = width;
}
private void Enter(Sides side, decimal atr)
{
_requestedDistance = UseAtrStop ? atr * AtrMultiplier : 0m;
_stopDistance ??= new Unit(_requestedDistance);
if (Position == 0m) _stopDistance.Value = _requestedDistance;
if (!_protectionStarted && _requestedDistance > 0m)
{
StartProtection(new Unit(), _stopDistance, useMarketOrders: true, isLocalStop: true);
_protectionStarted = true;
}
_entryOrder = new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = Volume + Math.Abs(Position),
Comment = "Squeeze signal",
};
RegisterOrder(_entryOrder);
}
private void ProcessEntryFill(MyTrade trade)
{
// Keep the old native distance until a partial reversal actually changes position direction.
if (trade.Order == _entryOrder && Position != 0m && (Position > 0m) == (trade.Order.Side == Sides.Buy))
_stopDistance.Value = _requestedDistance;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, OrderStates, OrderTypes, Sides, Level1Fields
from StockSharp.BusinessEntities import Subscription, Order
from StockSharp.Algo.Indicators import BollingerBands, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class bollinger_squeeze_strategy(Strategy):
"""Preceding-bar squeeze, outside-band crossing, middle exit and optional actual-fill ATR stop."""
def __init__(self):
super(bollinger_squeeze_strategy, self).__init__()
self._bollinger_period = self.Param("BollingerPeriod", 20) \
.SetDisplay("Bollinger Period", "Period for Bollinger Bands", "Indicators")
self._bollinger_deviation = self.Param("BollingerDeviation", 2.0) \
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._squeeze_threshold = self.Param("SqueezeThreshold", 0.1).SetGreaterThanZero() \
.SetDisplay("Squeeze Threshold", "Maximum preceding-bar band-width/middle ratio.", "Signal")
self._use_atr_stop = self.Param("UseAtrStop", False) \
.SetDisplay("Use ATR Stop", "Optional actual-fill local ATR protection.", "Protection")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero() \
.SetDisplay("ATR Period", "Wilder ATR lookback when protection is enabled.", "Protection")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetNotNegative() \
.SetDisplay("ATR Multiplier", "Frozen signal ATR distance; zero disables the stop.", "Protection")
self._reset_state()
self.OrderRegistering += self._track_pending
self.Trades.TradeAdded += self._process_entry_fill
def GetWorkingSecurities(self):
result = [(self.Security, self.candle_type)]
if self._use_atr_stop.Value:
result.append((self.Security, DataType.Level1))
return result
def _track_pending(self, order):
self._pending_order = order
@property
def bollinger_period(self):
return self._bollinger_period.Value
@property
def bollinger_deviation(self):
return self._bollinger_deviation.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(bollinger_squeeze_strategy, self).OnReseted()
self._reset_state()
def _reset_state(self):
self._prev_band_width = Decimal.Zero
self._has_prev_values = False
self._prev_close = self._prev_upper = self._prev_lower = Decimal.Zero
self._pending_order = self._entry_order = None
self._stop_distance = None
self._requested_distance = Decimal.Zero
self._protection_started = False
def OnStarted2(self, time):
super(bollinger_squeeze_strategy, self).OnStarted2(time)
bb = BollingerBands()
bb.Length = self.bollinger_period
bb.Width = Decimal(self.bollinger_deviation)
subscription = self.SubscribeCandles(self.candle_type)
atr = None
if self._use_atr_stop.Value:
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
subscription.BindEx(bb, atr, self._process_with_atr, False)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
else:
subscription.BindEx(bb, self._process_without_atr, False)
subscription.Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, bb)
if atr is not None:
self.DrawIndicator(area, atr)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native protection runs before this callback, including between signal bars.
pass
def _process_with_atr(self, candle, bb_value, atr_value):
self._process_candle(candle, bb_value, atr_value.GetValue[Decimal](None))
def _process_without_atr(self, candle, bb_value):
self._process_candle(candle, bb_value, Decimal.Zero)
def _process_candle(self, candle, bb_value, atr):
if candle.State != CandleStates.Finished or not self.IsFormedAndOnlineAndAllowTrading():
return
if bb_value.UpBand is None or bb_value.LowBand is None or bb_value.MovingAverage is None:
return
upper = bb_value.UpBand
lower = bb_value.LowBand
middle = bb_value.MovingAverage
if middle == 0:
return
band_width = (upper - lower) / middle
if not self._has_prev_values:
self._has_prev_values = True
self._save_previous(candle.ClosePrice, upper, lower, band_width)
return
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
self._save_previous(candle.ClosePrice, upper, lower, band_width)
return
price = candle.ClosePrice
narrow = self._prev_band_width <= Decimal(self._squeeze_threshold.Value)
if narrow and self._prev_close <= self._prev_upper and price > upper and self.Position <= 0:
self._enter(Sides.Buy, atr)
elif narrow and self._prev_close >= self._prev_lower and price < lower and self.Position >= 0:
self._enter(Sides.Sell, atr)
elif self.Position > 0 and price <= middle:
self.SellMarket(self.Position)
elif self.Position < 0 and price >= middle:
self.BuyMarket(Math.Abs(self.Position))
self._save_previous(price, upper, lower, band_width)
def _save_previous(self, close, upper, lower, width):
self._prev_close = close
self._prev_upper = upper
self._prev_lower = lower
self._prev_band_width = width
def _enter(self, side, atr):
self._requested_distance = atr * Decimal(self._atr_multiplier.Value) if self._use_atr_stop.Value else Decimal.Zero
if self._stop_distance is None:
self._stop_distance = Unit(self._requested_distance)
if self.Position == 0:
self._stop_distance.Value = self._requested_distance
if not self._protection_started and self._requested_distance > 0:
self.StartProtection(Unit(), self._stop_distance, useMarketOrders=True, isLocalStop=True)
self._protection_started = True
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = self.Volume + Math.Abs(self.Position)
order.Comment = "Squeeze signal"
self._entry_order = order
self.RegisterOrder(order)
def _process_entry_fill(self, trade):
# Preserve the old distance until an actual reversal fill changes direction.
if (trade.Order == self._entry_order and self.Position != 0
and (self.Position > 0) == (trade.Order.Side == Sides.Buy)):
self._stop_distance.Value = self._requested_distance
def CreateClone(self):
return bollinger_squeeze_strategy()