Berlin Range Index 策略
Berlin Range Index 策略通过基于 ATR 的因子过滤 Choppiness Index,以识别趋势与盘整阶段。当过滤后的指数低于最小阈值时,策略按当前 K 线方向开仓;当指数显示盘整或趋势减弱时,平掉持仓。
细节
- 入场条件:
- 过滤后的指数低于
ChopMin,并根据 K 线方向买入或卖出。
- 过滤后的指数低于
- 出场条件:
- 指数高于
ChopMax或趋势减弱。
- 指数高于
- 止损:无。
- 默认值:
Length= 9ChopMax= 40ChopMin= 10AtrLength= 14LowLookback= 14UseNormalized= trueStdDevLength= 14
- 过滤器:
- 类型:趋势跟随
- 方向:双向
- 指标:Choppiness Index、ATR、Standard Deviation
- 复杂度:中
- 时间框架:任意
- 季节性:无
- 神经网络:无
- 背离:无
- 风险等级:中
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Berlin Range Index strategy.
/// The Choppiness Index(Length) is multiplied by the ATR factor lowest ATR(AtrLength) over LowLookback bars / current ATR, so expanding
/// volatility pushes it down. With UseNormalized the filtered value is rescaled to 0-100 so that its StdDevLength-bar mean minus and
/// plus two standard deviations map to 0 and 100. Below ChopMin the strategy enters in the direction of the candle (reversing an
/// opposite position) and above ChopMax it closes the position.
/// </summary>
public class BerlinRangeIndexStrategy : Strategy
{
private readonly StrategyParam<int> _length;
private readonly StrategyParam<decimal> _chopMax;
private readonly StrategyParam<decimal> _chopMin;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<int> _lowLookback;
private readonly StrategyParam<bool> _useNormalized;
private readonly StrategyParam<int> _stdDevLength;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _atrs = new();
private readonly Queue<decimal> _filtered = new();
/// <summary>
/// Choppiness Index period.
/// </summary>
public int Length
{
get => _length.Value;
set => _length.Value = value;
}
/// <summary>
/// Index level above which the market is ranging and the position closes.
/// </summary>
public decimal ChopMax
{
get => _chopMax.Value;
set => _chopMax.Value = value;
}
/// <summary>
/// Index level below which the market trends and a position opens.
/// </summary>
public decimal ChopMin
{
get => _chopMin.Value;
set => _chopMin.Value = value;
}
/// <summary>
/// ATR period of the filter factor.
/// </summary>
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <summary>
/// Bars over which the lowest ATR is taken.
/// </summary>
public int LowLookback
{
get => _lowLookback.Value;
set => _lowLookback.Value = value;
}
/// <summary>
/// Rescale the filtered index by its standard deviation.
/// </summary>
public bool UseNormalized
{
get => _useNormalized.Value;
set => _useNormalized.Value = value;
}
/// <summary>
/// Bars of the normalization mean and standard deviation.
/// </summary>
public int StdDevLength
{
get => _stdDevLength.Value;
set => _stdDevLength.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public BerlinRangeIndexStrategy()
{
_length = Param(nameof(Length), 9)
.SetGreaterThanZero()
.SetDisplay("Length", "Choppiness Index period", "Indicators");
_chopMax = Param(nameof(ChopMax), 40m)
.SetDisplay("Chop Max", "Index level above which the position closes", "Signals");
_chopMin = Param(nameof(ChopMin), 10m)
.SetDisplay("Chop Min", "Index level below which a position opens", "Signals");
_atrLength = Param(nameof(AtrLength), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR period of the filter factor", "Indicators");
_lowLookback = Param(nameof(LowLookback), 14)
.SetGreaterThanZero()
.SetDisplay("Low Lookback", "Bars over which the lowest ATR is taken", "Indicators");
_useNormalized = Param(nameof(UseNormalized), true)
.SetDisplay("Use Normalized", "Rescale the filtered index by its standard deviation", "Indicators");
_stdDevLength = Param(nameof(StdDevLength), 14)
.SetGreaterThanZero()
.SetDisplay("StdDev Length", "Bars of the normalization mean and standard deviation", "Indicators");
_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();
_atrs.Clear();
_filtered.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_atrs.Clear();
_filtered.Clear();
var chop = new ChoppinessIndex { Length = Length };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(chop, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
var oscillators = CreateChartArea();
if (oscillators != null)
DrawIndicator(oscillators, chop);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue chopValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!chopValue.IsFormed || !atrValue.IsFormed)
return;
var chop = chopValue.GetValue<decimal>();
var atr = atrValue.GetValue<decimal>();
_atrs.Enqueue(atr);
while (_atrs.Count > LowLookback)
_atrs.Dequeue();
if (_atrs.Count < LowLookback || atr <= 0)
return;
var filtered = chop * _atrs.Min() / atr;
_filtered.Enqueue(filtered);
while (_filtered.Count > StdDevLength)
_filtered.Dequeue();
decimal index;
if (UseNormalized)
{
if (_filtered.Count < StdDevLength)
return;
var mean = _filtered.Average();
var variance = _filtered.Sum(v => (v - mean) * (v - mean)) / _filtered.Count;
var std = (decimal)Math.Sqrt((double)variance);
if (std <= 0)
return;
index = Math.Clamp(50m + 50m * (filtered - mean) / (2m * std), 0m, 100m);
}
else
{
index = filtered;
}
if (!IsFormedAndOnlineAndAllowTrading())
return;
var bullish = candle.ClosePrice > candle.OpenPrice;
var bearish = candle.ClosePrice < candle.OpenPrice;
if (index < ChopMin)
{
if (bullish && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (bearish && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
}
else if (index > ChopMax)
{
if (Position > 0)
SellMarket(Position);
else if (Position < 0)
BuyMarket(-Position);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from collections import deque
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ChoppinessIndex, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class berlin_range_index_strategy(Strategy):
"""
Berlin Range Index strategy.
The Choppiness Index(Length) is multiplied by the ATR factor lowest ATR(AtrLength) over LowLookback bars / current ATR, so expanding
volatility pushes it down. With UseNormalized the filtered value is rescaled to 0-100 so that its StdDevLength-bar mean minus and
plus two standard deviations map to 0 and 100. Below ChopMin the strategy enters in the direction of the candle (reversing an
opposite position) and above ChopMax it closes the position.
"""
def __init__(self):
super(berlin_range_index_strategy, self).__init__()
self._length = self.Param("Length", 9).SetGreaterThanZero().SetDisplay("Length", "Choppiness Index period", "Indicators")
self._chop_max = self.Param("ChopMax", 40.0).SetDisplay("Chop Max", "Index level above which the position closes", "Signals")
self._chop_min = self.Param("ChopMin", 10.0).SetDisplay("Chop Min", "Index level below which a position opens", "Signals")
self._atr_length = self.Param("AtrLength", 14).SetGreaterThanZero().SetDisplay("ATR Length", "ATR period of the filter factor", "Indicators")
self._low_lookback = self.Param("LowLookback", 14).SetGreaterThanZero().SetDisplay("Low Lookback", "Bars over which the lowest ATR is taken", "Indicators")
self._use_normalized = self.Param("UseNormalized", True).SetDisplay("Use Normalized", "Rescale the filtered index by its standard deviation", "Indicators")
self._std_dev_length = self.Param("StdDevLength", 14).SetGreaterThanZero().SetDisplay("StdDev Length", "Bars of the normalization mean and standard deviation", "Indicators")
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._atrs = deque()
self._filtered = deque()
def OnReseted(self):
super(berlin_range_index_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(berlin_range_index_strategy, self).OnStarted2(time)
self._reset_state()
chop = ChoppinessIndex()
chop.Length = self._length.Value
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(chop, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
oscillators = self.CreateChartArea()
if oscillators is not None:
self.DrawIndicator(oscillators, chop)
def _process_candle(self, candle, chop_value, atr_value):
if candle.State != CandleStates.Finished:
return
if not chop_value.IsFormed or not atr_value.IsFormed:
return
chop = float(chop_value.GetValue[Decimal](None))
atr = float(atr_value.GetValue[Decimal](None))
low_lookback = self._low_lookback.Value
self._atrs.append(atr)
while len(self._atrs) > low_lookback:
self._atrs.popleft()
if len(self._atrs) < low_lookback or atr <= 0:
return
filtered = chop * min(self._atrs) / atr
std_len = self._std_dev_length.Value
self._filtered.append(filtered)
while len(self._filtered) > std_len:
self._filtered.popleft()
if self._use_normalized.Value:
if len(self._filtered) < std_len:
return
mean = sum(self._filtered) / len(self._filtered)
variance = sum((v - mean) * (v - mean) for v in self._filtered) / len(self._filtered)
std = math.sqrt(variance)
if std <= 0:
return
index = min(100.0, max(0.0, 50.0 + 50.0 * (filtered - mean) / (2.0 * std)))
else:
index = filtered
if not self.IsFormedAndOnlineAndAllowTrading():
return
bullish = candle.ClosePrice > candle.OpenPrice
bearish = candle.ClosePrice < candle.OpenPrice
if index < float(self._chop_min.Value):
if bullish and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif bearish and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
elif index > float(self._chop_max.Value):
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(-self.Position)
def CreateClone(self):
return berlin_range_index_strategy()