Auf GitHub ansehen

UmnickTrader-Strategie

Adaptives Mean-Reversion-System, konvertiert aus dem ursprünglichen UmnickTrader MQL5-Expertenberater. Die Strategie arbeitet immer mit einer einzigen Position, wechselt je nach Ergebnis des vorherigen Trades zwischen Long- und Short-Bias. Sie bewertet Preisbewegungen anhand des Durchschnitts aus Eröffnungs-, Hoch-, Tief- und Schlusskursen und handelt erst, wenn sich dieser Durchschnitt mindestens um die konfigurierte StopBase-Distanz verschoben hat.

Kernlogik

  • Für jede abgeschlossene Kerze wird der Durchschnittspreis (O + H + L + C) / 4 berechnet.
  • Signale werden nur verarbeitet, wenn der absolute Unterschied zwischen dem aktuellen Durchschnitt und dem zuletzt verarbeiteten Durchschnitt größer oder gleich StopBase ist. Dies imitiert das ursprüngliche EA-Verhalten, auf eine ausreichend große Bewegung zu warten.
  • Wenn keine Position offen ist, berechnet die Strategie adaptive Take-Profit- und Stop-Loss-Abstände anhand von zwei Ringpuffern, die die acht jüngsten Gewinn- und Verlustausschläge speichern.
  • Nach einem profitablen Trade wird der maximale positive Ausschlag, der während der offenen Position beobachtet wurde, in den Gewinnpuffer gespeichert (minus Spread-Polster), während der Verlustpuffer StopBase + 7 * Spread erhält.
  • Nach einem Verlust-Trade wird der Gewinnpuffer auf StopBase - 3 * Spread zurückgesetzt, der Verlustpuffer mit dem aufgezeichneten Drawdown plus Spread-Polster aktualisiert, und die Handelsrichtung wird für das nächste Setup umgekehrt.

Trade-Management

  • Der Standardabstand für Take-Profit und Stop-Loss beträgt StopBase. Wenn der akkumulierte Gewinn- oder Verlustpuffer StopBase / 2 übersteigt, ersetzen ihre jeweiligen Durchschnittswerte den Standardabstand, um die Ausstiegsniveaus adaptiv zu erweitern oder zu verengen.
  • Für Einstiege werden Marktorders verwendet. Die erwarteten Take-Profit- und Stop-Loss-Preise werden von der Strategie selbst gespeichert und verwaltet, sodass Positionen geschlossen werden, wenn Kerzenhochs oder -tiefs die entsprechenden Niveaus berühren.
  • Während eine Position aktiv ist, werden die günstigste Bewegung und der tiefste Drawdown anhand von Intrabar-Extremen verfolgt. Diese Statistiken fließen in die Puffer ein, wenn der Trade schließt.
  • Es kann immer nur eine Position existieren. Ein neues Signal wird ignoriert, wenn der vorherige Trade noch nicht abgeschlossen ist.

Parameter

  • StopBase – Basisabstand (in Preiseinheiten), der erforderlich ist, um eine Bewegung als signifikant zu behandeln und den Standard-TP/SL-Abstand. Standard: 0.017.
  • TradeVolume – Volumen für Marktorders. Standard: 0.1.
  • Spread – Spread-Kompensation bei der Aktualisierung der adaptiven Puffer. Standard: 0.0005.
  • CandleType – Kerzenabonnement zur Auswertung von Durchschnittswerten. Standard: TimeSpan.FromMinutes(5).TimeFrame().

Klassifikation und Filter

  • Richtung: Beide (aber nie gleichzeitig).
  • Stil: Adaptiver Swing / konträrer Trend.
  • Indikatoren: Preisdurchschnitt, benutzerdefinierte Ausschlagpuffer.
  • Stops: Dynamischer Stop-Loss und Take-Profit, von der Strategie verwaltet.
  • Komplexität: Mittel – kombiniert zustandsbehaftete Puffer mit adaptiver Ausstiegsgrößenbestimmung.
  • Zeitrahmen: Konfigurierbar über CandleType.
  • Saisonalität / Nachrichtenfilter: Nicht verwendet.
  • Risikomanagement: Positionsgröße wird durch TradeVolume festgelegt; Ausstiegsabstände passen sich der jüngsten Performance an.
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>
/// Adaptive mean-reversion strategy converted from the UmnickTrader MQL5 expert advisor.
/// </summary>
public class UmnickTraderStrategy : Strategy
{
	// Number of trade results stored for adaptive calculations.
	private readonly StrategyParam<int> _bufferLength;

