Serial MA Swing 策略 (API/2782)
概述
- 将 MetaTrader 平台的 SerialMA 智能交易系统迁移到 StockSharp,高层 API 与自定义串行均线指标共同完成策略逻辑。
- 当串行均线与价格再次交叉时开立新的波段仓位,可选反转信号并限制同向持仓数量,复现原策略的“连环开仓”模式。
- 使用点数(PriceStep)定义的固定止损与止盈,和原始 EA 保持一致,在每根收盘 K 线时重新检查触发条件。
串行移动平均指标
原 EA 依赖自定义的 SerialMA 指标:价格每次穿越均线时,均线都会重新起算。本移植版本的指标按以下步骤工作:
- 从最近一次交叉开始累加收盘价,计算区间的算术平均值。
- 记录均值与当前收盘价的差值符号,用于检测方向变化。
- 当符号发生变化时,重置内部窗口,使均线从交叉处重新起算,并向策略发出“已交叉”的标记。
指标输出移动平均值以及一个布尔标志,标志为 true 表示上一根 K 线完成了交叉,这让策略无需直接访问指标缓冲即可得到完整信息。
交易逻辑
- 每当一根 K 线收盘,策略读取串行均线的数值和交叉标志。
- 若上一根 K 线标记为交叉:
- 上一根的收盘价高于上一根的均线,则产生做多信号。
- 上一根的收盘价低于上一根的均线,则产生做空信号。
- ReverseSignals 参数可以交换多空方向,实现反向交易。
- OpenedMode 控制仓位叠加方式:
- AllSwing:每次信号都新开仓,即使同向已有持仓。
- SingleSwing:同向只允许一笔仓位,若已持仓则忽略新信号。
- 开启新仓位前,会先平掉反向仓位,保持与原 EA 相同的“翻向再入场”风格。
- 在每根收盘 K 线上,根据价格是否触及止损/止盈价位来平仓,止损止盈距离均以点数换算到实际价格。
参数
| 名称 | 说明 | 默认值 |
|---|---|---|
OpenedMode |
波段持仓模式:叠加或单笔。 | AllSwing |
EnableBuy |
是否允许做多。 | true |
EnableSell |
是否允许做空。 | true |
ReverseSignals |
是否反转信号。 | false |
TradeVolume |
每次下单的手数。 | 1 |
StopLossPoints |
止损点数,0 表示不启用。 |
0 |
TakeProfitPoints |
止盈点数,0 表示不启用。 |
0 |
CandleType |
用于计算的 K 线类型。 | 5 分钟 K 线 |
风险控制与持仓管理
- 多头持仓:若当前 K 线最低价跌破止损价,立即市价平仓;若最高价触及止盈价,同样市价平仓。
- 空头持仓:最高价触及止损,或最低价触及止盈时平仓。
- 所有价位均使用
PriceStep换算。如果标的未提供价格步长,策略会跳过保护性检查,与原 EA 在缺少Point()时的表现相符。
使用提示
- 策略完全采用 StockSharp 高层接口 (
SubscribeCandles+BindEx),不需要手动维护指标缓存。 - 根据需求未提供 Python 版本;只有
CS/SerialMASwingStrategy.cs中的 C# 实现。 - 若希望最大程度保持与原策略一致,可同时允许多空并保持
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()