Auf GitHub ansehen

Martin 1 Strategie

Konvertierung des MetaTrader 5-Experten «Martin 1» in die StockSharp-High-Level-Strategie-API. Der Algorithmus hält kontinuierlich eine Exposition aufrecht und verwendet Hedging-artige Martingale-Schritte, um Drawdowns zu erholen, während er in profitable Trends pyramidisiert.

Handelslogik

  1. Initiale Exposition – wenn die Strategie flat ist, öffnet sie sofort eine Position in der durch StartDirection definierten Richtung, unabhängig vom Zeitfilter. Die Basisordergröße wird aus InitialVolume entnommen, nachdem auf den Volumenschritt des Instruments gerundet wurde.
  2. Zeitfensterfilter – wenn UseTradingHours aktiviert ist, sind nur Skalierungsaktionen (Pyramidisierung oder Hedging) zwischen StartHour und EndHour einschließlich erlaubt, wobei die Exchange-Zeit aus Kerzen-Zeitstempeln verwendet wird.
  3. Pyramidisierung von Gewinnern – jede offene Position wird bei jeder fertigen Kerze bewertet. Wenn der schwebende Gewinn einer Long-Position die Take-Profit-Distanz überschreitet und positiv bleibt, wird eine zusätzliche Long-Order mit dem aktuellen Volumen gesendet. Short-Positionen verhalten sich symmetrisch. Der Preis der neuen Order wird als Schlusskurs der aktuellen Kerze angenommen.
  4. Hedging-Martingale – wenn die Startrichtung Long ist und eine Long-Position mehr als (StopLossPips × Pip-Größe × (Multiplikationsindex + 1)) verliert, öffnet die Strategie eine entgegengesetzte Short-Order. Vor der Platzierung des Hedges wird das Volumen mit LotMultiplier multipliziert, auf den erlaubten Schritt gerundet, und der Multiplikationszähler erhöht. Die gleiche Logik wird umgekehrt für die Short-Startrichtung angewendet. Hedging stoppt, sobald MaxMultiplications Schritte erreicht wurden.
  5. Globales Gewinnziel – der unrealisierte Gewinn aller verbleibenden Positionen (in Geld umgerechnet mit PriceStep/StepPrice) wird summiert. Wenn er MinProfit überschreitet, wird jede offene Position durch eine Marktorder in der entgegengesetzten Richtung geschlossen, und der Martingale-Zustand wird zurückgesetzt.

Risiko- und Geldmanagement

  • Die Pip-Größe wird aus dem Kurspreis des Instruments berechnet. Drei- und fünfstellige Kurse multiplizieren den Schritt mit zehn, um die ursprüngliche MetaTrader-Pip-Anpassung zu emulieren.
  • Volumen werden auf den nächsten VolumeStep abgerundet. Wenn der gerundete Wert unter den Schritt fällt, wird die Order übersprungen.
  • Der Martingale-Zähler und das aktuelle Volumen werden zurückgesetzt, wann immer das Buch flat wird, entweder natürlich oder nach Erreichen des globalen Gewinnziels.
  • Die Gewinnschätzung ignoriert Provisionen und Swaps und spiegelt das Verhalten des ursprünglichen Skripts wider, das sich ausschließlich auf den schwebenden PnL stützte.

Parameter

Name Beschreibung Standard
CandleType Kerzentyp, der alle Berechnungen antreibt. 1 Minuten-Zeitrahmen
UseTradingHours Aktiviert oder deaktiviert den Zeitfensterfilter. true
StartHour Inklusive Stunde, in der der Zeitfilter neue Skalierungsaktionen erlaubt. 2
EndHour Inklusive Stunde, in der Skalierungsaktionen stoppen. 21
LotMultiplier Faktor, der auf das aktuelle Volumen vor dem Öffnen eines Hedges angewendet wird. 1.6
MaxMultiplications Maximale Anzahl von Hedging-Schritten, die ausgelöst werden dürfen. 5
StartDirection Richtung der ersten Order, nachdem die Strategie flat wird. Buy
MinProfit Schwebender Gewinn (in Geld), der alle Positionen zum Schließen zwingt. 1.5
InitialVolume Basisvolumen für die erste Order und Reset-Zustand. 0.1
StopLossPips Pip-Distanz, die den nächsten Martingale-Hedge auslöst. 40
TakeProfitPips Pip-Distanz, die einen Pyramidisierungs-Einstieg auslöst. 100

