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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// BreakRevert Pro strategy converted from the MetaTrader 5 expert advisor.
/// The strategy blends breakout and mean-reversion logic using multi-timeframe candles.
/// </summary>
public class BreakRevertProStrategy : Strategy
{
private readonly StrategyParam<decimal> _riskPerTrade;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _breakoutThreshold;
private readonly StrategyParam<decimal> _meanReversionThreshold;
private readonly StrategyParam<int> _tradeDelaySeconds;
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<bool> _enableSafetyTrade;
private readonly StrategyParam<int> _safetyTradeIntervalSeconds;
private readonly StrategyParam<DataType> _candleType;
private ISubscriptionHandler<ICandleMessage> _m1Subscription;
private ISubscriptionHandler<ICandleMessage> _m15Subscription;
private ISubscriptionHandler<ICandleMessage> _h1Subscription;
private AverageTrueRange _m1Atr;
private SimpleMovingAverage _m1TrendAverage;
private SimpleMovingAverage _m15TrendAverage;
private SimpleMovingAverage _h1TrendAverage;
private SimpleMovingAverage _eventFrequency;
private ExponentialMovingAverage _volatilityEma;
private decimal _poissonProbability = 0.5m;
private decimal _weibullProbability = 0.5m;
private decimal _exponentialProbability = 0.5m;
private decimal _m1Trend;
private decimal _m15Trend;
private decimal _h1Trend;
private decimal _h1Volatility;
private decimal? _previousM1Close;
private decimal _latestAtr;
private DateTimeOffset? _lastTradeTime;
private DateTimeOffset? _lastSafetyCheck;
private bool _safetyTradeSent;
/// <summary>
/// Initializes a new instance of the <see cref="BreakRevertProStrategy"/> class.
/// </summary>
public BreakRevertProStrategy()
{
_riskPerTrade = Param(nameof(RiskPerTrade), 1m)
.SetDisplay("Risk %", "Risk per trade as percentage of portfolio value", "Risk")
.SetOptimize(0.5m, 5m, 0.5m);
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetRange(10, 60)
.SetDisplay("Lookback", "Number of finished candles used for statistics", "Signals")
;
_breakoutThreshold = Param(nameof(BreakoutThreshold), 0.1m)
.SetDisplay("Breakout Threshold", "Minimum composite probability required for breakout entries", "Signals")
.SetOptimize(0.2m, 0.8m, 0.05m);
_meanReversionThreshold = Param(nameof(MeanReversionThreshold), 0.6m)
.SetDisplay("Reversion Threshold", "Maximum probability that still allows mean-reversion trades", "Signals")
.SetOptimize(0.2m, 0.8m, 0.05m);
_tradeDelaySeconds = Param(nameof(TradeDelaySeconds), 300)
.SetDisplay("Trade Delay", "Minimum delay between consecutive entries (seconds)", "Risk");
_maxPositions = Param(nameof(MaxPositions), 1)
.SetDisplay("Max Positions", "Maximum number of simultaneously open positions", "Risk");
_enableSafetyTrade = Param(nameof(EnableSafetyTrade), true)
.SetDisplay("Safety Trade", "Allow protective trades when validation requires at least one position", "Safety");
_safetyTradeIntervalSeconds = Param(nameof(SafetyTradeIntervalSeconds), 900)
.SetDisplay("Safety Interval", "Delay between safety trade checks (seconds)", "Safety");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Primary Candles", "Primary timeframe for signal generation", "Data");
}
/// <summary>
/// Gets or sets the risk per trade in percent.
/// </summary>
public decimal RiskPerTrade
{
get => _riskPerTrade.Value;
set => _riskPerTrade.Value = value;
}
/// <summary>
/// Gets or sets the number of candles used in rolling calculations.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Gets or sets the breakout probability threshold.
/// </summary>
public decimal BreakoutThreshold
{
get => _breakoutThreshold.Value;
set => _breakoutThreshold.Value = value;
}
/// <summary>
/// Gets or sets the mean-reversion probability threshold.
/// </summary>
public decimal MeanReversionThreshold
{
get => _meanReversionThreshold.Value;
set => _meanReversionThreshold.Value = value;
}
/// <summary>
/// Gets or sets the minimum delay between trades.
/// </summary>
public int TradeDelaySeconds
{
get => _tradeDelaySeconds.Value;
set => _tradeDelaySeconds.Value = value;
}
/// <summary>
/// Gets or sets the maximum simultaneous positions.
/// </summary>
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
/// <summary>
/// Gets or sets a value indicating whether safety trades are allowed.
/// </summary>
public bool EnableSafetyTrade
{
get => _enableSafetyTrade.Value;
set => _enableSafetyTrade.Value = value;
}
/// <summary>
/// Gets or sets the safety trade interval in seconds.
/// </summary>
public int SafetyTradeIntervalSeconds
{
get => _safetyTradeIntervalSeconds.Value;
set => _safetyTradeIntervalSeconds.Value = value;
}
/// <summary>
/// Gets or sets the primary candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_m1Subscription = null;
_m15Subscription = null;
_h1Subscription = null;
_m1Atr = null;
_m1TrendAverage = null;
_m15TrendAverage = null;
_h1TrendAverage = null;
_eventFrequency = null;
_volatilityEma = null;
_poissonProbability = 0.5m;
_weibullProbability = 0.5m;
_exponentialProbability = 0.5m;
_m1Trend = 0m;
_m15Trend = 0m;
_h1Trend = 0m;
_h1Volatility = 0m;
_previousM1Close = null;
_latestAtr = 0m;
_lastTradeTime = null;
_lastSafetyCheck = null;
_safetyTradeSent = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var lookback = Math.Max(1, LookbackPeriod);
_m1Atr = new AverageTrueRange { Length = lookback };
_m1TrendAverage = new SimpleMovingAverage { Length = lookback };
_m15TrendAverage = new SimpleMovingAverage { Length = lookback };
_h1TrendAverage = new SimpleMovingAverage { Length = lookback };
_eventFrequency = new SimpleMovingAverage { Length = lookback };
_volatilityEma = new ExponentialMovingAverage { Length = lookback };
// Subscribe to the main one-minute flow.
_m1Subscription = SubscribeCandles(CandleType);
_m1Subscription
.Bind(_m1Atr, ProcessPrimaryCandle)
.Start();
// Additional fifteen-minute stream provides mid-term trend confirmation.
_m15Subscription = SubscribeCandles(TimeSpan.FromMinutes(15).TimeFrame());
_m15Subscription
.Bind(ProcessM15Candle)
.Start();
// Hourly candles track the broader context and volatility envelope.
_h1Subscription = SubscribeCandles(TimeSpan.FromHours(1).TimeFrame());
_h1Subscription
.Bind(ProcessH1Candle)
.Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
}
private void ProcessPrimaryCandle(ICandleMessage candle, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
_latestAtr = atrValue;
var close = candle.ClosePrice;
var time = candle.CloseTime;
var pip = GetPipSize();
if (_m1TrendAverage is not null)
{
var trendValue = _m1TrendAverage.Process(new DecimalIndicatorValue(_m1TrendAverage, close, time) { IsFinal = true }).ToDecimal();
if (_m1TrendAverage.IsFormed)
_m1Trend = close - trendValue;
}
if (_previousM1Close is decimal previousClose)
{
var move = Math.Abs(close - previousClose);
var eventValue = move >= pip * 5m ? 1m : 0m;
if (_eventFrequency is not null)
{
var avg = _eventFrequency.Process(new DecimalIndicatorValue(_eventFrequency, eventValue, time) { IsFinal = true }).ToDecimal();
if (_eventFrequency.IsFormed)
_poissonProbability = Clamp(avg, 0m, 1m);
}
if (_volatilityEma is not null)
{
var ema = _volatilityEma.Process(new DecimalIndicatorValue(_volatilityEma, move, time) { IsFinal = true }).ToDecimal();
if (_volatilityEma.IsFormed)
{
var normalized = pip > 0m ? ema / (pip * 10m) : 0m;
_exponentialProbability = Clamp(normalized, 0m, 1m);
}
}
}
_previousM1Close = close;
var normalizedAtr = pip > 0m ? atrValue / (pip * 10m) : 0m;
_weibullProbability = Clamp(normalizedAtr, 0m, 1m);
EvaluateSignals(candle);
}
private void ProcessM15Candle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_m15TrendAverage is null)
return;
var close = candle.ClosePrice;
var trend = _m15TrendAverage.Process(new DecimalIndicatorValue(_m15TrendAverage, close, candle.CloseTime) { IsFinal = true }).ToDecimal();
if (_m15TrendAverage.IsFormed)
_m15Trend = close - trend;
}
private void ProcessH1Candle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_h1Volatility = candle.HighPrice - candle.LowPrice;
if (_h1TrendAverage is null)
return;
var close = candle.ClosePrice;
var trend = _h1TrendAverage.Process(new DecimalIndicatorValue(_h1TrendAverage, close, candle.CloseTime) { IsFinal = true }).ToDecimal();
if (_h1TrendAverage.IsFormed)
_h1Trend = close - trend;
}
private void EvaluateSignals(ICandleMessage candle)
{
var now = candle.CloseTime;
var pip = GetPipSize();
if (_lastTradeTime is DateTimeOffset last && (now - last).TotalSeconds < TradeDelaySeconds)
return;
var tradeVolume = GetTradeVolume();
if (tradeVolume <= 0m)
return;
if (Position != 0)
return;
var breakout = IsBreakoutSignal(pip);
var reversion = IsMeanReversionSignal(pip);
if (breakout)
{
BuyMarket();
_lastTradeTime = now;
}
else if (reversion)
{
SellMarket();
_lastTradeTime = now;
}
}
private bool IsBreakoutSignal(decimal pip)
{
var trendUp = _m1Trend > 0m;
var probabilityOk = _poissonProbability >= BreakoutThreshold || _weibullProbability >= BreakoutThreshold;
return trendUp && probabilityOk;
}
private bool IsMeanReversionSignal(decimal pip)
{
var trendDown = _m1Trend < 0m;
var probabilityOk = _weibullProbability <= MeanReversionThreshold || _poissonProbability <= MeanReversionThreshold;
return trendDown && probabilityOk;
}
private void EnterLong(decimal volume)
{
var totalVolume = volume;
if (Position < 0m)
{
totalVolume += Math.Abs(Position);
}
// Execute a market order to align with the breakout signal.
BuyMarket(totalVolume);
}
private void EnterShort(decimal volume)
{
var totalVolume = volume;
if (Position > 0m)
{
totalVolume += Math.Abs(Position);
}
// Execute a market order to capture the expected pullback.
SellMarket(totalVolume);
}
private void CheckSafetyTrade(DateTimeOffset time, decimal volume)
{
if (!EnableSafetyTrade || _safetyTradeSent || Position != 0m)
return;
if (_lastSafetyCheck is DateTimeOffset last && (time - last).TotalSeconds < SafetyTradeIntervalSeconds)
return;
_lastSafetyCheck = time;
var direction = _m1Trend + _m15Trend;
if (direction > 0m)
{
BuyMarket(volume);
}
else
{
SellMarket(volume);
}
_safetyTradeSent = true;
_lastTradeTime = time;
}
private bool HasReachedMaxExposure(int direction, decimal tradeVolume)
{
if (MaxPositions <= 0 || tradeVolume <= 0m)
return false;
var limit = MaxPositions * tradeVolume;
return direction switch
{
> 0 => Position >= limit,
< 0 => -Position >= limit,
_ => Math.Abs(Position) >= limit,
};
}
private decimal GetTradeVolume()
{
if (Volume > 0m)
return Volume;
var stepVolume = Security?.VolumeStep ?? 1m;
var lotStep = Security?.VolumeStep ?? stepVolume;
var minVolume = Security?.MinVolume ?? stepVolume;
var maxVolume = Security?.MaxVolume ?? decimal.MaxValue;
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
var atr = Math.Max(_latestAtr, GetPipSize());
if (stepVolume <= 0m)
stepVolume = 1m;
if (lotStep <= 0m)
lotStep = stepVolume;
if (minVolume <= 0m)
minVolume = stepVolume;
if (balance <= 0m || atr <= 0m)
return minVolume;
var riskAmount = balance * RiskPerTrade / 100m;
if (riskAmount <= 0m)
return minVolume;
var riskPerUnit = atr;
var rawVolume = riskPerUnit > 0m ? riskAmount / riskPerUnit : minVolume;
rawVolume = Math.Max(rawVolume, minVolume);
var normalized = Math.Floor(rawVolume / lotStep) * lotStep;
if (normalized <= 0m)
normalized = minVolume;
if (maxVolume > 0m && normalized > maxVolume)
normalized = maxVolume;
return normalized;
}
private decimal GetPipSize()
{
var step = Security?.PriceStep;
if (step is null || step.Value <= 0m)
return 0.0001m;
return step.Value;
}
private static decimal Clamp(decimal value, decimal min, decimal max)
{
if (value < min)
return min;
return value > max ? max : value;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import AverageTrueRange, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class break_revert_pro_strategy(Strategy):
def __init__(self):
super(break_revert_pro_strategy, self).__init__()
self._risk_per_trade = self.Param("RiskPerTrade", 1.0) \
.SetDisplay("Risk %", "Risk per trade as percentage of portfolio value", "Risk")
self._lookback_period = self.Param("LookbackPeriod", 20) \
.SetDisplay("Lookback", "Number of finished candles used for statistics", "Signals")
self._breakout_threshold = self.Param("BreakoutThreshold", 0.1) \
.SetDisplay("Breakout Threshold", "Minimum composite probability required for breakout entries", "Signals")
self._mean_reversion_threshold = self.Param("MeanReversionThreshold", 0.6) \
.SetDisplay("Reversion Threshold", "Maximum probability that still allows mean-reversion trades", "Signals")
self._trade_delay_seconds = self.Param("TradeDelaySeconds", 300) \
.SetDisplay("Trade Delay", "Minimum delay between consecutive entries (seconds)", "Risk")
self._max_positions = self.Param("MaxPositions", 1) \
.SetDisplay("Max Positions", "Maximum number of simultaneously open positions", "Risk")
self._enable_safety_trade = self.Param("EnableSafetyTrade", True) \
.SetDisplay("Safety Trade", "Allow protective trades", "Safety")
self._safety_trade_interval_seconds = self.Param("SafetyTradeIntervalSeconds", 900) \
.SetDisplay("Safety Interval", "Delay between safety trade checks (seconds)", "Safety")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Primary Candles", "Primary timeframe for signal generation", "Data")
self._m1_atr = None
self._m1_trend_sma = None
self._poisson_probability = 0.5
self._weibull_probability = 0.5
self._exponential_probability = 0.5
self._m1_trend = 0.0
self._m15_trend = 0.0
self._h1_trend = 0.0
self._h1_volatility = 0.0
self._previous_m1_close = None
self._latest_atr = 0.0
self._last_trade_time = None
self._last_safety_check = None
self._safety_trade_sent = False
# manual rolling buffers for event_frequency (SMA) and volatility (EMA)
self._event_buffer = []
self._vol_ema = None
self._vol_ema_k = 0.0
self._lookback = 20
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def RiskPerTrade(self):
return self._risk_per_trade.Value
@property
def LookbackPeriod(self):
return self._lookback_period.Value
@property
def BreakoutThreshold(self):
return self._breakout_threshold.Value
@property
def MeanReversionThreshold(self):
return self._mean_reversion_threshold.Value
@property
def TradeDelaySeconds(self):
return self._trade_delay_seconds.Value
@property
def MaxPositions(self):
return self._max_positions.Value
@property
def EnableSafetyTrade(self):
return self._enable_safety_trade.Value
@property
def SafetyTradeIntervalSeconds(self):
return self._safety_trade_interval_seconds.Value
def OnReseted(self):
super(break_revert_pro_strategy, self).OnReseted()
self._m1_atr = None
self._m1_trend_sma = None
self._poisson_probability = 0.5
self._weibull_probability = 0.5
self._exponential_probability = 0.5
self._m1_trend = 0.0
self._m15_trend = 0.0
self._h1_trend = 0.0
self._h1_volatility = 0.0
self._previous_m1_close = None
self._latest_atr = 0.0
self._last_trade_time = None
self._last_safety_check = None
self._safety_trade_sent = False
self._event_buffer = []
self._vol_ema = None
def OnStarted2(self, time):
super(break_revert_pro_strategy, self).OnStarted2(time)
self._lookback = max(1, self.LookbackPeriod)
self._m1_atr = AverageTrueRange()
self._m1_atr.Length = self._lookback
self._m1_trend_sma = SimpleMovingAverage()
self._m1_trend_sma.Length = self._lookback
self._m15_trend_sma = SimpleMovingAverage()
self._m15_trend_sma.Length = self._lookback
self._h1_trend_sma = SimpleMovingAverage()
self._h1_trend_sma.Length = self._lookback
# EMA multiplier for volatility EMA
self._vol_ema_k = 2.0 / (self._lookback + 1.0)
self._vol_ema = None
self._event_buffer = []
# Primary candle subscription with ATR and trend SMA bound
sub1 = self.SubscribeCandles(self.CandleType)
sub1.Bind(self._m1_atr, self._m1_trend_sma, self._process_primary_candle).Start()
# M15 subscription with trend SMA bound
sub15 = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(15)))
sub15.Bind(self._m15_trend_sma, self._process_m15_candle).Start()
# H1 subscription with trend SMA bound
sub_h1 = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromHours(1)))
sub_h1.Bind(self._h1_trend_sma, self._process_h1_candle).Start()
self.StartProtection(
takeProfit=Unit(2, UnitTypes.Percent),
stopLoss=Unit(1, UnitTypes.Percent))
def _get_pip_size(self):
if self.Security is not None and self.Security.PriceStep is not None:
s = float(self.Security.PriceStep)
if s > 0:
return s
return 0.0001
def _clamp(self, value, lo, hi):
if value < lo:
return lo
if value > hi:
return hi
return value
def _process_primary_candle(self, candle, atr_value, trend_sma_value):
if candle.State != CandleStates.Finished:
return
self._latest_atr = float(atr_value)
close = float(candle.ClosePrice)
pip = self._get_pip_size()
# M1 trend: close - SMA(close)
if self._m1_trend_sma is not None and self._m1_trend_sma.IsFormed:
self._m1_trend = close - float(trend_sma_value)
if self._previous_m1_close is not None:
move = abs(close - self._previous_m1_close)
# event_frequency: manual SMA of event values
event_value = 1.0 if move >= pip * 5.0 else 0.0
self._event_buffer.append(event_value)
if len(self._event_buffer) > self._lookback:
self._event_buffer.pop(0)
if len(self._event_buffer) >= self._lookback:
avg = sum(self._event_buffer) / len(self._event_buffer)
self._poisson_probability = self._clamp(avg, 0.0, 1.0)
# volatility EMA: manual EMA of move
if self._vol_ema is None:
self._vol_ema = move
else:
self._vol_ema = move * self._vol_ema_k + self._vol_ema * (1.0 - self._vol_ema_k)
normalized = self._vol_ema / (pip * 10.0) if pip > 0 else 0.0
self._exponential_probability = self._clamp(normalized, 0.0, 1.0)
self._previous_m1_close = close
normalized_atr = self._latest_atr / (pip * 10.0) if pip > 0 else 0.0
self._weibull_probability = self._clamp(normalized_atr, 0.0, 1.0)
self._evaluate_signals(candle)
def _process_m15_candle(self, candle, trend_sma_value):
if candle.State != CandleStates.Finished:
return
if self._m15_trend_sma is not None and self._m15_trend_sma.IsFormed:
self._m15_trend = float(candle.ClosePrice) - float(trend_sma_value)
def _process_h1_candle(self, candle, trend_sma_value):
if candle.State != CandleStates.Finished:
return
self._h1_volatility = float(candle.HighPrice) - float(candle.LowPrice)
if self._h1_trend_sma is not None and self._h1_trend_sma.IsFormed:
self._h1_trend = float(candle.ClosePrice) - float(trend_sma_value)
def _evaluate_signals(self, candle):
now = candle.CloseTime
if self._last_trade_time is not None:
diff = now.Subtract(self._last_trade_time)
if diff.TotalSeconds < self.TradeDelaySeconds:
return
if self.Position != 0:
return
breakout = self._m1_trend > 0 and (self._poisson_probability >= self.BreakoutThreshold or self._weibull_probability >= self.BreakoutThreshold)
reversion = self._m1_trend < 0 and (self._weibull_probability <= self.MeanReversionThreshold or self._poisson_probability <= self.MeanReversionThreshold)
if breakout:
self.BuyMarket()
self._last_trade_time = now
elif reversion:
self.SellMarket()
self._last_trade_time = now
def CreateClone(self):
return break_revert_pro_strategy()