Импульс по ROC
Стратегия основана на резких импульсах индикатора Rate of Change (ROC).
Тестирование показывает среднегодичную доходность около 91%. Стратегию лучше запускать на фондовом рынке.
ROC Impulse фиксирует внезапные всплески в индикаторе ROC. Резкие положительные пики приводят к открытию длинных позиций, а сильные отрицательные — к коротким. Когда импульс затухает и возвращается к нулю, позиция закрывается.
Уровни срабатывания можно настроить так, чтобы реагировать только на исключительные импульсные события. Стопы на основе ATR помогают избежать больших потерь, если всплеск быстро разворачивается.
Подробности
- Критерии входа: сигналы на основе ATR, ROC и Momentum.
- Длинные/короткие: оба направления.
- Критерии выхода: противоположный сигнал или стоп.
- Стопы: да.
- Параметры по умолчанию:
RocPeriod= 12AtrMultiplier= 2мCandleType= TimeSpan.FromMinutes(5)
- Фильтры:
- Категория: Тренд
- Направление: Оба
- Индикаторы: ATR, ROC, Momentum
- Стопы: Да
- Сложность: Базовая
- Таймфрейм: Внутридневной (5м)
- Сезонность: Нет
- Нейронные сети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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>
/// Signed percentage ROC breakouts, zero exits and native actual-fill ATR protection.
/// </summary>
public class RocImpulseStrategy : Strategy
{
private readonly StrategyParam<int> _rocPeriod;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<decimal> _thresholdPercent;
private decimal _prevRoc;
private bool _hasPrevValues;
private Unit _stopDistance;
private bool _protectionStarted;
private decimal _requestedDistance;
private Order _entryOrder;
private Order _pendingOrder;
/// <summary>
/// Percentage rate-of-change period.
/// </summary>
public int RocPeriod
{
get => _rocPeriod.Value;
set => _rocPeriod.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
public decimal ThresholdPercent { get => _thresholdPercent.Value; set => _thresholdPercent.Value = value; }
/// <summary>
/// Initializes a new instance of the <see cref="RocImpulseStrategy"/>.
/// </summary>
public RocImpulseStrategy()
{
_rocPeriod = Param(nameof(RocPeriod), 12)
.SetDisplay("ROC Period", "Lookback for percentage rate of change.", "Indicators")
.SetOptimize(8, 20, 4);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_atrPeriod = Param(nameof(AtrPeriod), 14).SetGreaterThanZero()
.SetDisplay("ATR Period", "Wilder ATR lookback.", "Indicators");
_atrMultiplier = Param(nameof(AtrMultiplier), 2m).SetNotNegative()
.SetDisplay("ATR Multiplier", "Entry ATR stop distance; zero disables it.", "Protection");
_thresholdPercent = Param(nameof(ThresholdPercent), 0.5m).SetGreaterThanZero()
.SetDisplay("Threshold (%)", "Symmetric percentage ROC breakout level.", "Signal");
OrderRegistering += order => _pendingOrder = order;
Trades.TradeAdded += ProcessEntryFill;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (Security, DataType.Level1)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevRoc = default;
_hasPrevValues = default;
_stopDistance = null;
_protectionStarted = false;
_requestedDistance = 0m;
_entryOrder = _pendingOrder = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var roc = new RateOfChange { Length = RocPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
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 subscription = SubscribeCandles(CandleType);
subscription
.Bind(roc, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, roc);
DrawIndicator(area, atr);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// The high-level handler activates native protection before the callback, including between bars.
}
private void ProcessCandle(ICandleMessage candle, decimal rocValue, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (!_hasPrevValues)
{
_hasPrevValues = true;
_prevRoc = rocValue;
return;
}
if (_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
{
_prevRoc = rocValue;
return;
}
if (_prevRoc <= ThresholdPercent && rocValue > ThresholdPercent && Position <= 0m)
Enter(Sides.Buy, atrValue);
else if (_prevRoc >= -ThresholdPercent && rocValue < -ThresholdPercent && Position >= 0m)
Enter(Sides.Sell, atrValue);
else if (Position > 0m && rocValue <= 0m)
SellMarket(Position);
else if (Position < 0m && rocValue >= 0m)
BuyMarket(Math.Abs(Position));
_prevRoc = rocValue;
}
private void Enter(Sides side, decimal atr)
{
_requestedDistance = atr * AtrMultiplier;
_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 = "ROC signal",
};
RegisterOrder(_entryOrder);
}
private void ProcessEntryFill(MyTrade trade)
{
// Preserve the old position's distance until a partial reversal actually changes direction.
// Native processors retain this Unit reference and anchor themselves to actual fills.
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, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Unit, Level1Fields, OrderStates, OrderTypes, Sides
from StockSharp.Algo.Indicators import RateOfChange, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Subscription, Order
class roc_impulse_strategy(Strategy):
"""
Signed percentage ROC breakouts, zero exits and native actual-fill ATR protection.
"""
def __init__(self):
super(roc_impulse_strategy, self).__init__()
self._roc_period = self.Param("RocPeriod", 12) \
.SetDisplay("ROC Period", "Lookback for percentage rate of change.", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero().SetDisplay("ATR Period", "Wilder ATR lookback.", "Indicators")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetNotNegative().SetDisplay("ATR Multiplier", "Entry ATR stop distance; zero disables it.", "Protection")
self._threshold_percent = self.Param("ThresholdPercent", 0.5).SetGreaterThanZero().SetDisplay("Threshold (%)", "Symmetric percentage ROC breakout level.", "Signal")
self._reset_state()
self.OrderRegistering += self._track_pending
self.Trades.TradeAdded += self._process_entry_fill
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(roc_impulse_strategy, self).OnReseted()
self._reset_state()
def _reset_state(self):
self._prev_roc = Decimal.Zero
self._has_prev_values = False
self._stop_distance = None
self._protection_started = False
self._requested_distance = Decimal.Zero
self._entry_order = None
self._pending_order = None
def OnStarted2(self, time):
super(roc_impulse_strategy, self).OnStarted2(time)
roc = RateOfChange()
roc.Length = self._roc_period.Value
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
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()
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(roc, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, roc)
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_candle(self, candle, roc, atr):
if candle.State != CandleStates.Finished or not self.IsFormedAndOnlineAndAllowTrading():
return
if not self._has_prev_values:
self._has_prev_values = True
self._prev_roc = roc
return
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
self._prev_roc = roc
return
threshold = Decimal(self._threshold_percent.Value)
if self._prev_roc <= threshold and roc > threshold and self.Position <= 0:
self._enter(Sides.Buy, atr)
elif self._prev_roc >= -threshold and roc < -threshold and self.Position >= 0:
self._enter(Sides.Sell, atr)
elif self.Position > 0 and roc <= 0:
self.SellMarket(self.Position)
elif self.Position < 0 and roc >= 0:
self.BuyMarket(Math.Abs(self.Position))
self._prev_roc = roc
def _enter(self, side, atr):
self._requested_distance = atr * Decimal(self._atr_multiplier.Value)
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 = "ROC signal"
self._entry_order = order
self.RegisterOrder(order)
def _process_entry_fill(self, trade):
# Keep the old distance until an actual reversal fill changes the position's 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 roc_impulse_strategy()