namespace StockSharp.Samples.Strategies;
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;
using StockSharp.Algo;
/// <summary>
/// Strategy converted from the "BULL row full EA" expert advisor.
/// </summary>
public class BullRowBreakoutStrategy : Strategy
{
private readonly List<ICandleMessage> _candles = new();
private readonly Queue<decimal> _stochasticHistory = new();
private StochasticOscillator _stochastic = null!;
private decimal? _stopPrice;
private decimal? _takeProfitPrice;
private readonly StrategyParam<TimeSpan> _candleTimeFrame;
private readonly StrategyParam<int> _stopLossLookback;
private readonly StrategyParam<decimal> _takeProfitPercent;
private readonly StrategyParam<int> _bearRowSize;
private readonly StrategyParam<decimal> _bearMinBody;
private readonly StrategyParam<RowSequenceModes> _bearRowMode;
private readonly StrategyParam<int> _bearShift;
private readonly StrategyParam<int> _bullRowSize;
private readonly StrategyParam<decimal> _bullMinBody;
private readonly StrategyParam<RowSequenceModes> _bullRowMode;
private readonly StrategyParam<int> _bullShift;
private readonly StrategyParam<int> _breakoutLookback;
private readonly StrategyParam<int> _stochasticKPeriod;
private readonly StrategyParam<int> _stochasticDPeriod;
private readonly StrategyParam<int> _stochasticSlowing;
private readonly StrategyParam<int> _stochasticRangePeriod;
private readonly StrategyParam<decimal> _stochasticUpperLevel;
private readonly StrategyParam<decimal> _stochasticLowerLevel;
/// <summary>
/// Initializes a new instance of the <see cref="BullRowBreakoutStrategy"/> class.
/// </summary>
public BullRowBreakoutStrategy()
{
_candleTimeFrame = Param(nameof(CandleTimeFrame), TimeSpan.FromMinutes(5))
.SetDisplay("Timeframe", "Primary candle timeframe", "Market")
;
_stopLossLookback = Param(nameof(StopLossLookback), 10)
.SetDisplay("Stop loss bars", "Bars used to locate protective stop", "Risk")
;
_takeProfitPercent = Param(nameof(TakeProfitPercent), 100m)
.SetDisplay("Take profit %", "Reward distance relative to stop", "Risk")
;
_bearRowSize = Param(nameof(BearRowSize), 3)
.SetDisplay("Bear row size", "Required consecutive bearish candles", "Pattern")
;
_bearMinBody = Param(nameof(BearMinBody), 0m)
.SetDisplay("Bear min body", "Minimum bearish candle body (price steps)", "Pattern")
;
_bearRowMode = Param(nameof(BearRowMode), RowSequenceModes.Normal)
.SetDisplay("Bear row mode", "Body size progression for bearish row", "Pattern")
;
_bearShift = Param(nameof(BearShift), 3)
.SetDisplay("Bear row shift", "How many bars back the bearish row starts", "Pattern")
;
_bullRowSize = Param(nameof(BullRowSize), 2)
.SetDisplay("Bull row size", "Required consecutive bullish candles", "Pattern")
;
_bullMinBody = Param(nameof(BullMinBody), 0m)
.SetDisplay("Bull min body", "Minimum bullish candle body (price steps)", "Pattern")
;
_bullRowMode = Param(nameof(BullRowMode), RowSequenceModes.Normal)
.SetDisplay("Bull row mode", "Body size progression for bullish row", "Pattern")
;
_bullShift = Param(nameof(BullShift), 1)
.SetDisplay("Bull row shift", "How many bars back the bullish row starts", "Pattern")
;
_breakoutLookback = Param(nameof(BreakoutLookback), 10)
.SetDisplay("Breakout lookback", "Bars checked for the breakout filter", "Pattern")
;
_stochasticKPeriod = Param(nameof(StochasticKPeriod), 40)
.SetDisplay("Stochastic %K", "%K period", "Indicators")
;
_stochasticDPeriod = Param(nameof(StochasticDPeriod), 8)
.SetDisplay("Stochastic %D", "%D period", "Indicators")
;
_stochasticSlowing = Param(nameof(StochasticSlowing), 10)
.SetDisplay("Stochastic slowing", "Smoothing applied to %K", "Indicators")
;
_stochasticRangePeriod = Param(nameof(StochasticRangePeriod), 3)
.SetDisplay("Stochastic bars", "Bars that must remain inside the oscillator channel", "Indicators")
;
_stochasticUpperLevel = Param(nameof(StochasticUpperLevel), 70m)
.SetDisplay("Stochastic upper", "Upper bound for the oscillator", "Indicators")
;
_stochasticLowerLevel = Param(nameof(StochasticLowerLevel), 30m)
.SetDisplay("Stochastic lower", "Lower bound for the oscillator", "Indicators")
;
}
/// <summary>
/// Primary candle timeframe.
/// </summary>
public TimeSpan CandleTimeFrame
{
get => _candleTimeFrame.Value;
set => _candleTimeFrame.Value = value;
}
/// <summary>
/// Bars used to locate the stop price.
/// </summary>
public int StopLossLookback
{
get => _stopLossLookback.Value;
set => _stopLossLookback.Value = value;
}
/// <summary>
/// Take profit distance relative to the stop in percent.
/// </summary>
public decimal TakeProfitPercent
{
get => _takeProfitPercent.Value;
set => _takeProfitPercent.Value = value;
}
/// <summary>
/// Bearish row length in candles.
/// </summary>
public int BearRowSize
{
get => _bearRowSize.Value;
set => _bearRowSize.Value = value;
}
/// <summary>
/// Minimum bearish body expressed in price steps.
/// </summary>
public decimal BearMinBody
{
get => _bearMinBody.Value;
set => _bearMinBody.Value = value;
}
/// <summary>
/// Bearish row body progression requirement.
/// </summary>
public RowSequenceModes BearRowMode
{
get => _bearRowMode.Value;
set => _bearRowMode.Value = value;
}
/// <summary>
/// Offset in bars where the bearish row starts.
/// </summary>
public int BearShift
{
get => _bearShift.Value;
set => _bearShift.Value = value;
}
/// <summary>
/// Bullish row length in candles.
/// </summary>
public int BullRowSize
{
get => _bullRowSize.Value;
set => _bullRowSize.Value = value;
}
/// <summary>
/// Minimum bullish body expressed in price steps.
/// </summary>
public decimal BullMinBody
{
get => _bullMinBody.Value;
set => _bullMinBody.Value = value;
}
/// <summary>
/// Bullish row body progression requirement.
/// </summary>
public RowSequenceModes BullRowMode
{
get => _bullRowMode.Value;
set => _bullRowMode.Value = value;
}
/// <summary>
/// Offset in bars where the bullish row starts.
/// </summary>
public int BullShift
{
get => _bullShift.Value;
set => _bullShift.Value = value;
}
/// <summary>
/// Lookback used to determine the breakout high.
/// </summary>
public int BreakoutLookback
{
get => _breakoutLookback.Value;
set => _breakoutLookback.Value = value;
}
/// <summary>
/// Stochastic %K length.
/// </summary>
public int StochasticKPeriod
{
get => _stochasticKPeriod.Value;
set => _stochasticKPeriod.Value = value;
}
/// <summary>
/// Stochastic %D length.
/// </summary>
public int StochasticDPeriod
{
get => _stochasticDPeriod.Value;
set => _stochasticDPeriod.Value = value;
}
/// <summary>
/// Additional smoothing applied to %K.
/// </summary>
public int StochasticSlowing
{
get => _stochasticSlowing.Value;
set => _stochasticSlowing.Value = value;
}
/// <summary>
/// Number of candles that must remain inside the Stochastic channel.
/// </summary>
public int StochasticRangePeriod
{
get => _stochasticRangePeriod.Value;
set => _stochasticRangePeriod.Value = value;
}
/// <summary>
/// Upper bound for the Stochastic filter.
/// </summary>
public decimal StochasticUpperLevel
{
get => _stochasticUpperLevel.Value;
set => _stochasticUpperLevel.Value = value;
}
/// <summary>
/// Lower bound for the Stochastic filter.
/// </summary>
public decimal StochasticLowerLevel
{
get => _stochasticLowerLevel.Value;
set => _stochasticLowerLevel.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_candles.Clear();
_stochasticHistory.Clear();
_stopPrice = null;
_takeProfitPrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_candles.Clear();
_stochasticHistory.Clear();
_stopPrice = null;
_takeProfitPrice = null;
_stochastic = new StochasticOscillator
{
K = { Length = StochasticKPeriod },
D = { Length = StochasticDPeriod },
};
var subscription = SubscribeCandles(CandleTimeFrame.TimeFrame());
subscription
.BindEx(_stochastic, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _stochastic);
DrawOwnTrades(area);
}
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue stochasticValue)
{
if (candle.State != CandleStates.Finished)
return;
var stoch = (StochasticOscillatorValue)stochasticValue;
if (!stochasticValue.IsFinal || stoch.K is not decimal kValue || stoch.D is not decimal dValue)
return;
_candles.Add(candle);
var maxNeeded = Math.Max(Math.Max(BearShift + BearRowSize - 1, BullShift + BullRowSize - 1), Math.Max(StopLossLookback, BreakoutLookback));
if (_candles.Count > Math.Max(maxNeeded + 5, StochasticRangePeriod + 5))
_candles.RemoveAt(0);
_stochasticHistory.Enqueue(kValue);
while (_stochasticHistory.Count > Math.Max(StochasticRangePeriod, 1))
_stochasticHistory.Dequeue();
ManageProtectiveLevels(candle);
if (Position > 0m)
return;
if (!HasEnoughHistory())
return;
if (!HasBearRow())
return;
if (!HasBullRow())
return;
if (!HasBreakout())
return;
if (!HasStochasticCross(kValue, dValue))
return;
if (!IsStochasticContained())
return;
var volume = Volume;
if (volume <= 0m)
return;
var stopPrice = CalculateStopPrice();
if (stopPrice is null)
return;
var entryPrice = candle.ClosePrice;
var risk = entryPrice - stopPrice.Value;
if (risk <= 0m)
return;
var reward = risk * TakeProfitPercent / 100m;
var takeProfitPrice = entryPrice + reward;
if (BuyMarket(volume) is null)
return;
_stopPrice = stopPrice;
_takeProfitPrice = takeProfitPrice;
}
private void ManageProtectiveLevels(ICandleMessage candle)
{
if (Position <= 0m)
return;
if (_stopPrice is decimal stop && candle.LowPrice <= stop)
{
SellMarket(Math.Abs(Position));
ResetProtection();
return;
}
if (_takeProfitPrice is decimal take && candle.HighPrice >= take)
{
SellMarket(Math.Abs(Position));
ResetProtection();
return;
}
}
private void ResetProtection()
{
_stopPrice = null;
_takeProfitPrice = null;
}
private bool HasEnoughHistory()
{
if (_candles.Count < Math.Max(BreakoutLookback, StopLossLookback))
return false;
var bearRequirement = BearShift + BearRowSize - 1;
var bullRequirement = BullShift + BullRowSize - 1;
var minCandles = Math.Max(bearRequirement, bullRequirement);
return _candles.Count >= Math.Max(minCandles, 2);
}
private bool HasBearRow() => HasRow(BearRowSize, BearMinBody, BearRowMode, BearShift, isBullish: false);
private bool HasBullRow() => HasRow(BullRowSize, BullMinBody, BullRowMode, BullShift, isBullish: true);
private bool HasRow(int size, decimal minBody, RowSequenceModes mode, int shift, bool isBullish)
{
if (size <= 0 || shift <= 0)
return false;
var maxShift = shift + size - 1;
if (_candles.Count < maxShift)
return false;
var bodyStep = Security?.PriceStep ?? 0m;
if (bodyStep <= 0m)
bodyStep = 1m;
var minBodyValue = minBody * bodyStep;
decimal previousBody = 0m;
for (var i = 0; i < size; i++)
{
var candle = GetCandle(shift + i);
var body = isBullish ? candle.ClosePrice - candle.OpenPrice : candle.OpenPrice - candle.ClosePrice;
if (body <= 0m)
return false;
if (body < minBodyValue)
return false;
if (mode == RowSequenceModes.Bigger && previousBody > 0m && body <= previousBody)
return false;
if (mode == RowSequenceModes.Smaller && previousBody > 0m && body >= previousBody)
return false;
previousBody = body;
}
return true;
}
private bool HasBreakout()
{
if (BreakoutLookback <= 2)
return false;
var prevClose = GetCandle(1).ClosePrice;
var highest = decimal.MinValue;
for (var shift = 2; shift <= BreakoutLookback; shift++)
{
var candle = GetCandle(shift);
highest = Math.Max(highest, candle.HighPrice);
}
return prevClose > highest;
}
private bool HasStochasticCross(decimal kValue, decimal dValue)
{
return kValue > dValue;
}
private bool IsStochasticContained()
{
if (StochasticRangePeriod <= 0)
return true;
if (_stochasticHistory.Count < StochasticRangePeriod)
return false;
var history = _stochasticHistory.ToArray();
return history.All(v => v <= StochasticUpperLevel && v >= StochasticLowerLevel);
}
private decimal? CalculateStopPrice()
{
if (StopLossLookback <= 0)
return null;
var lowest = decimal.MaxValue;
var bars = Math.Min(StopLossLookback, _candles.Count);
for (var shift = 1; shift <= bars; shift++)
{
var candle = GetCandle(shift);
lowest = Math.Min(lowest, candle.LowPrice);
}
return lowest == decimal.MaxValue ? null : lowest;
}
private ICandleMessage GetCandle(int shift)
{
return _candles[^shift];
}
public enum RowSequenceModes
{
/// <summary>
/// Only direction and minimum body size are checked.
/// </summary>
Normal,
/// <summary>
/// Each candle must have a larger body than the previous one.
/// </summary>
Bigger,
/// <summary>
/// Each candle must have a smaller body than the previous one.
/// </summary>
Smaller
}
}
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.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class bull_row_breakout_strategy(Strategy):
def __init__(self):
super(bull_row_breakout_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._ema_period = self.Param("EmaPeriod", 20)
self._bull_row_size = self.Param("BullRowSize", 2)
self._breakout_lookback = self.Param("BreakoutLookback", 10)
self._stop_loss_pct = self.Param("StopLossPct", 2.0)
self._take_profit_pct = self.Param("TakeProfitPct", 3.0)
self._candles = []
self._entry_price = 0.0
self._has_prev = False
self._prev_close = 0.0
self._prev_ema = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def EmaPeriod(self):
return self._ema_period.Value
@EmaPeriod.setter
def EmaPeriod(self, value):
self._ema_period.Value = value
@property
def BullRowSize(self):
return self._bull_row_size.Value
@BullRowSize.setter
def BullRowSize(self, value):
self._bull_row_size.Value = value
@property
def BreakoutLookback(self):
return self._breakout_lookback.Value
@BreakoutLookback.setter
def BreakoutLookback(self, value):
self._breakout_lookback.Value = value
@property
def StopLossPct(self):
return self._stop_loss_pct.Value
@StopLossPct.setter
def StopLossPct(self, value):
self._stop_loss_pct.Value = value
@property
def TakeProfitPct(self):
return self._take_profit_pct.Value
@TakeProfitPct.setter
def TakeProfitPct(self, value):
self._take_profit_pct.Value = value
def OnReseted(self):
super(bull_row_breakout_strategy, self).OnReseted()
self._candles = []
self._entry_price = 0.0
self._has_prev = False
self._prev_close = 0.0
self._prev_ema = 0.0
def OnStarted2(self, time):
super(bull_row_breakout_strategy, self).OnStarted2(time)
self._candles = []
self._entry_price = 0.0
self._has_prev = False
self._prev_close = 0.0
self._prev_ema = 0.0
ema = ExponentialMovingAverage()
ema.Length = self.EmaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, self._process_candle).Start()
def _has_bull_row(self):
size = self.BullRowSize
if len(self._candles) < size:
return False
for i in range(size):
c = self._candles[-(i + 1)]
if float(c.ClosePrice) <= float(c.OpenPrice):
return False
return True
def _has_breakout(self):
lookback = self.BreakoutLookback
if len(self._candles) < lookback + 1:
return False
prev_close = float(self._candles[-1].ClosePrice)
highest = float('-inf')
for i in range(2, min(lookback + 1, len(self._candles) + 1)):
if i <= len(self._candles):
highest = max(highest, float(self._candles[-i].HighPrice))
return prev_close > highest
def _process_candle(self, candle, ema_value):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
ema_val = float(ema_value)
# Check SL/TP on existing position
if self.Position != 0 and self._entry_price > 0:
if self.Position > 0:
pnl_pct = (close - self._entry_price) / self._entry_price * 100.0
if pnl_pct <= -float(self.StopLossPct) or pnl_pct >= float(self.TakeProfitPct):
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
pnl_pct = (self._entry_price - close) / self._entry_price * 100.0
if pnl_pct <= -float(self.StopLossPct) or pnl_pct >= float(self.TakeProfitPct):
self.BuyMarket()
self._entry_price = 0.0
self._candles.append(candle)
max_needed = max(self.BullRowSize, self.BreakoutLookback) + 5
if len(self._candles) > max_needed:
self._candles.pop(0)
if self._has_prev:
# Buy: EMA crossover + bull row + breakout
if self._prev_close <= self._prev_ema and close > ema_val and self._has_bull_row() and self.Position <= 0:
self.BuyMarket()
self._entry_price = close
elif self._prev_close >= self._prev_ema and close < ema_val and self.Position >= 0:
self.SellMarket()
self._entry_price = close
self._prev_close = close
self._prev_ema = ema_val
self._has_prev = True
def CreateClone(self):
return bull_row_breakout_strategy()