趋势捕捉突破策略
概述
趋势捕捉突破策略(Trend Catcher Breakout)来源于 MetaTrader 5 专家顾问 “Trend_Catcher_v2”的移植版本。策略将三条指数移动平均线与 Parabolic SAR 指标结合在一起,用于识别趋势反转和趋势延续信号。算法基于单一品种与单一周期,并且只在 K 线收盘后做出决策,因此既适合在 StockSharp Designer 中回测,也能通过 StockSharp API 应用在实时环境中运行。
指标与过滤条件
- Parabolic SAR:检测价格相对于 SAR 的翻转,提示潜在的趋势反向点。
- 慢速 EMA:衡量大周期的主方向。
- 快速 EMA:跟随价格的短期变化,用于确认当前动能。
- 触发 EMA:确保入场价格不过度偏离均线,避免追高或杀跌。
- 交易日开关:可按星期选择允许或禁止交易日。
交易规则
做多条件
- 收盘价位于当前 Parabolic SAR 之上;
- 前一根 K 线的收盘价低于前一周期的 Parabolic SAR(多头翻转);
- 快速 EMA 高于慢速 EMA,表明处于上升趋势;
- 收盘价高于触发 EMA,过滤逆势信号;
- 当前没有持仓,且本根 K 线内没有关闭过仓位。
做空条件
满足上述条件的镜像:
- 收盘价低于当前 Parabolic SAR;
- 前一根 K 线收盘价高于前一周期 Parabolic SAR(空头翻转);
- 快速 EMA 低于慢速 EMA;
- 收盘价低于触发 EMA;
- 当前没有持仓,且本根 K 线内没有关闭过仓位。
当启用 Reverse Signals(信号反转)参数时,做多与做空条件会互换,从而以相反方向执行突破信号。
仓位管理
- 自动止损:若启用,止损距离等于价格与 Parabolic SAR 的差值乘以
StopLossCoefficient,并限制在MinStopLoss与MaxStopLoss范围内。 - 自动止盈:止盈距离等于止损乘以
TakeProfitCoefficient。停用自动模式时使用ManualStopLoss与ManualTakeProfit的固定数值。 - 风险化仓位:根据账户权益和
RiskPercent计算下单数量。如果上一笔交易亏损且启用 Use Martingale,下单量会乘以MartingaleMultiplier。 - 保本与追踪止损:当浮盈达到
BreakevenTrigger时,将止损移动至入场价加上BreakevenOffset(做空则减去)。当收益继续扩大到TrailingTrigger时,止损会以TrailingStep的距离跟随价格移动。 - 反向信号平仓:启用
CloseOnOppositeSignal时,一旦出现反向信号会立即平掉已有仓位。 - 单根 K 线仅一次入场:策略记录最近的平仓时间,在同一根 K 线内不会再次开仓。
参数列表
| 参数 | 说明 | 默认值 |
|---|---|---|
CandleType |
用于计算的主要 K 线周期。 | 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 |
计算止损时的倍数。 | 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 |
使用提示
- 策略始终使用市价单入场和出场,若需要限制滑点应在交易适配器中配置。
- 由于所有计算基于收盘价,回测精度依赖于提供的 K 线粒度与历史数据质量。
- 全部参数通过
StrategyParam暴露,可在 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()