Адаптивный фрактальный сеточный скальпинг
Стратегия размещает лимитные ордера вокруг недавних фрактальных экстремумов, а расстояние рассчитывается по ATR. Тренд определяется простой скользящей средней. Когда волатильность выше порога, в восходящем тренде выставляется лимит на покупку ниже фрактального минимума, а в нисходящем — лимит на продажу выше фрактального максимума. Выход осуществляется на противоположном уровне сетки или по стопу на основе ATR.
Подробности
- Условия входа: ATR выше порога и цена относительно SMA; лимит на покупку от фрактального минимума минус множитель ATR или лимит на продажу от фрактального максимума плюс множитель ATR.
- Длинные/короткие позиции: обе стороны.
- Условия выхода: противоположный уровень сетки или стоп по фракталу.
- Стопы: да.
- Значения по умолчанию:
AtrLength= 14SmaLength= 50GridMultiplierHigh= 2.0mGridMultiplierLow= 0.5mTrailStopMultiplier= 0.5mVolatilityThreshold= 1.0mCandleType= TimeSpan.FromMinutes(5)
- Фильтры:
- Категория: Scalping
- Направление: обе стороны
- Индикаторы: Fractal, ATR, SMA
- Стопы: да
- Сложность: средняя
- Таймфрейм: внутридневной
- Сезонность: нет
- Нейросети: нет
- Дивергенция: нет
- Уровень риска: средний
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>
/// Adaptive Fractal Grid Scalping strategy.
/// Five-bar fractals mark the grid. While ATR is above VolatilityThreshold the strategy keeps a buy limit at the last fractal low minus
/// ATR * GridMultiplierLow when the close is above the SMA, or a sell limit at the last fractal high plus the same distance when it is
/// below. A filled long exits at the opposite grid level (fractal high plus ATR * GridMultiplierHigh) or by a trailing stop
/// ATR * TrailStopMultiplier behind the best price; shorts mirror this.
/// </summary>
public class AdaptiveFractalGridScalpingStrategy : Strategy
{
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<int> _smaLength;
private readonly StrategyParam<decimal> _gridMultiplierHigh;
private readonly StrategyParam<decimal> _gridMultiplierLow;
private readonly StrategyParam<decimal> _trailStopMultiplier;
private readonly StrategyParam<decimal> _volatilityThreshold;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _highs = [];
private readonly List<decimal> _lows = [];
private decimal? _fractalHigh;
private decimal? _fractalLow;
private Order _entryOrder;
private decimal? _target;
private decimal? _trailStop;
/// <summary>
/// ATR period.
/// </summary>
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <summary>
/// Period of the trend SMA.
/// </summary>
public int SmaLength
{
get => _smaLength.Value;
set => _smaLength.Value = value;
}
/// <summary>
/// ATR multiplier of the opposite grid level used as the target.
/// </summary>
public decimal GridMultiplierHigh
{
get => _gridMultiplierHigh.Value;
set => _gridMultiplierHigh.Value = value;
}
/// <summary>
/// ATR multiplier of the entry grid level.
/// </summary>
public decimal GridMultiplierLow
{
get => _gridMultiplierLow.Value;
set => _gridMultiplierLow.Value = value;
}
/// <summary>
/// ATR multiplier of the trailing stop distance.
/// </summary>
public decimal TrailStopMultiplier
{
get => _trailStopMultiplier.Value;
set => _trailStopMultiplier.Value = value;
}
/// <summary>
/// ATR level above which the grid is active.
/// </summary>
public decimal VolatilityThreshold
{
get => _volatilityThreshold.Value;
set => _volatilityThreshold.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public AdaptiveFractalGridScalpingStrategy()
{
_atrLength = Param(nameof(AtrLength), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR period", "Indicators");
_smaLength = Param(nameof(SmaLength), 50)
.SetGreaterThanZero()
.SetDisplay("SMA Length", "Period of the trend SMA", "Indicators");
_gridMultiplierHigh = Param(nameof(GridMultiplierHigh), 2.0m)
.SetNotNegative()
.SetDisplay("Grid Multiplier High", "ATR multiplier of the opposite grid level used as the target", "Grid");
_gridMultiplierLow = Param(nameof(GridMultiplierLow), 0.5m)
.SetNotNegative()
.SetDisplay("Grid Multiplier Low", "ATR multiplier of the entry grid level", "Grid");
_trailStopMultiplier = Param(nameof(TrailStopMultiplier), 0.5m)
.SetNotNegative()
.SetDisplay("Trail Stop Multiplier", "ATR multiplier of the trailing stop distance", "Risk");
_volatilityThreshold = Param(nameof(VolatilityThreshold), 1.0m)
.SetNotNegative()
.SetDisplay("Volatility Threshold", "ATR level above which the grid is active", "Grid");
_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();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
var atr = new AverageTrueRange { Length = AtrLength };
var sma = new SimpleMovingAverage { Length = SmaLength };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(atr, sma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ResetState()
{
_highs.Clear();
_lows.Clear();
_fractalHigh = null;
_fractalLow = null;
_entryOrder = null;
_target = null;
_trailStop = null;
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue atrValue, IIndicatorValue smaValue)
{
if (candle.State != CandleStates.Finished)
return;
UpdateFractals(candle);
if (!atrValue.IsFormed || !smaValue.IsFormed)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var atr = atrValue.ToDecimal();
var sma = smaValue.ToDecimal();
var close = candle.ClosePrice;
if (Position != 0)
{
CancelEntry();
ManagePosition(candle, atr);
return;
}
_target = null;
_trailStop = null;
// A pending order that is still being registered is left alone until its state is known.
if (_entryOrder != null && _entryOrder.State is OrderStates.None or OrderStates.Pending)
return;
CancelEntry();
if (atr <= VolatilityThreshold)
return;
var offset = atr * GridMultiplierLow;
if (close > sma && _fractalLow is decimal low)
_entryOrder = BuyLimit(RoundPrice(low - offset), Volume);
else if (close < sma && _fractalHigh is decimal high)
_entryOrder = SellLimit(RoundPrice(high + offset), Volume);
}
private void ManagePosition(ICandleMessage candle, decimal atr)
{
var trailDistance = atr * TrailStopMultiplier;
if (Position > 0)
{
_target ??= _fractalHigh is decimal fh ? fh + atr * GridMultiplierHigh : null;
var newStop = candle.HighPrice - trailDistance;
_trailStop = _trailStop is decimal s ? Math.Max(s, newStop) : newStop;
if ((_target is decimal t && candle.HighPrice >= t) || candle.ClosePrice <= _trailStop)
SellMarket(Position);
}
else
{
_target ??= _fractalLow is decimal fl ? fl - atr * GridMultiplierHigh : null;
var newStop = candle.LowPrice + trailDistance;
_trailStop = _trailStop is decimal s ? Math.Min(s, newStop) : newStop;
if ((_target is decimal t && candle.LowPrice <= t) || candle.ClosePrice >= _trailStop)
BuyMarket(-Position);
}
}
private void UpdateFractals(ICandleMessage candle)
{
_highs.Add(candle.HighPrice);
_lows.Add(candle.LowPrice);
if (_highs.Count > 5)
{
_highs.RemoveAt(0);
_lows.RemoveAt(0);
}
if (_highs.Count < 5)
return;
// The middle bar is a fractal when it beats the two bars on each side.
var midHigh = _highs[2];
if (midHigh > _highs[0] && midHigh > _highs[1] && midHigh > _highs[3] && midHigh > _highs[4])
_fractalHigh = midHigh;
var midLow = _lows[2];
if (midLow < _lows[0] && midLow < _lows[1] && midLow < _lows[3] && midLow < _lows[4])
_fractalLow = midLow;
}
private void CancelEntry()
{
if (_entryOrder != null && _entryOrder.State == OrderStates.Active)
CancelOrder(_entryOrder);
_entryOrder = null;
}
private decimal RoundPrice(decimal price)
{
var step = Security.PriceStep ?? 0m;
return step > 0 ? Math.Round(price / step) * step : price;
}
}
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, OrderStates
from StockSharp.Algo.Indicators import AverageTrueRange, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class adaptive_fractal_grid_scalping_strategy(Strategy):
"""
Adaptive Fractal Grid Scalping strategy.
Five-bar fractals mark the grid. While ATR is above VolatilityThreshold the strategy keeps a buy limit at the last fractal low minus
ATR * GridMultiplierLow when the close is above the SMA, or a sell limit at the last fractal high plus the same distance when it is
below. A filled long exits at the opposite grid level (fractal high plus ATR * GridMultiplierHigh) or by a trailing stop
ATR * TrailStopMultiplier behind the best price; shorts mirror this.
"""
def __init__(self):
super(adaptive_fractal_grid_scalping_strategy, self).__init__()
self._atr_length = self.Param("AtrLength", 14).SetGreaterThanZero().SetDisplay("ATR Length", "ATR period", "Indicators")
self._sma_length = self.Param("SmaLength", 50).SetGreaterThanZero().SetDisplay("SMA Length", "Period of the trend SMA", "Indicators")
self._grid_multiplier_high = self.Param("GridMultiplierHigh", 2.0).SetNotNegative().SetDisplay("Grid Multiplier High", "ATR multiplier of the opposite grid level used as the target", "Grid")
self._grid_multiplier_low = self.Param("GridMultiplierLow", 0.5).SetNotNegative().SetDisplay("Grid Multiplier Low", "ATR multiplier of the entry grid level", "Grid")
self._trail_stop_multiplier = self.Param("TrailStopMultiplier", 0.5).SetNotNegative().SetDisplay("Trail Stop Multiplier", "ATR multiplier of the trailing stop distance", "Risk")
self._volatility_threshold = self.Param("VolatilityThreshold", 1.0).SetNotNegative().SetDisplay("Volatility Threshold", "ATR level above which the grid is active", "Grid")
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._highs = []
self._lows = []
self._fractal_high = None
self._fractal_low = None
self._entry_order = None
self._target = None
self._trail_stop = None
def OnReseted(self):
super(adaptive_fractal_grid_scalping_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(adaptive_fractal_grid_scalping_strategy, self).OnStarted2(time)
self._reset_state()
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
sma = SimpleMovingAverage()
sma.Length = self._sma_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(atr, sma, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _process_candle(self, candle, atr_value, sma_value):
if candle.State != CandleStates.Finished:
return
self._update_fractals(candle)
if not atr_value.IsFormed or not sma_value.IsFormed:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
atr = atr_value.GetValue[Decimal](None)
sma = sma_value.GetValue[Decimal](None)
close = candle.ClosePrice
if self.Position != 0:
self._cancel_entry()
self._manage_position(candle, atr)
return
self._target = None
self._trail_stop = None
# A pending order that is still being registered is left alone until its state is known.
if self._entry_order is not None and self._entry_order.State != OrderStates.Active and self._entry_order.State != OrderStates.Done and self._entry_order.State != OrderStates.Failed:
return
self._cancel_entry()
if atr <= Decimal(self._volatility_threshold.Value):
return
offset = atr * Decimal(self._grid_multiplier_low.Value)
if close > sma and self._fractal_low is not None:
self._entry_order = self.BuyLimit(self._round_price(self._fractal_low - offset), self.Volume)
elif close < sma and self._fractal_high is not None:
self._entry_order = self.SellLimit(self._round_price(self._fractal_high + offset), self.Volume)
def _manage_position(self, candle, atr):
trail_distance = atr * Decimal(self._trail_stop_multiplier.Value)
grid_high = Decimal(self._grid_multiplier_high.Value)
if self.Position > 0:
if self._target is None and self._fractal_high is not None:
self._target = self._fractal_high + atr * grid_high
new_stop = candle.HighPrice - trail_distance
self._trail_stop = new_stop if self._trail_stop is None else max(self._trail_stop, new_stop)
if (self._target is not None and candle.HighPrice >= self._target) or candle.ClosePrice <= self._trail_stop:
self.SellMarket(self.Position)
else:
if self._target is None and self._fractal_low is not None:
self._target = self._fractal_low - atr * grid_high
new_stop = candle.LowPrice + trail_distance
self._trail_stop = new_stop if self._trail_stop is None else min(self._trail_stop, new_stop)
if (self._target is not None and candle.LowPrice <= self._target) or candle.ClosePrice >= self._trail_stop:
self.BuyMarket(-self.Position)
def _update_fractals(self, candle):
self._highs.append(candle.HighPrice)
self._lows.append(candle.LowPrice)
if len(self._highs) > 5:
self._highs.pop(0)
self._lows.pop(0)
if len(self._highs) < 5:
return
# The middle bar is a fractal when it beats the two bars on each side.
h = self._highs
if h[2] > h[0] and h[2] > h[1] and h[2] > h[3] and h[2] > h[4]:
self._fractal_high = h[2]
l = self._lows
if l[2] < l[0] and l[2] < l[1] and l[2] < l[3] and l[2] < l[4]:
self._fractal_low = l[2]
def _cancel_entry(self):
if self._entry_order is not None and self._entry_order.State == OrderStates.Active:
self.CancelOrder(self._entry_order)
self._entry_order = None
def _round_price(self, price):
step = self.Security.PriceStep
if step is None or step <= 0:
return price
return Math.Round(price / step) * step
def CreateClone(self):
return adaptive_fractal_grid_scalping_strategy()