View on GitHub

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

  1. Subscribes to candles and calculates ATR.
  2. 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.
  3. 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);
	}
}