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>
/// Daily ATR range follower using live best bid/ask and one entry per session.
/// </summary>
public class RangeFollowerStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _triggerPercent;
private decimal? _dailyAtr;
private decimal? _sessionAtr;
private decimal? _bestBid;
private decimal? _bestAsk;
private DateTime _sessionDate;
private decimal _sessionHigh;
private decimal _sessionLow;
private bool _tradedToday;
private bool _skipToday;
private decimal? _stopPrice;
private decimal? _takePrice;
private Unit _takeDistance;
private Unit _stopDistance;
private Order _entryOrder;
private Order _exitOrder;
private Sides _entrySide;
private decimal _entryVolume;
private decimal _entryValue;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public decimal TriggerPercent { get => _triggerPercent.Value; set => _triggerPercent.Value = value; }
public RangeFollowerStrategy()
{
Volume = 0.1m;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Working timeframe used for session boundaries.", "General");
_triggerPercent = Param(nameof(TriggerPercent), 60m)
.SetRange(10m, 90m)
.SetDisplay("Trigger Percent", "Percentage of daily ATR used as the entry/stop distance.", "Signal");
Trades.TradeAdded += ProcessTrade;
OrderRegistering += order =>
{
if (Position != 0m && order.Side == (Position > 0m ? Sides.Sell : Sides.Buy))
_exitOrder = order;
};
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, TimeSpan.FromDays(1).TimeFrame()), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
_dailyAtr = null;
_bestBid = null;
_bestAsk = null;
_takeDistance = _stopDistance = null;
_entryOrder = _exitOrder = null;
_entrySide = default;
_entryVolume = _entryValue = 0m;
ResetSession(default);
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var atr = new AverageTrueRange { Length = 20 };
SubscribeCandles(TimeSpan.FromDays(1).TimeFrame())
.BindEx(atr, ProcessDailyCandle)
.Start();
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ProcessLevel1).Start();
}
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessWorkingCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessDailyCandle(ICandleMessage candle, IIndicatorValue value)
{
if (candle.State != CandleStates.Finished || !value.IsFormed)
return;
_dailyAtr = value.GetValue<decimal>();
}
private void ProcessLevel1(Level1ChangeMessage message)
{
if (message.TryGetDecimal(Level1Fields.BestBidPrice) is decimal bid && bid > 0m)
_bestBid = bid;
if (message.TryGetDecimal(Level1Fields.BestAskPrice) is decimal ask && ask > 0m)
_bestAsk = ask;
if (_bestBid is decimal currentBid && _bestAsk is decimal currentAsk)
{
if (_sessionDate == message.ServerTime.Date)
{
_sessionHigh = Math.Max(_sessionHigh, Math.Max(currentBid, currentAsk));
_sessionLow = Math.Min(_sessionLow, Math.Min(currentBid, currentAsk));
}
if (Position > 0m)
ApplyProtection(currentBid, currentBid);
else if (Position < 0m)
ApplyProtection(currentAsk, currentAsk);
}
EvaluateQuote();
}
private void ProcessWorkingCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var date = candle.OpenTime.Date;
if (_sessionDate != date)
{
if (_sessionDate != default && Position != 0)
Flatten();
ResetSession(date);
_sessionHigh = candle.HighPrice;
_sessionLow = candle.LowPrice;
_sessionAtr = _dailyAtr;
if (_sessionAtr is decimal initialAtr)
_skipToday = _sessionHigh - _sessionLow > initialAtr * TriggerPercent / 100m;
}
else
{
_sessionHigh = Math.Max(_sessionHigh, candle.HighPrice);
_sessionLow = Math.Min(_sessionLow, candle.LowPrice);
}
if (Position != 0)
ApplyProtection(candle.HighPrice, candle.LowPrice);
EvaluateQuote();
}
private void EvaluateQuote()
{
if (_sessionAtr is not decimal atr || _sessionDate == default || CurrentTime.Date != _sessionDate || _tradedToday || _skipToday || Position != 0)
return;
if (IsPending(_entryOrder) || IsPending(_exitOrder) || !IsFormedAndOnlineAndAllowTrading())
return;
if (_bestBid is not decimal bid || _bestAsk is not decimal ask || _sessionLow == 0m || _sessionHigh == 0m)
return;
var trigger = atr * TriggerPercent / 100m;
var residual = atr - trigger;
var longDistance = bid - _sessionLow;
var shortDistance = _sessionHigh - ask;
if (longDistance <= trigger && shortDistance <= trigger)
return;
// If both are true on a wide session, take the side with the larger excursion.
if (longDistance >= shortDistance)
Enter(Sides.Buy, trigger, residual);
else
Enter(Sides.Sell, trigger, residual);
}
private static bool IsPending(Order order)
=> order is not null && order.State is not (OrderStates.Done or OrderStates.Failed);
private void Enter(Sides side, decimal trigger, decimal residual)
{
if (_takeDistance is null)
{
_takeDistance = new Unit(residual);
_stopDistance = new Unit(trigger);
StartProtection(_takeDistance, _stopDistance, useMarketOrders: true, isLocalStop: true);
}
else
{
// The native position controller retains these Unit objects. Reconfigure only while flat.
_takeDistance.Value = residual;
_stopDistance.Value = trigger;
}
_tradedToday = true;
_entrySide = side;
_entryVolume = _entryValue = 0m;
_entryOrder = side == Sides.Buy ? BuyMarket() : SellMarket();
}
private void ProcessTrade(MyTrade trade)
{
if (trade.Order.Side == _entrySide)
{
_entryVolume += trade.Trade.Volume;
_entryValue += trade.Trade.Price * trade.Trade.Volume;
}
else if (_entryVolume > 0m)
{
var closed = Math.Min(_entryVolume, trade.Trade.Volume);
_entryValue -= _entryValue / _entryVolume * closed;
_entryVolume -= closed;
}
if (_entryVolume > 0m)
{
var entry = _entryValue / _entryVolume;
var direction = _entrySide == Sides.Buy ? 1m : -1m;
_stopPrice = entry - direction * _stopDistance.Value;
_takePrice = entry + direction * _takeDistance.Value;
}
else
{
_entryValue = 0m;
_stopPrice = _takePrice = null;
}
}
private void ApplyProtection(decimal high, decimal low)
{
if (Position > 0 &&
((_stopPrice is decimal longStop && low <= longStop) || (_takePrice is decimal longTake && high >= longTake)))
Flatten();
else if (Position < 0 &&
((_stopPrice is decimal shortStop && high >= shortStop) || (_takePrice is decimal shortTake && low <= shortTake)))
Flatten();
}
private void Flatten()
{
if (IsPending(_exitOrder))
return;
if (Position > 0)
_exitOrder = SellMarket(Math.Abs(Position));
else if (Position < 0)
_exitOrder = BuyMarket(Math.Abs(Position));
}
private void ResetSession(DateTime date)
{
_sessionDate = date;
_sessionAtr = null;
_sessionHigh = 0m;
_sessionLow = 0m;
_tradedToday = false;
_skipToday = false;
_stopPrice = null;
_takePrice = null;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Indicators")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates, Level1Fields, Sides, Unit, OrderStates, ITickTradeMessage
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Indicators import AverageTrueRange
_tick_price = clr.GetClrType(ITickTradeMessage).GetProperty("Price")
_tick_volume = clr.GetClrType(ITickTradeMessage).GetProperty("Volume")
class range_follower_strategy(Strategy):
def __init__(self):
super(range_follower_strategy, self).__init__()
self.Volume = Decimal(0.1)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._trigger_percent = self.Param("TriggerPercent", 60.0).SetRange(10.0, 90.0)
self._reset_state()
self.Trades.TradeAdded += self._process_trade
self.OrderRegistering += self._track_exit
def GetWorkingSecurities(self):
return [
(self.Security, self._candle_type.Value),
(self.Security, DataType.TimeFrame(TimeSpan.FromDays(1))),
(self.Security, DataType.Level1),
]
def OnReseted(self):
super(range_follower_strategy, self).OnReseted()
self._reset_state()
def _reset_state(self):
self._daily_atr = None
self._best_bid = None
self._best_ask = None
self._take_distance = None
self._stop_distance = None
self._entry_order = None
self._exit_order = None
self._entry_side = Sides.Buy
self._entry_volume = Decimal.Zero
self._entry_value = Decimal.Zero
self._reset_session(None)
def OnStarted2(self, time):
super(range_follower_strategy, self).OnStarted2(time)
atr = AverageTrueRange()
atr.Length = 20
self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromDays(1))).BindEx(atr, self._process_daily).Start()
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._process_level1).Start()
self.SubscribeCandles(self._candle_type.Value).Bind(self._process_working).Start()
def _process_daily(self, candle, value):
if candle.State != CandleStates.Finished or not value.IsFormed:
return
self._daily_atr = value.GetValue[Decimal](None)
def _process_level1(self, message):
bid = message.Changes[Level1Fields.BestBidPrice] if message.Changes.ContainsKey(Level1Fields.BestBidPrice) else None
ask = message.Changes[Level1Fields.BestAskPrice] if message.Changes.ContainsKey(Level1Fields.BestAskPrice) else None
if bid is not None and bid > 0:
self._best_bid = bid
if ask is not None and ask > 0:
self._best_ask = ask
if self._best_bid is not None and self._best_ask is not None:
if self._session_date == message.ServerTime.Date:
self._session_high = max(self._session_high, self._best_bid, self._best_ask)
self._session_low = min(self._session_low, self._best_bid, self._best_ask)
if self.Position > 0:
self._apply_protection(self._best_bid, self._best_bid)
elif self.Position < 0:
self._apply_protection(self._best_ask, self._best_ask)
self._evaluate_quote()
def _process_working(self, candle):
if candle.State != CandleStates.Finished:
return
date = candle.OpenTime.Date
high = candle.HighPrice
low = candle.LowPrice
if self._session_date != date:
if self._session_date is not None and self.Position != 0:
self._flatten()
self._reset_session(date)
self._session_high = high
self._session_low = low
self._session_atr = self._daily_atr
if self._session_atr is not None:
self._skip_today = high - low > self._session_atr * Decimal(self._trigger_percent.Value) / Decimal(100)
else:
self._session_high = max(self._session_high, high)
self._session_low = min(self._session_low, low)
if self.Position != 0:
self._apply_protection(high, low)
self._evaluate_quote()
def _evaluate_quote(self):
if (self._session_atr is None or self._session_date is None or
self.CurrentTime.Date != self._session_date or self._traded_today or
self._skip_today or self.Position != 0 or self._best_bid is None or self._best_ask is None or
self._session_low == 0 or self._session_high == 0):
return
if self._is_pending(self._entry_order) or self._is_pending(self._exit_order) or not self.IsFormedAndOnlineAndAllowTrading():
return
trigger = self._session_atr * Decimal(self._trigger_percent.Value) / Decimal(100)
residual = self._session_atr - trigger
long_distance = self._best_bid - self._session_low
short_distance = self._session_high - self._best_ask
if long_distance <= trigger and short_distance <= trigger:
return
if long_distance >= short_distance:
self._enter(Sides.Buy, trigger, residual)
else:
self._enter(Sides.Sell, trigger, residual)
def _is_pending(self, order):
return order is not None and order.State not in (OrderStates.Done, OrderStates.Failed)
def _enter(self, side, trigger, residual):
if self._take_distance is None:
self._take_distance = Unit(residual)
self._stop_distance = Unit(trigger)
self.StartProtection(self._take_distance, self._stop_distance, useMarketOrders=True, isLocalStop=True)
else:
# Preserve Unit references held by the native controller; update only between flat sessions.
self._take_distance.Value = residual
self._stop_distance.Value = trigger
self._traded_today = True
self._entry_side = side
self._entry_volume = self._entry_value = Decimal.Zero
self._entry_order = self.BuyMarket() if side == Sides.Buy else self.SellMarket()
def _track_exit(self, order):
if self.Position != 0 and order.Side == (Sides.Sell if self.Position > 0 else Sides.Buy):
self._exit_order = order
def _process_trade(self, trade):
price = _tick_price.GetValue(trade.Trade)
volume = _tick_volume.GetValue(trade.Trade)
if trade.Order.Side == self._entry_side:
self._entry_volume += volume
self._entry_value += price * volume
elif self._entry_volume > 0:
closed = Math.Min(self._entry_volume, volume)
self._entry_value -= self._entry_value / self._entry_volume * closed
self._entry_volume -= closed
if self._entry_volume > 0:
entry = self._entry_value / self._entry_volume
direction = Decimal(1) if self._entry_side == Sides.Buy else Decimal(-1)
self._stop_price = entry - direction * self._stop_distance.Value
self._take_price = entry + direction * self._take_distance.Value
else:
self._entry_value = Decimal.Zero
self._stop_price = self._take_price = None
def _apply_protection(self, high, low):
if self.Position > 0 and ((self._stop_price is not None and low <= self._stop_price) or
(self._take_price is not None and high >= self._take_price)):
self._flatten()
elif self.Position < 0 and ((self._stop_price is not None and high >= self._stop_price) or
(self._take_price is not None and low <= self._take_price)):
self._flatten()
def _flatten(self):
if self._is_pending(self._exit_order):
return
if self.Position > 0:
self._exit_order = self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0:
self._exit_order = self.BuyMarket(Math.Abs(self.Position))
def _reset_session(self, date):
self._session_date = date
self._session_atr = None
self._session_high = Decimal.Zero
self._session_low = Decimal.Zero
self._traded_today = False
self._skip_today = False
self._stop_price = None
self._take_price = None
def CreateClone(self):
return range_follower_strategy()