Auf GitHub ansehen

New Martin Strategie

Überblick

Die New Martin Strategie repliziert den originalen MetaTrader "New Martin"-Expertenberater durch Ausführung einer symmetrischen Martingale-Absicherung auf beiden Seiten des Marktes. Die Strategie hält jederzeit eine anfängliche Long- und Short-Position offen und gleicht die Absicherung aus, wenn die schnellen und langsamen geglätteten gleitenden Durchschnitte (SMMA) sich kreuzen. Wenn eine Seite der Absicherung verliert, multipliziert der Algorithmus die Exposition auf dieser Seite und realisiert gleichzeitig Gewinne auf dem profitabelsten Bein. Take-Profit-Exits recyceln die Absicherung, indem sie die fehlende Seite wiedereröffnen und optional sowohl die besten als auch die schlechtesten Performer löschen, um das Grid kompakt zu halten.

Die Implementierung richtet sich an StockSharp's High-Level-API und erwartet ein Portfolio mit Hedging-Unterstützung, damit Long- und Short-Beine koexistieren können. Orders werden der Einfachheit halber als Marktorders gesendet, was die ursprüngliche MQL-Logik widerspiegelt, bei der Füllungen als sofortig angenommen werden.

Indikatoren und Signale

  • Schneller SMMA (Standardlänge 5): verfolgt die kurzfristige Preisrichtung.
  • Langsamer SMMA (Standardlänge 20): repräsentiert den dominanten Trend.
  • Kreuzungserkennung: ein Kreuzungspunkt der vorherigen zwei abgeschlossenen Bars löst die Martingale-Ergänzung auf dem schlechtesten Bein aus. Das Signal wird auf einmal pro Kerze gedrosselt, indem die Kerzen-Öffnungszeit des letzten Kreuzungspunkts gespeichert wird.

Positionsmanagement

  • Anfängliche Absicherung: sobald Indikatoren gebildet sind, eröffnet die Strategie eine Long- und eine Short-Position mit dem konfigurierten anfänglichen Volumen. Beide Trades verwenden einen symmetrischen Take-Profit-Abstand in Pips.
  • Take-Profit-Recycling: wenn der Preis das Take-Profit-Niveau eines Beins berührt, schließt die Strategie diese Position, erfasst das Ereignis und schließt optional sowohl die profitabelsten als auch die verlierendsten verbleibenden Positionen, um Gewinne und Verluste paarweise zu realisieren. Fehlende Seiten werden sofort mit dem Basisvolumen wiedereröffnet, damit die Absicherung ausgewogen bleibt.
  • Martingale-Mittelung: bei jeder SMMA-Kreuzung identifiziert der Algorithmus die Position mit dem niedrigsten nicht realisierten Gewinn. Er erhöht die Exposition auf dieser Seite durch Multiplikation des Trade-Volumens mit dem Martingale-Multiplikator (Standard 1.6) nach Anpassung an den Wertpapier-Volumenschritt. Die profitabelste offene Position wird direkt nach dem Mittelungs-Trade geschlossen, um gesperrten Gewinn freizusetzen.

Risikomanagement

  • Kapital-Drawdown-Schutz: das höchste beobachtete Portfolio-Kapital wird verfolgt. Wenn der Rückgang von diesem Höhepunkt den konfigurierten Prozentsatz überschreitet, werden alle offenen Positionen liquidiert und die Absicherungsinitialisierung bis zur nächsten Kerze verschoben.
  • Dynamisches Basisvolumen: wenn das Kapital um mindestens den Martingale-Multiplikator relativ zum zuvor aufgezeichneten Saldo wächst, wird das Basis-Absicherungsvolumen um denselben Multiplikator erhöht (unter Berücksichtigung der Exchange-Volumenlimits). Dies spiegelt das ursprüngliche EA-Verhalten wider, bei dem Gewinne reinvestiert werden, um das Grid zu skalieren.
  • Volumen-Normalisierung: jedes angeforderte Volumen wird auf den Exchange-Volumenschritt abgerundet und zwischen dem Mindest- und Höchstvolumen des Wertpapiers begrenzt, um Order-Ablehnungen zu vermeiden.

