Стратегия Breakouts With Timefilter
Стратегия на пробои, входящая при пересечении ценой недавних максимумов или минимумов в заданный торговый интервал. Дополнительный фильтр по скользящей средней подтверждает направление. Стоп‑лосс может рассчитываться по ATR, экстремумам свечей или фиксированным пунктам с настраиваемым соотношением риск/прибыль.
Детали
- Условия входа:
- Лонг: закрытие > максимум за
Lengthи внутри временного окна; опционально закрытие > MA. - Шорт: закрытие < минимум за
Lengthи внутри временного окна; опционально закрытие < MA.
- Лонг: закрытие > максимум за
- Направление: обе стороны
- Стопы: ATR, по свечам или фиксированные пункты с целевым соотношением
- Значения по умолчанию:
Length= 5MaLength= 99UseMaFilter= falseUseTimeFilter= true (14:30–15:00)SlType= AtrSlLength= 0AtrLength= 14AtrMultiplier= 0.5PointsStop= 50RiskReward= 3CandleType= TimeSpan.FromMinutes(5).TimeFrame()
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>
/// Breakouts with time filter strategy.
/// A close above the highest high of the previous Length candles goes long and a close below their lowest low goes short, only inside
/// the 14:30-15:00 UTC window when UseTimeFilter is set and only on the matching side of the moving average when UseMaFilter is set.
/// The stop is placed by ATR, by the candle extremes or by fixed points, and the target sits RiskReward times the stop distance away.
/// </summary>
public class BreakoutsWithTimeFilterStrategy : Strategy
{
/// <summary>
/// Stop loss placement methods.
/// </summary>
public enum StopLossTypes
{
/// <summary>
/// ATR multiple from the entry close.
/// </summary>
Atr,
/// <summary>
/// Extreme of the recent candles.
/// </summary>
Candle,
/// <summary>
/// Fixed number of price steps.
/// </summary>
Points,
}
private static readonly TimeSpan _sessionStart = new(14, 30, 0);
private static readonly TimeSpan _sessionEnd = new(15, 0, 0);
private readonly StrategyParam<int> _length;
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<bool> _useMaFilter;
private readonly StrategyParam<bool> _useTimeFilter;
private readonly StrategyParam<StopLossTypes> _slType;
private readonly StrategyParam<int> _slLength;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<decimal> _pointsStop;
private readonly StrategyParam<decimal> _riskReward;
private readonly StrategyParam<DataType> _candleType;
private readonly List<(decimal high, decimal low)> _bars = new();
private decimal _stopLevel;
private decimal _targetLevel;
/// <summary>
/// Previous candles the breakout levels span.
/// </summary>
public int Length
{
get => _length.Value;
set => _length.Value = value;
}
/// <summary>
/// Period of the moving average filter.
/// </summary>
public int MaLength
{
get => _maLength.Value;
set => _maLength.Value = value;
}
/// <summary>
/// Require the close on the trade side of the moving average.
/// </summary>
public bool UseMaFilter
{
get => _useMaFilter.Value;
set => _useMaFilter.Value = value;
}
/// <summary>
/// Allow entries only between 14:30 and 15:00 UTC.
/// </summary>
public bool UseTimeFilter
{
get => _useTimeFilter.Value;
set => _useTimeFilter.Value = value;
}
/// <summary>
/// Stop loss placement method.
/// </summary>
public StopLossTypes SlType
{
get => _slType.Value;
set => _slType.Value = value;
}
/// <summary>
/// Earlier candles whose extreme sets a candle-based stop; 0 uses the entry candle only.
/// </summary>
public int SlLength
{
get => _slLength.Value;
set => _slLength.Value = value;
}
/// <summary>
/// ATR period.
/// </summary>
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <summary>
/// ATR multiple of the ATR stop.
/// </summary>
public decimal AtrMultiplier
{
get => _atrMultiplier.Value;
set => _atrMultiplier.Value = value;
}
/// <summary>
/// Stop distance in price steps for the points stop.
/// </summary>
public decimal PointsStop
{
get => _pointsStop.Value;
set => _pointsStop.Value = value;
}
/// <summary>
/// Target distance as a multiple of the stop distance.
/// </summary>
public decimal RiskReward
{
get => _riskReward.Value;
set => _riskReward.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public BreakoutsWithTimeFilterStrategy()
{
_length = Param(nameof(Length), 5)
.SetGreaterThanZero()
.SetDisplay("Length", "Previous candles the breakout levels span", "Breakout");
_maLength = Param(nameof(MaLength), 99)
.SetGreaterThanZero()
.SetDisplay("MA Length", "Period of the moving average filter", "Filters");
_useMaFilter = Param(nameof(UseMaFilter), false)
.SetDisplay("Use MA Filter", "Require the close on the trade side of the moving average", "Filters");
_useTimeFilter = Param(nameof(UseTimeFilter), true)
.SetDisplay("Use Time Filter", "Allow entries only between 14:30 and 15:00 UTC", "Filters");
_slType = Param(nameof(SlType), StopLossTypes.Atr)
.SetDisplay("SL Type", "Stop loss placement method", "Risk");
_slLength = Param(nameof(SlLength), 0)
.SetNotNegative()
.SetDisplay("SL Length", "Earlier candles whose extreme sets a candle stop", "Risk");
_atrLength = Param(nameof(AtrLength), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR period", "Risk");
_atrMultiplier = Param(nameof(AtrMultiplier), 0.5m)
.SetGreaterThanZero()
.SetDisplay("ATR Multiplier", "ATR multiple of the ATR stop", "Risk");
_pointsStop = Param(nameof(PointsStop), 50m)
.SetGreaterThanZero()
.SetDisplay("Points Stop", "Stop distance in price steps", "Risk");
_riskReward = Param(nameof(RiskReward), 3m)
.SetGreaterThanZero()
.SetDisplay("Risk Reward", "Target distance as a multiple of the stop distance", "Risk");
_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 ma = new SimpleMovingAverage { Length = MaLength };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(ma, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ma);
DrawOwnTrades(area);
}
}
private void ResetState()
{
_bars.Clear();
_stopLevel = 0;
_targetLevel = 0;
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue maValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
// Breakout levels come from the candles before this one.
var history = _bars.Count >= Length ? _bars.Skip(_bars.Count - Length).ToList() : null;
_bars.Add((candle.HighPrice, candle.LowPrice));
var keep = Math.Max(Length, SlLength + 1);
while (_bars.Count > keep)
_bars.RemoveAt(0);
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (Position > 0)
{
if (candle.LowPrice <= _stopLevel || candle.HighPrice >= _targetLevel)
{
SellMarket(Position);
return;
}
}
else if (Position < 0)
{
if (candle.HighPrice >= _stopLevel || candle.LowPrice <= _targetLevel)
{
BuyMarket(-Position);
return;
}
}
if (history == null || !atrValue.IsFormed || (UseMaFilter && !maValue.IsFormed))
return;
if (UseTimeFilter)
{
var tod = candle.OpenTime.TimeOfDay;
if (tod < _sessionStart || tod >= _sessionEnd)
return;
}
var close = candle.ClosePrice;
var highest = history.Max(b => b.high);
var lowest = history.Min(b => b.low);
var ma = UseMaFilter ? maValue.GetValue<decimal>() : 0m;
var atr = atrValue.GetValue<decimal>();
if (close > highest && (!UseMaFilter || close > ma) && Position <= 0)
{
var stop = GetStop(true, close, atr);
if (stop >= close)
return;
_stopLevel = stop;
_targetLevel = close + RiskReward * (close - stop);
BuyMarket(Volume + Math.Abs(Position));
}
else if (close < lowest && (!UseMaFilter || close < ma) && Position >= 0)
{
var stop = GetStop(false, close, atr);
if (stop <= close)
return;
_stopLevel = stop;
_targetLevel = close - RiskReward * (stop - close);
SellMarket(Volume + Math.Abs(Position));
}
}
private decimal GetStop(bool isLong, decimal close, decimal atr)
{
switch (SlType)
{
case StopLossTypes.Atr:
return isLong ? close - atr * AtrMultiplier : close + atr * AtrMultiplier;
case StopLossTypes.Candle:
{
// The entry candle plus SlLength earlier candles.
var bars = _bars.Skip(Math.Max(0, _bars.Count - (SlLength + 1))).ToList();
return isLong ? bars.Min(b => b.low) : bars.Max(b => b.high);
}
default:
{
var distance = PointsStop * (Security?.PriceStep ?? 1m);
return isLong ? close - distance : close + distance;
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
SESSION_START = TimeSpan(14, 30, 0)
SESSION_END = TimeSpan(15, 0, 0)
class StopLossTypes:
"""Stop loss placement methods."""
Atr = 0
Candle = 1
Points = 2
class breakouts_with_time_filter_strategy(Strategy):
"""
Breakouts with time filter strategy.
A close above the highest high of the previous Length candles goes long and a close below their lowest low goes short, only inside
the 14:30-15:00 UTC window when UseTimeFilter is set and only on the matching side of the moving average when UseMaFilter is set.
The stop is placed by ATR, by the candle extremes or by fixed points, and the target sits RiskReward times the stop distance away.
"""
def __init__(self):
super(breakouts_with_time_filter_strategy, self).__init__()
self._length = self.Param("Length", 5).SetGreaterThanZero().SetDisplay("Length", "Previous candles the breakout levels span", "Breakout")
self._ma_length = self.Param("MaLength", 99).SetGreaterThanZero().SetDisplay("MA Length", "Period of the moving average filter", "Filters")
self._use_ma_filter = self.Param("UseMaFilter", False).SetDisplay("Use MA Filter", "Require the close on the trade side of the moving average", "Filters")
self._use_time_filter = self.Param("UseTimeFilter", True).SetDisplay("Use Time Filter", "Allow entries only between 14:30 and 15:00 UTC", "Filters")
self._sl_type = self.Param("SlType", StopLossTypes.Atr).SetDisplay("SL Type", "Stop loss placement method", "Risk")
self._sl_length = self.Param("SlLength", 0).SetNotNegative().SetDisplay("SL Length", "Earlier candles whose extreme sets a candle stop", "Risk")
self._atr_length = self.Param("AtrLength", 14).SetGreaterThanZero().SetDisplay("ATR Length", "ATR period", "Risk")
self._atr_multiplier = self.Param("AtrMultiplier", 0.5).SetGreaterThanZero().SetDisplay("ATR Multiplier", "ATR multiple of the ATR stop", "Risk")
self._points_stop = self.Param("PointsStop", 50.0).SetGreaterThanZero().SetDisplay("Points Stop", "Stop distance in price steps", "Risk")
self._risk_reward = self.Param("RiskReward", 3.0).SetGreaterThanZero().SetDisplay("Risk Reward", "Target distance as a multiple of the stop distance", "Risk")
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._bars = []
self._stop_level = Decimal(0)
self._target_level = Decimal(0)
def OnReseted(self):
super(breakouts_with_time_filter_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(breakouts_with_time_filter_strategy, self).OnStarted2(time)
self._reset_state()
ma = SimpleMovingAverage()
ma.Length = self._ma_length.Value
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(ma, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ma)
self.DrawOwnTrades(area)
def _process_candle(self, candle, ma_value, atr_value):
if candle.State != CandleStates.Finished:
return
length = self._length.Value
sl_length = self._sl_length.Value
# Breakout levels come from the candles before this one.
history = list(self._bars[-length:]) if len(self._bars) >= length else None
self._bars.append((candle.HighPrice, candle.LowPrice))
keep = max(length, sl_length + 1)
while len(self._bars) > keep:
self._bars.pop(0)
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self.Position > 0:
if candle.LowPrice <= self._stop_level or candle.HighPrice >= self._target_level:
self.SellMarket(self.Position)
return
elif self.Position < 0:
if candle.HighPrice >= self._stop_level or candle.LowPrice <= self._target_level:
self.BuyMarket(-self.Position)
return
use_ma = self._use_ma_filter.Value
if history is None or not atr_value.IsFormed or (use_ma and not ma_value.IsFormed):
return
if self._use_time_filter.Value:
tod = candle.OpenTime.TimeOfDay
if tod < SESSION_START or tod >= SESSION_END:
return
close = candle.ClosePrice
highest = max(b[0] for b in history)
lowest = min(b[1] for b in history)
ma = ma_value.GetValue[Decimal](None) if use_ma else Decimal(0)
atr = atr_value.GetValue[Decimal](None)
rr = Decimal(self._risk_reward.Value)
if close > highest and (not use_ma or close > ma) and self.Position <= 0:
stop = self._get_stop(True, close, atr)
if stop >= close:
return
self._stop_level = stop
self._target_level = close + rr * (close - stop)
self.BuyMarket(self.Volume + abs(self.Position))
elif close < lowest and (not use_ma or close < ma) and self.Position >= 0:
stop = self._get_stop(False, close, atr)
if stop <= close:
return
self._stop_level = stop
self._target_level = close - rr * (stop - close)
self.SellMarket(self.Volume + abs(self.Position))
def _get_stop(self, is_long, close, atr):
sl_type = self._sl_type.Value
if sl_type == StopLossTypes.Atr:
offset = atr * Decimal(self._atr_multiplier.Value)
return close - offset if is_long else close + offset
if sl_type == StopLossTypes.Candle:
# The entry candle plus SlLength earlier candles.
bars = self._bars[-(self._sl_length.Value + 1):]
return min(b[1] for b in bars) if is_long else max(b[0] for b in bars)
step = self.Security.PriceStep if self.Security is not None and self.Security.PriceStep is not None else Decimal(1)
distance = Decimal(self._points_stop.Value) * step
return close - distance if is_long else close + distance
def CreateClone(self):
return breakouts_with_time_filter_strategy()