Strategie Low Volatility Reversion
Diese Mean-Reversion-Strategie wird nur in ruhigen Märkten aktiviert. Sie misst den ATR über ein Rückblickfenster und tritt ein, wenn die Volatilität unter einen Prozentsatz dieses Durchschnitts fällt und der Preis von seinem gleitenden Durchschnitt abweicht.
Tests zeigen eine durchschnittliche jährliche Rendite von etwa 139%. Es funktioniert am besten auf dem Aktienmarkt.
Durch den Handel gegen kleine Bewegungen in ruhigen Bedingungen zielt es darauf ab, Rückpraller zu erfassen, ohne großen Trends nachzujagen.
Positionen schließen, sobald der Preis den gleitenden Durchschnitt berührt oder der ATR-basierte Stop-Loss erreicht wird.
Details
- Einstiegskriterien: Preis entfernt vom gleitenden Durchschnitt, während ATR unter dem Schwellenwert liegt.
- Long/Short: Beide Richtungen.
- Ausstiegskriterien: Preis kehrt zur MA zurück oder Stop wird ausgelöst.
- Stops: Ja.
- Standardwerte:
MAPeriod= 20AtrPeriod= 14AtrLookbackPeriod= 20AtrThresholdPercent= 50mAtrMultiplier= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Filter:
- Kategorie: Mean Reversion
- Richtung: Beide
- Indikatoren: ATR, MA
- Stops: Ja
- Komplexität: Mittel
- Zeitrahmen: Intraday
- Saisonalität: Nein
- Neuronale Netze: Nein
- Divergenz: Nein
- Risikolevel: Mittel
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 that trades on mean reversion during periods of low volatility.
/// It identifies periods of low ATR and opens positions when price
/// deviates from its moving average, expecting a return to the mean.
/// </summary>
public class LowVolReversionStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<int> _atrLookbackPeriod;
private readonly StrategyParam<decimal> _atrThresholdPercent;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<DataType> _candleType;
private SimpleMovingAverage _atrAverage;
private Order _pendingOrder;
private Unit _stopDistance;
private bool _protectionStarted;
/// <summary>
/// Period for Moving Average calculation.
/// </summary>
public int MAPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Period for ATR calculation.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Lookback period for ATR average calculation.
/// </summary>
public int AtrLookbackPeriod
{
get => _atrLookbackPeriod.Value;
set => _atrLookbackPeriod.Value = value;
}
/// <summary>
/// ATR threshold as percentage of average ATR.
/// </summary>
public decimal AtrThresholdPercent
{
get => _atrThresholdPercent.Value;
set => _atrThresholdPercent.Value = value;
}
/// <summary>
/// ATR multiplier for the stop-loss distance frozen at entry; zero disables the stop.
/// </summary>
public decimal AtrMultiplier
{
get => _atrMultiplier.Value;
set => _atrMultiplier.Value = value;
}
/// <summary>
/// Type of candles used for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize the Low Volatility Reversion strategy.
/// </summary>
public LowVolReversionStrategy()
{
_maPeriod = Param(nameof(MAPeriod), 20).SetGreaterThanZero()
.SetDisplay("MA Period", "Period for Moving Average calculation", "Indicators")
.SetOptimize(10, 50, 5);
_atrPeriod = Param(nameof(AtrPeriod), 14).SetGreaterThanZero()
.SetDisplay("ATR Period", "Period for ATR calculation", "Indicators")
.SetOptimize(7, 21, 7);
_atrLookbackPeriod = Param(nameof(AtrLookbackPeriod), 20).SetGreaterThanZero()
.SetDisplay("ATR Lookback", "Lookback period for ATR average calculation", "Indicators")
.SetOptimize(10, 50, 10);
_atrThresholdPercent = Param(nameof(AtrThresholdPercent), 50m).SetNotNegative()
.SetDisplay("ATR Threshold %", "ATR threshold as percentage of average ATR", "Entry")
.SetOptimize(30m, 90m, 10m);
_atrMultiplier = Param(nameof(AtrMultiplier), 2m).SetNotNegative()
.SetDisplay("ATR Stop Multiplier", "Frozen entry ATR stop distance; zero disables it", "Protection");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
OrderRegistering += order => _pendingOrder = order;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (Security, DataType.Level1)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_atrAverage = null;
_pendingOrder = null;
_stopDistance = null;
_protectionStarted = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_atrAverage = new SimpleMovingAverage { Length = AtrLookbackPeriod, Name = "ATR rolling mean" };
Indicators.Add(_atrAverage);
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();
}
var sma = new SimpleMovingAverage { Length = MAPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(sma, atr, ProcessCandle, false)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection runs before this callback, including between finished candles.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue smaValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished || !atrValue.Indicator.IsFormed)
return;
var atr = atrValue.GetValue<decimal>();
// Feed only fully formed ATR samples; the current sample belongs to the rolling window.
var average = _atrAverage.Process(new DecimalIndicatorValue(_atrAverage, atr, candle.OpenTime) { IsFinal = true }).GetValue<decimal>();
if (!_atrAverage.IsFormed || !smaValue.Indicator.IsFormed || !IsFormedAndOnlineAndAllowTrading())
return;
if (_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
var mean = smaValue.GetValue<decimal>();
// Quiet-market filtering never blocks an existing position's mean-touch exit.
if (Position > 0m && candle.ClosePrice >= mean)
SellMarket(Position);
else if (Position < 0m && candle.ClosePrice <= mean)
BuyMarket(Math.Abs(Position));
else if (Position == 0m && atr > 0m && atr < average * AtrThresholdPercent / 100m)
{
if (candle.ClosePrice < mean) Enter(Sides.Buy, atr);
else if (candle.ClosePrice > mean) Enter(Sides.Sell, atr);
}
}
private void Enter(Sides side, decimal atr)
{
var distance = atr * AtrMultiplier;
_stopDistance ??= new Unit(distance);
// Preserve the Unit reference retained by native cached protection controllers.
_stopDistance.Value = distance;
if (!_protectionStarted && distance > 0m)
{
StartProtection(new Unit(), _stopDistance, useMarketOrders: true, isLocalStop: true);
_protectionStarted = true;
}
RegisterOrder(new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = Volume,
Comment = "Low volatility entry",
});
}
}
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, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Unit, OrderStates, OrderTypes, Sides, Level1Fields
from StockSharp.BusinessEntities import Subscription, Order
from StockSharp.Algo.Indicators import SimpleMovingAverage, AverageTrueRange, DecimalIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class low_vol_reversion_strategy(Strategy):
"""
Low volatility mean reversion strategy.
Trades when ATR is below average, expecting price to revert to MA.
"""
def __init__(self):
super(low_vol_reversion_strategy, self).__init__()
self._ma_period = self.Param("MAPeriod", 20).SetGreaterThanZero().SetDisplay("MA Period", "Period for Moving Average calculation", "Indicators")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero().SetDisplay("ATR Period", "Period for ATR calculation", "Indicators")
self._atr_lookback = self.Param("AtrLookbackPeriod", 20).SetGreaterThanZero().SetDisplay("ATR Lookback", "Lookback period for ATR average calculation", "Indicators")
self._atr_threshold = self.Param("AtrThresholdPercent", 50.0).SetNotNegative().SetDisplay("ATR Threshold %", "ATR threshold as percentage of average ATR", "Entry")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetNotNegative().SetDisplay("ATR Stop Multiplier", "Frozen entry ATR stop distance; zero disables it", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._atr_average = None
self._pending_order = None
self._stop_distance = None
self._protection_started = False
self.OrderRegistering += self._track_pending
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(low_vol_reversion_strategy, self).OnReseted()
self._atr_average = None
self._pending_order = None
self._stop_distance = None
self._protection_started = False
def OnStarted2(self, time):
super(low_vol_reversion_strategy, self).OnStarted2(time)
self._atr_average = None
self._pending_order = None
self._stop_distance = None
self._protection_started = False
self._atr_average = SimpleMovingAverage()
self._atr_average.Length = self._atr_lookback.Value
self._atr_average.Name = "ATR rolling mean"
self.Indicators.Add(self._atr_average)
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()
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(sma, atr, self._process_candle, False).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# Native protection runs before this callback, including between finished candles.
pass
def _process_candle(self, candle, sma_value, atr_value):
if candle.State != CandleStates.Finished or not atr_value.Indicator.IsFormed:
return
atr = atr_value.GetValue[Decimal](None)
# Feed only fully formed ATR values, including the current sample in the window.
indicator_input = DecimalIndicatorValue(self._atr_average, atr, candle.OpenTime)
indicator_input.IsFinal = True
average = self._atr_average.Process(indicator_input).GetValue[Decimal](None)
if not self._atr_average.IsFormed or not sma_value.Indicator.IsFormed or not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
return
mean = sma_value.GetValue[Decimal](None)
# The quiet-market entry filter never blocks a held position's mean-touch exit.
if self.Position > 0 and candle.ClosePrice >= mean:
self.SellMarket(self.Position)
elif self.Position < 0 and candle.ClosePrice <= mean:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0 and atr > 0 and atr < average * Decimal(self._atr_threshold.Value) / Decimal(100):
if candle.ClosePrice < mean:
self._enter(Sides.Buy, atr)
elif candle.ClosePrice > mean:
self._enter(Sides.Sell, atr)
def _enter(self, side, atr):
distance = atr * Decimal(self._atr_multiplier.Value)
if self._stop_distance is None:
self._stop_distance = Unit(distance)
# Keep the same Unit held by native cached protection controllers.
self._stop_distance.Value = distance
if not self._protection_started and 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
order.Comment = "Low volatility entry"
self.RegisterOrder(order)
def CreateClone(self):
return low_vol_reversion_strategy()