Serial MA Swing Strategy (API/2782)
Summary
- Converts the MetaTrader SerialMA expert advisor into a StockSharp high-level strategy using candle subscriptions and a custom serial moving average indicator.
- Opens new swing positions whenever the serial moving average flips its direction relative to price, optionally reversing the signal and limiting the number of concurrent swings.
- Implements the same protective stop-loss and take-profit distances measured in instrument points, recalculated on every finished candle.
Serial Moving Average indicator
The original EA depends on the custom SerialMA indicator that rebuilds its moving average after each price crossover. The ported indicator replicates this behaviour by:
- Accumulating closing prices from the most recent crossover and calculating their arithmetic mean.
- Tracking the difference between the mean and the current close to detect a sign change.
- Resetting the internal window whenever the sign changes, effectively restarting the average from the crossover bar and flagging the event for the strategy.
This implementation exposes the moving average value together with a boolean flag indicating that a crossover occurred on the previous bar, allowing the strategy to mirror the MQL logic without manual buffer access.
Trading logic
- On every finished candle the strategy reads the serial moving average value and the crossover flag.
- When the previous candle triggered a crossover:
- If the previous close was above the previous moving average, a long signal is generated.
- If the previous close was below the previous moving average, a short signal is generated.
- The ReverseSignals parameter optionally swaps long and short entries.
- The OpenedMode parameter controls position stacking:
- AllSwing opens a new order on every signal, even if a position already exists in that direction.
- SingleSwing only opens a new order when no exposure exists in that direction.
- Before submitting a new order the strategy always closes existing exposure in the opposite direction to keep the swing logic consistent with the source EA.
- Stop-loss and take-profit distances are applied on each candle using the instrument price step, matching the point-based risk controls from the original expert.
Parameters
| Name |
Description |
Default |
OpenedMode |
Allows either stacking swings or keeping a single swing per direction. |
AllSwing |
EnableBuy |
Enables or disables long entries. |
true |
EnableSell |
Enables or disables short entries. |
true |
ReverseSignals |
Inverts the trading direction. |
false |
TradeVolume |
Order size (lots) for each new swing. |
1 |
StopLossPoints |
Stop-loss distance in price steps (points). A value of 0 disables the stop. |
0 |
TakeProfitPoints |
Take-profit distance in price steps (points). A value of 0 disables the take profit. |
0 |
CandleType |
Candle data type used for calculations. |
5 minute candles |
Order management and protection
- When long, the strategy checks whether the candle low violated the stop-loss level or the candle high reached the profit target and issues a market order to flatten accordingly.
- When short, the candle high triggers the stop-loss and the candle low triggers the profit target.
- Protective levels are measured in
PriceStep units. If the instrument does not provide a price step the protective checks remain idle, mirroring the behaviour of missing tick size information.
Usage notes
- The implementation uses the StockSharp high-level API (
SubscribeCandles + BindEx) and avoids low-level buffer management.
- No Python version is included, as requested. Only the C# port resides in
CS/SerialMASwingStrategy.cs.
- The strategy is intended for swing-style execution similar to the original EA; enabling both directions and keeping the default
AllSwing mode most closely resembles the MQL behaviour.
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()