GitHub で見る

MartingaleEA-5 レベル戦略 (StockSharp)

MartingaleEA-5 レベル戦略は、MetaTrader 5 エキスパート アドバイザー「MartingaleEA-5 レベル」を StockSharp の高レベル API に直接移植したものです。このシステムは既存のポジションを監視し、市場がそれに反する動きをするたびに 5 段階の平均グリッドを構築します。すべてのロジックは完成したローソク足で実行され、履歴テストとライブ取引の両方で動作の再現性が保たれます。

取引ロジック

  1. 既存のエクスポージャーの監視 – この戦略では、最初のロングまたはショート ポジションが存在することが予想されます。最初の取引は手動で行うことも、他の戦略を通じて行うこともできます。
  2. 逆動き検出 – 完了したローソク足ごとに、戦略は現在の価格がアクティブ グループの最悪価格のエントリー (最高値ロングまたは最低値ショート) からどれだけ離れているかを測定します。
  3. Martingale の追加 – グループの変動損失がマイナスで、逆の動きが設定された累積距離を超える場合、ストラテジーは追加の成行注文を送信します。追加の注文ごとに、前の注文に VolumeMultiplier が乗算されます。最大 5 つのレベルを設定できます。 MaxAdditions パラメータは、実際に使用される数を制限します。
  4. 損益目標 – グループがオープンしている間、戦略はその方向の未実現損益を継続的に合計します。合計が TakeProfitCurrency に達するか、StopLossCurrency を下回ると、その側のすべての注文が成行注文でクローズされ、マーチンゲール カウンターがリセットされます。
  5. 数量の正規化 – すべての注文数量は商品の VolumeStepMinVolume、および MaxVolume を経由して、約定不可能な数量の送信を回避します。

パラメーター

名前 説明 デフォルト
EnableMartingale 平均化および清算ロジックをオンまたはオフにします。 true
VolumeMultiplier 新しいレベルを追加するときに前の注文量に適用される係数。 2.0
MaxAdditions 方向ごとのマーチンゲール ステップの最大数 (最大 5)。 4
Level1DistancePips 2 番目のオーダーをオープンする前の最初の不利な距離 (pips 単位)。 300
Level2DistancePips 3次オーダーには追加の距離が必要です。 400
Level3DistancePips 4 次の場合は追加の距離が必要です。 500
Level4DistancePips 5 次の場合は追加の距離が必要です。 600
Level5DistancePips 6 次の場合は追加の距離が必要です (許可されている場合)。 700
TakeProfitCurrency グループ全体を締めくくる未実現利益(口座通貨)。 200
StopLossCurrency 緊急退場を余儀なくされる含み損(口座通貨)。 -500
CandleType 評価に使用される時間枠 (デフォルトの 1 分ローソク足)。 TimeFrame(1m)

ピップ換算 – すべての距離に商品価格ステップ (PriceStep または MinPriceStep) が掛けられます。小数ピップで引用されたシンボルの場合は、それに応じて値を調整します。

注意事項と推奨事項

  • この実装は、一度に 1 つの方向バスケットのみがアクティブであるという前提を含め、元の EA を反映しています。両方向で同時にポジションをオープンすると、各側が独立して管理されます。
  • この戦略はローソク足の終値にのみ反応するため、必要な反応性に一致する時間枠を選択してください。タイムフレームが低いほど、ティックレベルの動作がより厳密にエミュレートされます。
  • Martingale の手法はリスクを増幅します。ライブ市場で戦略を有効にする前に、常に現実的なスリッページと手数料モデルでバックテストを行い、保守的なストップレベルを定義してください。
  • この戦略ではまだ Python ポートを作成しません。要求に応じて、C# の高レベル実装のみが含まれます。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Martingale averaging strategy converted from "MartingaleEA-5 Levels".
