Intelligente Trendfolger-Strategie
Überblick
Die Smart Trend Follower Strategy ist eine StockSharp-Portierung des MetaTrader 5 Expertenberaters Smart Trend Follower. Die
Das ursprüngliche System wechselt zwischen einem konträren Crossover mit gleitendem Durchschnitt und einem Trendfolge-Setup, das Stochastik verwendet
Bestätigung. Es skaliert in Positionen mit einem Martingal-ähnlichen Volumenmultiplikator und behält einen gemeinsamen Take-Profit/Stop-Loss bei
für jeden Richtungskorb. Die Version StockSharp behält das gleiche Verhalten bei, wenn die High-Level-Version API (Kerze) verwendet wird
Abonnements, Indikatorbindungen und Marktaufträge).
Signallogik
Es stehen zwei unabhängige Signal-Engines zur Verfügung, die mit dem Parameter SignalMode umgeschaltet werden können:
- CrossMa – repliziert das ursprüngliche konträre Crossover. Wenn der schnelle SMA unter den langsamen SMA kreuzt (schnell < langsam
aber vorher schnell > langsam) eröffnet oder mittelt die Strategie Long-Positionen. Wenn das schnelle SMA über das langsame kreuzt
SMA (schnell > langsam, aber zuvor schnell < langsam) öffnet oder mittelt Kurzschlüsse.
- Trend – folgt dem ursprünglichen Trendmodus, der eine Bestätigung durch den stochastischen Oszillator erfordert. Ein bullisches Signal
erscheint, wenn der schnelle SMA über dem langsamen SMA bleibt, die Kerze höher schließt als sie öffnete und der stochastische %K-Wert
liegt bei oder unter 30. Ein bärisches Signal erfordert schnell < langsam, einen bärischen Kerzenkörper und stochastischen %K bei oder über 70.
Signale werden nur an fertigen Kerzen ausgewertet. Immer wenn ein neues Signal eintrifft, während Gegenpositionen noch offen sind, wird die
Die Strategie liquidiert zunächst den gegnerischen Korb und verarbeitet erst dann neue Einträge, um an der Richtung des Korbs ausgerichtet zu bleiben
aktuelles Signal.
Positionsskalierung
Die Strategie reproduziert die Martingallogik MQL:
- Die erste Bestellung verwendet
InitialVolume Lose.
- Jeder weitere Mittelungsauftrag multipliziert das vorherige Volumen mit
Multiplier (Werte ≤ 1 deaktivieren das Volumenwachstum).
- Eine neue Durchschnittsorder für die aktive Richtung ist erst zulässig, nachdem sich der Markt um
LayerDistancePips Pips entfernt hat
vom besten Einstiegspreis des aktuellen Warenkorbs (niedrigster Long-Fill oder höchster Short-Fill).
- Die Volumina werden unter Verwendung der Instrumentengrenzen
VolumeStep, VolumeMin und VolumeMax normalisiert, sofern verfügbar.
Risikomanagement
Für jeden Richtungskorb verfolgt die Strategie einen gemeinsamen Breakeven-Preis (volumengewichteter Durchschnitt aller Füllungen):
TakeProfitPips definiert den Abstand zwischen dem durchschnittlichen Einstiegspreis und einem Korb-Take-Profit. Lange Körbe werden ausgegeben, wenn die
Kerzenhoch erreicht dieses Niveau, Short-Körbe, wenn das Kerzentief dieses Niveau erreicht. Auf 0 setzen, um Take-Profit-Ziele zu deaktivieren.
StopLossPips spiegelt das Verhalten für Schutzausgänge wider. Long-Körbe schließen, wenn das Kerzentief unter den Stop fällt,
kurze Körbe, wenn das Kerzenhoch darüber kreuzt. Auf 0 setzen, um den Schutzstopp zu deaktivieren.
Exit-Orders werden über Market-Orders ausgeführt, wenn die nächste fertige Kerze das Erreichen des Levels bestätigt. Die
Die Strategie behält die Flags _longExitRequested und _shortExitRequested bei, um doppelte Exit-Übermittlungen während der Ausfüllungen zu vermeiden
noch ausstehend.
Parameter
| Parameter |
Typ |
Standard |
Beschreibung |
SignalMode |
Aufzählung (CrossMa, Trend) |
CrossMa |
Wählt die Signal-Engine aus (Contrarian Crossover oder Trend mit stochastischem Filter). |
CandleType |
DataType |
30-minütiger Zeitrahmen |
Primäre Kerzenserie, die für Berechnungen und Signalgenerierung verwendet wird. |
InitialVolume |
dezimal |
0.01 |
Basisauftragsgröße in Lots für die erste Eingabe eines Warenkorbs. |
Multiplier |
dezimal |
2 |
Der Volumenmultiplikator wird auf jeden weiteren Mittelungsauftrag angewendet. |
LayerDistancePips |
dezimal |
200 |
Mindest-Pip-Abstand vom besten Eintrag, bevor eine weitere Order in die gleiche Richtung hinzugefügt wird. |
FastPeriod |
int |
14 |
Periode des schnellen einfachen gleitenden Durchschnitts. |
SlowPeriod |
int |
28 |
Periode des langsamen einfachen gleitenden Durchschnitts (muss größer als FastPeriod sein). |
StochasticKPeriod |
int |
10 |
Lookback-Länge für die %K-Linie des stochastischen Oszillators. |
StochasticDPeriod |
int |
3 |
Glättungslänge für die stochastische %D-Linie. |
StochasticSlowing |
int |
3 |
Zusätzliche Glättung auf %K vor der %D-Berechnung angewendet. |
TakeProfitPips |
dezimal |
500 |
Abstand in Pips vom durchschnittlichen Eintrag, bei dem der Korb-Take-Profit platziert wird. Zum Deaktivieren auf 0 setzen. |
StopLossPips |
dezimal |
0 |
Schutzstoppabstand in Pips. Stellen Sie 0 ein, um den harten Stopp zu deaktivieren. |
Implementierungshinweise
- Die Pip-Größe wird aus den Instrumenten
PriceStep und Decimals abgeleitet und entspricht dem MetaTrader-Begriff von „Punkt“ (z. B.
0,0001 für 5-stellige FX-Kurse).
- Die Positionsverfolgung verwendet zwei Listen von
PositionEntry-Objekten, um die Ticketabrechnung von MetaTrader widerzuspiegeln. Einträge sind
reduzierter FIFO-Stil, wenn gegensätzliche Trades einen Teil eines Korbs schließen.
- Alle Indikatorberechnungen basieren auf der High-Level-Bindung API (
SubscribeCandles().BindEx(...)) von StockSharp. Keine manuellen Anrufe
bis GetValue sind erforderlich und Indikatoren werden niemals in Strategy.Indicators eingefügt.
- Die Strategie ruft
StartProtection() beim Start auf und ermöglicht es StockSharp, globale Risikokontrollmodule (Break-Even,
Margenkontrollen usw.).
- Da StockSharp Positionen netto nach Richtung konsolidiert, werden entgegengesetzte Positionen vollständig geschlossen, bevor neue Einträge erfolgen
ausgewertet. Dadurch bleibt die Implementierung deterministisch und eng am ursprünglichen EA-Verhalten ausgerichtet.
Dateien
CS/SmartTrendFollowerStrategy.cs – C#-Implementierung der Strategie unter Verwendung der StockSharp-Hochebene API.
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>
/// Port of the "Smart Trend Follower" MetaTrader 5 expert advisor that combines moving average signals
/// with stochastic confirmation and a martingale-style layering engine.
/// </summary>
public class SmartTrendFollowerStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<SignalModes> _signalMode;
private readonly StrategyParam<decimal> _initialVolume;
private readonly StrategyParam<decimal> _multiplier;
private readonly StrategyParam<decimal> _layerDistancePips;
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _stochasticKPeriod;
private readonly StrategyParam<int> _stochasticDPeriod;
private readonly StrategyParam<int> _stochasticSlowing;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _stopLossPips;
private SimpleMovingAverage _fastSma;
private SimpleMovingAverage _slowSma;
private StochasticOscillator _stochastic;
private readonly List<PositionEntry> _longEntries = new();
private readonly List<PositionEntry> _shortEntries = new();
private decimal? _prevFast;
private decimal? _prevSlow;
private decimal _pipSize;
private bool _longExitRequested;
private bool _shortExitRequested;
/// <summary>
/// Trading signal mode.
/// </summary>
public SignalModes SignalMode
{
get => _signalMode.Value;
set => _signalMode.Value = value;
}
/// <summary>
/// Base candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initial order volume expressed in lots.
/// </summary>
public decimal InitialVolume
{
get => _initialVolume.Value;
set => _initialVolume.Value = value;
}
/// <summary>
/// Multiplier applied to the volume of every additional averaging order.
/// </summary>
public decimal Multiplier
{
get => _multiplier.Value;
set => _multiplier.Value = value;
}
/// <summary>
/// Distance in pips required before stacking another order in the same direction.
/// </summary>
public decimal LayerDistancePips
{
get => _layerDistancePips.Value;
set => _layerDistancePips.Value = value;
}
/// <summary>
/// Fast simple moving average period.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Slow simple moving average period.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// Stochastic oscillator %K length.
/// </summary>
public int StochasticKPeriod
{
get => _stochasticKPeriod.Value;
set => _stochasticKPeriod.Value = value;
}
/// <summary>
/// Stochastic oscillator %D smoothing length.
/// </summary>
public int StochasticDPeriod
{
get => _stochasticDPeriod.Value;
set => _stochasticDPeriod.Value = value;
}
/// <summary>
/// Additional smoothing applied to the %K line.
/// </summary>
public int StochasticSlowing
{
get => _stochasticSlowing.Value;
set => _stochasticSlowing.Value = value;
}
/// <summary>
/// Take-profit distance in pips relative to the average entry price.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Stop-loss distance in pips relative to the average entry price.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="SmartTrendFollowerStrategy"/>.
/// </summary>
public SmartTrendFollowerStrategy()
{
_signalMode = Param(nameof(SignalMode), SignalModes.CrossMa)
.SetDisplay("Signal Mode", "Trading logic selection", "Signals");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe", "General");
_initialVolume = Param(nameof(InitialVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Initial Volume", "Starting order volume in lots", "Money Management");
_multiplier = Param(nameof(Multiplier), 2m)
.SetNotNegative()
.SetDisplay("Volume Multiplier", "Martingale multiplier applied to additional entries", "Money Management");
_layerDistancePips = Param(nameof(LayerDistancePips), 200m)
.SetNotNegative()
.SetDisplay("Layer Distance", "Pip distance before adding another order", "Money Management");
_fastPeriod = Param(nameof(FastPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("Fast MA", "Fast moving average period", "Indicators")
;
_slowPeriod = Param(nameof(SlowPeriod), 28)
.SetGreaterThanZero()
.SetDisplay("Slow MA", "Slow moving average period", "Indicators")
;
_stochasticKPeriod = Param(nameof(StochasticKPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Stochastic %K", "%K lookback length", "Indicators");
_stochasticDPeriod = Param(nameof(StochasticDPeriod), 3)
.SetGreaterThanZero()
.SetDisplay("Stochastic %D", "%D smoothing length", "Indicators");
_stochasticSlowing = Param(nameof(StochasticSlowing), 3)
.SetGreaterThanZero()
.SetDisplay("Stochastic Slowing", "Extra smoothing for %K", "Indicators");
_takeProfitPips = Param(nameof(TakeProfitPips), 500m)
.SetNotNegative()
.SetDisplay("Take Profit", "Target distance in pips", "Risk Management");
_stopLossPips = Param(nameof(StopLossPips), 0m)
.SetNotNegative()
.SetDisplay("Stop Loss", "Protective distance in pips", "Risk Management");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fastSma = null;
_slowSma = null;
_stochastic = null;
_longEntries.Clear();
_shortEntries.Clear();
_prevFast = null;
_prevSlow = null;
_pipSize = 0m;
_longExitRequested = false;
_shortExitRequested = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastSma = new SimpleMovingAverage { Length = Math.Max(1, FastPeriod) };
_slowSma = new SimpleMovingAverage { Length = Math.Max(1, SlowPeriod) };
_stochastic = new StochasticOscillator();
_stochastic.K.Length = Math.Max(1, StochasticKPeriod);
_stochastic.D.Length = Math.Max(1, StochasticDPeriod);
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_fastSma, _slowSma, _stochastic, ProcessCandle)
.Start();
_pipSize = CalculatePipSize();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _fastSma);
DrawIndicator(area, _slowSma);
DrawIndicator(area, _stochastic);
DrawOwnTrades(area);
}
// protection managed manually via ManageExits
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
var price = trade.Trade.Price;
var volume = trade.Trade.Volume;
if (trade.Order.Side == Sides.Buy)
{
ReduceEntries(_shortEntries, ref volume);
if (volume > 0m)
{
_longEntries.Add(new PositionEntry(price, volume));
}
}
else if (trade.Order.Side == Sides.Sell)
{
ReduceEntries(_longEntries, ref volume);
if (volume > 0m)
{
_shortEntries.Add(new PositionEntry(price, volume));
}
}
if (GetTotalVolume(_longEntries) <= 0m)
{
_longEntries.Clear();
_longExitRequested = false;
}
if (GetTotalVolume(_shortEntries) <= 0m)
{
_shortEntries.Clear();
_shortExitRequested = false;
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue fastValue, IIndicatorValue slowValue, IIndicatorValue stochasticValue)
{
if (candle.State != CandleStates.Finished)
return;
var fast = fastValue.ToDecimal();
var slow = slowValue.ToDecimal();
ManageExits(candle);
var signal = SignalDirections.None;
if (SignalMode == SignalModes.CrossMa)
{
if (_prevFast.HasValue && _prevSlow.HasValue)
{
var crossBuy = fast < slow && _prevSlow.Value < _prevFast.Value;
var crossSell = fast > slow && _prevSlow.Value > _prevFast.Value;
if (crossBuy)
signal = SignalDirections.Buy;
else if (crossSell)
signal = SignalDirections.Sell;
}
}
else if (_stochastic?.IsFormed == true)
{
var kValue = stochasticValue.ToDecimal();
var bullish = candle.ClosePrice > candle.OpenPrice;
var bearish = candle.ClosePrice < candle.OpenPrice;
if (fast > slow && bullish && kValue <= 30m)
signal = SignalDirections.Buy;
else if (fast < slow && bearish && kValue >= 70m)
signal = SignalDirections.Sell;
}
if (signal != SignalDirections.None)
{
ProcessSignal(signal, candle.ClosePrice);
}
_prevFast = fast;
_prevSlow = slow;
}
private void ProcessSignal(SignalDirections signal, decimal referencePrice)
{
switch (signal)
{
case SignalDirections.Buy:
{
var shortVolume = GetTotalVolume(_shortEntries);
if (shortVolume > 0m)
{
if (!_shortExitRequested)
{
_shortExitRequested = true;
BuyMarket(shortVolume);
}
return;
}
var longCount = _longEntries.Count;
var requested = CalculateRequestedVolume(longCount);
var volume = PrepareNextVolume(requested);
if (volume <= 0m)
return;
if (longCount == 0)
{
BuyMarket(volume);
return;
}
var lowest = GetExtremePrice(_longEntries, true);
var threshold = lowest - LayerDistancePips * (_pipSize > 0m ? _pipSize : 1m);
if (referencePrice <= threshold)
{
BuyMarket(volume);
}
break;
}
case SignalDirections.Sell:
{
var longVolume = GetTotalVolume(_longEntries);
if (longVolume > 0m)
{
if (!_longExitRequested)
{
_longExitRequested = true;
SellMarket(longVolume);
}
return;
}
var shortCount = _shortEntries.Count;
var requested = CalculateRequestedVolume(shortCount);
var volume = PrepareNextVolume(requested);
if (volume <= 0m)
return;
if (shortCount == 0)
{
SellMarket(volume);
return;
}
var highest = GetExtremePrice(_shortEntries, false);
var threshold = highest + LayerDistancePips * (_pipSize > 0m ? _pipSize : 1m);
if (referencePrice >= threshold)
{
SellMarket(volume);
}
break;
}
}
}
private void ManageExits(ICandleMessage candle)
{
var longVolume = GetTotalVolume(_longEntries);
if (longVolume > 0m && !_longExitRequested)
{
var average = GetAveragePrice(_longEntries);
var takeProfit = TakeProfitPips > 0m ? average + TakeProfitPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
var stopLoss = StopLossPips > 0m ? average - StopLossPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
if (takeProfit.HasValue && candle.HighPrice >= takeProfit.Value)
{
_longExitRequested = true;
SellMarket(longVolume);
return;
}
if (stopLoss.HasValue && candle.LowPrice <= stopLoss.Value)
{
_longExitRequested = true;
SellMarket(longVolume);
return;
}
}
var shortVolume = GetTotalVolume(_shortEntries);
if (shortVolume > 0m && !_shortExitRequested)
{
var average = GetAveragePrice(_shortEntries);
var takeProfit = TakeProfitPips > 0m ? average - TakeProfitPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
var stopLoss = StopLossPips > 0m ? average + StopLossPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
if (takeProfit.HasValue && candle.LowPrice <= takeProfit.Value)
{
_shortExitRequested = true;
BuyMarket(shortVolume);
return;
}
if (stopLoss.HasValue && candle.HighPrice >= stopLoss.Value)
{
_shortExitRequested = true;
BuyMarket(shortVolume);
}
}
}
private decimal CalculateRequestedVolume(int existingCount)
{
if (InitialVolume <= 0m)
return 0m;
var result = InitialVolume;
if (existingCount > 0 && Multiplier > 0m)
{
result *= (decimal)Math.Pow((double)Math.Max(Multiplier, 1m), existingCount);
}
return result;
}
private decimal PrepareNextVolume(decimal requested)
{
if (requested <= 0m)
return 0m;
var security = Security;
if (security == null)
return requested;
var step = security.VolumeStep ?? 0m;
if (step > 0m)
{
requested = step * Math.Round(requested / step, MidpointRounding.AwayFromZero);
}
var min = security.MinVolume ?? 0m;
if (min > 0m && requested < min)
return 0m;
var max = security.MaxVolume ?? decimal.MaxValue;
if (requested > max)
{
requested = max;
}
return requested;
}
private void ReduceEntries(List<PositionEntry> entries, ref decimal volume)
{
var index = 0;
while (volume > 0m && index < entries.Count)
{
var entry = entries[index];
if (volume >= entry.Volume)
{
volume -= entry.Volume;
entries.RemoveAt(index);
}
else
{
entry.Volume -= volume;
volume = 0m;
entries[index] = entry;
}
}
}
private static decimal GetTotalVolume(List<PositionEntry> entries)
{
var total = 0m;
for (var i = 0; i < entries.Count; i++)
total += entries[i].Volume;
return total;
}
private static decimal GetAveragePrice(List<PositionEntry> entries)
{
var totalVolume = GetTotalVolume(entries);
if (totalVolume <= 0m)
return 0m;
var weighted = 0m;
for (var i = 0; i < entries.Count; i++)
weighted += entries[i].Price * entries[i].Volume;
return weighted / totalVolume;
}
private static decimal GetExtremePrice(List<PositionEntry> entries, bool forLong)
{
if (entries.Count == 0)
return 0m;
var extreme = entries[0].Price;
for (var i = 1; i < entries.Count; i++)
{
var price = entries[i].Price;
if (forLong)
{
if (price < extreme)
extreme = price;
}
else if (price > extreme)
{
extreme = price;
}
}
return extreme;
}
private decimal CalculatePipSize()
{
var security = Security;
if (security == null)
return 0m;
var step = security.PriceStep ?? 0m;
if (step <= 0m)
return 0m;
var decimals = security.Decimals;
if (decimals == 3 || decimals == 5)
return step * 10m;
return step;
}
private enum SignalDirections
{
None,
Buy,
Sell
}
/// <summary>
/// Signal selector for the strategy.
/// </summary>
public enum SignalModes
{
/// <summary>
/// Use moving average crossovers in a contrarian fashion.
/// </summary>
CrossMa,
/// <summary>
/// Follow trend direction using moving averages with stochastic confirmation.
/// </summary>
Trend
}
private sealed class PositionEntry
{
public PositionEntry(decimal price, decimal volume)
{
Price = price;
Volume = volume;
}
public decimal Price { get; }
public decimal Volume { get; set; }
}
}
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, Sides
from StockSharp.Algo.Indicators import SimpleMovingAverage, StochasticOscillator
from StockSharp.Algo.Strategies import Strategy
class smart_trend_follower_strategy(Strategy):
# Signal mode constants
CROSS_MA = 0
TREND = 1
def __init__(self):
super(smart_trend_follower_strategy, self).__init__()
self._signal_mode = self.Param("SignalMode", self.CROSS_MA) \
.SetDisplay("Signal Mode", "Trading logic selection", "Signals")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Primary timeframe", "General")
self._initial_volume = self.Param("InitialVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Initial Volume", "Starting order volume in lots", "Money Management")
self._multiplier = self.Param("Multiplier", 2.0) \
.SetNotNegative() \
.SetDisplay("Volume Multiplier", "Martingale multiplier applied to additional entries", "Money Management")
self._layer_distance_pips = self.Param("LayerDistancePips", 200.0) \
.SetNotNegative() \
.SetDisplay("Layer Distance", "Pip distance before adding another order", "Money Management")
self._fast_period = self.Param("FastPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("Fast MA", "Fast moving average period", "Indicators")
self._slow_period = self.Param("SlowPeriod", 28) \
.SetGreaterThanZero() \
.SetDisplay("Slow MA", "Slow moving average period", "Indicators")
self._stochastic_k_period = self.Param("StochasticKPeriod", 10) \
.SetGreaterThanZero() \
.SetDisplay("Stochastic %K", "%K lookback length", "Indicators")
self._stochastic_d_period = self.Param("StochasticDPeriod", 3) \
.SetGreaterThanZero() \
.SetDisplay("Stochastic %D", "%D smoothing length", "Indicators")
self._stochastic_slowing = self.Param("StochasticSlowing", 3) \
.SetGreaterThanZero() \
.SetDisplay("Stochastic Slowing", "Extra smoothing for %K", "Indicators")
self._take_profit_pips = self.Param("TakeProfitPips", 500.0) \
.SetNotNegative() \
.SetDisplay("Take Profit", "Target distance in pips", "Risk Management")
self._stop_loss_pips = self.Param("StopLossPips", 0.0) \
.SetNotNegative() \
.SetDisplay("Stop Loss", "Protective distance in pips", "Risk Management")
self._fast_sma = None
self._slow_sma = None
self._stochastic = None
self._long_entries = []
self._short_entries = []
self._prev_fast = None
self._prev_slow = None
self._pip_size = 0.0
self._long_exit_requested = False
self._short_exit_requested = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def SignalMode(self):
return self._signal_mode.Value
@SignalMode.setter
def SignalMode(self, value):
self._signal_mode.Value = value
@property
def InitialVolume(self):
return self._initial_volume.Value
@InitialVolume.setter
def InitialVolume(self, value):
self._initial_volume.Value = value
@property
def Multiplier(self):
return self._multiplier.Value
@Multiplier.setter
def Multiplier(self, value):
self._multiplier.Value = value
@property
def LayerDistancePips(self):
return self._layer_distance_pips.Value
@LayerDistancePips.setter
def LayerDistancePips(self, value):
self._layer_distance_pips.Value = value
@property
def FastPeriod(self):
return self._fast_period.Value
@FastPeriod.setter
def FastPeriod(self, value):
self._fast_period.Value = value
@property
def SlowPeriod(self):
return self._slow_period.Value
@SlowPeriod.setter
def SlowPeriod(self, value):
self._slow_period.Value = value
@property
def StochasticKPeriod(self):
return self._stochastic_k_period.Value
@StochasticKPeriod.setter
def StochasticKPeriod(self, value):
self._stochastic_k_period.Value = value
@property
def StochasticDPeriod(self):
return self._stochastic_d_period.Value
@StochasticDPeriod.setter
def StochasticDPeriod(self, value):
self._stochastic_d_period.Value = value
@property
def StochasticSlowing(self):
return self._stochastic_slowing.Value
@StochasticSlowing.setter
def StochasticSlowing(self, value):
self._stochastic_slowing.Value = value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@TakeProfitPips.setter
def TakeProfitPips(self, value):
self._take_profit_pips.Value = value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@StopLossPips.setter
def StopLossPips(self, value):
self._stop_loss_pips.Value = value
def OnReseted(self):
super(smart_trend_follower_strategy, self).OnReseted()
self._fast_sma = None
self._slow_sma = None
self._stochastic = None
self._long_entries = []
self._short_entries = []
self._prev_fast = None
self._prev_slow = None
self._pip_size = 0.0
self._long_exit_requested = False
self._short_exit_requested = False
def OnStarted2(self, time):
super(smart_trend_follower_strategy, self).OnStarted2(time)
self._fast_sma = SimpleMovingAverage()
self._fast_sma.Length = max(1, self.FastPeriod)
self._slow_sma = SimpleMovingAverage()
self._slow_sma.Length = max(1, self.SlowPeriod)
self._stochastic = StochasticOscillator()
self._stochastic.K.Length = max(1, self.StochasticKPeriod)
self._stochastic.D.Length = max(1, self.StochasticDPeriod)
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.Bind(self._fast_sma, self._slow_sma, self._process_candle) \
.Start()
self._pip_size = self._calculate_pip_size()
def OnOwnTradeReceived(self, trade):
super(smart_trend_follower_strategy, self).OnOwnTradeReceived(trade)
price = float(trade.Trade.Price)
volume = float(trade.Trade.Volume)
if trade.Order.Side == Sides.Buy:
volume = self._reduce_entries(self._short_entries, volume)
if volume > 0:
self._long_entries.append([price, volume])
elif trade.Order.Side == Sides.Sell:
volume = self._reduce_entries(self._long_entries, volume)
if volume > 0:
self._short_entries.append([price, volume])
if self._get_total_volume(self._long_entries) <= 0:
self._long_entries.clear()
self._long_exit_requested = False
if self._get_total_volume(self._short_entries) <= 0:
self._short_entries.clear()
self._short_exit_requested = False
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast = float(fast_value)
slow = float(slow_value)
self._manage_exits(candle)
# Signal detection
signal = 0 # 0=None, 1=Buy, 2=Sell
if self.SignalMode == self.CROSS_MA:
if self._prev_fast is not None and self._prev_slow is not None:
cross_buy = fast < slow and self._prev_slow < self._prev_fast
cross_sell = fast > slow and self._prev_slow > self._prev_fast
if cross_buy:
signal = 1
elif cross_sell:
signal = 2
else:
bullish = float(candle.ClosePrice) > float(candle.OpenPrice)
bearish = float(candle.ClosePrice) < float(candle.OpenPrice)
if fast > slow and bullish:
signal = 1
elif fast < slow and bearish:
signal = 2
if signal != 0:
self._process_signal(signal, float(candle.ClosePrice))
self._prev_fast = fast
self._prev_slow = slow
def _process_signal(self, signal, reference_price):
pip = self._pip_size if self._pip_size > 0 else 1.0
if signal == 1: # Buy
short_volume = self._get_total_volume(self._short_entries)
if short_volume > 0:
if not self._short_exit_requested:
self._short_exit_requested = True
self.BuyMarket(float(short_volume))
return
long_count = len(self._long_entries)
requested = self._calculate_requested_volume(long_count)
volume = self._prepare_next_volume(requested)
if volume <= 0:
return
if long_count == 0:
self.BuyMarket(float(volume))
return
lowest = self._get_extreme_price(self._long_entries, True)
threshold = lowest - float(self.LayerDistancePips) * pip
if reference_price <= threshold:
self.BuyMarket(float(volume))
elif signal == 2: # Sell
long_volume = self._get_total_volume(self._long_entries)
if long_volume > 0:
if not self._long_exit_requested:
self._long_exit_requested = True
self.SellMarket(float(long_volume))
return
short_count = len(self._short_entries)
requested = self._calculate_requested_volume(short_count)
volume = self._prepare_next_volume(requested)
if volume <= 0:
return
if short_count == 0:
self.SellMarket(float(volume))
return
highest = self._get_extreme_price(self._short_entries, False)
threshold = highest + float(self.LayerDistancePips) * pip
if reference_price >= threshold:
self.SellMarket(float(volume))
def _manage_exits(self, candle):
pip = self._pip_size if self._pip_size > 0 else 1.0
long_volume = self._get_total_volume(self._long_entries)
if long_volume > 0 and not self._long_exit_requested:
average = self._get_average_price(self._long_entries)
take_profit = average + float(self.TakeProfitPips) * pip if float(self.TakeProfitPips) > 0 else None
stop_loss = average - float(self.StopLossPips) * pip if float(self.StopLossPips) > 0 else None
if take_profit is not None and float(candle.HighPrice) >= take_profit:
self._long_exit_requested = True
self.SellMarket(float(long_volume))
return
if stop_loss is not None and float(candle.LowPrice) <= stop_loss:
self._long_exit_requested = True
self.SellMarket(float(long_volume))
return
short_volume = self._get_total_volume(self._short_entries)
if short_volume > 0 and not self._short_exit_requested:
average = self._get_average_price(self._short_entries)
take_profit = average - float(self.TakeProfitPips) * pip if float(self.TakeProfitPips) > 0 else None
stop_loss = average + float(self.StopLossPips) * pip if float(self.StopLossPips) > 0 else None
if take_profit is not None and float(candle.LowPrice) <= take_profit:
self._short_exit_requested = True
self.BuyMarket(float(short_volume))
return
if stop_loss is not None and float(candle.HighPrice) >= stop_loss:
self._short_exit_requested = True
self.BuyMarket(float(short_volume))
def _calculate_requested_volume(self, existing_count):
if self.InitialVolume <= 0:
return 0.0
result = float(self.InitialVolume)
if existing_count > 0 and self.Multiplier > 0:
result *= float(self.Multiplier) ** existing_count if float(self.Multiplier) >= 1 else 1.0
return result
def _prepare_next_volume(self, requested):
if requested <= 0:
return 0.0
return requested
@staticmethod
def _reduce_entries(entries, volume):
idx = 0
while volume > 0 and idx < len(entries):
entry = entries[idx]
if volume >= entry[1]:
volume -= entry[1]
entries.pop(idx)
else:
entry[1] -= volume
volume = 0
return volume
@staticmethod
def _get_total_volume(entries):
total = 0.0
for e in entries:
total += e[1]
return total
@staticmethod
def _get_average_price(entries):
total_vol = 0.0
weighted = 0.0
for e in entries:
weighted += e[0] * e[1]
total_vol += e[1]
if total_vol <= 0:
return 0.0
return weighted / total_vol
@staticmethod
def _get_extreme_price(entries, for_long):
if len(entries) == 0:
return 0.0
extreme = entries[0][0]
for i in range(1, len(entries)):
price = entries[i][0]
if for_long:
if price < extreme:
extreme = price
else:
if price > extreme:
extreme = price
return extreme
def _calculate_pip_size(self):
security = self.Security
if security is None:
return 0.0
step = security.PriceStep
if step is None or float(step) <= 0:
return 0.0
decimals = security.Decimals
if decimals == 3 or decimals == 5:
return float(step) * 10.0
return float(step)
def CreateClone(self):
return smart_trend_follower_strategy()