Стратегия Trend Catcher Breakout
Обзор
Trend Catcher — это конвертация эксперта MetaTrader 5 «Trend_Catcher_v2». Стратегия объединяет три экспоненциальные скользящие средние и индикатор Parabolic SAR для поиска разворотных точек и продолжений тренда. Алгоритм работает по одному инструменту и таймфрейму, анализируя только закрытые свечи. Такой подход упрощает тестирование в StockSharp Designer и позволяет запускать стратегию через любые приложения на базе StockSharp API.
Индикаторы и фильтры
- Parabolic SAR — определяет смену направления (flip), указывающую на потенциальный разворот.
- Медленная EMA — основной фильтр тренда старшего порядка.
- Быстрая EMA — подтверждает направление текущего импульса.
- Триггерная EMA — контролирует расстояние до цены и отфильтровывает запоздалые входы.
- Фильтр по дням недели — позволяет отключать торговлю в выбранные будни.
Торговые правила
Условия для покупок
- Цена закрытия находится выше текущего значения Parabolic SAR.
- Предыдущая свеча закрылась ниже предыдущего значения Parabolic SAR (бычий flip).
- Быстрая EMA выше медленной EMA, подтверждая восходящий тренд.
- Цена закрытия выше триггерной EMA, что исключает контртрендовые сигналы.
- Позиция отсутствует и в текущей свече ещё не было закрытий.
Условия для продаж
Условия зеркальные:
- Цена закрытия ниже текущего Parabolic SAR.
- Предыдущая свеча закрылась выше предыдущего значения Parabolic SAR (медвежий flip).
- Быстрая EMA ниже медленной EMA.
- Цена закрытия ниже триггерной EMA.
- Позиция отсутствует и в текущей свече ещё не было закрытий.
При активации параметра Reverse Signals логика входов зеркально инвертируется, что позволяет торговать сигналы в противоположную сторону.
Сопровождение позиции
- Авто-стоп — расстояние до стоп-лосса рассчитывается как модуль разницы между ценой и Parabolic SAR, умноженный на
StopLossCoefficient, и ограничивается диапазономMinStopLoss–MaxStopLoss. - Авто-таргет — тейк-профит определяется умножением стопа на
TakeProfitCoefficient. При отключении автоматического режима используются фиксированные значенияManualStopLossиManualTakeProfit. - Расчёт объёма по риску — размер позиции вычисляется на основе капитала портфеля и параметра
RiskPercent. Если последняя сделка закрылась с убытком и включён Use Martingale, объём умножается наMartingaleMultiplier. - Перевод в безубыток и трейлинг — после достижения прибыли
BreakevenTriggerстоп переносится в точку входа плюсBreakevenOffset(для продаж — минус). При дальнейшем росте прибыли доTrailingTriggerстоп следует за ценой на расстоянииTrailingStep. - Закрытие по противоположному сигналу — при активном флаге
CloseOnOppositeSignalпозиция закрывается сразу после появления обратного сигнала. - Ограничение «одна сделка на свечу» — время последнего выхода сохраняется, и новые входы разрешаются только после открытия следующей свечи.
Параметры
| Параметр | Описание | Значение по умолчанию |
|---|---|---|
CandleType |
Таймфрейм, на котором строятся свечи и индикаторы. | 15 минут |
CloseOnOppositeSignal |
Закрывать позицию при противоположном сигнале. | true |
ReverseSignals |
Инвертировать условия для покупок и продаж. | false |
TradeMonday … TradeFriday |
Разрешение торговли по дням недели. | true |
SlowMaPeriod |
Период медленной EMA. | 200 |
FastMaPeriod |
Период быстрой EMA. | 50 |
FastFilterPeriod |
Период триггерной EMA. | 25 |
SarStep |
Шаг ускорения Parabolic SAR. | 0.004 |
SarMax |
Максимальное ускорение Parabolic SAR. | 0.2 |
AutoStopLoss |
Включить автоматический расчёт стоп-лосса. | true |
AutoTakeProfit |
Включить автоматический расчёт тейк-профита. | true |
MinStopLoss / MaxStopLoss |
Минимальная и максимальная дистанция стопа. | 0.001 / 0.2 |
StopLossCoefficient |
Множитель для расчёта стопа по SAR. | 1 |
TakeProfitCoefficient |
Множитель для расчёта тейк-профита. | 1 |
ManualStopLoss |
Фиксированный стоп-лосс при ручном режиме. | 0.002 |
ManualTakeProfit |
Фиксированный тейк-профит при ручном режиме. | 0.02 |
RiskPercent |
Процент капитала под риск на сделку. | 2 |
UseMartingale |
Увеличивать объём после убыточной сделки. | true |
MartingaleMultiplier |
Множитель мартингейла. | 2 |
BreakevenTrigger |
Прибыль для перевода в безубыток. | 0.005 |
BreakevenOffset |
Запас при переносе стопа в безубыток. | 0.0001 |
TrailingTrigger |
Прибыль для включения трейлинга. | 0.005 |
TrailingStep |
Расстояние трейлинг-стопа. | 0.001 |
Рекомендации по использованию
- Стратегия отправляет рыночные заявки; контроль проскальзывания следует реализовывать на уровне брокерского адаптера.
- Поскольку расчёт выполняется по закрытым свечам, точность тестирования зависит от используемого таймфрейма и качества исторических данных.
- Все параметры доступны через
StrategyParam, поэтому стратегию можно оптимизировать в 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()