/// Opens initial position on simple momentum, then averages down with
/// increasing lot sizes up to 5 levels. Closes when floating profit
/// reaches target or stop threshold.
/// </summary>
public class MartingaleEa5LevelsStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<int> _maxAdditions;
	private readonly StrategyParam<decimal> _takeProfitPercent;
	private readonly StrategyParam<decimal> _stopLossPercent;

	private SimpleMovingAverage _sma;
	private decimal? _prevClose;
	private decimal? _prevMa;

	private readonly List<(decimal price, decimal vol)> _entries = new();
	private int _additions;
	private decimal _lastVolume;
	private Sides? _activeSide;
	private int _candleCount;
	private int _lastOrderCandle;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	public int MaxAdditions
	{
		get => _maxAdditions.Value;
		set => _maxAdditions.Value = value;
	}

	public decimal TakeProfitPercent
	{
		get => _takeProfitPercent.Value;
		set => _takeProfitPercent.Value = value;
	}

	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}

	public MartingaleEa5LevelsStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe", "General");

		_maPeriod = Param(nameof(MaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "SMA period for entry signal", "Indicators");

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Multiplier", "Multiplier for each martingale level", "Money Management");

		_maxAdditions = Param(nameof(MaxAdditions), 4)
			.SetDisplay("Max Additions", "Maximum martingale additions", "Money Management");

		_takeProfitPercent = Param(nameof(TakeProfitPercent), 0.5m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit %", "Floating profit % to close group", "Risk");

		_stopLossPercent = Param(nameof(StopLossPercent), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss %", "Floating loss % to close group", "Risk");
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_sma = default;
		_prevClose = null;
		_prevMa = null;
		_entries.Clear();
		_additions = 0;
		_lastVolume = 0;
		_activeSide = null;
		_candleCount = 0;
		_lastOrderCandle = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_sma = new SimpleMovingAverage { Length = MaPeriod };
		_prevClose = null;
		_prevMa = null;
		_entries.Clear();
		_additions = 0;
		_lastVolume = 0;
		_activeSide = null;
		_candleCount = 0;
		_lastOrderCandle = 0;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_sma, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _sma);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal smaValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_sma.IsFormed)
		{
			_prevClose = candle.ClosePrice;
			_prevMa = smaValue;
			return;
		}

		_candleCount++;

		var close = candle.ClosePrice;
		var volume = Volume;
		if (volume <= 0)
			volume = 1;

		// Cooldown: allow at most one order per 100 candles
		var cooldownPassed = (_candleCount - _lastOrderCandle) >= 100;

		// Check martingale closure first
		if (_entries.Count > 0)
		{
			var floatingPnl = CalculateFloatingPnl(close);
			var totalCost = CalculateTotalCost();

			if (totalCost > 0)
			{
				var pnlPercent = floatingPnl / totalCost * 100m;

				if (cooldownPassed && (pnlPercent >= TakeProfitPercent || pnlPercent <= -StopLossPercent))
				{
					// Close entire position
					if (Position > 0)
						SellMarket(Position);
					else if (Position < 0)
						BuyMarket(Math.Abs(Position));

					_lastOrderCandle = _candleCount;
					_entries.Clear();
					_additions = 0;
					_lastVolume = 0;
					_activeSide = null;

					_prevClose = close;
					_prevMa = smaValue;
					return;
				}
			}

			// Check for martingale additions
			if (cooldownPassed && _additions < MaxAdditions)
			{
				var avgPrice = CalculateAvgPrice();
				var adversePercent = _activeSide == Sides.Buy
					? (avgPrice - close) / avgPrice * 100m
					: (close - avgPrice) / avgPrice * 100m;

				// Add at each 0.3% adverse move beyond previous level
				var threshold = 0.3m * (_additions + 1);
				if (adversePercent >= threshold)
				{
					var nextVol = _lastVolume * VolumeMultiplier;
					if (nextVol < 1) nextVol = 1;

					if (_activeSide == Sides.Buy)
					{
						BuyMarket(nextVol);
						_entries.Add((close, nextVol));
					}
					else
					{
						SellMarket(nextVol);
						_entries.Add((close, nextVol));
					}

					_lastVolume = nextVol;
					_additions++;
					_lastOrderCandle = _candleCount;
				}
			}
		}

		// Initial entry signal: MA crossover
		if (cooldownPassed && _prevClose != null && _prevMa != null && _activeSide == null)
		{
			var buySignal = _prevClose.Value < _prevMa.Value && close > smaValue;
			var sellSignal = _prevClose.Value > _prevMa.Value && close < smaValue;

			if (buySignal)
			{
				BuyMarket(volume);
				_entries.Clear();
				_entries.Add((close, volume));
				_additions = 0;
				_lastVolume = volume;
				_activeSide = Sides.Buy;
				_lastOrderCandle = _candleCount;
			}
			else if (sellSignal)
			{
				SellMarket(volume);
				_entries.Clear();
				_entries.Add((close, volume));
				_additions = 0;
				_lastVolume = volume;
				_activeSide = Sides.Sell;
				_lastOrderCandle = _candleCount;
			}
		}

		_prevClose = close;
		_prevMa = smaValue;
	}

	private decimal CalculateFloatingPnl(decimal currentPrice)
	{
		var pnl = 0m;
		foreach (var (price, vol) in _entries)
		{
			if (_activeSide == Sides.Buy)
				pnl += (currentPrice - price) * vol;
			else
				pnl += (price - currentPrice) * vol;
		}
		return pnl;
	}

	private decimal CalculateTotalCost()
	{
		var cost = 0m;
		foreach (var (price, vol) in _entries)
			cost += price * vol;
		return cost;
	}

	private decimal CalculateAvgPrice()
	{
		var totalVol = 0m;
		var totalCost = 0m;
		foreach (var (price, vol) in _entries)
		{
			totalVol += vol;
			totalCost += price * vol;
		}
		return totalVol > 0 ? totalCost / totalVol : 0;
	}
}