using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Big Dog range breakout strategy that trades breakouts from a tight range built between configurable hours.
/// </summary>
public class BigDogStrategy : Strategy
{
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _stopHour;
private readonly StrategyParam<decimal> _maxRangePoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _distancePoints;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<DataType> _candleType;
private decimal? _rangeHigh;
private decimal? _rangeLow;
private DateTime? _rangeDate;
private bool _longReady;
private bool _shortReady;
private decimal? _longStopPrice;
private decimal? _longTakeProfitPrice;
private decimal? _shortStopPrice;
private decimal? _shortTakeProfitPrice;
private decimal _longEntryPrice;
private decimal _shortEntryPrice;
private decimal? _bestBid;
private decimal? _bestAsk;
private decimal _adjustedPointSize;
/// <summary>
/// Hour (0-23) when the consolidation range calculation starts.
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Hour (0-23) when the consolidation range calculation stops.
/// </summary>
public int StopHour
{
get => _stopHour.Value;
set => _stopHour.Value = value;
}
/// <summary>
/// Maximum acceptable range height measured in adjusted points.
/// </summary>
public decimal MaxRangePoints
{
get => _maxRangePoints.Value;
set => _maxRangePoints.Value = value;
}
/// <summary>
/// Take-profit distance measured in adjusted points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Minimum distance required between the current price and breakout level, measured in adjusted points.
/// </summary>
public decimal DistancePoints
{
get => _distancePoints.Value;
set => _distancePoints.Value = value;
}
/// <summary>
/// Order volume that will be used for entries.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Candle type used for range calculation and breakout detection.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="BigDogStrategy"/> class.
/// </summary>
public BigDogStrategy()
{
_startHour = Param(nameof(StartHour), 2)
.SetRange(0, 23)
.SetDisplay("Start Hour", "Hour to begin measuring the range", "Session");
_stopHour = Param(nameof(StopHour), 8)
.SetRange(0, 23)
.SetDisplay("Stop Hour", "Hour to stop measuring the range", "Session");
_maxRangePoints = Param(nameof(MaxRangePoints), 50000m)
.SetGreaterThanZero()
.SetDisplay("Max Range", "Maximum allowed height of the consolidation range (points)", "Trading");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 50m)
.SetGreaterThanZero()
.SetDisplay("Take Profit", "Take-profit distance in adjusted points", "Trading");
_distancePoints = Param(nameof(DistancePoints), 1m)
.SetGreaterThanZero()
.SetDisplay("Min Distance", "Minimum distance from current price to breakout level (points)", "Trading");
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Volume used for each breakout order", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candles timeframe used for range detection", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_rangeHigh = null;
_rangeLow = null;
_rangeDate = null;
_longReady = false;
_shortReady = false;
_longStopPrice = null;
_longTakeProfitPrice = null;
_shortStopPrice = null;
_shortTakeProfitPrice = null;
_longEntryPrice = 0m;
_shortEntryPrice = 0m;
_bestBid = null;
_bestAsk = null;
_adjustedPointSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = OrderVolume;
_adjustedPointSize = CalculateAdjustedPointSize();
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var currentDate = candle.OpenTime.Date;
if (_rangeDate != currentDate)
{
ResetDailyState(currentDate);
}
UpdateRange(candle);
if (candle.OpenTime.Hour >= StopHour)
{
PrepareBreakoutLevels(candle);
}
ProcessEntries(candle);
ProcessRiskManagement(candle);
}
private void ResetDailyState(DateTime date)
{
_rangeDate = date;
_rangeHigh = null;
_rangeLow = null;
_longReady = false;
_shortReady = false;
_longStopPrice = null;
_longTakeProfitPrice = null;
_shortStopPrice = null;
_shortTakeProfitPrice = null;
}
private void UpdateRange(ICandleMessage candle)
{
var hour = candle.OpenTime.Hour;
if (hour < StartHour || hour >= StopHour)
return;
_rangeHigh = _rangeHigh.HasValue
? Math.Max(_rangeHigh.Value, candle.HighPrice)
: candle.HighPrice;
_rangeLow = _rangeLow.HasValue
? Math.Min(_rangeLow.Value, candle.LowPrice)
: candle.LowPrice;
}
private void PrepareBreakoutLevels(ICandleMessage candle)
{
if (!_rangeHigh.HasValue || !_rangeLow.HasValue)
return;
var rangeHeight = _rangeHigh.Value - _rangeLow.Value;
var maxRange = ConvertToPrice(MaxRangePoints);
if (rangeHeight >= maxRange)
{
// Reset pending plans when the range becomes too wide.
_longReady = false;
_shortReady = false;
return;
}
var minDistance = ConvertToPrice(DistancePoints);
var ask = _bestAsk ?? candle.ClosePrice;
var bid = _bestBid ?? candle.ClosePrice;
if (!_longReady && Position >= 0 && (_rangeHigh.Value - ask) > minDistance)
{
_longReady = true;
_longEntryPrice = _rangeHigh.Value;
_longStopPrice = _rangeLow.Value;
_longTakeProfitPrice = _rangeHigh.Value + ConvertToPrice(TakeProfitPoints);
}
if (!_shortReady && Position <= 0 && (bid - _rangeLow.Value) > minDistance)
{
_shortReady = true;
_shortEntryPrice = _rangeLow.Value;
_shortStopPrice = _rangeHigh.Value;
_shortTakeProfitPrice = _rangeLow.Value - ConvertToPrice(TakeProfitPoints);
}
}
private void ProcessEntries(ICandleMessage candle)
{
var volume = OrderVolume;
if (_longReady && candle.HighPrice >= _longEntryPrice && Position <= 0)
{
// Enter long on breakout of the session high.
BuyMarket();
_longReady = false;
_shortReady = false;
}
if (_shortReady && candle.LowPrice <= _shortEntryPrice && Position >= 0)
{
// Enter short on breakout of the session low.
SellMarket();
_shortReady = false;
_longReady = false;
}
}
private void ProcessRiskManagement(ICandleMessage candle)
{
if (Position > 0 && _longStopPrice.HasValue && _longTakeProfitPrice.HasValue)
{
// Close the long position if stop-loss or take-profit levels are touched.
if (candle.LowPrice <= _longStopPrice.Value)
{
SellMarket();
_longStopPrice = null;
_longTakeProfitPrice = null;
}
else if (candle.HighPrice >= _longTakeProfitPrice.Value)
{
SellMarket();
_longStopPrice = null;
_longTakeProfitPrice = null;
}
}
else if (Position < 0 && _shortStopPrice.HasValue && _shortTakeProfitPrice.HasValue)
{
// Close the short position if stop-loss or take-profit levels are touched.
if (candle.HighPrice >= _shortStopPrice.Value)
{
BuyMarket();
_shortStopPrice = null;
_shortTakeProfitPrice = null;
}
else if (candle.LowPrice <= _shortTakeProfitPrice.Value)
{
BuyMarket();
_shortStopPrice = null;
_shortTakeProfitPrice = null;
}
}
else
{
_longStopPrice = null;
_longTakeProfitPrice = null;
_shortStopPrice = null;
_shortTakeProfitPrice = null;
}
}
private decimal ConvertToPrice(decimal points)
{
return points * _adjustedPointSize;
}
private decimal CalculateAdjustedPointSize()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
step = 1m;
var decimals = Security?.Decimals ?? 0;
var multiplier = decimals == 3 || decimals == 5 ? 10m : 1m;
return step * multiplier;
}
}
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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class big_dog_strategy(Strategy):
def __init__(self):
super(big_dog_strategy, self).__init__()
self._start_hour = self.Param("StartHour", 2) \
.SetDisplay("Start Hour", "Hour to begin measuring the range", "Session")
self._stop_hour = self.Param("StopHour", 8) \
.SetDisplay("Stop Hour", "Hour to stop measuring the range", "Session")
self._max_range_points = self.Param("MaxRangePoints", 50000.0) \
.SetDisplay("Max Range", "Maximum allowed height of consolidation range", "Trading")
self._take_profit_points = self.Param("TakeProfitPoints", 50.0) \
.SetDisplay("Take Profit", "Take-profit distance in adjusted points", "Trading")
self._distance_points = self.Param("DistancePoints", 1.0) \
.SetDisplay("Min Distance", "Minimum distance from price to breakout level", "Trading")
self._order_volume = self.Param("OrderVolume", 1.0) \
.SetDisplay("Order Volume", "Volume used for each breakout order", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Candles timeframe used for range detection", "Data")
self._range_high = None
self._range_low = None
self._range_date = None
self._long_ready = False
self._short_ready = False
self._long_stop_price = None
self._long_tp_price = None
self._short_stop_price = None
self._short_tp_price = None
self._long_entry_price = 0.0
self._short_entry_price = 0.0
self._adjusted_point_size = 0.0
@property
def start_hour(self):
return self._start_hour.Value
@property
def stop_hour(self):
return self._stop_hour.Value
@property
def max_range_points(self):
return self._max_range_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
@property
def distance_points(self):
return self._distance_points.Value
@property
def order_volume(self):
return self._order_volume.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(big_dog_strategy, self).OnReseted()
self._range_high = None
self._range_low = None
self._range_date = None
self._long_ready = False
self._short_ready = False
self._long_stop_price = None
self._long_tp_price = None
self._short_stop_price = None
self._short_tp_price = None
self._long_entry_price = 0.0
self._short_entry_price = 0.0
self._adjusted_point_size = 0.0
def _calc_adjusted_point_size(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and sec.PriceStep > 0 else 1.0
decimals = sec.Decimals if sec is not None and sec.Decimals is not None else 0
multiplier = 10.0 if decimals == 3 or decimals == 5 else 1.0
return step * multiplier
def _convert_to_price(self, points):
return points * self._adjusted_point_size
def OnStarted2(self, time):
super(big_dog_strategy, self).OnStarted2(time)
self.Volume = self._order_volume.Value
self._adjusted_point_size = self._calc_adjusted_point_size()
subscription = self.SubscribeCandles(self._candle_type.Value)
subscription.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _reset_daily(self, date):
self._range_date = date
self._range_high = None
self._range_low = None
self._long_ready = False
self._short_ready = False
self._long_stop_price = None
self._long_tp_price = None
self._short_stop_price = None
self._short_tp_price = None
def _update_range(self, candle):
hour = candle.OpenTime.Hour
if hour < self.start_hour or hour >= self.stop_hour:
return
h = float(candle.HighPrice)
l = float(candle.LowPrice)
self._range_high = max(self._range_high, h) if self._range_high is not None else h
self._range_low = min(self._range_low, l) if self._range_low is not None else l
def _prepare_breakout(self, candle):
if self._range_high is None or self._range_low is None:
return
range_height = self._range_high - self._range_low
max_range = self._convert_to_price(self.max_range_points)
if range_height >= max_range:
self._long_ready = False
self._short_ready = False
return
min_dist = self._convert_to_price(self.distance_points)
ask = float(candle.ClosePrice)
bid = float(candle.ClosePrice)
if not self._long_ready and self.Position >= 0 and (self._range_high - ask) > min_dist:
self._long_ready = True
self._long_entry_price = self._range_high
self._long_stop_price = self._range_low
self._long_tp_price = self._range_high + self._convert_to_price(self.take_profit_points)
if not self._short_ready and self.Position <= 0 and (bid - self._range_low) > min_dist:
self._short_ready = True
self._short_entry_price = self._range_low
self._short_stop_price = self._range_high
self._short_tp_price = self._range_low - self._convert_to_price(self.take_profit_points)
def _process_entries(self, candle):
if self._long_ready and float(candle.HighPrice) >= self._long_entry_price and self.Position <= 0:
self.BuyMarket()
self._long_ready = False
self._short_ready = False
if self._short_ready and float(candle.LowPrice) <= self._short_entry_price and self.Position >= 0:
self.SellMarket()
self._short_ready = False
self._long_ready = False
def _process_risk(self, candle):
if self.Position > 0 and self._long_stop_price is not None and self._long_tp_price is not None:
if float(candle.LowPrice) <= self._long_stop_price:
self.SellMarket()
self._long_stop_price = None
self._long_tp_price = None
elif float(candle.HighPrice) >= self._long_tp_price:
self.SellMarket()
self._long_stop_price = None
self._long_tp_price = None
elif self.Position < 0 and self._short_stop_price is not None and self._short_tp_price is not None:
if float(candle.HighPrice) >= self._short_stop_price:
self.BuyMarket()
self._short_stop_price = None
self._short_tp_price = None
elif float(candle.LowPrice) <= self._short_tp_price:
self.BuyMarket()
self._short_stop_price = None
self._short_tp_price = None
else:
self._long_stop_price = None
self._long_tp_price = None
self._short_stop_price = None
self._short_tp_price = None
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
current_date = candle.OpenTime.Date
if self._range_date != current_date:
self._reset_daily(current_date)
self._update_range(candle)
if candle.OpenTime.Hour >= self.stop_hour:
self._prepare_breakout(candle)
self._process_entries(candle)
self._process_risk(candle)
def CreateClone(self):
return big_dog_strategy()