ATR Range Breakout 戦略
ATR Range Breakoutは、固定本数のバーにわたる価格変動を計測し、平均真の値幅(ATR)と比較します。変動がATRを超えると、その方向にポジションを建てます。
テストでは年平均リターン約169%を示しています。暗号資産市場での運用に最も適しています。
戦略はNバーごとに価格を確認し、移動平均をエグジットシグナルに使用します。ボラティリティが通常レベルを超えて拡大する際のモメンタムを捉えることを目指しています。
価格が移動平均に戻ってクロスするか、ATRに基づくストップが発動した時点でトレードを決済します。
詳細
- エントリー条件: ルックバック期間中に価格がATRを超えて動く。
- ロング/ショート: 両方向。
- エグジット条件: 価格がMAをクロスまたはストップ。
- ストップ: あり。
- デフォルト値:
MAPeriod= 20ATRPeriod= 14LookbackPeriod= 5CandleType= TimeSpan.FromMinutes(5)
- フィルター:
- カテゴリ: ブレイクアウト
- 方向: 両方
- インジケーター: ATR, MA
- ストップ: はい
- 複雑さ: 基本
- 時間軸: イントラデイ
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: いいえ
- リスクレベル: 中
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>
/// Trades strict N-interval Close movement beyond current Wilder ATR at every Nth bar.
/// Exits on any ready bar's adverse SMA crossing or frozen entry-ATR actual-fill protection.
/// </summary>
public class AtrRangeStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _closes = new();
private long _barCount;
private decimal? _previousClose;
private decimal _previousMean;
private Order _pendingOrder;
private Unit _stopDistance;
private bool _protectionStarted;
public int MAPeriod { get => _maPeriod.Value; set => _maPeriod.Value = value; }
public int ATRPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public int LookbackPeriod { get => _lookbackPeriod.Value; set => _lookbackPeriod.Value = value; }
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public AtrRangeStrategy()
{
_maPeriod = Param(nameof(MAPeriod), 20).SetGreaterThanZero()
.SetDisplay("MA Period", "Period for Moving Average calculation", "Indicators")
.SetOptimize(10, 50, 10);
_atrPeriod = Param(nameof(ATRPeriod), 14).SetGreaterThanZero()
.SetDisplay("ATR Period", "Period for ATR calculation", "Indicators")
.SetOptimize(7, 28, 7);
_lookbackPeriod = Param(nameof(LookbackPeriod), 5).SetGreaterThanZero()
.SetDisplay("Lookback Period", "N intervals for movement and N-bar entry-check cadence", "Entry")
.SetOptimize(3, 10, 1);
_atrMultiplier = Param(nameof(AtrMultiplier), 2m).SetNotNegative()
.SetDisplay("ATR Stop Multiplier", "Frozen entry ATR distance; zero disables only protection", "Protection");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
OrderRegistering += order => _pendingOrder = order;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
private void ResetState()
{
_closes.Clear();
_barCount = 0;
_previousClose = null;
_previousMean = default;
_pendingOrder = null;
_stopDistance = null;
_protectionStarted = false;
}
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
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 ma = new SimpleMovingAverage { Length = MAPeriod };
var atr = new AverageTrueRange { Length = ATRPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.BindEx(ma, atr, ProcessCandle, false).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ma);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// Native protection runs before this callback, including between finished candles.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue maValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
_barCount++;
_closes.Enqueue(candle.ClosePrice);
if (_closes.Count > LookbackPeriod + 1) _closes.Dequeue();
if (!maValue.Indicator.IsFormed || !atrValue.Indicator.IsFormed || !IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
var mean = maValue.GetValue<decimal>();
var downwardCross = _previousClose is decimal down && down >= _previousMean && close < mean;
var upwardCross = _previousClose is decimal up && up <= _previousMean && close > mean;
_previousClose = close;
_previousMean = mean;
if (_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
// Exits run on every ready bar, independent of the entry checkpoint.
if (Position > 0m && downwardCross)
SellMarket(Position);
else if (Position < 0m && upwardCross)
BuyMarket(Math.Abs(Position));
else if (Position == 0m && _barCount % LookbackPeriod == 0 && _closes.Count == LookbackPeriod + 1)
{
// N intervals need N+1 closes, not N closes in a non-overlapping block.
var movement = close - _closes.Peek();
var atr = atrValue.GetValue<decimal>();
if (Math.Abs(movement) > atr)
Enter(movement > 0m ? Sides.Buy : Sides.Sell, atr);
}
}
private void Enter(Sides side, decimal atr)
{
var distance = atr * AtrMultiplier;
_stopDistance ??= new Unit(distance);
// Keep the Unit reference held 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 = "ATR range 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 Order, Subscription
from StockSharp.Algo.Indicators import SimpleMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class atr_range_strategy(Strategy):
"""
Trades strict N-interval Close movement beyond Wilder ATR at every Nth bar.
Exits on any ready bar's adverse SMA crossing or frozen entry-ATR protection.
"""
def __init__(self):
super(atr_range_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._lookback_period = self.Param("LookbackPeriod", 5).SetGreaterThanZero().SetDisplay("Lookback Period", "N intervals for movement and N-bar entry-check cadence", "Entry")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetNotNegative().SetDisplay("ATR Stop Multiplier", "Frozen entry ATR distance; zero disables only protection", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
self.OrderRegistering += self._track_pending
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
def _reset_state(self):
self._closes = []
self._bar_count = 0
self._previous_close = None
self._previous_mean = Decimal.Zero
self._pending_order = None
self._stop_distance = None
self._protection_started = False
def OnReseted(self):
super(atr_range_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(atr_range_strategy, self).OnStarted2(time)
self._reset_state()
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()
ma = SimpleMovingAverage()
ma.Length = self._ma_period.Value
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(ma, atr, self._process_candle, False).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ma)
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, ma_value, atr_value):
if candle.State != CandleStates.Finished:
return
self._bar_count += 1
self._closes.append(candle.ClosePrice)
lookback = self._lookback_period.Value
if len(self._closes) > lookback + 1:
self._closes.pop(0)
if not ma_value.Indicator.IsFormed or not atr_value.Indicator.IsFormed or not self.IsFormedAndOnlineAndAllowTrading():
return
close = candle.ClosePrice
mean = ma_value.GetValue[Decimal](None)
downward_cross = self._previous_close is not None and self._previous_close >= self._previous_mean and close < mean
upward_cross = self._previous_close is not None and self._previous_close <= self._previous_mean and close > mean
self._previous_close = close
self._previous_mean = mean
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
return
# Exits do not wait for the next N-bar entry checkpoint.
if self.Position > 0 and downward_cross:
self.SellMarket(self.Position)
elif self.Position < 0 and upward_cross:
self.BuyMarket(Math.Abs(self.Position))
elif self.Position == 0 and self._bar_count % lookback == 0 and len(self._closes) == lookback + 1:
movement = close - self._closes[0]
atr = atr_value.GetValue[Decimal](None)
if Math.Abs(movement) > atr:
self._enter(Sides.Buy if movement > 0 else 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 = "ATR range entry"
self.RegisterOrder(order)
def CreateClone(self):
return atr_range_strategy()