Multi-Band Comparison
Multi-Band Comparison uses SMA, standard deviation and price quantile bands. The strategy goes long when price closes above the upper quantile minus standard deviation for a defined number of bars and exits when price falls below that level for a set number of bars.
Details
- Data: Price candles.
- Entry Criteria:
- Long: Close above (upper quantile - std dev) for
EntryConfirmBarsbars.
- Long: Close above (upper quantile - std dev) for
- Exit Criteria: Close below that line for
ExitConfirmBarsbars. - Stops: None.
- Default Values:
Length= 20BollingerMultiplier= 1UpperQuantile= 0.95EntryConfirmBars= 1ExitConfirmBars= 1
- Filters:
- Category: Statistical
- Direction: Long
- Indicators: SMA, Standard Deviation
- Complexity: Medium
- Risk level: Medium
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Multi-band comparison strategy.
/// The trigger line is the UpperQuantile quantile of the last Length closes minus BollingerMultiplier standard deviations.
/// A long opens after EntryConfirmBars consecutive closes above the line and closes after ExitConfirmBars consecutive closes below it.
/// The SMA middle band is drawn for comparison. Long only, no stops.
/// </summary>
public class MultiBandComparisonStrategy : Strategy
{
private readonly StrategyParam<int> _length;
private readonly StrategyParam<decimal> _bollingerMultiplier;
private readonly StrategyParam<decimal> _upperQuantile;
private readonly StrategyParam<int> _entryConfirmBars;
private readonly StrategyParam<int> _exitConfirmBars;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _closes = new();
private int _aboveCount;
private int _belowCount;
/// <summary>
/// Period of the SMA, standard deviation and quantile window.
/// </summary>
public int Length
{
get => _length.Value;
set => _length.Value = value;
}
/// <summary>
/// Standard deviation multiplier.
/// </summary>
public decimal BollingerMultiplier
{
get => _bollingerMultiplier.Value;
set => _bollingerMultiplier.Value = value;
}
/// <summary>
/// Quantile of the closes that forms the upper band.
/// </summary>
public decimal UpperQuantile
{
get => _upperQuantile.Value;
set => _upperQuantile.Value = value;
}
/// <summary>
/// Consecutive closes above the line required to enter.
/// </summary>
public int EntryConfirmBars
{
get => _entryConfirmBars.Value;
set => _entryConfirmBars.Value = value;
}
/// <summary>
/// Consecutive closes below the line required to exit.
/// </summary>
public int ExitConfirmBars
{
get => _exitConfirmBars.Value;
set => _exitConfirmBars.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public MultiBandComparisonStrategy()
{
_length = Param(nameof(Length), 20)
.SetGreaterThanZero()
.SetDisplay("Length", "Period of the SMA, standard deviation and quantile window", "Bands");
_bollingerMultiplier = Param(nameof(BollingerMultiplier), 1m)
.SetNotNegative()
.SetDisplay("BB Mult", "Standard deviation multiplier", "Bands");
_upperQuantile = Param(nameof(UpperQuantile), 0.95m)
.SetRange(0m, 1m)
.SetDisplay("Upper Quantile", "Quantile of the closes that forms the upper band", "Bands");
_entryConfirmBars = Param(nameof(EntryConfirmBars), 1)
.SetGreaterThanZero()
.SetDisplay("Entry Confirm Bars", "Consecutive closes above the line required to enter", "Trading");
_exitConfirmBars = Param(nameof(ExitConfirmBars), 1)
.SetGreaterThanZero()
.SetDisplay("Exit Confirm Bars", "Consecutive closes below the line required to exit", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
private void ResetState()
{
_closes.Clear();
_aboveCount = 0;
_belowCount = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
var sma = new SimpleMovingAverage { Length = Length };
var std = new StandardDeviation { Length = Length };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(sma, std, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private decimal GetQuantile()
{
var sorted = _closes.OrderBy(c => c).ToArray();
var position = UpperQuantile * (sorted.Length - 1);
var lower = (int)Math.Floor(position);
var upper = Math.Min(lower + 1, sorted.Length - 1);
var fraction = position - lower;
return sorted[lower] + (sorted[upper] - sorted[lower]) * fraction;
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue smaValue, IIndicatorValue stdValue)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
_closes.Enqueue(close);
if (_closes.Count > Length)
_closes.Dequeue();
if (!smaValue.IsFormed || !stdValue.IsFormed || _closes.Count < Length)
return;
var line = GetQuantile() - stdValue.ToDecimal() * BollingerMultiplier;
if (close > line)
{
_aboveCount++;
_belowCount = 0;
}
else if (close < line)
{
_belowCount++;
_aboveCount = 0;
}
else
{
_aboveCount = 0;
_belowCount = 0;
}
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (Position <= 0 && _aboveCount >= EntryConfirmBars)
BuyMarket(Volume + Math.Abs(Position));
else if (Position > 0 && _belowCount >= ExitConfirmBars)
SellMarket(Position);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, StandardDeviation
from StockSharp.Algo.Strategies import Strategy
class multi_band_comparison_strategy(Strategy):
"""
Multi-band comparison strategy.
The trigger line is the UpperQuantile quantile of the last Length closes minus BollingerMultiplier standard deviations.
A long opens after EntryConfirmBars consecutive closes above the line and closes after ExitConfirmBars consecutive closes below it.
The SMA middle band is drawn for comparison. Long only, no stops.
"""
def __init__(self):
super(multi_band_comparison_strategy, self).__init__()
self._length = self.Param("Length", 20).SetGreaterThanZero().SetDisplay("Length", "Period of the SMA, standard deviation and quantile window", "Bands")
self._bollinger_multiplier = self.Param("BollingerMultiplier", 1.0).SetNotNegative().SetDisplay("BB Mult", "Standard deviation multiplier", "Bands")
self._upper_quantile = self.Param("UpperQuantile", 0.95).SetDisplay("Upper Quantile", "Quantile of the closes that forms the upper band", "Bands")
self._entry_confirm_bars = self.Param("EntryConfirmBars", 1).SetGreaterThanZero().SetDisplay("Entry Confirm Bars", "Consecutive closes above the line required to enter", "Trading")
self._exit_confirm_bars = self.Param("ExitConfirmBars", 1).SetGreaterThanZero().SetDisplay("Exit Confirm Bars", "Consecutive closes below the line required to exit", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._closes = []
self._above_count = 0
self._below_count = 0
def OnReseted(self):
super(multi_band_comparison_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(multi_band_comparison_strategy, self).OnStarted2(time)
self._reset_state()
sma = SimpleMovingAverage()
sma.Length = self._length.Value
std = StandardDeviation()
std.Length = self._length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(sma, std, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _get_quantile(self):
ordered = sorted(self._closes)
position = float(self._upper_quantile.Value) * (len(ordered) - 1)
lower = int(math.floor(position))
upper = min(lower + 1, len(ordered) - 1)
fraction = position - lower
return ordered[lower] + (ordered[upper] - ordered[lower]) * fraction
def _process_candle(self, candle, sma_value, std_value):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
length = self._length.Value
self._closes.append(close)
if len(self._closes) > length:
self._closes.pop(0)
if not sma_value.IsFormed or not std_value.IsFormed or len(self._closes) < length:
return
std = float(std_value.GetValue[Decimal](None))
line = self._get_quantile() - std * float(self._bollinger_multiplier.Value)
if close > line:
self._above_count += 1
self._below_count = 0
elif close < line:
self._below_count += 1
self._above_count = 0
else:
self._above_count = 0
self._below_count = 0
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self.Position <= 0 and self._above_count >= self._entry_confirm_bars.Value:
self.BuyMarket(self.Volume + abs(self.Position))
elif self.Position > 0 and self._below_count >= self._exit_confirm_bars.Value:
self.SellMarket(self.Position)
def CreateClone(self):
return multi_band_comparison_strategy()