	private readonly StrategyParam<decimal> _stopBase;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<decimal> _spread;
	private readonly StrategyParam<int> _entryCooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	// Adaptive buffers storing profit and loss distances observed recently.
	private decimal[] _profitBuffer = Array.Empty<decimal>();
	private decimal[] _lossBuffer = Array.Empty<decimal>();

	// Rolling state for signal detection and risk metrics.
	private decimal _lastAveragePrice;
	private decimal _entryPrice;
	private decimal _takeProfitPrice;
	private decimal _stopLossPrice;
	private decimal _maxProfit;
	private decimal _drawdown;
	private decimal _lastTradeProfit;

	private int _currentIndex;
	private int _currentDirection = 1;
	private int _cooldownRemaining;

	private bool _positionActive;
	private bool _isLongPosition;
	private bool _positionJustClosed;

	/// <summary>
	/// Number of trade results stored for adaptive calculations.
	/// </summary>
	public int BufferLength
	{
		get => _bufferLength.Value;
		set
		{
			if (_bufferLength.Value == value)
				return;

			_bufferLength.Value = value;
			ResizeBuffers();
		}
	}

	public decimal StopBase
	{
		get => _stopBase.Value;
		set => _stopBase.Value = value;
	}

	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	public decimal Spread
	{
		get => _spread.Value;
		set => _spread.Value = value;
	}

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

	public int EntryCooldownBars
	{
		get => _entryCooldownBars.Value;
		set => _entryCooldownBars.Value = value;
	}

	public UmnickTraderStrategy()
	{
		_bufferLength = Param(nameof(BufferLength), 8)
		.SetGreaterThanZero()
		.SetDisplay("Buffer Length", "Number of trade results stored for adaptive calculations.", "Parameters")
		
		.SetOptimize(4, 32, 1);

		ResizeBuffers();

		_stopBase = Param(nameof(StopBase), 0.017m)
			.SetDisplay("Base Stop Distance", "Minimum average price move required to trigger evaluation.", "Parameters")
			
			.SetOptimize(0.005m, 0.05m, 0.005m);

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetDisplay("Trade Volume", "Order volume for each position.", "Parameters")
			
			.SetOptimize(0.05m, 1m, 0.05m);

		_spread = Param(nameof(Spread), 0.0005m)
			.SetDisplay("Spread Padding", "Spread compensation used when updating adaptive buffers.", "Parameters")
			
			.SetOptimize(0.0001m, 0.002m, 0.0001m);

		_entryCooldownBars = Param(nameof(EntryCooldownBars), 6)
			.SetGreaterThanZero()
			.SetDisplay("Entry Cooldown", "Bars to wait after a position is closed.", "Parameters");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Source candle series.", "General");
	}

	private void ResizeBuffers()
	{
		var length = BufferLength;

		_profitBuffer = new decimal[length];
		_lossBuffer = new decimal[length];
		_currentIndex = 0;
	}

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

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

		_lastAveragePrice = 0m;
		_entryPrice = 0m;
		_takeProfitPrice = 0m;
		_stopLossPrice = 0m;
		_maxProfit = 0m;
		_drawdown = 0m;
		_lastTradeProfit = 0m;
		_currentIndex = 0;
		_currentDirection = 1;
		_cooldownRemaining = 0;
		_positionActive = false;
		_isLongPosition = false;
		_positionJustClosed = false;

