T3 MA Richtungswechsel-Strategie
Übersicht
Diese Strategie reproduziert das Verhalten des originalen T3MA(barabashkakvn's edition) Expertenberaters. Der Expertenberater nutzt den Indikator "T3MA-ALARM", der exponentielles Glätten zweimal anwendet und ein Signal gibt, wenn die geglättete Linie die Richtung wechselt. Der StockSharp-Port behält dasselbe Konzept: Er erstellt einen doppelt geglätteten exponentiellen gleitenden Durchschnitt (EMA der EMA) und handelt, wenn die Steigung dieser Kurve von fallend zu steigend oder umgekehrt wechselt.
Die Strategie arbeitet ausschließlich mit abgeschlossenen Kerzen. Signale können um eine konfigurierbare Anzahl von Bars verzögert werden, um die ursprüngliche InpBarNumber-Option nachzuahmen (Standard-Verzögerung: eine Bar). Orders werden per Marktausführung platziert, sodass das Portfolio zwischen Long- und Short-Exposition wechselt, ohne mehrere gleichzeitige gehedgte Positionen aufzubauen.
Handelsregeln
- Die konfigurierte Kerzenserie abonnieren und eine EMA der Schlusskurse berechnen. Eine zweite EMA über die Ausgabe der ersten EMA ausführen, wodurch die vom Indikator verwendete geglättete Reihe erzeugt wird.
- Den aktuellen Wert der geglätteten Reihe (optional um
EMA Shift Bars nach vorne verschoben) mit dem vorherigen Wert vergleichen. Die Steigung gilt als bullisch, wenn die Reihe steigt, und als bearisch, wenn sie fällt.
- Wenn die Steigung von bearisch zu bullisch wechselt, ein Kauf-Signal in die Warteschlange stellen. Wenn es von bullisch zu bearisch wechselt, ein Verkauf-Signal einreihen. Neutrale Kerzen schieben ein Null-Signal in die Warteschlange, damit der Verzögerungszähler korrekt bleibt.
- Nachdem die konfigurierte Anzahl abgeschlossener Kerzen (
Signal Delay) verstrichen ist, das Warteschlangensignal ausführen. Ein verzögerter Kauf schließt jede offene Short-Position und geht Long mit dem Basis-Trade Volume. Ebenso schließt ein verzögerter Verkauf eine Long-Position und geht Short.
- Schutzstop-Loss- und Take-Profit-Orders werden über
StartProtection initialisiert. Beide Abstände werden in Kursschritten ausgedrückt, sodass sie sich automatisch an die Tick-Größe des ausgewählten Instruments anpassen.
Parameter
| Name |
Beschreibung |
EMA Length |
Länge der EMA für beide Glättungsläufe. Entspricht dem MAPeriod-Input in der MetaTrader-Implementierung. |
EMA Shift |
Anzahl der Bars, um die die geglättete EMA vor dem Steigungsvergleich verschoben wird. Entspricht dem MAShift des Indikators. |
Signal Delay |
Anzahl abgeschlossener Kerzen, die vor der Signalausführung gewartet wird. Spiegelt InpBarNumber wider, sodass ein Wert von 1 das Signal der vorherigen Bar handelt. |
Stop Loss (steps) |
Stop-Loss-Abstand in Kursschritten. Auf null setzen zum Deaktivieren. |
Take Profit (steps) |
Take-Profit-Abstand in Kursschritten. Auf null setzen zum Deaktivieren. |
Trade Volume |
Basisordergröße für neue Einstiege. Bei einer Positionsumkehr addiert die Strategie die absolute aktuelle Positionsgröße zu diesem Wert. |
Candle Type |
Kerzendatentyp für Berechnungen (Standard: 5-Minuten-Zeitrahmen). |
Risikomanagement
StartProtection registriert automatisch Stop-Loss- und Take-Profit-Niveaus beim Strategiestart. Beide Niveaus folgen der Tick-Größe des Instruments und bleiben für die gesamte Lebensdauer der Strategie aktiv.
- Positionsumkehrungen werden mit Marktorders ausgeführt. Wenn die Signalrichtung mit der aktuellen Exposition übereinstimmt, werden keine zusätzlichen Trades ausgelöst, was ungewolltes Pyramidisieren verhindert.
- Bei jedem Trade werden Logs ausgegeben, um den Grund und den Referenzpreis aus der Quellkerze nachzuverfolgen.
Unterschiede zur MQL5-Version
- MetaTrader 5 benötigte ein Hedging-Konto und konnte mehrere Positionen akkumulieren. Die StockSharp-Version hält eine einzelne Nettoposition und kehrt sie um, wenn das entgegengesetzte Signal ausgelöst wird.
- Die Signalverarbeitung ist kerzenbasiert und erfolgt einmal pro abgeschlossener Kerze statt bei jedem Tick, was innerhalb der High-Level-API von StockSharp natürlicher ist.
- Die Stop-Loss- und Take-Profit-Verwaltung wird über
StartProtection gehandhabt, anstatt manuell SL/TP-Preise mit jeder Order zu übermitteln.
- Englische Kommentare, strukturierte Parameter und Chart-Hilfsmittel wurden für bessere Lesbarkeit in der StockSharp-Umgebung hinzugefügt.
Verwendungshinweise
- Die Strategie an das gewünschte Wertpapier anhängen und sicherstellen, dass der Kerzentyp dem Zeitrahmen entspricht, der bei der Optimierung des ursprünglichen Expertenberaters verwendet wurde.
EMA Length und die Risikoparameter an die Instrumentenvolatilität anpassen. Höhere Verzögerungen (Signal Delay) verlangsamen Reaktionen und können Rauschen filtern.
- Da die Strategie mit Kursschritten arbeitet, sicherstellen, dass die
PriceStep-Eigenschaft des Wertpapiers korrekt konfiguriert ist, damit Schutzorders in sinnvollen Abständen platziert werden.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Strategy that trades when a double-smoothed EMA changes its slope direction.
/// </summary>
public class T3MaDirectionChangeStrategy : Strategy
{
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<int> _maShift;
private readonly StrategyParam<int> _signalBarOffset;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<int> _signalCooldownBars;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _recentSmoothed = new();
private readonly Queue<SignalInfo> _pendingSignals = new();
private ExponentialMovingAverage _emaPrice;
private ExponentialMovingAverage _emaSmooth;
private int _previousDirection;
private int _cooldownRemaining;
public int MaLength { get => _maLength.Value; set => _maLength.Value = value; }
public int MaShift { get => _maShift.Value; set => _maShift.Value = value; }
public int SignalBarOffset { get => _signalBarOffset.Value; set => _signalBarOffset.Value = value; }
public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public decimal TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
public decimal TradeVolume { get => _tradeVolume.Value; set => _tradeVolume.Value = value; }
public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public T3MaDirectionChangeStrategy()
{
_maLength = Param(nameof(MaLength), 4)
.SetGreaterThanZero()
.SetDisplay("EMA Length", "Length of the EMA used for the double smoothing", "Indicator");
_maShift = Param(nameof(MaShift), 0)
.SetNotNegative()
.SetDisplay("EMA Shift", "Shift applied to the smoothed EMA when evaluating slope changes", "Indicator");
_signalBarOffset = Param(nameof(SignalBarOffset), 1)
.SetNotNegative()
.SetDisplay("Signal Delay", "How many completed candles to wait before acting on a signal", "Trading rules");
_stopLossPoints = Param(nameof(StopLossPoints), 20m)
.SetNotNegative()
.SetDisplay("Stop Loss (steps)", "Stop loss distance expressed in price steps", "Risk management");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 125m)
.SetNotNegative()
.SetDisplay("Take Profit (steps)", "Take profit distance expressed in price steps", "Risk management");
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Base volume used for entries", "Trading rules");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 12)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait after entries and exits", "Trading rules");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_emaPrice = null;
_emaSmooth = null;
_recentSmoothed.Clear();
_pendingSignals.Clear();
_previousDirection = 0;
_cooldownRemaining = 0;
Volume = TradeVolume;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = TradeVolume;
_emaPrice = new EMA { Length = MaLength };
_emaSmooth = new EMA { Length = MaLength };
_recentSmoothed.Clear();
_pendingSignals.Clear();
_previousDirection = 0;
_cooldownRemaining = 0;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
var slUnit = StopLossPoints > 0m ? new Unit(StopLossPoints, UnitTypes.Absolute) : null;
var tpUnit = TakeProfitPoints > 0m ? new Unit(TakeProfitPoints, UnitTypes.Absolute) : null;
StartProtection(slUnit, tpUnit);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var emaPriceValue = _emaPrice.Process(new DecimalIndicatorValue(_emaPrice, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
var emaSmoothValue = _emaSmooth.Process(emaPriceValue);
if (!emaSmoothValue.IsFormed)
return;
AddSmoothedValue(emaSmoothValue.ToDecimal(), MaShift + 2);
if (_recentSmoothed.Count < MaShift + 2)
{
EnqueueSignal(new SignalInfo(0));
return;
}
var currentIndex = _recentSmoothed.Count - 1 - MaShift;
var previousIndex = _recentSmoothed.Count - 2 - MaShift;
var current = _recentSmoothed[currentIndex];
var previous = _recentSmoothed[previousIndex];
var direction = _previousDirection;
if (current > previous)
direction = 1;
else if (current < previous)
direction = -1;
var signal = 0;
if (_previousDirection == -1 && direction == 1)
signal = 1;
else if (_previousDirection == 1 && direction == -1)
signal = -1;
_previousDirection = direction;
EnqueueSignal(new SignalInfo(signal));
}
private void AddSmoothedValue(decimal value, int limit)
{
_recentSmoothed.Add(value);
if (_recentSmoothed.Count > limit)
_recentSmoothed.RemoveAt(0);
}
private void EnqueueSignal(SignalInfo signal)
{
_pendingSignals.Enqueue(signal);
while (_pendingSignals.Count > SignalBarOffset)
{
var readySignal = _pendingSignals.Dequeue();
ExecuteSignal(readySignal);
}
}
private void ExecuteSignal(SignalInfo signal)
{
if (signal.Direction == 0 || _cooldownRemaining > 0)
return;
if (signal.Direction > 0 && Position <= 0)
{
var volume = Volume + Math.Abs(Position);
BuyMarket(volume);
_cooldownRemaining = SignalCooldownBars;
}
else if (signal.Direction < 0 && Position >= 0)
{
var volume = Volume + Math.Abs(Position);
SellMarket(volume);
_cooldownRemaining = SignalCooldownBars;
}
}
private readonly record struct SignalInfo(int Direction);
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class t3_ma_direction_change_strategy(Strategy):
"""Double-smoothed EMA slope direction change with signal delay and StartProtection."""
def __init__(self):
super(t3_ma_direction_change_strategy, self).__init__()
self._ma_length = self.Param("MaLength", 4).SetGreaterThanZero().SetDisplay("EMA Length", "Length of EMA for double smoothing", "Indicator")
self._ma_shift = self.Param("MaShift", 0).SetNotNegative().SetDisplay("EMA Shift", "Shift applied to smoothed EMA", "Indicator")
self._signal_bar_offset = self.Param("SignalBarOffset", 1).SetNotNegative().SetDisplay("Signal Delay", "Candles to wait before acting on signal", "Trading rules")
self._sl_points = self.Param("StopLossPoints", 20.0).SetNotNegative().SetDisplay("Stop Loss (steps)", "SL distance in price steps", "Risk management")
self._tp_points = self.Param("TakeProfitPoints", 125.0).SetNotNegative().SetDisplay("Take Profit (steps)", "TP distance in price steps", "Risk management")
self._cooldown = self.Param("SignalCooldownBars", 12).SetGreaterThanZero().SetDisplay("Signal Cooldown", "Bars to wait after entries/exits", "Trading rules")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))).SetDisplay("Candle Type", "Type of candles", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(t3_ma_direction_change_strategy, self).OnReseted()
self._recent_smoothed = []
self._pending_signals = []
self._prev_direction = 0
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(t3_ma_direction_change_strategy, self).OnStarted2(time)
self._recent_smoothed = []
self._pending_signals = []
self._prev_direction = 0
self._cooldown_remaining = 0
self._ema_price = ExponentialMovingAverage()
self._ema_price.Length = self._ma_length.Value
self._ema_smooth = ExponentialMovingAverage()
self._ema_smooth.Length = self._ma_length.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
sl_val = float(self._sl_points.Value)
tp_val = float(self._tp_points.Value)
sl_unit = Unit(sl_val, UnitTypes.Absolute) if sl_val > 0 else None
tp_unit = Unit(tp_val, UnitTypes.Absolute) if tp_val > 0 else None
self.StartProtection(sl_unit, tp_unit)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
ema_price_result = process_float(self._ema_price, candle.ClosePrice, candle.OpenTime, True)
ema_smooth_result = self._ema_smooth.Process(ema_price_result)
if not ema_smooth_result.IsFormed:
self._enqueue_signal(0)
return
smoothed_val = float(ema_smooth_result)
shift = self._ma_shift.Value
required = shift + 2
self._recent_smoothed.append(smoothed_val)
if len(self._recent_smoothed) > required:
self._recent_smoothed.pop(0)
if len(self._recent_smoothed) < required:
self._enqueue_signal(0)
return
current_idx = len(self._recent_smoothed) - 1 - shift
prev_idx = len(self._recent_smoothed) - 2 - shift
current = self._recent_smoothed[current_idx]
previous = self._recent_smoothed[prev_idx]
if current > previous:
direction = 1
elif current < previous:
direction = -1
else:
direction = self._prev_direction
signal = 0
if self._prev_direction == -1 and direction == 1:
signal = 1
elif self._prev_direction == 1 and direction == -1:
signal = -1
self._prev_direction = direction
self._enqueue_signal(signal)
def _enqueue_signal(self, signal):
self._pending_signals.append(signal)
offset = self._signal_bar_offset.Value
while len(self._pending_signals) > offset:
ready = self._pending_signals.pop(0)
self._execute_signal(ready)
def _execute_signal(self, direction):
if direction == 0 or self._cooldown_remaining > 0:
return
if direction > 0 and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._cooldown_remaining = self._cooldown.Value
elif direction < 0 and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._cooldown_remaining = self._cooldown.Value
def CreateClone(self):
return t3_ma_direction_change_strategy()