Стратегия Timer
Стратегия Timer пересчитывает уровни пробоя через заданные интервалы времени и открывает позиции при пересечении ценой этих динамических уровней. Уровни формируются на основе индикатора ATR и дополнительного смещения в пунктах, что позволяет реагировать на краткосрочные импульсы в любую сторону.
Каждые WaitSeconds стратегия устанавливает:
- Уровень покупки на
close + pipDistance + ATR. - Уровень продажи на
close - pipDistance - ATR.
Если следующая завершённая свеча закрывается за пределами одного из уровней, размещается рыночный ордер в соответствующем направлении. Позиция защищается стоп-лоссом, тейк-профитом и трейлинг-стопом.
При необходимости торговля может ограничиваться определённым временным окном.
Параметры
WaitSeconds– интервал в секундах между пересчётом уровней.PipDistance– дополнительное смещение от текущей цены в пунктах.AtrPeriod– период индикатора ATR.TakeProfit– расстояние до тейк-профита в пунктах.StopLoss– расстояние до стоп-лосса в пунктах.TrailingStop– расстояние трейлинг-стопа в пунктах.TradeVolume– объём ордера.CandleType– тип свечей для расчётов.UseTradingHours– включение фильтра торгового времени.StartTime– начало торгового окна.StopTime– конец торгового окна.
Логика работы
- Подписка на свечи и расчёт ATR.
- Для каждой завершённой свечи:
- При истечении заданного интервала пересчитываются уровни.
- При включённом фильтре проверяется попадание во временной диапазон.
- При пересечении ценой соответствующего уровня отправляется рыночный ордер.
- Защитные ордера управляются инфраструктурой стратегии автоматически.
Особенности
- Торговля ведётся в обе стороны.
- Подходит для любого инструмента и таймфрейма.
- Уровни, основанные на ATR, адаптируются к волатильности рынка.
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()