Trend Catcher Breakout Strategy
Overview
The Trend Catcher strategy is a conversion of the MetaTrader 5 expert advisor "Trend_Catcher_v2". It combines three exponential moving averages with the Parabolic SAR indicator to identify trend reversals and trend continuation opportunities. The system operates on a single symbol and timeframe and relies on end-of-candle calculations, which makes it suitable for backtesting in StockSharp Designer as well as for live execution through StockSharp API-based runners.
Indicators and Filters
- Parabolic SAR — detects bullish and bearish flips that indicate potential reversals.
- Slow EMA — the higher timeframe trend filter that defines the dominant direction.
- Fast EMA — reacts faster to price changes to confirm the direction of the current swing.
- Trigger EMA — keeps the entry close to price action and avoids trades taken too far away from the mean.
- Trading day switches — optional filters to disable trading on selected weekdays.
Trading Logic
Long entries
- The close price finishes above the current Parabolic SAR value.
- The previous candle closed below the previous Parabolic SAR value (bullish flip).
- The fast EMA is above the slow EMA, confirming an uptrend.
- The close price is above the trigger EMA to avoid counter-trend signals.
- No position is open and no position was closed during the current candle.
Short entries
All the above conditions are mirrored:
- The close price finishes below the current Parabolic SAR value.
- The previous candle closed above the previous Parabolic SAR value (bearish flip).
- The fast EMA is below the slow EMA.
- The close price is below the trigger EMA.
- No position is open and no position was closed during the current candle.
When the Reverse Signals switch is enabled the long and short conditions are inverted, allowing the strategy to trade breakouts in the opposite direction.
Position Management
- Automatic stop-loss – when enabled the stop is calculated from the distance between price and Parabolic SAR multiplied by the
StopLossCoefficient. The distance is clamped betweenMinStopLossandMaxStopLoss. - Automatic take-profit – multiplies the stop distance by
TakeProfitCoefficient. Manual distances can be used when automation is disabled. - Risk-based position sizing – the trade size is derived from portfolio equity and
RiskPercent. When the most recent closed trade is a loss and Use Martingale is enabled the calculated size is multiplied byMartingaleMultiplier. - Breakeven and trailing stop – after reaching
BreakevenTriggerprofit the stop is moved to the entry price plusBreakevenOffset(or minus for short trades). Once the position gainsTrailingTrigger, the stop trails price byTrailingStep. - Close on opposite signal – when active, the strategy exits an existing position as soon as an opposing setup appears.
- One trade per candle – the algorithm stores the timestamp of the latest exit and skips entries until the next candle opens.
Parameters
| Name | Description | Default |
|---|---|---|
CandleType |
Main timeframe used for all indicators. | 15-minute time frame |
CloseOnOppositeSignal |
Exit immediately when the reverse setup is detected. | true |
ReverseSignals |
Swap long and short conditions. | false |
TradeMonday … TradeFriday |
Enable or disable trading on specific weekdays. | true |
SlowMaPeriod |
Period of the slow EMA trend filter. | 200 |
FastMaPeriod |
Period of the fast EMA confirmation. | 50 |
FastFilterPeriod |
Period of the trigger EMA. | 25 |
SarStep |
Parabolic SAR acceleration step. | 0.004 |
SarMax |
Maximum Parabolic SAR acceleration. | 0.2 |
AutoStopLoss |
Enable dynamic stop-loss calculation. | true |
AutoTakeProfit |
Enable dynamic take-profit calculation. | true |
MinStopLoss / MaxStopLoss |
Lower and upper bounds for the stop distance. | 0.001 / 0.2 |
StopLossCoefficient |
Multiplier applied to the SAR distance. | 1 |
TakeProfitCoefficient |
Multiplier used for the take-profit distance. | 1 |
ManualStopLoss |
Fixed stop distance when automation is disabled. | 0.002 |
ManualTakeProfit |
Fixed target distance when automation is disabled. | 0.02 |
RiskPercent |
Percentage of portfolio equity risked per trade. | 2 |
UseMartingale |
Increase size after a losing trade. | true |
MartingaleMultiplier |
Multiplier applied after a loss. | 2 |
BreakevenTrigger |
Profit needed before moving the stop to breakeven. | 0.005 |
BreakevenOffset |
Buffer added when the stop is moved to breakeven. | 0.0001 |
TrailingTrigger |
Profit required to start trailing the stop. | 0.005 |
TrailingStep |
Distance maintained by the trailing stop. | 0.001 |
Usage Notes
- The strategy sends market orders for both entries and exits; slippage controls should be added at the brokerage adapter level if required.
- Because the logic uses end-of-candle data, the accuracy of backtests depends on the granularity of the candle series supplied to the strategy.
- Parameters are fully exposed through
StrategyParamobjects, making them available for optimization in StockSharp Designer.
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>
/// Trend Catcher strategy converted from MetaTrader 5 implementation.
/// Combines Parabolic SAR flips with EMA trend filters and adaptive risk management.
/// </summary>
public class TrendCatcherBreakoutStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<bool> _closeOnOppositeSignal;
private readonly StrategyParam<bool> _reverseSignals;
private readonly StrategyParam<bool> _tradeMonday;
private readonly StrategyParam<bool> _tradeTuesday;
private readonly StrategyParam<bool> _tradeWednesday;
private readonly StrategyParam<bool> _tradeThursday;
private readonly StrategyParam<bool> _tradeFriday;
private readonly StrategyParam<int> _slowMaPeriod;
private readonly StrategyParam<int> _fastMaPeriod;
private readonly StrategyParam<int> _fastFilterPeriod;
private readonly StrategyParam<decimal> _sarStep;
private readonly StrategyParam<decimal> _sarMax;
private readonly StrategyParam<bool> _autoStopLoss;
private readonly StrategyParam<bool> _autoTakeProfit;
private readonly StrategyParam<decimal> _minStopLoss;
private readonly StrategyParam<decimal> _maxStopLoss;
private readonly StrategyParam<decimal> _stopLossCoefficient;
private readonly StrategyParam<decimal> _takeProfitCoefficient;
private readonly StrategyParam<decimal> _manualStopLoss;
private readonly StrategyParam<decimal> _manualTakeProfit;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<bool> _useMartingale;
private readonly StrategyParam<decimal> _martingaleMultiplier;
private readonly StrategyParam<decimal> _breakevenTrigger;
private readonly StrategyParam<decimal> _breakevenOffset;
private readonly StrategyParam<decimal> _trailingTrigger;
private readonly StrategyParam<decimal> _trailingStep;
private ExponentialMovingAverage _slowMa = null!;
private ExponentialMovingAverage _fastMa = null!;
private ExponentialMovingAverage _fastFilterMa = null!;
private ParabolicSar _parabolicSar = null!;
private decimal _previousClose;
private decimal? _previousSar;
private decimal? _entryPrice;
private decimal _stopLossPrice;
private decimal _takeProfitPrice;
private bool _lastTradeWasLoss;
private DateTimeOffset? _lastExitTime;
/// <summary>
/// Initializes a new instance of the <see cref="TrendCatcherBreakoutStrategy"/> class.
/// </summary>
public TrendCatcherBreakoutStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for signal calculations", "General");
_closeOnOppositeSignal = Param(nameof(CloseOnOppositeSignal), true)
.SetDisplay("Close On Opposite", "Exit when an opposite signal appears", "General");
_reverseSignals = Param(nameof(ReverseSignals), false)
.SetDisplay("Reverse Signals", "Invert long and short entries", "General");
_tradeMonday = Param(nameof(TradeMonday), true)
.SetDisplay("Trade Monday", "Allow trading on Mondays", "Trading Days");
_tradeTuesday = Param(nameof(TradeTuesday), true)
.SetDisplay("Trade Tuesday", "Allow trading on Tuesdays", "Trading Days");
_tradeWednesday = Param(nameof(TradeWednesday), true)
.SetDisplay("Trade Wednesday", "Allow trading on Wednesdays", "Trading Days");
_tradeThursday = Param(nameof(TradeThursday), true)
.SetDisplay("Trade Thursday", "Allow trading on Thursdays", "Trading Days");
_tradeFriday = Param(nameof(TradeFriday), true)
.SetDisplay("Trade Friday", "Allow trading on Fridays", "Trading Days");
_slowMaPeriod = Param(nameof(SlowMaPeriod), 200)
.SetGreaterThanZero()
.SetDisplay("Slow EMA", "Length of the slow EMA filter", "Indicators");
_fastMaPeriod = Param(nameof(FastMaPeriod), 50)
.SetGreaterThanZero()
.SetDisplay("Fast EMA", "Length of the fast EMA", "Indicators");
_fastFilterPeriod = Param(nameof(FastFilterPeriod), 25)
.SetGreaterThanZero()
.SetDisplay("Trigger EMA", "Length of the trigger EMA", "Indicators");
_sarStep = Param(nameof(SarStep), 0.004m)
.SetGreaterThanZero()
.SetDisplay("SAR Step", "Acceleration step for Parabolic SAR", "Indicators");
_sarMax = Param(nameof(SarMax), 0.2m)
.SetGreaterThanZero()
.SetDisplay("SAR Max", "Maximum acceleration for Parabolic SAR", "Indicators");
_autoStopLoss = Param(nameof(AutoStopLoss), true)
.SetDisplay("Auto Stop Loss", "Derive stop-loss from Parabolic SAR", "Risk");
_autoTakeProfit = Param(nameof(AutoTakeProfit), true)
.SetDisplay("Auto Take Profit", "Derive take-profit from stop-loss", "Risk");
_minStopLoss = Param(nameof(MinStopLoss), 0.001m)
.SetGreaterThanZero()
.SetDisplay("Min Stop", "Minimum allowed stop distance", "Risk");
_maxStopLoss = Param(nameof(MaxStopLoss), 0.2m)
.SetGreaterThanZero()
.SetDisplay("Max Stop", "Maximum allowed stop distance", "Risk");
_stopLossCoefficient = Param(nameof(StopLossCoefficient), 1m)
.SetGreaterThanZero()
.SetDisplay("SL Coefficient", "Multiplier applied to SAR distance", "Risk");
_takeProfitCoefficient = Param(nameof(TakeProfitCoefficient), 1m)
.SetGreaterThanZero()
.SetDisplay("TP Coefficient", "Multiplier applied to take-profit distance", "Risk");
_manualStopLoss = Param(nameof(ManualStopLoss), 0.002m)
.SetGreaterThanZero()
.SetDisplay("Manual Stop", "Fixed stop distance when automation is disabled", "Risk");
_manualTakeProfit = Param(nameof(ManualTakeProfit), 0.02m)
.SetGreaterThanZero()
.SetDisplay("Manual Target", "Fixed target distance when automation is disabled", "Risk");
_riskPercent = Param(nameof(RiskPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Risk %", "Account risk per trade", "Risk");
_useMartingale = Param(nameof(UseMartingale), true)
.SetDisplay("Use Martingale", "Increase risk after a losing trade", "Risk");
_martingaleMultiplier = Param(nameof(MartingaleMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Martingale Mult", "Multiplier applied after a loss", "Risk");
_breakevenTrigger = Param(nameof(BreakevenTrigger), 0.005m)
.SetGreaterThanZero()
.SetDisplay("Breakeven Trigger", "Profit needed before moving stop to entry", "Exits");
_breakevenOffset = Param(nameof(BreakevenOffset), 0.0001m)
.SetGreaterThanZero()
.SetDisplay("Breakeven Offset", "Extra buffer when moving stop to breakeven", "Exits");
_trailingTrigger = Param(nameof(TrailingTrigger), 0.005m)
.SetGreaterThanZero()
.SetDisplay("Trailing Trigger", "Profit needed to activate trailing stop", "Exits");
_trailingStep = Param(nameof(TrailingStep), 0.001m)
.SetGreaterThanZero()
.SetDisplay("Trailing Step", "Distance maintained by the trailing stop", "Exits");
}
/// <summary>
/// Selected candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Gets or sets whether to close on opposite signal.
/// </summary>
public bool CloseOnOppositeSignal
{
get => _closeOnOppositeSignal.Value;
set => _closeOnOppositeSignal.Value = value;
}
/// <summary>
/// Gets or sets whether to reverse signals.
/// </summary>
public bool ReverseSignals
{
get => _reverseSignals.Value;
set => _reverseSignals.Value = value;
}
public bool TradeMonday
{
get => _tradeMonday.Value;
set => _tradeMonday.Value = value;
}
public bool TradeTuesday
{
get => _tradeTuesday.Value;
set => _tradeTuesday.Value = value;
}
public bool TradeWednesday
{
get => _tradeWednesday.Value;
set => _tradeWednesday.Value = value;
}
public bool TradeThursday
{
get => _tradeThursday.Value;
set => _tradeThursday.Value = value;
}
public bool TradeFriday
{
get => _tradeFriday.Value;
set => _tradeFriday.Value = value;
}
public int SlowMaPeriod
{
get => _slowMaPeriod.Value;
set => _slowMaPeriod.Value = value;
}
public int FastMaPeriod
{
get => _fastMaPeriod.Value;
set => _fastMaPeriod.Value = value;
}
public int FastFilterPeriod
{
get => _fastFilterPeriod.Value;
set => _fastFilterPeriod.Value = value;
}
public decimal SarStep
{
get => _sarStep.Value;
set => _sarStep.Value = value;
}
public decimal SarMax
{
get => _sarMax.Value;
set => _sarMax.Value = value;
}
public bool AutoStopLoss
{
get => _autoStopLoss.Value;
set => _autoStopLoss.Value = value;
}
public bool AutoTakeProfit
{
get => _autoTakeProfit.Value;
set => _autoTakeProfit.Value = value;
}
public decimal MinStopLoss
{
get => _minStopLoss.Value;
set => _minStopLoss.Value = value;
}
public decimal MaxStopLoss
{
get => _maxStopLoss.Value;
set => _maxStopLoss.Value = value;
}
public decimal StopLossCoefficient
{
get => _stopLossCoefficient.Value;
set => _stopLossCoefficient.Value = value;
}
public decimal TakeProfitCoefficient
{
get => _takeProfitCoefficient.Value;
set => _takeProfitCoefficient.Value = value;
}
public decimal ManualStopLoss
{
get => _manualStopLoss.Value;
set => _manualStopLoss.Value = value;
}
public decimal ManualTakeProfit
{
get => _manualTakeProfit.Value;
set => _manualTakeProfit.Value = value;
}
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
public bool UseMartingale
{
get => _useMartingale.Value;
set => _useMartingale.Value = value;
}
public decimal MartingaleMultiplier
{
get => _martingaleMultiplier.Value;
set => _martingaleMultiplier.Value = value;
}
public decimal BreakevenTrigger
{
get => _breakevenTrigger.Value;
set => _breakevenTrigger.Value = value;
}
public decimal BreakevenOffset
{
get => _breakevenOffset.Value;
set => _breakevenOffset.Value = value;
}
public decimal TrailingTrigger
{
get => _trailingTrigger.Value;
set => _trailingTrigger.Value = value;
}
public decimal TrailingStep
{
get => _trailingStep.Value;
set => _trailingStep.Value = value;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousClose = 0m;
_previousSar = null;
_entryPrice = null;
_stopLossPrice = 0m;
_takeProfitPrice = 0m;
_lastTradeWasLoss = false;
_lastExitTime = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create indicators for Parabolic SAR and EMA filters.
_slowMa = new EMA { Length = SlowMaPeriod };
_fastMa = new EMA { Length = FastMaPeriod };
_fastFilterMa = new EMA { Length = FastFilterPeriod };
_parabolicSar = new ParabolicSar
{
Acceleration = SarStep,
AccelerationStep = SarStep,
AccelerationMax = SarMax
};
// Subscribe to candle flow and bind indicators to the processing method.
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_slowMa, _fastMa, _fastFilterMa, _parabolicSar, ProcessCandle)
.Start();
// Draw indicators and trades on the chart when possible.
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _slowMa);
DrawIndicator(area, _fastMa);
DrawIndicator(area, _fastFilterMa);
DrawIndicator(area, _parabolicSar);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal slow, decimal fast, decimal fastFilter, decimal sar)
{
// Skip unfinished candles.
if (candle.State != CandleStates.Finished)
return;
// Ensure data and connections are ready before trading.
if (!IsFormedAndOnlineAndAllowTrading())
return;
// Manage existing position and handle trailing logic.
var exitTriggered = ManageActivePosition(candle);
if (exitTriggered)
{
_previousClose = candle.ClosePrice;
_previousSar = sar;
return;
}
// Ignore signals on disabled trading days.
if (!IsTradingDay(candle.OpenTime.DayOfWeek))
{
_previousClose = candle.ClosePrice;
_previousSar = sar;
return;
}
// Detect SAR flips confirmed by EMA alignment.
var longSignal = false;
var shortSignal = false;
if (_previousSar is decimal prevSar && _previousClose != 0)
{
longSignal = candle.ClosePrice > sar &&
_previousClose < prevSar &&
fast > slow &&
candle.ClosePrice > fastFilter;
shortSignal = candle.ClosePrice < sar &&
_previousClose > prevSar &&
fast < slow &&
candle.ClosePrice < fastFilter;
}
if (ReverseSignals)
{
var temp = longSignal;
longSignal = shortSignal;
shortSignal = temp;
}
// Optionally exit when an opposite setup appears.
if (CloseOnOppositeSignal)
{
if (longSignal && Position < 0)
{
CloseShort(candle, candle.ClosePrice);
}
else if (shortSignal && Position > 0)
{
CloseLong(candle, candle.ClosePrice);
}
}
// Allow only one fresh entry per candle.
var canOpen = Position == 0 && (!_lastExitTime.HasValue || _lastExitTime < candle.OpenTime);
if (canOpen && longSignal)
{
TryOpenLong(candle, sar);
}
else if (canOpen && shortSignal)
{
TryOpenShort(candle, sar);
}
_previousClose = candle.ClosePrice;
_previousSar = sar;
}
private bool ManageActivePosition(ICandleMessage candle)
{
// Handle long positions.
if (Position > 0 && _entryPrice.HasValue)
{
var exitPrice = 0m;
if (_stopLossPrice > 0 && candle.LowPrice <= _stopLossPrice)
exitPrice = _stopLossPrice;
else if (_takeProfitPrice > 0 && candle.HighPrice >= _takeProfitPrice)
exitPrice = _takeProfitPrice;
if (exitPrice > 0)
{
CloseLong(candle, exitPrice);
return true;
}
var profit = candle.ClosePrice - _entryPrice.Value;
if (profit >= BreakevenTrigger)
{
var breakeven = _entryPrice.Value + BreakevenOffset;
if (_stopLossPrice < breakeven)
_stopLossPrice = breakeven;
}
if (profit >= TrailingTrigger)
{
var newStop = candle.ClosePrice - TrailingStep;
if (_stopLossPrice < newStop)
_stopLossPrice = newStop;
}
}
// Handle short positions.
else if (Position < 0 && _entryPrice.HasValue)
{
var exitPrice = 0m;
if (_stopLossPrice > 0 && candle.HighPrice >= _stopLossPrice)
exitPrice = _stopLossPrice;
else if (_takeProfitPrice > 0 && candle.LowPrice <= _takeProfitPrice)
exitPrice = _takeProfitPrice;
if (exitPrice > 0)
{
CloseShort(candle, exitPrice);
return true;
}
var profit = _entryPrice.Value - candle.ClosePrice;
if (profit >= BreakevenTrigger)
{
var breakeven = _entryPrice.Value - BreakevenOffset;
if (_stopLossPrice == 0 || _stopLossPrice > breakeven)
_stopLossPrice = breakeven;
}
if (profit >= TrailingTrigger)
{
var newStop = candle.ClosePrice + TrailingStep;
if (_stopLossPrice == 0 || _stopLossPrice > newStop)
_stopLossPrice = newStop;
}
}
return false;
}
private void TryOpenLong(ICandleMessage candle, decimal sar)
{
// Calculate stops and determine volume for a potential long entry.
if (!TryCalculateStops(candle.ClosePrice, sar, true, out var stopPrice, out var takePrice, out var stopDistance))
return;
var volume = CalculateOrderVolume(stopDistance);
if (volume <= 0)
return;
BuyMarket();
_entryPrice = candle.ClosePrice;
_stopLossPrice = stopPrice;
_takeProfitPrice = takePrice;
}
private void TryOpenShort(ICandleMessage candle, decimal sar)
{
// Calculate stops and determine volume for a potential short entry.
if (!TryCalculateStops(candle.ClosePrice, sar, false, out var stopPrice, out var takePrice, out var stopDistance))
return;
var volume = CalculateOrderVolume(stopDistance);
if (volume <= 0)
return;
SellMarket();
_entryPrice = candle.ClosePrice;
_stopLossPrice = stopPrice;
_takeProfitPrice = takePrice;
}
private void CloseLong(ICandleMessage candle, decimal exitPrice)
{
// Close long position with a market order.
var volume = Position;
if (volume <= 0)
return;
SellMarket();
FinalizeTrade(exitPrice, candle.OpenTime, false);
}
private void CloseShort(ICandleMessage candle, decimal exitPrice)
{
// Close short position with a market order.
var volume = Math.Abs(Position);
if (volume <= 0)
return;
BuyMarket();
FinalizeTrade(exitPrice, candle.OpenTime, true);
}
private void FinalizeTrade(decimal exitPrice, DateTimeOffset time, bool wasShort)
{
// Store result of the latest position for future sizing decisions.
if (_entryPrice.HasValue)
{
_lastTradeWasLoss = !wasShort ? exitPrice <= _entryPrice.Value : exitPrice >= _entryPrice.Value;
}
else
{
_lastTradeWasLoss = false;
}
_entryPrice = null;
_stopLossPrice = 0;
_takeProfitPrice = 0;
_lastExitTime = time;
}
private decimal CalculateOrderVolume(decimal stopDistance)
{
// Determine order size according to risk settings.
if (stopDistance <= 0)
return 0;
var volume = Volume;
var equity = Portfolio?.CurrentValue ?? 0m;
var riskAmount = equity * (RiskPercent / 100m);
if (riskAmount > 0)
{
var size = riskAmount / stopDistance;
if (size > 0)
volume = size;
}
if (UseMartingale && _lastTradeWasLoss)
volume *= MartingaleMultiplier;
return volume;
}
private bool TryCalculateStops(decimal entryPrice, decimal sar, bool isLong, out decimal stopPrice, out decimal takePrice, out decimal stopDistance)
{
// Build stop-loss and take-profit levels for the next order.
stopPrice = 0m;
takePrice = 0m;
stopDistance = 0m;
decimal distance;
if (AutoStopLoss)
{
if (sar == 0)
return false;
distance = Math.Abs(entryPrice - sar) * StopLossCoefficient;
}
else
{
distance = ManualStopLoss;
}
if (distance <= 0)
return false;
var minStop = Math.Min(MinStopLoss, MaxStopLoss);
var maxStop = Math.Max(MinStopLoss, MaxStopLoss);
distance = Clamp(distance, minStop, maxStop);
stopDistance = distance;
stopPrice = isLong ? entryPrice - distance : entryPrice + distance;
decimal targetDistance;
if (AutoTakeProfit)
{
targetDistance = distance * TakeProfitCoefficient;
}
else
{
targetDistance = ManualTakeProfit;
}
if (targetDistance > 0)
takePrice = isLong ? entryPrice + targetDistance : entryPrice - targetDistance;
return true;
}
private static decimal Clamp(decimal value, decimal min, decimal max)
{
// Helper method to clamp decimal values within a range.
if (value < min)
return min;
if (value > max)
return max;
return value;
}
private bool IsTradingDay(DayOfWeek day)
{
// Evaluate day-of-week trading switches.
return day switch
{
DayOfWeek.Monday => TradeMonday,
DayOfWeek.Tuesday => TradeTuesday,
DayOfWeek.Wednesday => TradeWednesday,
DayOfWeek.Thursday => TradeThursday,
DayOfWeek.Friday => TradeFriday,
_ => 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, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage, ParabolicSar
from StockSharp.Algo.Strategies import Strategy
class trend_catcher_breakout_strategy(Strategy):
def __init__(self):
super(trend_catcher_breakout_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._close_on_opposite_signal = self.Param("CloseOnOppositeSignal", True)
self._reverse_signals = self.Param("ReverseSignals", False)
self._slow_ma_period = self.Param("SlowMaPeriod", 200)
self._fast_ma_period = self.Param("FastMaPeriod", 50)
self._fast_filter_period = self.Param("FastFilterPeriod", 25)
self._sar_step = self.Param("SarStep", 0.004)
self._sar_max = self.Param("SarMax", 0.2)
self._auto_stop_loss = self.Param("AutoStopLoss", True)
self._auto_take_profit = self.Param("AutoTakeProfit", True)
self._min_stop_loss = self.Param("MinStopLoss", 0.001)
self._max_stop_loss = self.Param("MaxStopLoss", 0.2)
self._stop_loss_coefficient = self.Param("StopLossCoefficient", 1.0)
self._take_profit_coefficient = self.Param("TakeProfitCoefficient", 1.0)
self._manual_stop_loss = self.Param("ManualStopLoss", 0.002)
self._manual_take_profit = self.Param("ManualTakeProfit", 0.02)
self._breakeven_trigger = self.Param("BreakevenTrigger", 0.005)
self._breakeven_offset = self.Param("BreakevenOffset", 0.0001)
self._trailing_trigger = self.Param("TrailingTrigger", 0.005)
self._trailing_step = self.Param("TrailingStep", 0.001)
self._trade_monday = self.Param("TradeMonday", True)
self._trade_tuesday = self.Param("TradeTuesday", True)
self._trade_wednesday = self.Param("TradeWednesday", True)
self._trade_thursday = self.Param("TradeThursday", True)
self._trade_friday = self.Param("TradeFriday", True)
self._risk_percent = self.Param("RiskPercent", 2.0)
self._use_martingale = self.Param("UseMartingale", True)
self._martingale_multiplier = self.Param("MartingaleMultiplier", 2.0)
self._previous_close = 0.0
self._previous_sar = None
self._entry_price = None
self._stop_loss_price = 0.0
self._take_profit_price = 0.0
self._last_trade_was_loss = False
self._last_exit_time = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def CloseOnOppositeSignal(self):
return self._close_on_opposite_signal.Value
@CloseOnOppositeSignal.setter
def CloseOnOppositeSignal(self, value):
self._close_on_opposite_signal.Value = value
@property
def ReverseSignals(self):
return self._reverse_signals.Value
@ReverseSignals.setter
def ReverseSignals(self, value):
self._reverse_signals.Value = value
@property
def SlowMaPeriod(self):
return self._slow_ma_period.Value
@SlowMaPeriod.setter
def SlowMaPeriod(self, value):
self._slow_ma_period.Value = value
@property
def FastMaPeriod(self):
return self._fast_ma_period.Value
@FastMaPeriod.setter
def FastMaPeriod(self, value):
self._fast_ma_period.Value = value
@property
def FastFilterPeriod(self):
return self._fast_filter_period.Value
@FastFilterPeriod.setter
def FastFilterPeriod(self, value):
self._fast_filter_period.Value = value
@property
def SarStep(self):
return self._sar_step.Value
@SarStep.setter
def SarStep(self, value):
self._sar_step.Value = value
@property
def SarMax(self):
return self._sar_max.Value
@SarMax.setter
def SarMax(self, value):
self._sar_max.Value = value
@property
def AutoStopLoss(self):
return self._auto_stop_loss.Value
@AutoStopLoss.setter
def AutoStopLoss(self, value):
self._auto_stop_loss.Value = value
@property
def AutoTakeProfit(self):
return self._auto_take_profit.Value
@AutoTakeProfit.setter
def AutoTakeProfit(self, value):
self._auto_take_profit.Value = value
@property
def MinStopLoss(self):
return self._min_stop_loss.Value
@MinStopLoss.setter
def MinStopLoss(self, value):
self._min_stop_loss.Value = value
@property
def MaxStopLoss(self):
return self._max_stop_loss.Value
@MaxStopLoss.setter
def MaxStopLoss(self, value):
self._max_stop_loss.Value = value
@property
def StopLossCoefficient(self):
return self._stop_loss_coefficient.Value
@StopLossCoefficient.setter
def StopLossCoefficient(self, value):
self._stop_loss_coefficient.Value = value
@property
def TakeProfitCoefficient(self):
return self._take_profit_coefficient.Value
@TakeProfitCoefficient.setter
def TakeProfitCoefficient(self, value):
self._take_profit_coefficient.Value = value
@property
def ManualStopLoss(self):
return self._manual_stop_loss.Value
@ManualStopLoss.setter
def ManualStopLoss(self, value):
self._manual_stop_loss.Value = value
@property
def ManualTakeProfit(self):
return self._manual_take_profit.Value
@ManualTakeProfit.setter
def ManualTakeProfit(self, value):
self._manual_take_profit.Value = value
@property
def BreakevenTrigger(self):
return self._breakeven_trigger.Value
@BreakevenTrigger.setter
def BreakevenTrigger(self, value):
self._breakeven_trigger.Value = value
@property
def BreakevenOffset(self):
return self._breakeven_offset.Value
@BreakevenOffset.setter
def BreakevenOffset(self, value):
self._breakeven_offset.Value = value
@property
def TrailingTrigger(self):
return self._trailing_trigger.Value
@TrailingTrigger.setter
def TrailingTrigger(self, value):
self._trailing_trigger.Value = value
@property
def TrailingStep(self):
return self._trailing_step.Value
@TrailingStep.setter
def TrailingStep(self, value):
self._trailing_step.Value = value
def OnStarted2(self, time):
super(trend_catcher_breakout_strategy, self).OnStarted2(time)
self._previous_close = 0.0
self._previous_sar = None
self._entry_price = None
self._stop_loss_price = 0.0
self._take_profit_price = 0.0
self._last_trade_was_loss = False
self._last_exit_time = None
slow_ma = ExponentialMovingAverage()
slow_ma.Length = self.SlowMaPeriod
fast_ma = ExponentialMovingAverage()
fast_ma.Length = self.FastMaPeriod
fast_filter_ma = ExponentialMovingAverage()
fast_filter_ma.Length = self.FastFilterPeriod
sar = ParabolicSar()
sar.Acceleration = float(self.SarStep)
sar.AccelerationStep = float(self.SarStep)
sar.AccelerationMax = float(self.SarMax)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(slow_ma, fast_ma, fast_filter_ma, sar, self.ProcessCandle).Start()
def _is_trading_day(self, day_of_week):
from System import DayOfWeek
if day_of_week == DayOfWeek.Monday:
return bool(self._trade_monday.Value)
if day_of_week == DayOfWeek.Tuesday:
return bool(self._trade_tuesday.Value)
if day_of_week == DayOfWeek.Wednesday:
return bool(self._trade_wednesday.Value)
if day_of_week == DayOfWeek.Thursday:
return bool(self._trade_thursday.Value)
if day_of_week == DayOfWeek.Friday:
return bool(self._trade_friday.Value)
return False
def ProcessCandle(self, candle, slow_value, fast_value, fast_filter_value, sar_value):
if candle.State != CandleStates.Finished:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
slow_val = float(slow_value)
fast_val = float(fast_value)
fast_filter_val = float(fast_filter_value)
sar_val = float(sar_value)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
exit_triggered = self._manage_active_position(candle)
if exit_triggered:
self._previous_close = close
self._previous_sar = sar_val
return
if not self._is_trading_day(candle.OpenTime.DayOfWeek):
self._previous_close = close
self._previous_sar = sar_val
return
long_signal = False
short_signal = False
if self._previous_sar is not None and self._previous_close != 0.0:
long_signal = (close > sar_val and
self._previous_close < self._previous_sar and
fast_val > slow_val and
close > fast_filter_val)
short_signal = (close < sar_val and
self._previous_close > self._previous_sar and
fast_val < slow_val and
close < fast_filter_val)
if self.ReverseSignals:
long_signal, short_signal = short_signal, long_signal
if self.CloseOnOppositeSignal:
if long_signal and self.Position < 0:
self.BuyMarket()
self._finalize_trade(close, candle.OpenTime, True)
elif short_signal and self.Position > 0:
self.SellMarket()
self._finalize_trade(close, candle.OpenTime, False)
can_open = (self.Position == 0 and
(self._last_exit_time is None or self._last_exit_time < candle.OpenTime))
if can_open and long_signal:
self._try_open_long(candle, sar_val, close)
elif can_open and short_signal:
self._try_open_short(candle, sar_val, close)
self._previous_close = close
self._previous_sar = sar_val
def _manage_active_position(self, candle):
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
be_trigger = float(self.BreakevenTrigger)
be_offset = float(self.BreakevenOffset)
trail_trigger = float(self.TrailingTrigger)
trail_step = float(self.TrailingStep)
if self.Position > 0 and self._entry_price is not None:
exit_price = 0.0
if self._stop_loss_price > 0.0 and low <= self._stop_loss_price:
exit_price = self._stop_loss_price
elif self._take_profit_price > 0.0 and high >= self._take_profit_price:
exit_price = self._take_profit_price
if exit_price > 0.0:
self.SellMarket()
self._finalize_trade(exit_price, candle.OpenTime, False)
return True
profit = close - self._entry_price
if profit >= be_trigger:
breakeven = self._entry_price + be_offset
if self._stop_loss_price < breakeven:
self._stop_loss_price = breakeven
if profit >= trail_trigger:
new_stop = close - trail_step
if self._stop_loss_price < new_stop:
self._stop_loss_price = new_stop
elif self.Position < 0 and self._entry_price is not None:
exit_price = 0.0
if self._stop_loss_price > 0.0 and high >= self._stop_loss_price:
exit_price = self._stop_loss_price
elif self._take_profit_price > 0.0 and low <= self._take_profit_price:
exit_price = self._take_profit_price
if exit_price > 0.0:
self.BuyMarket()
self._finalize_trade(exit_price, candle.OpenTime, True)
return True
profit = self._entry_price - close
if profit >= be_trigger:
breakeven = self._entry_price - be_offset
if self._stop_loss_price == 0.0 or self._stop_loss_price > breakeven:
self._stop_loss_price = breakeven
if profit >= trail_trigger:
new_stop = close + trail_step
if self._stop_loss_price == 0.0 or self._stop_loss_price > new_stop:
self._stop_loss_price = new_stop
return False
def _try_open_long(self, candle, sar_val, close):
stops = self._calculate_stops(close, sar_val, True)
if stops is None:
return
stop_price, take_price = stops
self.BuyMarket()
self._entry_price = close
self._stop_loss_price = stop_price
self._take_profit_price = take_price
def _try_open_short(self, candle, sar_val, close):
stops = self._calculate_stops(close, sar_val, False)
if stops is None:
return
stop_price, take_price = stops
self.SellMarket()
self._entry_price = close
self._stop_loss_price = stop_price
self._take_profit_price = take_price
def _calculate_stops(self, entry_price, sar, is_long):
sl_coeff = float(self.StopLossCoefficient)
tp_coeff = float(self.TakeProfitCoefficient)
min_sl = float(self.MinStopLoss)
max_sl = float(self.MaxStopLoss)
if self.AutoStopLoss:
if sar == 0.0:
return None
distance = abs(entry_price - sar) * sl_coeff
else:
distance = float(self.ManualStopLoss)
if distance <= 0.0:
return None
min_val = min(min_sl, max_sl)
max_val = max(min_sl, max_sl)
if distance < min_val:
distance = min_val
if distance > max_val:
distance = max_val
if is_long:
stop_price = entry_price - distance
else:
stop_price = entry_price + distance
if self.AutoTakeProfit:
target_distance = distance * tp_coeff
else:
target_distance = float(self.ManualTakeProfit)
take_price = 0.0
if target_distance > 0.0:
if is_long:
take_price = entry_price + target_distance
else:
take_price = entry_price - target_distance
return (stop_price, take_price)
def _finalize_trade(self, exit_price, time, was_short):
if self._entry_price is not None:
if not was_short:
self._last_trade_was_loss = exit_price <= self._entry_price
else:
self._last_trade_was_loss = exit_price >= self._entry_price
else:
self._last_trade_was_loss = False
self._entry_price = None
self._stop_loss_price = 0.0
self._take_profit_price = 0.0
self._last_exit_time = time
def OnReseted(self):
super(trend_catcher_breakout_strategy, self).OnReseted()
self._previous_close = 0.0
self._previous_sar = None
self._entry_price = None
self._stop_loss_price = 0.0
self._take_profit_price = 0.0
self._last_trade_was_loss = False
self._last_exit_time = None
def CreateClone(self):
return trend_catcher_breakout_strategy()