Implementierungshinweise

  • ProcessCandle verwendet die High-Level-Kerzen-Abonnement-Pipeline (SubscribeCandles().Bind(...)) und arbeitet ausschließlich auf fertigen Kerzen, entsprechend den Plattformrichtlinien.
  • Die gesicherte Exposition wird intern mit zwei FIFO-Listen verfolgt, sodass die Strategie das MetaTrader-Hedging-Verhalten auch auf Netting-Konten emulieren kann.
  • Die Gewinnkonvertierung stützt sich auf Security.PriceStep und Security.StepPrice. Wenn diese Werte nicht verfügbar sind, wird die Preisdifferenz direkt mit dem gehandelten Volumen als Fallback multipliziert.
  • Die Strategie handelt kontinuierlich; das Deaktivieren des Zeitfilters oder das Einstellen breiter Stunden lässt den Algorithmus wie den ursprünglichen Dauerbetrieb-Experten verhalten.
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>
/// Hedging martingale strategy converted from the MetaTrader script "Martin 1".
/// Adds pyramid entries in profit and opens opposite hedges with increased volume after drawdowns.
/// </summary>
public class Martin1Strategy : Strategy
{
	private sealed class PositionRecord
	{
		public decimal Volume { get; set; }
		public decimal EntryPrice { get; set; }
	}

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<bool> _useTradingHours;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<decimal> _lotMultiplier;
	private readonly StrategyParam<int> _maxMultiplications;
	private readonly StrategyParam<Sides> _startDirection;
	private readonly StrategyParam<decimal> _minProfit;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;

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

	private decimal _currentVolume;
	private int _multiplicationCount;

	/// <summary>
	/// Candle type for driving the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Enable the trading hour filter.
	/// </summary>
	public bool UseTradingHours
	{
		get => _useTradingHours.Value;
		set => _useTradingHours.Value = value;
	}

	/// <summary>
	/// Inclusive hour (exchange time) when the trading window opens.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Inclusive hour (exchange time) when the trading window closes.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the current volume after each hedging step.
	/// </summary>
	public decimal LotMultiplier
	{
		get => _lotMultiplier.Value;
		set => _lotMultiplier.Value = value;
	}

	/// <summary>
	/// Maximum number of hedging multiplications that can be triggered.
	/// </summary>
	public int MaxMultiplications
	{
		get => _maxMultiplications.Value;
		set => _maxMultiplications.Value = value;
	}

	/// <summary>
	/// Direction of the very first position that is opened when flat.
	/// </summary>
	public Sides StartDirection
	{
		get => _startDirection.Value;
		set => _startDirection.Value = value;
	}

	/// <summary>
	/// Minimum floating profit that triggers closing all positions.
	/// </summary>
	public decimal MinProfit
	{
		get => _minProfit.Value;
		set => _minProfit.Value = value;
	}

	/// <summary>
	/// Base order volume used for the initial trade.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="Martin1Strategy"/> class.
	/// </summary>
	public Martin1Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to evaluate conditions", "General");

		_useTradingHours = Param(nameof(UseTradingHours), false)
			.SetDisplay("Use Trading Hours", "Restrict entries to a time window", "General");

		_startHour = Param(nameof(StartHour), 2)
			.SetRange(0, 23)
			.SetDisplay("Start Hour", "Hour to start monitoring for new trades", "General");

		_endHour = Param(nameof(EndHour), 21)
			.SetRange(0, 23)
			.SetDisplay("End Hour", "Hour to stop opening hedges/pyramids", "General");

		_lotMultiplier = Param(nameof(LotMultiplier), 1.6m)
			.SetGreaterThanZero()
			.SetDisplay("Lot Multiplier", "Factor applied to volume after a loss", "Money Management")
			
			.SetOptimize(1.1m, 3m, 0.1m);

		_maxMultiplications = Param(nameof(MaxMultiplications), 5)
			.SetGreaterThanZero()
			.SetDisplay("Max Multiplications", "Maximum hedging steps", "Money Management")
			
			.SetOptimize(1, 10, 1);

