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>
/// Strategy based on the Percentage Crossover indicator.
/// </summary>
public class PercentageCrossoverStrategy : Strategy
{
private readonly StrategyParam<bool> _buyPosOpen;
private readonly StrategyParam<bool> _sellPosOpen;
private readonly StrategyParam<bool> _buyPosClose;
private readonly StrategyParam<bool> _sellPosClose;
private readonly StrategyParam<bool> _useTimeFilter;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _startMinute;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<int> _endMinute;
private readonly StrategyParam<decimal> _percent;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<DataType> _candleType;
private readonly List<int> _colorHistory = new();
private decimal? _previousMiddle;
private int? _lastColor;
public bool BuyPosOpen
{
get => _buyPosOpen.Value;
set => _buyPosOpen.Value = value;
}
public bool SellPosOpen
{
get => _sellPosOpen.Value;
set => _sellPosOpen.Value = value;
}
public bool BuyPosClose
{
get => _buyPosClose.Value;
set => _buyPosClose.Value = value;
}
public bool SellPosClose
{
get => _sellPosClose.Value;
set => _sellPosClose.Value = value;
}
public bool UseTimeFilter
{
get => _useTimeFilter.Value;
set => _useTimeFilter.Value = value;
}
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
public int StartMinute
{
get => _startMinute.Value;
set => _startMinute.Value = value;
}
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
public int EndMinute
{
get => _endMinute.Value;
set => _endMinute.Value = value;
}
public decimal Percent
{
get => _percent.Value;
set => _percent.Value = value;
}
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public PercentageCrossoverStrategy()
{
_buyPosOpen = Param(nameof(BuyPosOpen), true)
.SetDisplay("Enable Buy Entries", "Allow opening long positions", "General");
_sellPosOpen = Param(nameof(SellPosOpen), true)
.SetDisplay("Enable Sell Entries", "Allow opening short positions", "General");
_buyPosClose = Param(nameof(BuyPosClose), true)
.SetDisplay("Enable Buy Exits", "Allow closing long positions", "General");
_sellPosClose = Param(nameof(SellPosClose), true)
.SetDisplay("Enable Sell Exits", "Allow closing short positions", "General");
_useTimeFilter = Param(nameof(UseTimeFilter), true)
.SetDisplay("Use Time Filter", "Restrict trading to specific hours", "Time Filter");
_startHour = Param(nameof(StartHour), 0)
.SetDisplay("Start Hour", "Trading window start hour", "Time Filter");
_startMinute = Param(nameof(StartMinute), 0)
.SetDisplay("Start Minute", "Trading window start minute", "Time Filter");
_endHour = Param(nameof(EndHour), 23)
.SetDisplay("End Hour", "Trading window end hour", "Time Filter");
_endMinute = Param(nameof(EndMinute), 59)
.SetDisplay("End Minute", "Trading window end minute", "Time Filter");
_percent = Param(nameof(Percent), 1m)
.SetGreaterThanZero()
.SetDisplay("Percent", "Percentage offset for the indicator", "Indicator");
_signalBar = Param(nameof(SignalBar), 1)
.SetGreaterThanZero()
.SetDisplay("Signal Bar", "Closed bars to look back for the signal", "Indicator");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Time frame for signal candles", "Data");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_colorHistory.Clear();
_previousMiddle = null;
_lastColor = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_colorHistory.Clear();
_previousMiddle = null;
_lastColor = null;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
var percentFactor = Percent / 100m;
if (_previousMiddle is null)
{
_previousMiddle = close;
_lastColor = 0;
_colorHistory.Clear();
_colorHistory.Add(0);
return;
}
var previousMiddle = _previousMiddle.Value;
var lowerBoundary = close * (1 - percentFactor);
var upperBoundary = close * (1 + percentFactor);
var middle = previousMiddle;
if (lowerBoundary > previousMiddle)
middle = lowerBoundary;
else if (upperBoundary < previousMiddle)
middle = upperBoundary;
var color = _lastColor ?? 0;
if (middle > previousMiddle)
color = 0;
else if (middle < previousMiddle)
color = 1;
_previousMiddle = middle;
_lastColor = color;
_colorHistory.Add(color);
var maxSize = Math.Max(SignalBar + 2, 4);
while (_colorHistory.Count > maxSize)
{
try { _colorHistory.RemoveAt(0); }
catch { break; }
}
var currentIndex = _colorHistory.Count - SignalBar;
if (currentIndex <= 0)
return;
var previousIndex = currentIndex - 1;
if (previousIndex < 0)
return;
var currentColor = _colorHistory[currentIndex];
var previousColor = _colorHistory[previousIndex];
var buyOpen = BuyPosOpen && currentColor == 0 && previousColor == 1;
var sellOpen = SellPosOpen && currentColor == 1 && previousColor == 0;
var buyClose = BuyPosClose && currentColor == 1;
var sellClose = SellPosClose && currentColor == 0;
var inTradingWindow = !UseTimeFilter || IsTradingTime(candle.CloseTime);
if (UseTimeFilter && !inTradingWindow)
{
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
return;
}
if (buyClose && Position > 0)
SellMarket();
if (sellClose && Position < 0)
BuyMarket();
if (!inTradingWindow)
return;
if (buyOpen && Position <= 0)
BuyMarket();
else if (sellOpen && Position >= 0)
SellMarket();
}
private bool IsTradingTime(DateTimeOffset time)
{
var hour = time.Hour;
var minute = time.Minute;
if (StartHour < EndHour)
{
if (hour == StartHour && minute >= StartMinute)
return true;
if (hour > StartHour && hour < EndHour)
return true;
if (hour > StartHour && hour == EndHour && minute < EndMinute)
return true;
return false;
}
if (StartHour == EndHour)
{
return hour == StartHour && minute >= StartMinute && minute < EndMinute;
}
if (hour >= StartHour && minute >= StartMinute)
return true;
if (hour < EndHour)
return true;
if (hour == EndHour && minute < EndMinute)
return true;
return false;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class percentage_crossover_strategy(Strategy):
"""Percentage Crossover indicator-based strategy with time filter."""
def __init__(self):
super(percentage_crossover_strategy, self).__init__()
self._buy_pos_open = self.Param("BuyPosOpen", True) \
.SetDisplay("Enable Buy Entries", "Allow opening long positions", "General")
self._sell_pos_open = self.Param("SellPosOpen", True) \
.SetDisplay("Enable Sell Entries", "Allow opening short positions", "General")
self._buy_pos_close = self.Param("BuyPosClose", True) \
.SetDisplay("Enable Buy Exits", "Allow closing long positions", "General")
self._sell_pos_close = self.Param("SellPosClose", True) \
.SetDisplay("Enable Sell Exits", "Allow closing short positions", "General")
self._use_time_filter = self.Param("UseTimeFilter", True) \
.SetDisplay("Use Time Filter", "Restrict trading to specific hours", "Time Filter")
self._start_hour = self.Param("StartHour", 0) \
.SetDisplay("Start Hour", "Trading window start hour", "Time Filter")
self._start_minute = self.Param("StartMinute", 0) \
.SetDisplay("Start Minute", "Trading window start minute", "Time Filter")
self._end_hour = self.Param("EndHour", 23) \
.SetDisplay("End Hour", "Trading window end hour", "Time Filter")
self._end_minute = self.Param("EndMinute", 59) \
.SetDisplay("End Minute", "Trading window end minute", "Time Filter")
self._percent = self.Param("Percent", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Percent", "Percentage offset for the indicator", "Indicator")
self._signal_bar = self.Param("SignalBar", 1) \
.SetGreaterThanZero() \
.SetDisplay("Signal Bar", "Closed bars to look back for the signal", "Indicator")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Time frame for signal candles", "Data")
self._color_history = []
self._prev_middle = None
self._last_color = None
@property
def BuyPosOpen(self):
return self._buy_pos_open.Value
@property
def SellPosOpen(self):
return self._sell_pos_open.Value
@property
def BuyPosClose(self):
return self._buy_pos_close.Value
@property
def SellPosClose(self):
return self._sell_pos_close.Value
@property
def UseTimeFilter(self):
return self._use_time_filter.Value
@property
def StartHour(self):
return self._start_hour.Value
@property
def StartMinute(self):
return self._start_minute.Value
@property
def EndHour(self):
return self._end_hour.Value
@property
def EndMinute(self):
return self._end_minute.Value
@property
def Percent(self):
return self._percent.Value
@property
def SignalBar(self):
return self._signal_bar.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(percentage_crossover_strategy, self).OnStarted2(time)
self._color_history = []
self._prev_middle = None
self._last_color = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
pf = float(self.Percent) / 100.0
if self._prev_middle is None:
self._prev_middle = close
self._last_color = 0
self._color_history = [0]
return
prev_mid = self._prev_middle
lower_b = close * (1.0 - pf)
upper_b = close * (1.0 + pf)
middle = prev_mid
if lower_b > prev_mid:
middle = lower_b
elif upper_b < prev_mid:
middle = upper_b
color = self._last_color if self._last_color is not None else 0
if middle > prev_mid:
color = 0
elif middle < prev_mid:
color = 1
self._prev_middle = middle
self._last_color = color
self._color_history.append(color)
max_size = max(self.SignalBar + 2, 4)
while len(self._color_history) > max_size:
self._color_history.pop(0)
ci = len(self._color_history) - self.SignalBar
if ci <= 0:
return
pi = ci - 1
if pi < 0:
return
cc = self._color_history[ci]
pc = self._color_history[pi]
buy_open = self.BuyPosOpen and cc == 0 and pc == 1
sell_open = self.SellPosOpen and cc == 1 and pc == 0
buy_close = self.BuyPosClose and cc == 1
sell_close = self.SellPosClose and cc == 0
in_window = (not self.UseTimeFilter) or self._is_trading_time(candle.CloseTime)
if self.UseTimeFilter and not in_window:
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
return
if buy_close and self.Position > 0:
self.SellMarket()
if sell_close and self.Position < 0:
self.BuyMarket()
if not in_window:
return
if buy_open and self.Position <= 0:
self.BuyMarket()
elif sell_open and self.Position >= 0:
self.SellMarket()
def _is_trading_time(self, time):
h = time.Hour
m = time.Minute
sh = self.StartHour
sm = self.StartMinute
eh = self.EndHour
em = self.EndMinute
if sh < eh:
if h == sh and m >= sm:
return True
if h > sh and h < eh:
return True
if h > sh and h == eh and m < em:
return True
return False
if sh == eh:
return h == sh and m >= sm and m < em
if h >= sh and m >= sm:
return True
if h < eh:
return True
if h == eh and m < em:
return True
return False
def OnReseted(self):
super(percentage_crossover_strategy, self).OnReseted()
self._color_history = []
self._prev_middle = None
self._last_color = None
def CreateClone(self):
return percentage_crossover_strategy()