シリアル MA スイング戦略 (API/2782)
概要
- MetaTrader の SerialMA エキスパートアドバイザーを、ローソク足サブスクリプションとカスタムのシリアル移動平均インジケーターを使って StockSharp の高レベル戦略に変換します。
- シリアル移動平均が価格に対して方向を反転するたびに新しいスイングポジションを開き、オプションでシグナルを逆転させ、同時スイング数を制限します。
- 銘柄ポイントで測定された同じ保護的なストップロスとテイクプロフィット距離を実装し、完成した各ローソク足で再計算されます。
シリアル移動平均インジケーター
元の EA は、価格の各クロスオーバー後に移動平均を再構築するカスタム SerialMA インジケーターに依存しています。ポートされたインジケーターはこの動作を以下の方法で複製します:
- 最近のクロスオーバーから終値を累積し、その算術平均を計算します。
- 平均と現在の終値の差を追跡して符号の変化を検出します。
- 符号が変化するたびに内部ウィンドウをリセットし、クロスオーバーバーから平均を効果的に再起動し、戦略にイベントをシグナルします。
この実装は移動平均値と、前のバーでクロスオーバーが発生したことを示すブール値フラグを公開しており、手動バッファアクセスなしに戦略が MQL ロジックを反映できます。
トレードロジック
- 完成した各ローソク足で、戦略はシリアル移動平均値とクロスオーバーフラグを読み取ります。
- 前のローソク足がクロスオーバーを引き起こした場合:
- 前の終値が前の移動平均より上にあった場合、ロングシグナルが生成されます。
- 前の終値が前の移動平均より下にあった場合、ショートシグナルが生成されます。
- ReverseSignals パラメーターはオプションでロングとショートのエントリーを入れ替えます。
- OpenedMode パラメーターはポジションのスタッキングを制御します:
- AllSwing は、その方向のポジションが既に存在していても、すべてのシグナルに新しい注文を出します。
- SingleSwing は、その方向にエクスポージャーが存在しない場合にのみ新しい注文を出します。
- 新しい注文を送信する前に、戦略は常に逆方向の既存のエクスポージャーをクローズして、スワング ロジックをソース EA と一致させます。
- ストップロスとテイクプロフィットの距離は、銘柄の価格ステップを使って各ローソク足に適用され、元のエキスパートのポイントベースのリスク制御と一致します。
パラメーター
| 名前 | 説明 | デフォルト値 |
|---|---|---|
OpenedMode |
スイングをスタックするか、方向ごとに単一のスイングを維持するかを許可します。 | AllSwing |
EnableBuy |
ロングエントリーを有効または無効にします。 | true |
EnableSell |
ショートエントリーを有効または無効にします。 | true |
ReverseSignals |
取引方向を反転します。 | false |
TradeVolume |
各新しいスイングの注文サイズ(ロット)。 | 1 |
StopLossPoints |
価格ステップ(ポイント)単位のストップロス距離。0 の値はストップを無効にします。 |
0 |
TakeProfitPoints |
価格ステップ(ポイント)単位のテイクプロフィット距離。0 の値はテイクプロフィットを無効にします。 |
0 |
CandleType |
計算に使用するローソク足データタイプ。 | 5 分ローソク足 |
注文管理と保護
- ロング時、戦略はローソク足の安値がストップロスレベルを違反したか、ローソク足の高値が利益目標に達したかを確認し、それに応じて平坦化するための成行注文を出します。
- ショート時、ローソク足の高値がストップロスを引き起こし、ローソク足の安値が利益目標を引き起こします。
- 保護レベルは
PriceStep単位で測定されます。銘柄が価格ステップを提供しない場合、保護確認は非アクティブのままになり、ティックサイズ情報がない場合の動作を反映します。
使用上の注意
- 実装は StockSharp の高レベル API(
SubscribeCandles+BindEx)を使用し、低レベルのバッファ管理を避けています。 - Python バージョンは要求に応じて含まれていません。C# ポートのみが
CS/SerialMASwingStrategy.csに存在します。 - 戦略は元の EA と同様のスイングスタイルの実行を意図しており、両方の方向を有効にしてデフォルトの
AllSwingモードを維持することで MQL の動作に最も近くなります。
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()