using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Breakout strategy that prepares daily buy/sell levels at a specified time.
/// The offset and profit targets are derived from the average range of previous days.
/// </summary>
public class TimeBasedRangeBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _checkHour;
private readonly StrategyParam<int> _checkMinute;
private readonly StrategyParam<int> _daysToCheck;
private readonly StrategyParam<int> _checkMode;
private readonly StrategyParam<decimal> _profitFactor;
private readonly StrategyParam<decimal> _lossFactor;
private readonly StrategyParam<decimal> _offsetFactor;
private readonly StrategyParam<int> _closeMode;
private readonly StrategyParam<int> _tradesPerDay;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _lastOpenHour;
private Queue<decimal> _rangeHistory;
private Queue<decimal> _closeDiffHistory;
private DateTime? _currentDay;
private DateTime? _levelsDay;
private decimal _dayHigh;
private decimal _dayLow;
private decimal _buyBreakout;
private decimal _sellBreakout;
private decimal _profitDistance;
private decimal _lossDistance;
private decimal? _previousCheckClose;
private decimal? _currentCheckClose;
private int _tradesOpenedToday;
private bool _levelsReady;
private decimal _entryPrice;
/// <summary>
/// Hour of the day when the reference range is calculated.
/// </summary>
public int CheckHour
{
get => _checkHour.Value;
set => _checkHour.Value = value;
}
/// <summary>
/// Minute of the hour when the reference range is calculated.
/// </summary>
public int CheckMinute
{
get => _checkMinute.Value;
set => _checkMinute.Value = value;
}
/// <summary>
/// Number of previous days used for averaging.
/// </summary>
public int DaysToCheck
{
get => _daysToCheck.Value;
set => _daysToCheck.Value = value;
}
/// <summary>
/// Mode of averaging: 1 - daily range, 2 - absolute close-to-close difference.
/// </summary>
public int CheckMode
{
get => _checkMode.Value;
set => _checkMode.Value = value;
}
/// <summary>
/// Divisor applied to convert the average range into a take-profit distance.
/// </summary>
public decimal ProfitFactor
{
get => _profitFactor.Value;
set => _profitFactor.Value = value;
}
/// <summary>
/// Divisor applied to convert the average range into a stop-loss distance.
/// </summary>
public decimal LossFactor
{
get => _lossFactor.Value;
set => _lossFactor.Value = value;
}
/// <summary>
/// Divisor applied to convert the average range into the breakout offset.
/// </summary>
public decimal OffsetFactor
{
get => _offsetFactor.Value;
set => _offsetFactor.Value = value;
}
/// <summary>
/// Defines whether to flatten at the daily boundary (2 = close on new day).
/// </summary>
public int CloseMode
{
get => _closeMode.Value;
set => _closeMode.Value = value;
}
/// <summary>
/// Maximum number of trades allowed per day.
/// </summary>
public int TradesPerDay
{
get => _tradesPerDay.Value;
set => _tradesPerDay.Value = value;
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Last hour of the day when breakout orders are allowed to remain open.
/// </summary>
public int LastOpenHour
{
get => _lastOpenHour.Value;
set => _lastOpenHour.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public TimeBasedRangeBreakoutStrategy()
{
_checkHour = Param(nameof(CheckHour), 8)
.SetDisplay("Check Hour", "Hour of the day used for daily calculations", "Schedule")
.SetRange(0, 23);
_checkMinute = Param(nameof(CheckMinute), 0)
.SetDisplay("Check Minute", "Minute of the hour used for daily calculations", "Schedule")
.SetRange(0, 59);
_daysToCheck = Param(nameof(DaysToCheck), 7)
.SetGreaterThanZero()
.SetDisplay("Days To Check", "Number of previous days used in averaging", "Averaging")
.SetOptimize(3, 15, 1);
_checkMode = Param(nameof(CheckMode), 1)
.SetDisplay("Check Mode", "1 - use daily range, 2 - use absolute close difference", "Averaging")
.SetRange(1, 2);
_profitFactor = Param(nameof(ProfitFactor), 2m)
.SetGreaterThanZero()
.SetDisplay("Profit Factor", "Divisor applied to average range for take-profit", "Risk")
.SetOptimize(1m, 4m, 0.5m);
_lossFactor = Param(nameof(LossFactor), 2m)
.SetGreaterThanZero()
.SetDisplay("Loss Factor", "Divisor applied to average range for stop-loss", "Risk")
.SetOptimize(1m, 4m, 0.5m);
_offsetFactor = Param(nameof(OffsetFactor), 2m)
.SetGreaterThanZero()
.SetDisplay("Offset Factor", "Divisor applied to average range for breakout levels", "Entries")
.SetOptimize(1m, 4m, 0.5m);
_closeMode = Param(nameof(CloseMode), 1)
.SetDisplay("Close Mode", "1 - keep positions overnight, 2 - close on new day", "Risk")
.SetRange(1, 2);
_tradesPerDay = Param(nameof(TradesPerDay), 1)
.SetGreaterThanZero()
.SetDisplay("Trades Per Day", "Maximum entries allowed within one day", "Risk")
.SetOptimize(1, 3, 1);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Primary candle series used by the strategy", "Data");
_lastOpenHour = Param(nameof(LastOpenHour), 23)
.SetDisplay("Last Open Hour", "Hour after which new trades are not opened", "Schedule")
.SetRange(0, 23);
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_rangeHistory = null;
_closeDiffHistory = null;
_currentDay = null;
_levelsDay = null;
_dayHigh = 0m;
_dayLow = 0m;
_buyBreakout = 0m;
_sellBreakout = 0m;
_profitDistance = 0m;
_lossDistance = 0m;
_previousCheckClose = null;
_currentCheckClose = null;
_tradesOpenedToday = 0;
_levelsReady = false;
_entryPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_rangeHistory = new();
_closeDiffHistory = new();
StartProtection(null, null);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
UpdateDailyState(candle);
TryCalculateLevels(candle);
ManageOpenPosition(candle);
TryEnterPosition(candle);
}
private void UpdateDailyState(ICandleMessage candle)
{
var candleDate = candle.OpenTime.Date;
if (_currentDay is null || candleDate != _currentDay.Value)
{
if (_currentDay is not null)
FinalizePreviousDay();
if (CloseMode == 2 && Position != 0m)
ClosePosition();
_currentDay = candleDate;
_dayHigh = candle.HighPrice;
_dayLow = candle.LowPrice;
_levelsReady = false;
_levelsDay = null;
_currentCheckClose = null;
_tradesOpenedToday = 0;
}
else
{
if (candle.HighPrice > _dayHigh)
_dayHigh = candle.HighPrice;
if (candle.LowPrice < _dayLow)
_dayLow = candle.LowPrice;
}
}
private void FinalizePreviousDay()
{
var dayRange = _dayHigh - _dayLow;
if (dayRange > 0m)
if (_rangeHistory != null)
EnqueueWithLimit(_rangeHistory, dayRange, DaysToCheck);
if (_currentCheckClose is decimal checkClose)
{
if (_previousCheckClose is decimal previousClose)
{
var difference = Math.Abs(checkClose - previousClose);
if (difference > 0m)
if (_closeDiffHistory != null)
EnqueueWithLimit(_closeDiffHistory, difference, DaysToCheck);
}
_previousCheckClose = checkClose;
}
_currentCheckClose = null;
}
private void TryCalculateLevels(ICandleMessage candle)
{
if (candle.OpenTime.Hour != CheckHour || candle.OpenTime.Minute != CheckMinute)
return;
_currentCheckClose = candle.ClosePrice;
if (Position != 0m)
ClosePosition();
if (!TryGetAverage(out var average))
{
_levelsReady = false;
_levelsDay = null;
return;
}
var offset = OffsetFactor > 0m ? average / OffsetFactor : 0m;
_profitDistance = ProfitFactor > 0m ? average / ProfitFactor : 0m;
_lossDistance = LossFactor > 0m ? average / LossFactor : 0m;
_buyBreakout = _dayHigh + offset;
_sellBreakout = _dayLow - offset;
_levelsReady = true;
_levelsDay = _currentDay;
LogInfo($"Levels prepared for {candle.OpenTime:yyyy-MM-dd}. High={_dayHigh}, Low={_dayLow}, Avg={average}, BuyLevel={_buyBreakout}, SellLevel={_sellBreakout}.");
}
private void ManageOpenPosition(ICandleMessage candle)
{
if (Position == 0m)
return;
var entryPrice = _entryPrice;
if (entryPrice == 0m)
return;
if (Position > 0m)
{
var reachedProfit = _profitDistance > 0m && candle.ClosePrice - entryPrice >= _profitDistance;
var reachedLoss = _lossDistance > 0m && entryPrice - candle.ClosePrice >= _lossDistance;
if (reachedProfit || reachedLoss)
SellMarket();
}
else if (Position < 0m)
{
var reachedProfit = _profitDistance > 0m && entryPrice - candle.ClosePrice >= _profitDistance;
var reachedLoss = _lossDistance > 0m && candle.ClosePrice - entryPrice >= _lossDistance;
if (reachedProfit || reachedLoss)
BuyMarket();
}
}
private void TryEnterPosition(ICandleMessage candle)
{
if (!_levelsReady || _levelsDay is null || _currentDay is null)
return;
if (_levelsDay != _currentDay)
return;
if (_tradesOpenedToday >= TradesPerDay)
return;
if (candle.OpenTime.Hour > LastOpenHour)
return;
if (Position != 0m)
return;
if (candle.ClosePrice >= _buyBreakout)
{
BuyMarket();
_entryPrice = candle.ClosePrice;
_tradesOpenedToday++;
}
else if (candle.ClosePrice <= _sellBreakout)
{
SellMarket();
_entryPrice = candle.ClosePrice;
_tradesOpenedToday++;
}
}
private bool TryGetAverage(out decimal average)
{
average = 0m;
var source = CheckMode == 2 ? _closeDiffHistory : _rangeHistory;
if (source == null)
return false;
var sum = 0m;
var count = 0;
foreach (var value in source)
{
sum += value;
count++;
}
if (count == 0)
return false;
average = sum / count;
return true;
}
private static void EnqueueWithLimit(Queue<decimal> queue, decimal value, int limit)
{
queue.Enqueue(value);
while (queue.Count > limit)
queue.Dequeue();
}
private void ClosePosition()
{
if (Position > 0m)
{
SellMarket();
}
else if (Position < 0m)
{
BuyMarket();
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class time_based_range_breakout_strategy(Strategy):
"""Breakout strategy that prepares daily buy/sell levels at a specified time.
The offset and profit targets are derived from the average range of previous days."""
def __init__(self):
super(time_based_range_breakout_strategy, self).__init__()
self._check_hour = self.Param("CheckHour", 8) \
.SetDisplay("Check Hour", "Hour of the day used for daily calculations", "Schedule")
self._check_minute = self.Param("CheckMinute", 0) \
.SetDisplay("Check Minute", "Minute of the hour used for daily calculations", "Schedule")
self._days_to_check = self.Param("DaysToCheck", 7) \
.SetGreaterThanZero() \
.SetDisplay("Days To Check", "Number of previous days used in averaging", "Averaging")
self._check_mode = self.Param("CheckMode", 1) \
.SetDisplay("Check Mode", "1 - use daily range, 2 - use absolute close difference", "Averaging")
self._profit_factor = self.Param("ProfitFactor", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Profit Factor", "Divisor applied to average range for take-profit", "Risk")
self._loss_factor = self.Param("LossFactor", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Loss Factor", "Divisor applied to average range for stop-loss", "Risk")
self._offset_factor = self.Param("OffsetFactor", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Offset Factor", "Divisor applied to average range for breakout levels", "Entries")
self._close_mode = self.Param("CloseMode", 1) \
.SetDisplay("Close Mode", "1 - keep positions overnight, 2 - close on new day", "Risk")
self._trades_per_day = self.Param("TradesPerDay", 1) \
.SetGreaterThanZero() \
.SetDisplay("Trades Per Day", "Maximum entries allowed within one day", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Primary candle series used by the strategy", "Data")
self._last_open_hour = self.Param("LastOpenHour", 23) \
.SetDisplay("Last Open Hour", "Hour after which new trades are not opened", "Schedule")
self._range_history = []
self._close_diff_history = []
self._current_day = None
self._levels_day = None
self._day_high = 0.0
self._day_low = 0.0
self._buy_breakout = 0.0
self._sell_breakout = 0.0
self._profit_distance = 0.0
self._loss_distance = 0.0
self._previous_check_close = None
self._current_check_close = None
self._trades_opened_today = 0
self._levels_ready = False
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def CheckHour(self):
return self._check_hour.Value
@property
def CheckMinute(self):
return self._check_minute.Value
@property
def DaysToCheck(self):
return self._days_to_check.Value
@property
def CheckMode(self):
return self._check_mode.Value
@property
def ProfitFactor(self):
return self._profit_factor.Value
@property
def LossFactor(self):
return self._loss_factor.Value
@property
def OffsetFactor(self):
return self._offset_factor.Value
@property
def CloseMode(self):
return self._close_mode.Value
@property
def TradesPerDay(self):
return self._trades_per_day.Value
@property
def LastOpenHour(self):
return self._last_open_hour.Value
def OnReseted(self):
super(time_based_range_breakout_strategy, self).OnReseted()
self._range_history = []
self._close_diff_history = []
self._current_day = None
self._levels_day = None
self._day_high = 0.0
self._day_low = 0.0
self._buy_breakout = 0.0
self._sell_breakout = 0.0
self._profit_distance = 0.0
self._loss_distance = 0.0
self._previous_check_close = None
self._current_check_close = None
self._trades_opened_today = 0
self._levels_ready = False
self._entry_price = 0.0
def OnStarted2(self, time):
super(time_based_range_breakout_strategy, self).OnStarted2(time)
self._range_history = []
self._close_diff_history = []
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _enqueue_with_limit(self, lst, value, limit):
lst.append(value)
while len(lst) > limit:
lst.pop(0)
def _finalize_previous_day(self):
day_range = self._day_high - self._day_low
if day_range > 0:
self._enqueue_with_limit(self._range_history, day_range, self.DaysToCheck)
if self._current_check_close is not None:
if self._previous_check_close is not None:
difference = abs(self._current_check_close - self._previous_check_close)
if difference > 0:
self._enqueue_with_limit(self._close_diff_history, difference, self.DaysToCheck)
self._previous_check_close = self._current_check_close
self._current_check_close = None
def _try_get_average(self):
source = self._close_diff_history if self.CheckMode == 2 else self._range_history
if len(source) == 0:
return None
return sum(source) / len(source)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
candle_date = candle.OpenTime.Date
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
# Update daily state
if self._current_day is None or candle_date != self._current_day:
if self._current_day is not None:
self._finalize_previous_day()
if self.CloseMode == 2 and self.Position != 0:
if self.Position > 0:
self.SellMarket()
else:
self.BuyMarket()
self._current_day = candle_date
self._day_high = high
self._day_low = low
self._levels_ready = False
self._levels_day = None
self._current_check_close = None
self._trades_opened_today = 0
else:
if high > self._day_high:
self._day_high = high
if low < self._day_low:
self._day_low = low
# Try to calculate levels at the designated time
if candle.OpenTime.Hour == self.CheckHour and candle.OpenTime.Minute == self.CheckMinute:
self._current_check_close = close
if self.Position != 0:
if self.Position > 0:
self.SellMarket()
else:
self.BuyMarket()
average = self._try_get_average()
if average is None:
self._levels_ready = False
self._levels_day = None
else:
offset_factor = float(self.OffsetFactor)
profit_factor = float(self.ProfitFactor)
loss_factor = float(self.LossFactor)
offset = average / offset_factor if offset_factor > 0 else 0.0
self._profit_distance = average / profit_factor if profit_factor > 0 else 0.0
self._loss_distance = average / loss_factor if loss_factor > 0 else 0.0
self._buy_breakout = self._day_high + offset
self._sell_breakout = self._day_low - offset
self._levels_ready = True
self._levels_day = self._current_day
# Manage open position
if self.Position != 0:
entry_price = self._entry_price
if entry_price != 0:
if self.Position > 0:
reached_profit = self._profit_distance > 0 and close - entry_price >= self._profit_distance
reached_loss = self._loss_distance > 0 and entry_price - close >= self._loss_distance
if reached_profit or reached_loss:
self.SellMarket()
elif self.Position < 0:
reached_profit = self._profit_distance > 0 and entry_price - close >= self._profit_distance
reached_loss = self._loss_distance > 0 and close - entry_price >= self._loss_distance
if reached_profit or reached_loss:
self.BuyMarket()
# Try to enter position
if not self._levels_ready or self._levels_day is None or self._current_day is None:
return
if self._levels_day != self._current_day:
return
if self._trades_opened_today >= self.TradesPerDay:
return
if candle.OpenTime.Hour > self.LastOpenHour:
return
if self.Position != 0:
return
if close >= self._buy_breakout:
self.BuyMarket()
self._entry_price = close
self._trades_opened_today += 1
elif close <= self._sell_breakout:
self.SellMarket()
self._entry_price = close
self._trades_opened_today += 1
def CreateClone(self):
return time_based_range_breakout_strategy()