Parameter

  • Take Profit (pips): Abstand vom Einstiegspreis zum Take-Profit-Ziel für jedes Bein. Standard 50 Pips.
  • Initial Volume: Basisvolumen pro Seite der Absicherung. Standard 0.1 Kontrakte.
  • Slow MA / Fast MA: Längen der langsamen und schnellen SMMA-Indikatoren (Standards 20 und 5). Die langsame Periode muss größer als die schnelle Periode bleiben.
  • Equity DD %: maximaler erlaubter Drawdown vom Kapitalspitzenwert, bevor alle Positionen geschlossen werden. Standard 12%.
  • Multiplier: Martingale-Faktor für Averaging-Down und für Skalierung des Basisvolumens nach bedeutendem Kapitalwachstum. Standard 1.6.
  • Candle Type: Zeitrahmen der für Berechnungen verwendeten Kerzen. Standard 15-Minuten-Kerzen, kann aber geändert werden, um dem Chartzeitrahmen des ursprünglichen EAs zu entsprechen.

Hinweise

  • Die Strategie erfordert Hedging-fähige Konten, da sie Long- und Short-Positionen gleichzeitig offen hält.
  • Marktorders werden für Einstiege und Ausstiege verwendet, genau wie der MQL-Experte, der auf sofortige Füllungen angewiesen war. Passen Sie die Order-Logik an, wenn Slippage-Kontrolle benötigt wird.
  • Stellen Sie sicher, dass die Wertpapier-Metadaten (Preisschritt, Volumenschritt, Min/Max-Volumen) korrekt konfiguriert sind, damit die Volumen-Normalisierung wie erwartet funktioniert.
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>
/// Hedged martingale strategy that scales positions on moving average crossovers.
/// </summary>
public class NewMartinStrategy : Strategy
{
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<decimal> _lossPercent;
	private readonly StrategyParam<decimal> _multiplier;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<PositionEntry> _longPositions = new();
	private readonly List<PositionEntry> _shortPositions = new();

	private SmoothedMovingAverage _slowMa;
	private SmoothedMovingAverage _fastMa;

	private decimal? _slowPrev1;
	private decimal? _slowPrev2;
	private decimal? _fastPrev1;
	private decimal? _fastPrev2;

	private decimal _currentVolume;
	private decimal _pipSize;
	private decimal _startBalance;
	private decimal _peakBalance;
	private DateTimeOffset? _lastCrossTime;
	private bool _positionsInitialized;

