Konvertiert den MetaTrader SerialMA Expert Advisor in eine StockSharp High-Level-Strategie mithilfe von Kerzenabonnements und einem benutzerdefinierten seriellen gleitenden Durchschnittsindikator.
Eröffnet neue Swing-Positionen immer dann, wenn der serielle gleitende Durchschnitt seine Richtung relativ zum Preis dreht, mit optionaler Signalumkehr und Begrenzung der Anzahl gleichzeitiger Swings.
Implementiert dieselben schützenden Stop-Loss- und Take-Profit-Abstände, gemessen in Instrumentenpunkten, die bei jeder abgeschlossenen Kerze neu berechnet werden.
Serieller Gleitender Durchschnittsindikator
Der ursprüngliche EA hängt vom benutzerdefinierten SerialMA-Indikator ab, der seinen gleitenden Durchschnitt nach jedem Preiskreuzungspunkt neu aufbaut. Der portierte Indikator repliziert dieses Verhalten durch:
Akkumulieren von Schlusskursen seit dem letzten Kreuzungspunkt und Berechnen ihres arithmetischen Mittels.
Verfolgen der Differenz zwischen dem Mittelwert und dem aktuellen Schluss, um eine Vorzeichenänderung zu erkennen.
Zurücksetzen des internen Fensters, wenn sich das Vorzeichen ändert, wodurch der Durchschnitt effektiv von der Kreuzungsleiste neu gestartet wird und das Ereignis für die Strategie signalisiert wird.
Diese Implementierung stellt den gleitenden Durchschnittswert zusammen mit einem booleschen Flag bereit, das angibt, dass ein Kreuzungspunkt auf dem vorherigen Balken aufgetreten ist, was es der Strategie ermöglicht, die MQL-Logik ohne manuellen Pufferzugriff zu spiegeln.
Handelslogik
Bei jeder abgeschlossenen Kerze liest die Strategie den seriellen gleitenden Durchschnittswert und das Kreuzungs-Flag.
Wenn die vorherige Kerze einen Kreuzungspunkt ausgelöst hat:
Wenn der vorherige Schluss über dem vorherigen gleitenden Durchschnitt lag, wird ein Long-Signal generiert.
Wenn der vorherige Schluss unter dem vorherigen gleitenden Durchschnitt lag, wird ein Short-Signal generiert.
Der Parameter ReverseSignals tauscht optional Long- und Short-Einstiege.
Der Parameter OpenedMode steuert das Positionsstapeln:
AllSwing öffnet bei jedem Signal eine neue Order, auch wenn bereits eine Position in dieser Richtung besteht.
SingleSwing öffnet nur dann eine neue Order, wenn keine Exposition in dieser Richtung besteht.
Vor dem Einreichen einer neuen Order schließt die Strategie immer das bestehende Engagement in der entgegengesetzten Richtung, um die Swing-Logik konsistent mit dem Quell-EA zu halten.
Stop-Loss- und Take-Profit-Abstände werden bei jeder Kerze unter Verwendung des Instrumentenpreisschritts angewendet, was den punktbasierten Risikokontrollen des ursprünglichen Experten entspricht.
Parameter
Name
Beschreibung
Standardwert
OpenedMode
Ermöglicht entweder das Stapeln von Swings oder das Halten eines einzelnen Swings pro Richtung.
AllSwing
EnableBuy
Aktiviert oder deaktiviert Long-Einstiege.
true
EnableSell
Aktiviert oder deaktiviert Short-Einstiege.
true
ReverseSignals
Kehrt die Handelsrichtung um.
false
TradeVolume
Ordergröße (Lots) für jeden neuen Swing.
1
StopLossPoints
Stop-Loss-Abstand in Preisschritten (Punkten). Ein Wert von 0 deaktiviert den Stop.
0
TakeProfitPoints
Take-Profit-Abstand in Preisschritten (Punkten). Ein Wert von 0 deaktiviert den Take-Profit.
0
CandleType
Kerzendatentyp für Berechnungen.
5-Minuten-Kerzen
Orderverwaltung und Schutz
Bei Long-Positionen prüft die Strategie, ob das Kerzentief das Stop-Loss-Niveau verletzt oder das Kerzenhoch das Gewinnziel erreicht hat, und gibt entsprechend eine Marktorder zum Glätten aus.
Bei Short-Positionen löst das Kerzenhoch den Stop-Loss aus und das Kerzentief löst das Gewinnziel aus.
Schutzniveaus werden in PriceStep-Einheiten gemessen. Wenn das Instrument keinen Preisschritt bereitstellt, bleiben die Schutzprüfungen inaktiv, was das Verhalten bei fehlenden Tick-Size-Informationen widerspiegelt.
Verwendungshinweise
Die Implementierung verwendet die StockSharp High-Level-API (SubscribeCandles + BindEx) und vermeidet Low-Level-Pufferverwaltung.
Es ist keine Python-Version enthalten, wie angefordert. Nur der C#-Port befindet sich in CS/SerialMASwingStrategy.cs.
Die Strategie ist für Swing-artige Ausführung ähnlich dem ursprünglichen EA gedacht; das Aktivieren beider Richtungen und das Beibehalten des Standard-AllSwing-Modus ähnelt am ehesten dem MQL-Verhalten.
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>
/// Serial moving average swing strategy converted from the MQL SerialMA EA.
/// It opens trades when the custom serial moving average flips across price.
/// </summary>
public class SerialMASwingStrategy : Strategy
{
/// <summary>
/// Mode describing how the strategy manages swing positions.
/// </summary>
public enum SerialMaOpenedModes
{
/// <summary>
/// Open a new position on every signal, even if a same-direction position exists.
/// </summary>
AllSwing,
/// <summary>
/// Allow only a single swing position per direction.
/// </summary>
SingleSwing,
}
private readonly StrategyParam<SerialMaOpenedModes> _openedMode;
private readonly StrategyParam<bool> _enableBuy;
private readonly StrategyParam<bool> _enableSell;
private readonly StrategyParam<bool> _reverseSignals;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<DataType> _candleType;
private decimal _serialMaSum;
private int _serialMaCount;
private decimal? _serialMaPrevDiff;
private int _serialMaHistory;
private bool _previousBarHadCross;
private decimal? _previousMovingAverage;
private decimal? _previousClose;
private bool _previousValuesReady;
private decimal _entryPrice;
/// <summary>
/// Defines how many concurrent swing trades are allowed.
/// </summary>
public SerialMaOpenedModes OpenedMode
{
get => _openedMode.Value;
set => _openedMode.Value = value;
}
/// <summary>
/// Enables long trades.
/// </summary>
public bool EnableBuy
{
get => _enableBuy.Value;
set => _enableBuy.Value = value;
}
/// <summary>
/// Enables short trades.
/// </summary>
public bool EnableSell
{
get => _enableSell.Value;
set => _enableSell.Value = value;
}
/// <summary>
/// Reverses every generated signal when set to <c>true</c>.
/// </summary>
public bool ReverseSignals
{
get => _reverseSignals.Value;
set => _reverseSignals.Value = value;
}
/// <summary>
/// Default order volume in lots.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Stop loss distance expressed in points (price steps).
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit distance expressed in points (price steps).
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="SerialMASwingStrategy"/>.
/// </summary>
public SerialMASwingStrategy()
{
_openedMode = Param(nameof(OpenedMode), SerialMaOpenedModes.SingleSwing)
.SetDisplay("Opened Mode", "How many swing positions may coexist", "Trading");
_enableBuy = Param(nameof(EnableBuy), true)
.SetDisplay("Enable Buy", "Allow opening long positions", "Trading");
_enableSell = Param(nameof(EnableSell), true)
.SetDisplay("Enable Sell", "Allow opening short positions", "Trading");
_reverseSignals = Param(nameof(ReverseSignals), false)
.SetDisplay("Reverse Signals", "Invert the generated direction", "Trading");
_tradeVolume = Param(nameof(TradeVolume), 0.01m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Default order volume", "Trading");
_stopLossPoints = Param(nameof(StopLossPoints), 0m)
.SetNotNegative()
.SetDisplay("Stop Loss (points)", "Protective stop distance in points", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 0m)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Target distance in points", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Data series used for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousBarHadCross = false;
_previousMovingAverage = null;
_previousClose = null;
_previousValuesReady = false;
_serialMaSum = 0m;
_serialMaCount = 0;
_serialMaPrevDiff = null;
_serialMaHistory = 0;
_entryPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = TradeVolume;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// Process serial MA inline
var close = candle.ClosePrice;
_serialMaHistory++;
if (_serialMaCount == 0)
{
_serialMaSum = close;
_serialMaCount = 1;
_serialMaPrevDiff = 0m;
_previousClose = close;
_previousValuesReady = _serialMaHistory > 2;
return;
}
_serialMaSum += close;
_serialMaCount++;
var movingAverage = _serialMaSum / _serialMaCount;
var diff = movingAverage - close;
var isCross = false;
var signalFromCross = 0;
if (_serialMaPrevDiff.HasValue && diff * _serialMaPrevDiff.Value < 0m)
{
isCross = true;
signalFromCross = diff < 0m ? 1 : -1;
movingAverage = close;
diff = 0m;
_serialMaSum = close;
_serialMaCount = 1;
}
_serialMaPrevDiff = diff;
if (!_previousValuesReady)
{
_previousBarHadCross = isCross;
_previousMovingAverage = movingAverage;
_previousClose = close;
_previousValuesReady = _serialMaHistory > 2;
return;
}
HandleProtectiveLevels(candle);
var signal = signalFromCross != 0 ? signalFromCross : GetPendingSignal();
if (signal != 0)
{
var openLong = signal > 0;
var openShort = signal < 0;
if (ReverseSignals)
{
(openLong, openShort) = (openShort, openLong);
}
if (!EnableBuy)
openLong = false;
if (!EnableSell)
openShort = false;
if (openLong)
ExecuteLongEntry();
if (openShort)
ExecuteShortEntry();
}
_previousBarHadCross = isCross;
_previousMovingAverage = movingAverage;
_previousClose = close;
}
private void ExecuteLongEntry()
{
if (TradeVolume <= 0m)
return;
// Close short exposure before building a long swing.
if (Position < 0m)
{
BuyMarket(Math.Abs(Position));
}
// Add a new long swing if allowed by the opening mode.
if (OpenedMode == SerialMaOpenedModes.AllSwing || Position <= 0m)
{
BuyMarket(TradeVolume);
}
}
private void ExecuteShortEntry()
{
if (TradeVolume <= 0m)
return;
// Close long exposure before building a short swing.
if (Position > 0m)
{
SellMarket(Position);
}
// Add a new short swing if allowed by the opening mode.
if (OpenedMode == SerialMaOpenedModes.AllSwing || Position >= 0m)
{
SellMarket(TradeVolume);
}
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Trade == null) return;
if (Position != 0m && _entryPrice == 0m)
_entryPrice = trade.Trade.Price;
if (Position == 0m)
_entryPrice = 0m;
}
private void HandleProtectiveLevels(ICandleMessage candle)
{
var step = Security?.PriceStep ?? 1m;
if (step <= 0m)
return;
if (Position > 0m)
{
if (StopLossPoints > 0m)
{
var stopPrice = _entryPrice - StopLossPoints * step;
// Exit on stop loss for a long position.
if (candle.LowPrice <= stopPrice)
{
SellMarket(Position);
return;
}
}
if (TakeProfitPoints > 0m)
{
var targetPrice = _entryPrice + TakeProfitPoints * step;
// Lock in profit once the target is reached.
if (candle.HighPrice >= targetPrice)
{
SellMarket(Position);
}
}
}
else if (Position < 0m)
{
var absPosition = Math.Abs(Position);
if (StopLossPoints > 0m)
{
var stopPrice = _entryPrice + StopLossPoints * step;
// Exit on stop loss for a short position.
if (candle.HighPrice >= stopPrice)
{
BuyMarket(absPosition);
return;
}
}
if (TakeProfitPoints > 0m)
{
var targetPrice = _entryPrice - TakeProfitPoints * step;
// Capture profit when the downside target is achieved.
if (candle.LowPrice <= targetPrice)
{
BuyMarket(absPosition);
}
}
}
}
private int GetPendingSignal()
{
if (!_previousBarHadCross || _previousMovingAverage == null || _previousClose == null)
return 0;
if (_previousClose > _previousMovingAverage)
return 1;
if (_previousClose < _previousMovingAverage)
return -1;
return 0;
}
}
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
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class serial_ma_swing_strategy(Strategy):
"""Serial MA swing: custom serial moving average that resets on cross, with SL/TP."""
def __init__(self):
super(serial_ma_swing_strategy, self).__init__()
self._sl_points = self.Param("StopLossPoints", 0.0).SetNotNegative().SetDisplay("Stop Loss (points)", "SL distance in price steps", "Risk")
self._tp_points = self.Param("TakeProfitPoints", 0.0).SetNotNegative().SetDisplay("Take Profit (points)", "TP distance in price steps", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))).SetDisplay("Candle Type", "Data series", "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(serial_ma_swing_strategy, self).OnReseted()
self._ma_sum = 0
self._ma_count = 0
self._prev_diff = None
self._history_count = 0
self._prev_had_cross = False
self._prev_ma = None
self._prev_close = None
self._entry_price = 0
def OnStarted2(self, time):
super(serial_ma_swing_strategy, self).OnStarted2(time)
self._ma_sum = 0
self._ma_count = 0
self._prev_diff = None
self._history_count = 0
self._prev_had_cross = False
self._prev_ma = None
self._prev_close = None
self._entry_price = 0
self._step = 1.0
if self.Security is not None and self.Security.PriceStep is not None and self.Security.PriceStep > 0:
self._step = float(self.Security.PriceStep)
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)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
self._history_count += 1
if self._ma_count == 0:
self._ma_sum = close
self._ma_count = 1
self._prev_diff = 0
self._prev_close = close
return
self._ma_sum += close
self._ma_count += 1
ma = self._ma_sum / self._ma_count
diff = ma - close
is_cross = False
signal = 0
if self._prev_diff is not None and diff * self._prev_diff < 0:
is_cross = True
signal = 1 if diff < 0 else -1
ma = close
diff = 0
self._ma_sum = close
self._ma_count = 1
self._prev_diff = diff
if self._history_count <= 2:
self._prev_had_cross = is_cross
self._prev_ma = ma
self._prev_close = close
return
# Manage SL/TP
self._handle_protection(candle, close)
if signal == 0:
signal = self._get_pending_signal()
if signal > 0:
if self.Position < 0:
self.BuyMarket()
if self.Position <= 0:
self.BuyMarket()
self._entry_price = close
elif signal < 0:
if self.Position > 0:
self.SellMarket()
if self.Position >= 0:
self.SellMarket()
self._entry_price = close
self._prev_had_cross = is_cross
self._prev_ma = ma
self._prev_close = close
def _get_pending_signal(self):
if not self._prev_had_cross or self._prev_ma is None or self._prev_close is None:
return 0
if self._prev_close > self._prev_ma:
return 1
if self._prev_close < self._prev_ma:
return -1
return 0
def _handle_protection(self, candle, close):
step = self._step
if self.Position > 0 and self._entry_price > 0:
if self._sl_points.Value > 0:
sl = self._entry_price - self._sl_points.Value * step
if float(candle.LowPrice) <= sl:
self.SellMarket()
self._entry_price = 0
return
if self._tp_points.Value > 0:
tp = self._entry_price + self._tp_points.Value * step
if float(candle.HighPrice) >= tp:
self.SellMarket()
self._entry_price = 0
elif self.Position < 0 and self._entry_price > 0:
if self._sl_points.Value > 0:
sl = self._entry_price + self._sl_points.Value * step
if float(candle.HighPrice) >= sl:
self.BuyMarket()
self._entry_price = 0
return
if self._tp_points.Value > 0:
tp = self._entry_price - self._tp_points.Value * step
if float(candle.LowPrice) <= tp:
self.BuyMarket()
self._entry_price = 0
def CreateClone(self):
return serial_ma_swing_strategy()