Стратегия Serial MA Swing (API/2782)
Краткое описание
- Перенос советника MetaTrader SerialMA в экосистему StockSharp с использованием высокоуровневого API и кастомного индикатора последовательного скользящего среднего.
- Открывает новые свинговые позиции при смене направления индикатора относительно цены, позволяет инвертировать сигналы и ограничивать количество одновременно удерживаемых сделок.
- Повторяет точечные стоп-лоссы и тейк-профиты из оригинала: расстояния задаются в пунктах инструмента и пересчитываются на каждой завершённой свече.
Индикатор Serial Moving Average
Первоначальный советник опирается на индикатор SerialMA, который после каждого пересечения цены заново строит своё среднее. Перенесённый индикатор реализует тот же алгоритм:
- Накапливает цены закрытия, начиная с последнего пересечения, и вычисляет их среднее арифметическое.
- Отслеживает знак разницы между средним и текущим закрытием, чтобы обнаружить смену направления.
- При смене знака «обнуляет» внутреннее окно, фиксируя момент пересечения и сигнализируя об этом стратегии.
Индикатор возвращает пару значений: само последовательное среднее и логический флаг, сообщающий о пересечении на предыдущей свече. Это позволяет повторить логику MQL без прямого обращения к буферам индикатора.
Логика торговли
- На каждой завершённой свече стратегия получает значение индикатора и флаг пересечения.
- Если предыдущая свеча породила пересечение:
- Закрытие выше среднего — формируется длинный сигнал.
- Закрытие ниже среднего — формируется короткий сигнал.
- Параметр ReverseSignals при необходимости меняет направление сделок на противоположное.
- Параметр OpenedMode управляет наращиванием позиций:
- AllSwing позволяет открывать новую сделку при каждом сигнале, даже если уже есть позиция в ту же сторону.
- SingleSwing блокирует новые входы, пока открыта позиция соответствующего направления.
- Перед открытием новой сделки стратегия закрывает противоположную позицию, чтобы сохранить «свинговую» структуру, как в оригинальном советнике.
- Стоп-лосс и тейк-профит вычисляются в пунктах (price step) и проверяются на каждой свече.
Параметры
| Имя |
Назначение |
Значение по умолчанию |
OpenedMode |
Режим открытия: стек позиций или одиночный свинг. |
AllSwing |
EnableBuy |
Разрешить длинные сделки. |
true |
EnableSell |
Разрешить короткие сделки. |
true |
ReverseSignals |
Инверсия сигналов. |
false |
TradeVolume |
Объём (в лотах) каждой новой сделки. |
1 |
StopLossPoints |
Стоп-лосс в пунктах. 0 отключает защиту. |
0 |
TakeProfitPoints |
Тейк-профит в пунктах. 0 отключает цель. |
0 |
CandleType |
Тип свечей для расчётов. |
Свечи 5 минут |
Управление ордерами и защитой
- Для длинных позиций проверяется пробой стоп-лосса по минимуму свечи и достижение тейк-профита по максимуму, после чего отправляется рыночный ордер на закрытие.
- Для коротких позиций аналогично контролируются максимум (стоп) и минимум (прибыль).
- Уровни переводятся из пунктов в цену через
PriceStep. При отсутствии шага цены защитные проверки отключаются, что соответствует ситуации с неизвестным Point() в MQL.
Примечания по использованию
- Стратегия использует высокоуровневые методы StockSharp (
SubscribeCandles, BindEx) и не обращается к низкоуровневым буферам.
- Python-версия отсутствует по требованию: реализована только C#-стратегия
CS/SerialMASwingStrategy.cs.
- Для наибольшего сходства с оригиналом рекомендуется оставлять оба направления включёнными и режим
AllSwing.
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()