Auf GitHub ansehen

Strategie für das Positionssystem mit gleitendem Durchschnitt

Überblick

Das Moving Average Position System ist eine direkte Portierung des MetaTrader 4 Expertenberaters „MovingAveragePositionSystem.mq4“. Die Strategie überwacht einen langen gleitenden Lookback-Durchschnitt und reagiert auf Preiskreuzungen, die bei abgeschlossenen Kerzen auftreten. Es unterstützt sowohl die manuelle Losauswahl als auch eine optionale Martingal-ähnliche Volumeneskalationsroutine, die auf kumulierte Gewinne und Verluste reagiert, ausgedrückt in MetaTrader Punkten.

Handelslogik

  1. Signalerkennung
    • Das System berechnet einen konfigurierbaren gleitenden Durchschnitt (einfach, exponentiell, geglättet oder linear gewichtet).
    • Wenn der Schlusskurs der zuletzt abgeschlossenen Kerze den gleitenden Durchschnitt in der entgegengesetzten Richtung zum vorherigen Schlusskurs kreuzt, eröffnet die Strategie eine neue Position.
    • Pro Richtung ist nur eine Position erlaubt; Wenn die Strategie bereits Long ist, wird die Position erst dann erhöht, wenn die aktuelle Strategie geschlossen wird, und das Gleiche gilt für Short-Trades.
  2. Positionsverwaltung
    • Wenn die Kerze, die gerade geschlossen wurde, wieder unter dem gleitenden Durchschnitt endet, während eine Long-Position offen ist, wird die Position sofort zum Marktwert geschlossen.
    • Wenn die Kerze wieder über dem gleitenden Durchschnitt schließt, während eine Short-Position geöffnet ist, wird der Short geschlossen.
    • Über die Strategieparameter kann ein Take-Profit im MetaTrader-Stil, ausgedrückt in Preisschritten (Punkten), aktiviert werden. Ansonsten werden Stopps durch das Kreuz des gleitenden Durchschnitts verwaltet.
  3. Geldmanagement
    • Wenn der Martingale-Block aktiviert ist, akkumuliert die Strategie realisierte und variable PnL in MetaTrader Punkten für den aktuellen Zyklus.
    • Wenn die kumulierten Verluste die konfigurierte Verlustschwelle überschreiten, wird das nächste Handelsvolumen verdoppelt (wobei die maximale Losgröße nie überschritten wird) und alle offenen Positionen werden abgeflacht.
    • Wenn die kumulierten Gewinne das konfigurierte Gewinnziel überschreiten, wird das Volumen auf die anfängliche Losgröße zurückgesetzt und alle offenen Positionen werden geschlossen, um Gewinne zu sichern.

Parameter

Parameter Beschreibung
MaType Methode zur Berechnung des gleitenden Durchschnitts: Einfach, exponentiell, geglättet oder linear gewichtet. Spiegelt die TypeMA-Eingabe des ursprünglichen Experten wider.
MaPeriod Lookback-Zeitraum für den gleitenden Durchschnitt (Standard 240).
MaShift Vorwärtsverschiebung, die auf die gleitenden Durchschnittswerte angewendet wird, bevor Signale generiert werden. Entspricht der Eingabe SdvigMA.
Kerzentyp Kerzendatentyp, der für Signalberechnungen verwendet wird. Standardmäßig werden Kerzen mit einem Zeitrahmen von 1 Stunde verwendet.
Anfangsvolumen Das verwendete Volumen, bevor die Martingal-Routine es ändert. Entspricht der Eingabe Lots.
StartVolume Basislosgröße, auf die das Martingal nach einem profitablen Zyklus zurückgesetzt wird (StarLots).
Maximale Lautstärke Obergrenze für das Handelsvolumen (MaxLots). Bei Überschreiten dieser Grenze halbiert die Strategie das Arbeitsvolumen.
LossThresholdPips Verlustschwelle in MetaTrader Punkten, die ein Volumenverdoppelungsereignis auslöst (LossPips).
ProfitThresholdPips Gewinnziel in Punkten, das das Volumen wieder auf den Startwert (ProfitPips) zurücksetzt.
TakeProfitPips Optionale feste Take-Profit-Distanz in Punkten, die durch den integrierten Schutzhelfer angewendet wird (TakeProfit).
UseMoneyManagement Aktiviert oder deaktiviert die Martingal-ähnliche Positionsgrößenroutine (MM).

