Timer Strategy
The Timer strategy recalculates breakout levels at fixed time intervals and trades when price crosses these dynamic thresholds. The levels are positioned using Average True Range (ATR) and an optional additional pip distance. The approach seeks to capture short-term breakouts in either direction.
Every WaitSeconds, the strategy sets:
- Buy level at
close + pipDistance + ATR. - Sell level at
close - pipDistance - ATR.
When the next finished candle closes beyond one of these levels, a market order is placed in the corresponding direction. The position is protected by configurable stop-loss, take-profit, and trailing stop distances.
Trading can be limited to a specific time window using the trading hours settings.
Parameters
WaitSeconds– seconds between level recalculations.PipDistance– additional distance from the current price, in points.AtrPeriod– ATR indicator period.TakeProfit– take-profit distance in points.StopLoss– stop-loss distance in points.TrailingStop– trailing stop distance in points.TradeVolume– order volume.CandleType– candle type for calculations.UseTradingHours– enable time-of-day filter.StartTime– trading start time.StopTime– trading stop time.
How It Works
- Subscribes to candles and calculates ATR.
- On each finished candle:
- If the configured time interval passed, new buy and sell levels are calculated.
- If trading hours are enabled, checks that current time is within the allowed window.
- Places buy or sell market order if price crosses the corresponding level.
- Stop-loss, take-profit, and trailing stop are managed automatically by the strategy infrastructure.
Notes
- The strategy trades both long and short.
- Works on any instrument and timeframe.
- ATR-based levels adapt to market volatility, allowing flexible breakout detection.
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>
/// Timer ATR breakout strategy.
/// </summary>
public class TimerStrategy : Strategy
{
private readonly StrategyParam<int> _waitSeconds;
private readonly StrategyParam<decimal> _pipDistance;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<decimal> _trailingStop;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<bool> _useTradingHours;
private readonly StrategyParam<TimeSpan> _startTime;
private readonly StrategyParam<TimeSpan> _stopTime;
private readonly List<Order> _pendingOrders = [];
private DateTimeOffset? _lastLevelTime;
private decimal? _buyLevel;
private decimal? _sellLevel;
private decimal? _trailExtreme;
public int WaitSeconds { get => _waitSeconds.Value; set => _waitSeconds.Value = value; }
public decimal PipDistance { get => _pipDistance.Value; set => _pipDistance.Value = value; }
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal TakeProfit { get => _takeProfit.Value; set => _takeProfit.Value = value; }
public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }
public decimal TrailingStop { get => _trailingStop.Value; set => _trailingStop.Value = value; }
public decimal TradeVolume { get => _tradeVolume.Value; set => _tradeVolume.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public bool UseTradingHours { get => _useTradingHours.Value; set => _useTradingHours.Value = value; }
public TimeSpan StartTime { get => _startTime.Value; set => _startTime.Value = value; }
public TimeSpan StopTime { get => _stopTime.Value; set => _stopTime.Value = value; }
public TimerStrategy()
{
_waitSeconds = Param(nameof(WaitSeconds), 60).SetGreaterThanZero();
_pipDistance = Param(nameof(PipDistance), 10m).SetNotNegative();
_atrPeriod = Param(nameof(AtrPeriod), 14).SetGreaterThanZero();
_takeProfit = Param(nameof(TakeProfit), 100m).SetNotNegative();
_stopLoss = Param(nameof(StopLoss), 50m).SetNotNegative();
_trailingStop = Param(nameof(TrailingStop), 0m).SetNotNegative();
_tradeVolume = Param(nameof(TradeVolume), 1m).SetGreaterThanZero();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame());
_useTradingHours = Param(nameof(UseTradingHours), false);
_startTime = Param(nameof(StartTime), TimeSpan.Zero);
_stopTime = Param(nameof(StopTime), new TimeSpan(23, 59, 59));
OrderRegistering += _pendingOrders.Add;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_lastLevelTime = null;
_buyLevel = null;
_sellLevel = null;
_trailExtreme = null;
_pendingOrders.Clear();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var point = Security?.PriceStep ?? 1m;
if (point <= 0m)
point = 1m;
var take = TakeProfit > 0m ? new Unit(TakeProfit * point, UnitTypes.Absolute) : null;
var stop = StopLoss > 0m ? new Unit(StopLoss * point, UnitTypes.Absolute) : null;
if (take is not null || stop is not null)
StartProtection(take, stop, useMarketOrders: true);
var atr = new AverageTrueRange { Length = AtrPeriod };
SubscribeCandles(CandleType)
.Bind(atr, (candle, atrValue) =>
{
if (candle.State != CandleStates.Finished || !atr.IsFormed || atrValue <= 0m)
return;
ProcessCandle(candle, atrValue);
})
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal atr)
{
var now = candle.CloseTime;
var point = Security?.PriceStep ?? 1m;
if (point <= 0m)
point = 1m;
var position = ApplyTrailingStop(candle, GetExpectedPosition(), point);
if (IsTradingTime(now.TimeOfDay))
{
if (_buyLevel is decimal buy && candle.ClosePrice >= buy && position <= 0m)
{
BuyMarket(TradeVolume + Math.Abs(position));
_trailExtreme = candle.ClosePrice;
}
else if (_sellLevel is decimal sell && candle.ClosePrice <= sell && position >= 0m)
{
SellMarket(TradeVolume + Math.Abs(position));
_trailExtreme = candle.ClosePrice;
}
}
if (_lastLevelTime is null || now - _lastLevelTime.Value >= TimeSpan.FromSeconds(WaitSeconds))
{
(_buyLevel, _sellLevel) = CalculateLevels(candle.ClosePrice, PipDistance, point, atr);
_lastLevelTime = now;
}
}
/// <summary>
/// Position once the orders still in flight are filled. Protection registers its exit before the
/// candle handler runs, so the handler must not close or size against the position that exit closes.
/// </summary>
private decimal GetExpectedPosition()
{
_pendingOrders.RemoveAll(order => order.State is OrderStates.Done or OrderStates.Failed);
var position = Position;
foreach (var order in _pendingOrders)
position += order.Side == Sides.Buy ? order.Balance : -order.Balance;
return position;
}
private decimal ApplyTrailingStop(ICandleMessage candle, decimal position, decimal point)
{
if (position == 0m)
{
_trailExtreme = null;
return position;
}
if (TrailingStop <= 0m || _trailExtreme is not decimal extreme)
return position;
var trail = TrailingStop * point;
// Earlier candles set the extreme: within this candle the order of high and low is unknown.
if (position > 0m)
{
if (candle.LowPrice <= extreme - trail)
{
SellMarket(position);
_trailExtreme = null;
return 0m;
}
_trailExtreme = Math.Max(extreme, candle.HighPrice);
}
else
{
if (candle.HighPrice >= extreme + trail)
{
BuyMarket(-position);
_trailExtreme = null;
return 0m;
}
_trailExtreme = Math.Min(extreme, candle.LowPrice);
}
return position;
}
private bool IsTradingTime(TimeSpan time)
{
if (!UseTradingHours)
return true;
return StartTime <= StopTime
? time >= StartTime && time <= StopTime
: time >= StartTime || time <= StopTime;
}
internal static (decimal buy, decimal sell) CalculateLevels(
decimal close,
decimal pipDistancePoints,
decimal priceStep,
decimal atr)
{
var distance = pipDistancePoints * priceStep + atr;
return (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, Math, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes, OrderStates, Sides
from StockSharp.Algo.Indicators import AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class timer_strategy(Strategy):
def __init__(self):
super(timer_strategy, self).__init__()
self._wait_seconds = self.Param("WaitSeconds", 60).SetGreaterThanZero()
self._pip_distance = self.Param("PipDistance", 10.0).SetNotNegative()
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero()
self._take_profit = self.Param("TakeProfit", 100.0).SetNotNegative()
self._stop_loss = self.Param("StopLoss", 50.0).SetNotNegative()
self._trailing_stop = self.Param("TrailingStop", 0.0).SetNotNegative()
self._trade_volume = self.Param("TradeVolume", 1.0).SetGreaterThanZero()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1)))
self._use_trading_hours = self.Param("UseTradingHours", False)
self._start_time = self.Param("StartTime", TimeSpan.Zero)
self._stop_time = self.Param("StopTime", TimeSpan(23, 59, 59))
self._pending_orders = []
self._last_level_time = None
self._buy_level = None
self._sell_level = None
self._trail_extreme = None
self.OrderRegistering += self._track_pending_order
def GetWorkingSecurities(self):
return [(self.Security, self._candle_type.Value)]
def OnReseted(self):
super(timer_strategy, self).OnReseted()
self._last_level_time = None
self._buy_level = None
self._sell_level = None
self._trail_extreme = None
self._pending_orders = []
def OnStarted2(self, time):
super(timer_strategy, self).OnStarted2(time)
point = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if point <= 0:
point = 1.0
tp = float(self._take_profit.Value)
sl = float(self._stop_loss.Value)
take = Unit(tp * point, UnitTypes.Absolute) if tp > 0 else None
stop = Unit(sl * point, UnitTypes.Absolute) if sl > 0 else None
if take is not None or stop is not None:
self.StartProtection(take, stop, useMarketOrders=True)
atr = AverageTrueRange()
atr.Length = int(self._atr_period.Value)
def on_candle(candle, atr_value):
if candle.State != CandleStates.Finished or not atr.IsFormed or float(atr_value) <= 0:
return
self._process_candle(candle, float(atr_value))
self.SubscribeCandles(self._candle_type.Value).Bind(atr, on_candle).Start()
def _process_candle(self, candle, atr):
now = candle.CloseTime
step = self.Security.PriceStep if self.Security is not None else None
point = step if step is not None and step > 0 else Decimal.One
position = self._apply_trailing_stop(candle, self._expected_position(), point)
if self._is_trading_time(now.TimeOfDay):
close = float(candle.ClosePrice)
volume = float(self._trade_volume.Value) + abs(float(position))
if self._buy_level is not None and close >= self._buy_level and position <= 0:
self.BuyMarket(volume)
self._trail_extreme = candle.ClosePrice
elif self._sell_level is not None and close <= self._sell_level and position >= 0:
self.SellMarket(volume)
self._trail_extreme = candle.ClosePrice
wait = int(self._wait_seconds.Value)
if self._last_level_time is None or (now - self._last_level_time).TotalSeconds >= wait:
self._buy_level, self._sell_level = self.calculate_levels(
float(candle.ClosePrice), float(self._pip_distance.Value), float(point), atr)
self._last_level_time = now
def _track_pending_order(self, order):
self._pending_orders.append(order)
def _expected_position(self):
# Protection registers its exit before the candle handler runs, so the handler must not
# close or size against the position that exit closes.
self._pending_orders = [o for o in self._pending_orders if o.State not in (OrderStates.Done, OrderStates.Failed)]
position = self.Position
for order in self._pending_orders:
if order.Side == Sides.Buy:
position += order.Balance
else:
position -= order.Balance
return position
def _apply_trailing_stop(self, candle, position, point):
if position == 0:
self._trail_extreme = None
return position
trailing_stop = float(self._trailing_stop.Value)
if trailing_stop <= 0 or self._trail_extreme is None:
return position
trail = Decimal(trailing_stop) * point
extreme = self._trail_extreme
# Earlier candles set the extreme: within this candle the order of high and low is unknown.
if position > 0:
if candle.LowPrice <= extreme - trail:
self.SellMarket(position)
self._trail_extreme = None
return Decimal.Zero
self._trail_extreme = Math.Max(extreme, candle.HighPrice)
else:
if candle.HighPrice >= extreme + trail:
self.BuyMarket(Math.Abs(position))
self._trail_extreme = None
return Decimal.Zero
self._trail_extreme = Math.Min(extreme, candle.LowPrice)
return position
def _is_trading_time(self, value):
if not bool(self._use_trading_hours.Value):
return True
start = self._start_time.Value
stop = self._stop_time.Value
return start <= value <= stop if start <= stop else value >= start or value <= stop
@staticmethod
def calculate_levels(close, pip_distance_points, price_step, atr):
distance = pip_distance_points * price_step + atr
return close + distance, close - distance
def CreateClone(self):
return timer_strategy()