		Array.Clear(_profitBuffer, 0, _profitBuffer.Length);
		Array.Clear(_lossBuffer, 0, _lossBuffer.Length);
	}

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

		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;

		// Update metrics for an active position before generating new signals.
		UpdateOpenPosition(candle);

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		// Average of OHLC replicates the MQL5 price smoothing logic.
		var averagePrice = (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m;
		if (!ShouldProcessAverage(averagePrice))
			return;

		if (Position != 0)
			return;

		var limitDistance = StopBase;
		var stopDistance = StopBase;

		decimal sumProfit = 0m;
		decimal sumLoss = 0m;
		var bufferLength = _profitBuffer.Length;

		if (bufferLength == 0)
			return;

		for (var i = 0; i < bufferLength; i++)
		{
			sumProfit += _profitBuffer[i];
			sumLoss += _lossBuffer[i];
		}

		// Recalculate adaptive take-profit and stop-loss distances.
		if (sumProfit > StopBase / 2m)
			limitDistance = sumProfit / bufferLength;

		if (sumLoss > StopBase / 2m)
			stopDistance = sumLoss / bufferLength;

		if (_positionJustClosed)
		{
			_positionJustClosed = false;

			// Store the most recent excursion metrics.
			if (_lastTradeProfit > 0m)
			{
				_profitBuffer[_currentIndex] = _maxProfit - Spread * 3m;
				_lossBuffer[_currentIndex] = StopBase + Spread * 7m;
			}
			else
			{
				_profitBuffer[_currentIndex] = StopBase - Spread * 3m;
				_lossBuffer[_currentIndex] = _drawdown + Spread * 7m;
				_currentDirection = -_currentDirection;
			}

			_currentIndex++;
			if (_currentIndex >= bufferLength)
				_currentIndex = 0;

			_cooldownRemaining = EntryCooldownBars;
			return;
		}

		if (limitDistance <= 0m || stopDistance <= 0m)
			return;

		if (_cooldownRemaining > 0)
			return;

		var volume = TradeVolume;
		if (volume <= 0m)
			return;

		// Enter in the current direction using market orders.
		if (_currentDirection > 0)
			OpenLong(candle.ClosePrice, limitDistance, stopDistance, volume);
		else
			OpenShort(candle.ClosePrice, limitDistance, stopDistance, volume);
	}

	private bool ShouldProcessAverage(decimal averagePrice)
	{
		if (_lastAveragePrice == 0m)
		{
			_lastAveragePrice = averagePrice;
			return true;
		}

		var difference = Math.Abs(averagePrice - _lastAveragePrice);
		if (difference >= StopBase)
		{
			_lastAveragePrice = averagePrice;
			return true;
		}

		return false;
	}

	private void UpdateOpenPosition(ICandleMessage candle)
	{
		if (!_positionActive)
			return;

		// Track intrabar extremes to measure maximum favorable and adverse excursions.
		if (_isLongPosition)
		{
			var profitMove = candle.HighPrice - _entryPrice;
			if (profitMove > _maxProfit)
				_maxProfit = profitMove;

			var lossMove = _entryPrice - candle.LowPrice;
			if (lossMove > _drawdown)
				_drawdown = lossMove;

			if (candle.LowPrice <= _stopLossPrice)
			{
				CloseCurrentPosition(_stopLossPrice);
				return;
			}

			if (candle.HighPrice >= _takeProfitPrice)
			{
				CloseCurrentPosition(_takeProfitPrice);
				return;
			}
		}
		else
		{
			var profitMove = _entryPrice - candle.LowPrice;
			if (profitMove > _maxProfit)
				_maxProfit = profitMove;

			var lossMove = candle.HighPrice - _entryPrice;
			if (lossMove > _drawdown)
				_drawdown = lossMove;

			if (candle.HighPrice >= _stopLossPrice)
			{
				CloseCurrentPosition(_stopLossPrice);
				return;
			}

			if (candle.LowPrice <= _takeProfitPrice)
			{
				CloseCurrentPosition(_takeProfitPrice);
				return;
			}
		}
	}

	private void CloseCurrentPosition(decimal exitPrice)
	{
		// Close the position and record realized profit for buffer updates.
		var profit = _isLongPosition ? exitPrice - _entryPrice : _entryPrice - exitPrice;
		_positionActive = false;
		_isLongPosition = false;
		_entryPrice = 0m;
		_takeProfitPrice = 0m;
		_stopLossPrice = 0m;

		_lastTradeProfit = profit;
		_positionJustClosed = true;

		if (Position > 0)
			SellMarket();
		else if (Position < 0)
			BuyMarket();
	}

	private void OpenLong(decimal price, decimal limitDistance, decimal stopDistance, decimal volume)
	{
		BuyMarket(volume);

		// Store trade parameters for managing exits on subsequent candles.
		_entryPrice = price;
		_takeProfitPrice = price + limitDistance;
		_stopLossPrice = price - stopDistance;
		_positionActive = true;
		_isLongPosition = true;
		_lastTradeProfit = 0m;
		_maxProfit = 0m;
		_drawdown = 0m;
	}

	private void OpenShort(decimal price, decimal limitDistance, decimal stopDistance, decimal volume)
	{
		SellMarket(volume);

		// Store trade parameters for managing exits on subsequent candles.
		_entryPrice = price;
		_takeProfitPrice = price - limitDistance;
		_stopLossPrice = price + stopDistance;
		_positionActive = true;
		_isLongPosition = false;
		_lastTradeProfit = 0m;
		_maxProfit = 0m;
		_drawdown = 0m;
	}
}