Nutzungshinweise

  • Konfigurieren Sie die Strategie mit demselben Symbol und Zeitrahmen, die in MetaTrader verwendet wurden. Der Standardzeitraum von 240 funktioniert gut mit H1-Kerzen und reproduziert das ursprüngliche Setup.
  • Bei den Punktschwellenwerten wird davon ausgegangen, dass das Gerät gültige PriceStep und StepPrice bereitstellt. Für Symbole, denen diese Metadaten fehlen, müssen Sie die Schwellenwerte möglicherweise manuell anpassen.
  • Da der ursprüngliche Code die Margen vor jedem Eintrag neu berechnet, führt der Port einen vereinfachten Volumennormalisierungsschritt durch, der die Handelsgröße halbiert, wenn sie MaxVolume überschreitet. Bei Bedarf können über die standardmäßigen StockSharp-Risikoanbieter zusätzliche Risikokontrollen hinzugefügt werden.
  • Nur abgeschlossene Kerzen lösen Ein- und Ausgänge aus und spiegeln die MQL-Implementierung wider, die die Werte Close[1] und Close[2] auf jedem neuen Balken überprüfte.

Dateien

  • CS/MovingAveragePositionSystemStrategy.cs – C#-Implementierung der Handelslogik unter Verwendung der StockSharp-High-Level-Strategie API.
  • README.md – Englische Dokumentation (diese Datei).
  • README_ru.md – Russische Dokumentation.
  • README_zh.md – Chinesische Dokumentation.
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Port of the FORTRADER MovingAveragePositionSystem expert advisor.
/// The strategy opens or closes positions on moving average crossings and optionally applies
/// a martingale-like position sizing routine based on cumulative results expressed in MetaTrader points.
/// </summary>
public class MovingAveragePositionSystemStrategy : Strategy
{
	/// <summary>
	/// Moving average calculation mode.
	/// </summary>
	public enum MovingAverageModes
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted,
	}

	private readonly StrategyParam<MovingAverageModes> _maType;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<decimal> _startVolume;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _lossThresholdPips;
	private readonly StrategyParam<decimal> _profitThresholdPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<bool> _useMoneyManagement;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closeHistory = new();
	private readonly List<decimal> _maHistory = new();

	private decimal _currentVolume;
	private decimal _cycleStartRealizedPnL;
	private decimal _priceStep;
	private decimal _stepPrice;
	private decimal _entryPrice;

	/// <summary>
	/// Moving average type used for signal calculation.
	/// </summary>
	public MovingAverageModes MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	/// <summary>
	/// Moving average length.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Forward shift applied to the moving average before generating signals.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

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

	/// <summary>
	/// Initial lot size before the martingale routine modifies it.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Base lot size restored after profitable cycles.
	/// </summary>
	public decimal StartVolume
	{
		get => _startVolume.Value;
		set => _startVolume.Value = value;
	}

	/// <summary>
	/// Maximum allowed lot size.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Loss threshold in MetaTrader points that doubles the next trade volume.
	/// </summary>
	public decimal LossThresholdPips
	{
		get => _lossThresholdPips.Value;
		set => _lossThresholdPips.Value = value;
	}

	/// <summary>
	/// Profit target in MetaTrader points that resets the volume to the starting lot.
	/// </summary>
	public decimal ProfitThresholdPips
	{
		get => _profitThresholdPips.Value;
		set => _profitThresholdPips.Value = value;
	}

	/// <summary>
	/// Fixed take profit distance in MetaTrader points.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Enables the martingale-style money management block.
	/// </summary>
	public bool UseMoneyManagement
	{
		get => _useMoneyManagement.Value;
		set => _useMoneyManagement.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="MovingAveragePositionSystemStrategy"/> class.
	/// </summary>
	public MovingAveragePositionSystemStrategy()
	{
		_maType = Param(nameof(MaType), MovingAverageModes.LinearWeighted)
		.SetDisplay("MA Type", "Moving average method", "Indicators");

		_maPeriod = Param(nameof(MaPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("MA Period", "Moving average length", "Indicators");

		_maShift = Param(nameof(MaShift), 0)
		.SetRange(0, 100)
		.SetDisplay("MA Shift", "Forward shift for the moving average", "Indicators");

		_initialVolume = Param(nameof(InitialVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Initial Volume", "Starting lot size", "Trading");

		_startVolume = Param(nameof(StartVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Start Volume", "Base lot restored after profits", "Trading");

		_maxVolume = Param(nameof(MaxVolume), 10m)
		.SetGreaterThanZero()
		.SetDisplay("Max Volume", "Maximum allowed lot size", "Trading");

		_lossThresholdPips = Param(nameof(LossThresholdPips), 90m)
		.SetGreaterThanZero()
		.SetDisplay("Loss Threshold (pts)", "Loss in points that doubles the lot", "Risk");

		_profitThresholdPips = Param(nameof(ProfitThresholdPips), 170m)
		.SetGreaterThanZero()
		.SetDisplay("Profit Target (pts)", "Profit in points that resets the lot", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 1000m)
		.SetNotNegative()
		.SetDisplay("Take Profit (pts)", "Fixed take profit distance", "Risk");

		_useMoneyManagement = Param(nameof(UseMoneyManagement), true)
		.SetDisplay("Use Money Management", "Enable martingale volume control", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Candles used for calculations", "Market Data");
	}

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

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

		_closeHistory.Clear();
		_maHistory.Clear();
		_currentVolume = InitialVolume;
		Volume = _currentVolume;
		_cycleStartRealizedPnL = PnLManager?.RealizedPnL ?? 0m;
		_priceStep = 0m;
		_stepPrice = 0m;
		_entryPrice = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		_currentVolume = InitialVolume;
		Volume = _currentVolume;
		_cycleStartRealizedPnL = PnLManager?.RealizedPnL ?? 0m;

		_priceStep = Security?.PriceStep ?? 1m;
		_stepPrice = _priceStep;

		var movingAverage = CreateMovingAverage();

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

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

		var takeProfitUnit = TakeProfitPips > 0m ? new Unit(TakeProfitPips, UnitTypes.Absolute) : null;
		StartProtection(takeProfitUnit, null);

		base.OnStarted2(time);
	}

	private DecimalLengthIndicator CreateMovingAverage()
	{
		return MaType switch
		{
			MovingAverageModes.Exponential => new EMA { Length = MaPeriod },
			MovingAverageModes.Smoothed => new SmoothedMovingAverage { Length = MaPeriod },
			MovingAverageModes.LinearWeighted => new WeightedMovingAverage { Length = MaPeriod },
			_ => new SMA { Length = MaPeriod },
		};
	}

	private void ProcessCandle(ICandleMessage candle, decimal maValue)
	{
		// Work only with finished candles to reproduce the MQL4 behaviour.
		if (candle.State != CandleStates.Finished)
		return;

		var canTrade = IsFormedAndOnlineAndAllowTrading();

		var previousClose = _closeHistory.Count >= 1 ? _closeHistory[^1] : (decimal?)null;
		var previousPreviousClose = _closeHistory.Count >= 2 ? _closeHistory[^2] : (decimal?)null;

		decimal? shiftedMa = null;
		if (_maHistory.Count > MaShift)
		{
			var index = _maHistory.Count - 1 - MaShift;
			if (index >= 0)
			shiftedMa = _maHistory[index];
		}

		if (previousClose.HasValue && previousPreviousClose.HasValue && shiftedMa.HasValue)
		{
			// Manage existing positions based on the opposite crossing.
			ManageOpenPosition(previousClose.Value, shiftedMa.Value);

			// Update the working volume according to the martingale routine.
			UpdateVolume(previousClose.Value, shiftedMa.Value);

			if (canTrade)
			{
				TryEnter(previousClose.Value, previousPreviousClose.Value, shiftedMa.Value);
			}
		}

		// Store the latest values for the next iteration.
		_closeHistory.Add(candle.ClosePrice);
		_maHistory.Add(maValue);
	}

	private void ManageOpenPosition(decimal previousClose, decimal shiftedMa)
	{
		// Close long positions when the latest closed candle falls back below the moving average.
		if (Position > 0 && previousClose < shiftedMa)
		{
			if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
			return;
		}

		// Close short positions when the latest closed candle climbs back above the average.
		if (Position < 0 && previousClose > shiftedMa)
		{
			if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
		}
	}

	private void UpdateVolume(decimal previousClose, decimal shiftedMa)
	{
		if (!UseMoneyManagement)
		return;

		var realizedPnL = PnLManager?.RealizedPnL ?? 0m;
		var realizedDiff = realizedPnL - _cycleStartRealizedPnL;

		var stepPrice = _stepPrice != 0m ? _stepPrice : GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 1m;
		var priceStep = _priceStep != 0m ? _priceStep : Security?.PriceStep ?? 1m;

		var resultInSteps = stepPrice != 0m ? realizedDiff / stepPrice : 0m;

		if (Position != 0 && priceStep > 0m && _entryPrice > 0m)
		{
			// Consider only floating losses as in the original script.
			var diff = Position > 0
			? previousClose - _entryPrice
			: _entryPrice - previousClose;

			if (diff < 0m)
			{
				resultInSteps += diff / priceStep;
			}
		}

		if (resultInSteps <= -LossThresholdPips)
		{
			// Double the lot size while keeping it within the maximum allowed range.
			var newVolume = Math.Min(_currentVolume * 2m, MaxVolume);
			if (newVolume > 0m)
			{
				_currentVolume = newVolume;
				NormalizeVolume();
				Volume = _currentVolume;
			}

			if (Position != 0)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
			}

			_cycleStartRealizedPnL = realizedPnL;
		}
		else if (resultInSteps >= ProfitThresholdPips)
		{
			// Reset the lot size to the configured starting volume and lock in profits.
			_currentVolume = StartVolume;
			NormalizeVolume();
			Volume = _currentVolume;

			if (Position != 0)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
			}

			_cycleStartRealizedPnL = realizedPnL;
		}
		else
		{
			NormalizeVolume();
		}
	}

	private void TryEnter(decimal previousClose, decimal previousPreviousClose, decimal shiftedMa)
	{
		NormalizeVolume();

		if (_currentVolume <= 0m)
		return;

		// Detect upward crossing: price moved from below the moving average to above it.
		var crossedUp = previousClose > shiftedMa && previousPreviousClose < shiftedMa;
		if (crossedUp && Position <= 0)
		{
			BuyMarket(_currentVolume);
			return;
		}

		// Detect downward crossing: price moved from above the moving average to below it.
		var crossedDown = previousClose < shiftedMa && previousPreviousClose > shiftedMa;
		if (crossedDown && Position >= 0)
		{
			SellMarket(_currentVolume);
		}
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;

		if (Position == 0m)
			_entryPrice = 0m;
	}

	private void NormalizeVolume()
	{
		// Reduce the working lot if it exceeds the maximum allowed size.
		while (_currentVolume > MaxVolume && _currentVolume > 0m)
		{
			_currentVolume /= 2m;
		}

		if (Portfolio is not null)
		{
			var portfolioValue = Portfolio.CurrentValue ?? Portfolio.BeginValue ?? 0m;
			var marginThreshold = 1000m * _currentVolume;

			while (_currentVolume > 0m && portfolioValue < marginThreshold)
			{
				_currentVolume /= 2m;
				marginThreshold = 1000m * _currentVolume;
			}
		}

		if (_currentVolume < 0m)
		{
			_currentVolume = 0m;
		}
	}
}