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>
/// Percentage Crossover Channel breakout system translated from MQL.
/// </summary>
public class PercentageCrossoverChannelSystemStrategy : Strategy
{
private readonly StrategyParam<decimal> _percent;
private readonly StrategyParam<int> _shift;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<bool> _buyOpen;
private readonly StrategyParam<bool> _sellOpen;
private readonly StrategyParam<bool> _buyClose;
private readonly StrategyParam<bool> _sellClose;
private readonly StrategyParam<int> _stopLoss;
private readonly StrategyParam<int> _takeProfit;
private readonly StrategyParam<DataType> _candleType;
private readonly List<int> _colorHistory = new();
private readonly List<decimal> _upperHistory = new();
private readonly List<decimal> _lowerHistory = new();
private decimal _previousMiddle;
private bool _hasMiddle;
private decimal? _entryPrice;
public decimal Percent
{
get => _percent.Value;
set => _percent.Value = value;
}
public int Shift
{
get => _shift.Value;
set => _shift.Value = value;
}
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = value;
}
public bool BuyPositionsOpen
{
get => _buyOpen.Value;
set => _buyOpen.Value = value;
}
public bool SellPositionsOpen
{
get => _sellOpen.Value;
set => _sellOpen.Value = value;
}
public bool BuyPositionsClose
{
get => _buyClose.Value;
set => _buyClose.Value = value;
}
public bool SellPositionsClose
{
get => _sellClose.Value;
set => _sellClose.Value = value;
}
public int StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
public int TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public PercentageCrossoverChannelSystemStrategy()
{
_percent = Param(nameof(Percent), 1.0m)
.SetGreaterThanZero()
.SetDisplay("Channel Percent", "Percentage width of the channel", "Indicator");
_shift = Param(nameof(Shift), 1)
.SetGreaterThanZero()
.SetDisplay("Shift", "Number of bars used for crossover comparison", "Indicator");
_signalBar = Param(nameof(SignalBar), 1)
.SetGreaterThanZero()
.SetDisplay("Signal Bar", "Bars back to evaluate indicator colors", "Trading Rules");
_buyOpen = Param(nameof(BuyPositionsOpen), true)
.SetDisplay("Enable Long Entries", "Allow long position openings", "Trading Rules");
_sellOpen = Param(nameof(SellPositionsOpen), true)
.SetDisplay("Enable Short Entries", "Allow short position openings", "Trading Rules");
_buyClose = Param(nameof(BuyPositionsClose), true)
.SetDisplay("Allow Long Exits", "Permit closing long trades on bearish signals", "Trading Rules");
_sellClose = Param(nameof(SellPositionsClose), true)
.SetDisplay("Allow Short Exits", "Permit closing short trades on bullish signals", "Trading Rules");
_stopLoss = Param(nameof(StopLoss), 1000)
.SetDisplay("Stop Loss (steps)", "Protective stop loss distance in price steps", "Risk Management");
_takeProfit = Param(nameof(TakeProfit), 2000)
.SetDisplay("Take Profit (steps)", "Target profit distance in price steps", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for analysis", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_colorHistory.Clear();
_upperHistory.Clear();
_lowerHistory.Clear();
_hasMiddle = false;
_previousMiddle = 0m;
_entryPrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
// Ignore interim updates; we only react on closed candles.
if (candle.State != CandleStates.Finished)
return;
// Evaluate protective orders before generating new signals.
var stopTriggered = HandleRisk(candle);
// Mirror the MQL signal logic using cached indicator colors.
if (_colorHistory.Count > SignalBar)
{
// Equivalent to CopyBuffer(..., SignalBar, 2, ...) from the EA.
var recentIndex = _colorHistory.Count - SignalBar;
var olderIndex = recentIndex - 1;
if (olderIndex >= 0)
{
var recentColor = _colorHistory[recentIndex];
var olderColor = _colorHistory[olderIndex];
var shouldCloseShort = SellPositionsClose && olderColor > 2;
var shouldCloseLong = BuyPositionsClose && olderColor < 2;
var shouldOpenBuy = BuyPositionsOpen && olderColor > 2 && recentColor < 3;
var shouldOpenSell = SellPositionsOpen && olderColor < 2 && recentColor > 1;
// Close existing positions according to the original toggles.
if (shouldCloseLong && Position > 0)
{
SellMarket();
_entryPrice = null;
}
if (shouldCloseShort && Position < 0)
{
BuyMarket();
_entryPrice = null;
}
// Enter only when we are flat to match the EA behaviour.
if (!stopTriggered && Position == 0)
{
if (shouldOpenBuy)
{
BuyMarket();
_entryPrice = candle.ClosePrice;
}
else if (shouldOpenSell)
{
SellMarket();
_entryPrice = candle.ClosePrice;
}
}
}
}
// Update indicator state after trading decisions are made.
var color = CalculateColor(candle);
_colorHistory.Add(color);
TrimHistory();
}
private bool HandleRisk(ICandleMessage candle)
{
// Exit early if there is no stored entry price.
if (_entryPrice is null)
return false;
// Price step is required to translate MQL points into absolute prices.
if (Security?.PriceStep is not decimal step || step <= 0)
return false;
var triggered = false;
if (Position > 0)
{
// Long position risk checks.
if (StopLoss > 0)
{
var stopLevel = _entryPrice.Value - StopLoss * step;
if (candle.LowPrice <= stopLevel)
{
SellMarket();
_entryPrice = null;
triggered = true;
}
}
if (!triggered && TakeProfit > 0)
{
var takeLevel = _entryPrice.Value + TakeProfit * step;
if (candle.HighPrice >= takeLevel)
{
SellMarket();
_entryPrice = null;
triggered = true;
}
}
}
else if (Position < 0)
{
// Short position risk checks.
if (StopLoss > 0)
{
var stopLevel = _entryPrice.Value + StopLoss * step;
if (candle.HighPrice >= stopLevel)
{
BuyMarket();
_entryPrice = null;
triggered = true;
}
}
if (!triggered && TakeProfit > 0)
{
var takeLevel = _entryPrice.Value - TakeProfit * step;
if (candle.LowPrice <= takeLevel)
{
BuyMarket();
_entryPrice = null;
triggered = true;
}
}
}
// Reset cached entry price once we are flat.
if (Position == 0)
_entryPrice = null;
return triggered;
}
private int CalculateColor(ICandleMessage candle)
{
// Recreate the Percentage Crossover Channel midline and colour logic.
var percentFactor = Percent / 100m;
var plusVar = 1m + percentFactor;
var minusVar = 1m - percentFactor;
var close = candle.ClosePrice;
// Initialise the midline on the very first candle.
if (!_hasMiddle)
{
_previousMiddle = close;
_hasMiddle = true;
}
var middle = _previousMiddle;
var lowerCandidate = close * minusVar;
var upperCandidate = close * plusVar;
// Adjust the midline exactly as in the original indicator.
if (lowerCandidate > _previousMiddle)
{
middle = lowerCandidate;
}
else if (upperCandidate < _previousMiddle)
{
middle = upperCandidate;
}
var upper = middle + middle * percentFactor;
var lower = middle - middle * percentFactor;
_previousMiddle = middle;
var color = 2;
// Determine candle colour relative to past channel values.
if (_upperHistory.Count >= Shift)
{
var referenceIndex = _upperHistory.Count - Shift;
var referenceUpper = _upperHistory[referenceIndex];
var referenceLower = _lowerHistory[referenceIndex];
if (close > referenceUpper)
{
color = candle.OpenPrice <= close ? 4 : 3;
}
else if (close < referenceLower)
{
color = candle.OpenPrice > close ? 0 : 1;
}
}
// Persist channel history for future signal checks.
_upperHistory.Add(upper);
_lowerHistory.Add(lower);
return color;
}
private void TrimHistory()
{
// Keep only as much history as needed for Shift and SignalBar lookbacks.
var maxCapacity = Math.Max(Shift + SignalBar + 5, 16);
if (_colorHistory.Count <= maxCapacity)
return;
var removeCount = _colorHistory.Count - maxCapacity;
for (var i = 0; i < removeCount; i++)
{
try
{
_colorHistory.RemoveAt(0);
_upperHistory.RemoveAt(0);
_lowerHistory.RemoveAt(0);
}
catch { break; }
}
}
}
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_channel_system_strategy(Strategy):
"""Percentage Crossover Channel breakout system with SL/TP."""
def __init__(self):
super(percentage_crossover_channel_system_strategy, self).__init__()
self._percent = self.Param("Percent", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Channel Percent", "Percentage width of the channel", "Indicator")
self._shift = self.Param("Shift", 1) \
.SetGreaterThanZero() \
.SetDisplay("Shift", "Number of bars used for crossover comparison", "Indicator")
self._signal_bar = self.Param("SignalBar", 1) \
.SetGreaterThanZero() \
.SetDisplay("Signal Bar", "Bars back to evaluate indicator colors", "Trading Rules")
self._buy_open = self.Param("BuyPositionsOpen", True) \
.SetDisplay("Enable Long Entries", "Allow long position openings", "Trading Rules")
self._sell_open = self.Param("SellPositionsOpen", True) \
.SetDisplay("Enable Short Entries", "Allow short position openings", "Trading Rules")
self._buy_close = self.Param("BuyPositionsClose", True) \
.SetDisplay("Allow Long Exits", "Permit closing long trades on bearish signals", "Trading Rules")
self._sell_close = self.Param("SellPositionsClose", True) \
.SetDisplay("Allow Short Exits", "Permit closing short trades on bullish signals", "Trading Rules")
self._stop_loss = self.Param("StopLoss", 1000) \
.SetDisplay("Stop Loss (steps)", "Protective stop loss distance in price steps", "Risk Management")
self._take_profit = self.Param("TakeProfit", 2000) \
.SetDisplay("Take Profit (steps)", "Target profit distance in price steps", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Primary timeframe for analysis", "General")
self._color_history = []
self._upper_history = []
self._lower_history = []
self._prev_middle = 0.0
self._has_middle = False
self._entry_price = None
@property
def Percent(self):
return self._percent.Value
@property
def Shift(self):
return self._shift.Value
@property
def SignalBar(self):
return self._signal_bar.Value
@property
def BuyPositionsOpen(self):
return self._buy_open.Value
@property
def SellPositionsOpen(self):
return self._sell_open.Value
@property
def BuyPositionsClose(self):
return self._buy_close.Value
@property
def SellPositionsClose(self):
return self._sell_close.Value
@property
def StopLoss(self):
return self._stop_loss.Value
@property
def TakeProfit(self):
return self._take_profit.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(percentage_crossover_channel_system_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
stop_triggered = self._handle_risk(candle)
if len(self._color_history) > self.SignalBar:
ri = len(self._color_history) - self.SignalBar
oi = ri - 1
if oi >= 0:
rc = self._color_history[ri]
oc = self._color_history[oi]
should_close_short = self.SellPositionsClose and oc > 2
should_close_long = self.BuyPositionsClose and oc < 2
should_buy = self.BuyPositionsOpen and oc > 2 and rc < 3
should_sell = self.SellPositionsOpen and oc < 2 and rc > 1
if should_close_long and self.Position > 0:
self.SellMarket()
self._entry_price = None
if should_close_short and self.Position < 0:
self.BuyMarket()
self._entry_price = None
if not stop_triggered and self.Position == 0:
if should_buy:
self.BuyMarket()
self._entry_price = float(candle.ClosePrice)
elif should_sell:
self.SellMarket()
self._entry_price = float(candle.ClosePrice)
color = self._calc_color(candle)
self._color_history.append(color)
self._trim()
def _handle_risk(self, candle):
if self._entry_price is None:
return False
sec = self.Security
if sec is None or sec.PriceStep is None or float(sec.PriceStep) <= 0:
return False
step = float(sec.PriceStep)
triggered = False
if self.Position > 0:
if self.StopLoss > 0:
sl = self._entry_price - self.StopLoss * step
if float(candle.LowPrice) <= sl:
self.SellMarket()
self._entry_price = None
triggered = True
if not triggered and self.TakeProfit > 0:
tp = self._entry_price + self.TakeProfit * step
if float(candle.HighPrice) >= tp:
self.SellMarket()
self._entry_price = None
triggered = True
elif self.Position < 0:
if self.StopLoss > 0:
sl = self._entry_price + self.StopLoss * step
if float(candle.HighPrice) >= sl:
self.BuyMarket()
self._entry_price = None
triggered = True
if not triggered and self.TakeProfit > 0:
tp = self._entry_price - self.TakeProfit * step
if float(candle.LowPrice) <= tp:
self.BuyMarket()
self._entry_price = None
triggered = True
if self.Position == 0:
self._entry_price = None
return triggered
def _calc_color(self, candle):
pf = float(self.Percent) / 100.0
plus_var = 1.0 + pf
minus_var = 1.0 - pf
close = float(candle.ClosePrice)
if not self._has_middle:
self._prev_middle = close
self._has_middle = True
middle = self._prev_middle
lower_c = close * minus_var
upper_c = close * plus_var
if lower_c > self._prev_middle:
middle = lower_c
elif upper_c < self._prev_middle:
middle = upper_c
upper = middle + middle * pf
lower = middle - middle * pf
self._prev_middle = middle
color = 2
if len(self._upper_history) >= self.Shift:
ref_idx = len(self._upper_history) - self.Shift
ref_upper = self._upper_history[ref_idx]
ref_lower = self._lower_history[ref_idx]
if close > ref_upper:
color = 4 if float(candle.OpenPrice) <= close else 3
elif close < ref_lower:
color = 0 if float(candle.OpenPrice) > close else 1
self._upper_history.append(upper)
self._lower_history.append(lower)
return color
def _trim(self):
max_cap = max(self.Shift + self.SignalBar + 5, 16)
if len(self._color_history) <= max_cap:
return
remove = len(self._color_history) - max_cap
self._color_history = self._color_history[remove:]
self._upper_history = self._upper_history[remove:]
self._lower_history = self._lower_history[remove:]
def OnReseted(self):
super(percentage_crossover_channel_system_strategy, self).OnReseted()
self._color_history = []
self._upper_history = []
self._lower_history = []
self._has_middle = False
self._prev_middle = 0.0
self._entry_price = None
def CreateClone(self):
return percentage_crossover_channel_system_strategy()