定时器策略
定时器策略按照固定的时间间隔重新计算突破价格,当价格突破这些动态阈值时入场。突破水平由平均真实波幅(ATR)和可选的附加点差确定,用于捕捉双向的短期突破。
每隔 WaitSeconds 秒,策略设置:
- 买入水平:
close + pipDistance + ATR - 卖出水平:
close - pipDistance - ATR
如果下一根完成的K线收盘价超出其中一个水平,就按对应方向发送市价单。仓位通过可配置的止盈、止损和跟踪止损进行保护。
可选的交易时间过滤器允许仅在指定的时间段内交易。
参数
WaitSeconds– 重新计算水平的时间间隔(秒)。PipDistance– 与当前价格的额外距离(点)。AtrPeriod– ATR 指标周期。TakeProfit– 止盈距离(点)。StopLoss– 止损距离(点)。TrailingStop– 跟踪止损距离(点)。TradeVolume– 下单数量。CandleType– 用于计算的K线类型。UseTradingHours– 是否启用交易时间过滤。StartTime– 交易开始时间。StopTime– 交易结束时间。
工作流程
- 订阅K线并计算 ATR。
- 对每根完成的K线:
- 当时间间隔到达时重新计算买卖水平。
- 如果启用时间过滤,则检查当前时间是否在允许范围内。
- 价格突破水平时发送相应方向的市价单。
- 止盈、止损和跟踪止损由策略框架自动管理。
特点
- 支持做多和做空。
- 适用于任何品种和时间周期。
- 基于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()