		_startDirection = Param(nameof(StartDirection), Sides.Buy)
			.SetDisplay("Start Direction", "Side of the initial order", "Trading");

		_minProfit = Param(nameof(MinProfit), 1.5m)
			.SetDisplay("Min Profit", "Floating profit target to flatten", "Risk")
			
			.SetOptimize(0.5m, 10m, 0.5m);

		_initialVolume = Param(nameof(InitialVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Initial Volume", "Baseline order size", "Money Management");

		_stopLossPips = Param(nameof(StopLossPips), 400)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Distance before hedging the opposite side", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 1000)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Distance to pyramid in the same direction", "Risk");
	}

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

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

		_longPositions.Clear();
		_shortPositions.Clear();
		_multiplicationCount = 0;
		_currentVolume = AdjustVolume(InitialVolume);
	}

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

		if (UseTradingHours && StartHour >= EndHour)
			throw new InvalidOperationException("Start hour must be less than end hour when the filter is enabled.");

		_longPositions.Clear();
		_shortPositions.Clear();
		_multiplicationCount = 0;
		_currentVolume = AdjustVolume(InitialVolume);

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

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

		// No indicators to check.

		var closePrice = candle.ClosePrice;
		var totalProfit = CalculateOpenProfit(closePrice);
		var withinHours = !UseTradingHours || IsWithinTradingHours(candle.CloseTime);

		if (withinHours)
		{
			if (_longPositions.Count > 0)
				EvaluateLongPositions(closePrice);

			if (_shortPositions.Count > 0)
				EvaluateShortPositions(closePrice);
		}

		if (_longPositions.Count == 0 && _shortPositions.Count == 0)
		{
			ResetMartingale();
			OpenInitialPosition(closePrice);
			return;
		}

		if ((_longPositions.Count > 0 || _shortPositions.Count > 0) && totalProfit > MinProfit)
		{
			CloseAllPositions(closePrice);
			ResetMartingale();
		}
	}

	private void EvaluateLongPositions(decimal closePrice)
	{
		var takeProfitDistance = GetTakeProfitDistance();
		var stopLossDistance = GetStopLossDistance();
		var snapshot = _longPositions.ToArray();

		foreach (var position in snapshot)
		{
			var priceGain = closePrice - position.EntryPrice;
			var profit = ConvertPriceToMoney(priceGain, position.Volume);

			if (profit > 0m && priceGain > takeProfitDistance)
				ExecuteOrder(Sides.Buy, _currentVolume, closePrice);

			if (StartDirection == Sides.Buy && stopLossDistance > 0m)
			{
				var lossDistance = position.EntryPrice - closePrice;
				if (lossDistance > stopLossDistance * (_multiplicationCount + 1) &&
					_multiplicationCount + 1 <= MaxMultiplications)
				{
					var newVolume = AdjustVolume(_currentVolume * LotMultiplier);
					if (newVolume > 0m)
					{
						_multiplicationCount++;
						_currentVolume = newVolume;
						ExecuteOrder(Sides.Sell, newVolume, closePrice);
					}
				}
			}
		}
	}

	private void EvaluateShortPositions(decimal closePrice)
	{
		var takeProfitDistance = GetTakeProfitDistance();
		var stopLossDistance = GetStopLossDistance();
		var snapshot = _shortPositions.ToArray();

		foreach (var position in snapshot)
		{
			var priceGain = position.EntryPrice - closePrice;
			var profit = ConvertPriceToMoney(priceGain, position.Volume);

			if (profit > 0m && priceGain > takeProfitDistance)
				ExecuteOrder(Sides.Sell, _currentVolume, closePrice);

			if (StartDirection == Sides.Sell && stopLossDistance > 0m)
			{
				var lossDistance = closePrice - position.EntryPrice;
				if (lossDistance > stopLossDistance * (_multiplicationCount + 1) &&
					_multiplicationCount + 1 <= MaxMultiplications)
				{
					var newVolume = AdjustVolume(_currentVolume * LotMultiplier);
					if (newVolume > 0m)
					{
						_multiplicationCount++;
						_currentVolume = newVolume;
						ExecuteOrder(Sides.Buy, newVolume, closePrice);
					}
				}
			}
		}
	}

	private void OpenInitialPosition(decimal price)
	{
		var volume = _currentVolume;
		if (volume <= 0m)
			return;

		var side = StartDirection == Sides.Sell ? Sides.Sell : Sides.Buy;
		ExecuteOrder(side, volume, price);
	}

	private void CloseAllPositions(decimal price)
	{
		var longVolume = GetTotalVolume(_longPositions);
		if (longVolume > 0m)
		{
			ExecuteOrder(Sides.Sell, longVolume, price);
		}

		var shortVolume = GetTotalVolume(_shortPositions);
		if (shortVolume > 0m)
		{
			ExecuteOrder(Sides.Buy, shortVolume, price);
		}
	}

	private void ExecuteOrder(Sides side, decimal volume, decimal price)
	{
		if (volume <= 0m)
			return;

		Volume = volume;

		if (side == Sides.Buy)
			BuyMarket();
		else
			SellMarket();

		UpdatePositions(side, volume, price);
	}

	private void UpdatePositions(Sides side, decimal volume, decimal price)
	{
		if (volume <= 0m)
			return;

		if (side == Sides.Buy)
		{
			var remaining = volume;
			var index = 0;
			while (remaining > 0m && index < _shortPositions.Count)
			{
				var position = _shortPositions[index];
				var qty = Math.Min(position.Volume, remaining);
				position.Volume -= qty;
				remaining -= qty;
				if (position.Volume <= 0m)
				{
					_shortPositions.RemoveAt(index);
					continue;
				}

				index++;
			}

			if (remaining > 0m)
			{
				_longPositions.Add(new PositionRecord
				{
					Volume = remaining,
					EntryPrice = price
				});
			}
		}
		else
		{
			var remaining = volume;
			var index = 0;
			while (remaining > 0m && index < _longPositions.Count)
			{
				var position = _longPositions[index];
				var qty = Math.Min(position.Volume, remaining);
				position.Volume -= qty;
				remaining -= qty;
				if (position.Volume <= 0m)
				{
					_longPositions.RemoveAt(index);
					continue;
				}

				index++;
			}

			if (remaining > 0m)
			{
				_shortPositions.Add(new PositionRecord
				{
					Volume = remaining,
					EntryPrice = price
				});
			}
		}
	}

	private decimal CalculateOpenProfit(decimal currentPrice)
	{
		var profit = 0m;

		foreach (var position in _longPositions)
		{
			var diff = currentPrice - position.EntryPrice;
			profit += ConvertPriceToMoney(diff, position.Volume);
		}

		foreach (var position in _shortPositions)
		{
			var diff = position.EntryPrice - currentPrice;
			profit += ConvertPriceToMoney(diff, position.Volume);
		}

		return profit;
	}

	private decimal ConvertPriceToMoney(decimal priceDifference, decimal volume)
	{
		var priceStep = Security?.PriceStep ?? 0m;
		var stepPrice = priceStep;

		if (priceStep <= 0m || stepPrice <= 0m)
			return priceDifference * volume;

		var steps = priceDifference / priceStep;
		return steps * stepPrice * volume;
	}

	private decimal GetStopLossDistance()
	{
		var pip = GetPipSize();
		return StopLossPips * pip;
	}

	private decimal GetTakeProfitDistance()
	{
		var pip = GetPipSize();
		return TakeProfitPips * pip;
	}

	private decimal GetPipSize()
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
			return 1m;

		var step = priceStep;
		var digits = 0;
		while (step < 1m && digits < 10)
		{
			step *= 10m;
			digits++;
		}

		if (digits == 3 || digits == 5)
			return priceStep * 10m;

		return priceStep;
	}

	private decimal AdjustVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			volume = Math.Floor(volume / step) * step;
			if (volume < step)
				return 0m;
		}

		return volume;
	}

	private static decimal GetTotalVolume(List<PositionRecord> positions)
	{
		var total = 0m;
		foreach (var position in positions)
			total += position.Volume;
		return total;
	}

	private bool IsWithinTradingHours(DateTime time)
	{
		var hour = time.Hour;
		return hour >= StartHour && hour <= EndHour;
	}

	private void ResetMartingale()
	{
		_multiplicationCount = 0;
		_currentVolume = AdjustVolume(InitialVolume);
	}
}