	/// <summary>
	/// Take profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Initial hedge volume per side.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Period of the slow smoothed moving average.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Period of the fast smoothed moving average.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Maximum equity drawdown percentage before all positions are liquidated.
	/// </summary>
	public decimal LossPercent
	{
		get => _lossPercent.Value;
		set => _lossPercent.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the martingale additions and base volume growth.
	/// </summary>
	public decimal Multiplier
	{
		get => _multiplier.Value;
		set => _multiplier.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="NewMartinStrategy"/>.
	/// </summary>
	public NewMartinStrategy()
	{
		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
		.SetGreaterThanZero()
		.SetDisplay("Take Profit", "Target distance in pips", "Risk")
		
		.SetOptimize(10m, 200m, 10m);

		_initialVolume = Param(nameof(InitialVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Initial Volume", "Volume per hedge side", "Trading")
		
		.SetOptimize(0.01m, 1m, 0.01m);

		_slowPeriod = Param(nameof(SlowPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("Slow MA", "Slow smoothed MA period", "Indicators")
		
		.SetOptimize(10, 80, 5);

		_fastPeriod = Param(nameof(FastPeriod), 5)
		.SetGreaterThanZero()
		.SetDisplay("Fast MA", "Fast smoothed MA period", "Indicators")
		
		.SetOptimize(2, 20, 1);

		_lossPercent = Param(nameof(LossPercent), 12m)
		.SetGreaterThanZero()
		.SetDisplay("Equity DD %", "Maximum drawdown before reset", "Risk")
		
		.SetOptimize(5m, 30m, 1m);

		_multiplier = Param(nameof(Multiplier), 1.6m)
		.SetGreaterThanZero()
		.SetDisplay("Multiplier", "Martingale growth factor", "Trading")
		
		.SetOptimize(1.1m, 3m, 0.1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Time frame for calculations", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_longPositions.Clear();
		_shortPositions.Clear();
		_slowPrev1 = null;
		_slowPrev2 = null;
		_fastPrev1 = null;
		_fastPrev2 = null;
		_currentVolume = 0m;
		_pipSize = 0m;
		_startBalance = 0m;
		_peakBalance = 0m;
		_lastCrossTime = null;
		_positionsInitialized = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		if (SlowPeriod <= FastPeriod)
			throw new InvalidOperationException("Slow period must be greater than fast period.");

		_currentVolume = AdjustVolume(InitialVolume);

		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
		{
			_pipSize = 1m;
		}
		else
		{
			_pipSize = step;
			var decimals = Security?.Decimals ?? 0;
			if (decimals == 3 || decimals == 5)
				_pipSize = step * 10m;
		}

		_slowMa = new SmoothedMovingAverage { Length = SlowPeriod };
		_fastMa = new SmoothedMovingAverage { Length = FastPeriod };

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

		_startBalance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		_peakBalance = _startBalance;
	}

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

		if (!_slowMa.IsFormed || !_fastMa.IsFormed)
		{
			UpdateAverageHistory(slow, fast);
			return;
		}

		UpdateAccountMetrics();

		if (ShouldCloseAllPositions())
		{
			CloseAllPositions();
			_positionsInitialized = false;
		}

		if (!_positionsInitialized)
		{
			InitializeHedge(candle.ClosePrice);
		}

		var tpTriggered = CheckTakeProfits(candle);

		if (tpTriggered)
		{
			CloseExtremePositions(candle.ClosePrice);
		}

		if (_longPositions.Count == 0)
			OpenPosition(Sides.Buy, _currentVolume, candle.ClosePrice);

		if (_shortPositions.Count == 0)
			OpenPosition(Sides.Sell, _currentVolume, candle.ClosePrice);

		HandleCrossing(candle, slow, fast);

		UpdateAverageHistory(slow, fast);
	}

	private void InitializeHedge(decimal price)
	{
		if (_currentVolume <= 0m)
			return;

		// Start with symmetric hedge on both sides.
		OpenPosition(Sides.Buy, _currentVolume, price);
		OpenPosition(Sides.Sell, _currentVolume, price);
		_positionsInitialized = _longPositions.Count > 0 && _shortPositions.Count > 0;
	}

	private void UpdateAccountMetrics()
	{
		var equity = Portfolio?.CurrentValue ?? 0m;

		if (equity > _peakBalance)
			_peakBalance = equity;

		if (_startBalance > 0m && equity >= _startBalance * Multiplier)
		{
			_startBalance = equity;

			var newVolume = AdjustVolume(_currentVolume * Multiplier);
			if (newVolume > 0m)
				_currentVolume = newVolume;
		}
	}

	private bool ShouldCloseAllPositions()
	{
		if (_peakBalance <= 0m)
			return false;

		var equity = Portfolio?.CurrentValue ?? 0m;
		if (equity <= 0m)
			return false;

		var drawdown = (_peakBalance - equity) / _peakBalance * 100m;
		return drawdown >= LossPercent;
	}

	private bool CheckTakeProfits(ICandleMessage candle)
	{
		var triggered = false;
		var offset = TakeProfitPips * _pipSize;
		if (offset <= 0m)
			return false;

		foreach (var entry in _longPositions.ToArray())
		{
			if (entry is null)
				continue;

			if (candle.HighPrice >= entry.TakeProfit)
			{
				CloseEntry(entry);
				triggered = true;
			}
		}

		foreach (var entry in _shortPositions.ToArray())
		{
			if (entry is null)
				continue;

			if (candle.LowPrice <= entry.TakeProfit)
			{
				CloseEntry(entry);
				triggered = true;
			}
		}

		return triggered;
	}

	private void CloseExtremePositions(decimal price)
	{
		var (lossEntry, lossValue, profitEntry, profitValue) = GetExtremePositions(price);

		if (lossEntry is not null && lossValue < 0m)
			CloseEntry(lossEntry);

		if (profitEntry is not null && profitEntry != lossEntry)
			CloseEntry(profitEntry);
	}

	private void HandleCrossing(ICandleMessage candle, decimal slow, decimal fast)
	{
		if (!_slowPrev2.HasValue || !_slowPrev1.HasValue || !_fastPrev2.HasValue || !_fastPrev1.HasValue)
			return;

		var crossDetected = (_slowPrev2.Value > _fastPrev2.Value && _slowPrev1.Value < _fastPrev1.Value)
		|| (_slowPrev2.Value < _fastPrev2.Value && _slowPrev1.Value > _fastPrev1.Value);

		if (!crossDetected)
			return;

		if (_lastCrossTime == candle.OpenTime)
			return;

		_lastCrossTime = candle.OpenTime;

		var (lossEntry, _, profitEntry, _) = GetExtremePositions(candle.ClosePrice);
		if (lossEntry is null)
			return;

		var volume = AdjustVolume(lossEntry.Volume * Multiplier);
		if (volume <= 0m)
			return;

		// Average down on the weakest side.
		OpenPosition(lossEntry.Side, volume, candle.ClosePrice);

		if (profitEntry is not null && profitEntry != lossEntry)
		{
			// Lock in profit on the strongest position after the new hedge.
			CloseEntry(profitEntry);
		}
	}

	private void UpdateAverageHistory(decimal slow, decimal fast)
	{
		_slowPrev2 = _slowPrev1;
		_slowPrev1 = slow;
		_fastPrev2 = _fastPrev1;
		_fastPrev1 = fast;
	}

	private void OpenPosition(Sides side, decimal requestedVolume, decimal price)
	{
		var volume = AdjustVolume(requestedVolume);
		if (volume <= 0m)
			return;

		var offset = TakeProfitPips * _pipSize;
		if (offset <= 0m)
			return;

		var takeProfit = side == Sides.Buy ? price + offset : price - offset;
		var entry = new PositionEntry(side, volume, price, takeProfit);

		if (side == Sides.Buy)
		{
			_longPositions.Add(entry);
			BuyMarket(volume);
		}
		else
		{
			_shortPositions.Add(entry);
			SellMarket(volume);
		}
	}

	private void CloseAllPositions()
	{
		foreach (var entry in _longPositions.ToArray())
			CloseEntry(entry);

		foreach (var entry in _shortPositions.ToArray())
			CloseEntry(entry);
	}

	private void CloseEntry(PositionEntry entry)
	{
		if (entry.Side == Sides.Buy)
		{
			SellMarket(entry.Volume);

			for (var i = _longPositions.Count - 1; i >= 0; i--)
			{
				if (_longPositions[i] == entry)
				{
					_longPositions.RemoveAt(i);
					break;
				}
			}
		}
		else
		{
			BuyMarket(entry.Volume);

			for (var i = _shortPositions.Count - 1; i >= 0; i--)
			{
				if (_shortPositions[i] == entry)
				{
					_shortPositions.RemoveAt(i);
					break;
				}
			}
		}
	}

	private (PositionEntry lossEntry, decimal lossValue, PositionEntry profitEntry, decimal profitValue) GetExtremePositions(decimal price)
	{
		PositionEntry lossEntry = null;
		PositionEntry profitEntry = null;
		var lossValue = 0m;
		var profitValue = 0m;

		foreach (var entry in _longPositions)
		{
			var pnl = (price - entry.EntryPrice) * entry.Volume;
			if (lossEntry is null || pnl < lossValue)
			{
				lossEntry = entry;
				lossValue = pnl;
			}

			if (profitEntry is null || pnl > profitValue)
			{
				profitEntry = entry;
				profitValue = pnl;
			}
		}

		foreach (var entry in _shortPositions)
		{
			var pnl = (entry.EntryPrice - price) * entry.Volume;
			if (lossEntry is null || pnl < lossValue)
			{
				lossEntry = entry;
				lossValue = pnl;
			}

			if (profitEntry is null || pnl > profitValue)
			{
				profitEntry = entry;
				profitValue = pnl;
			}
		}

		return (lossEntry, lossValue, profitEntry, profitValue);
	}

	private decimal AdjustVolume(decimal volume)
	{
		var security = Security;
		if (security is null)
			return volume;

		var min = security.MinVolume ?? 0m;
		if (min > 0m && volume < min)
			return 0m;

		var max = security.MaxVolume ?? 0m;
		if (max > 0m && volume > max)
			volume = max;

		return volume;
	}

	private sealed record PositionEntry(Sides Side, decimal Volume, decimal EntryPrice, decimal TakeProfit);
}