Auf GitHub ansehen

Open Close2 Ampn Stochastic Strategie

Überblick

  • Port des MetaTrader 4 Expert open_close2ampnstochastic_strategy, neu aufgebaut auf dem StockSharp High-Level API.
  • Verwendet einen klassischen Stochastic-Oszillator (Länge 9, Glättung 3/3) zusammen mit einem Preisaktionsfilter mit zwei Balken: Die aktuelle Kerze muss die Richtung der vorherigen fortsetzen, bevor eine Order gesendet wird.
  • Konzipiert für den Einzelpositionshandel. Die Standardkerzenquelle ist eine Stunde, aber über den Parameter CandleType kann ein beliebiger Zeitrahmen eingegeben werden.

Signallogik

  1. Eingangsschutz – es kann jeweils nur eine Position geöffnet sein. Wenn die Strategie flach ist, wertet sie die letzte vollständig geformte Kerze aus:
    • Long-Einstieg, wenn die Stochastic-Hauptlinie über der Signallinie liegt und sowohl der Eröffnungs- als auch der Schlusskurs der letzten Kerze unter ihren vorherigen Werten liegen (Anhalten des Abwärtsdrucks, gefolgt von der Oszillatorstärke).
    • Kurzer Einstieg, wenn die Hauptlinie Stochastic unter der Signallinie liegt und die Kerze einen höheren Eröffnungs- und Schlusskurs als die vorherige zeigt (Aufwärtsschub mit Bestätigung des rückläufigen Oszillators).
  2. Ausstiegsregeln – solange eine Position besteht, gelten die gleichen Bedingungen in umgekehrter Reihenfolge:
    • Long schließen, wenn die Hauptlinie unter die Signallinie fällt und die neue Kerze höhere Eröffnungs-/Schlusskurse druckt.
    • Short schließen, wenn die Hauptlinie über die Signallinie steigt und die neue Kerze niedrigere Eröffnungs-/Schlusskurse druckt.
  3. Drawdown Guard – repliziert den MT4-Notausstieg: Wenn die schwebende Verlustgröße (realisierter PnL + aktuelle kerzenbasierte Schätzung) MaximumRisk × account_margin erreicht, liquidiert die Strategie die Position sofort. StockSharp stellt die AccountMargin von MetaTrader nicht zur Verfügung, daher nähert sich der Port dieser über Portfolio.BlockedValue an und greift auf Portfolio.CurrentValue zurück, wenn die blockierte Marge nicht verfügbar ist.

Money-Management

  • BaseVolume spiegelt die ursprüngliche Lots-Eingabe wider und wird immer dann verwendet, wenn keine Kontoinformationen verfügbar sind.
  • Wenn eine Portfoliobewertung vorliegt, beträgt die Rohauftragsgröße Portfolio.CurrentValue × MaximumRisk / 1000 und entspricht der ursprünglichen AccountFreeMargin-basierten Größenbestimmung.
  • Nach jedem Verlusthandel wird die nächste Position um losses / DecreaseFactor reduziert; Der Streak-Zähler wird nach einem profitablen Trade zurückgesetzt. Die resultierende Größe darf niemals unter MinimumVolume fallen, der wie beim Skript MQL standardmäßig 0,1 Lose beträgt.
  • Alle berechneten Volumina werden vor dem Senden von Marktaufträgen an die Instrumentenlimits (VolumeStep, MinVolume, MaxVolume) angepasst.

Parameter

Name Typ Standard Beschreibung
BaseVolume dezimal 0.1 Ersatz-Ordergröße, wenn die risikobasierte Größenbestimmung nicht berechnet werden kann.
MaximumRisk dezimal 0.3 Anteil des Eigenkapitals, der sowohl für die dynamische Größenbestimmung als auch für den Drawdown-Schutz verwendet wird. Auf 0 setzen, um Risikoberechnungen zu deaktivieren.
DecreaseFactor dezimal 100 Divisor nach aufeinanderfolgenden Verlusten angewendet. Höhere Werte verlangsamen die Reduzierung.
MinimumVolume dezimal 0.1 Absoluter Mindestwert für das berechnete Volumen.
StochasticLength int 9 Rückblickperiode des Stochastic-Oszillators.
StochasticKLength int 3 Glättungszeitraum der %K-Linie.
StochasticDLength int 3 Glättungsperiode der %D-Signalleitung.
CandleType DataType TimeFrame(1h) Kerzenquelle, die zum Ansteuern der Indikator- und Preisfilter verwendet wird.

Implementierungshinweise

  • Der für den Notausgang erforderliche variable PnL wird mit dem letzten Kerzenschluss und Strategy.PositionPrice geschätzt. Dies spiegelt die Absicht von AccountProfit in MetaTrader wider, die tatsächlichen Berechnungen auf Brokerseite können jedoch abweichen.
  • Wenn der Connector weder die blockierte Marge noch den Portfoliowert offenlegt, bleibt der Drawdown-Schutz inaktiv, während die Strategie weiterhin mit BaseVolume handelt.
  • StartProtection() ist beim Start aktiviert, sodass die Schutzmechanismen von StockSharp (Stop/Take-Routing, Wiederverbindungen) das in der MQL-Version vorhandene Sicherheitsnetz widerspiegeln.

Unterschiede zum Original-Experten

  • Die Lotrundung von MetaTrader wird mithilfe der Instrumentenmetadaten emuliert, die über StockSharp verfügbar sind. Überprüfen Sie die VolumeStep/MinVolume-Werte für das gehandelte Wertpapier, damit die Positionsgröße mit den Einschränkungen des Brokers übereinstimmt.
  • Der MT4-Code wertete Tick für Tick aus, während er mit Volume[0] schützte. Der Port verarbeitet nur abgeschlossene Kerzen, was doppelte Signale verhindert und das empfohlene Muster für StockSharp-Strategien ist.
  • Bei den Kontometriken handelt es sich um Näherungswerte. Wenn Sie sich auf strenge Margin-Limits verlassen, passen Sie MaximumRisk an oder überschreiben Sie den Schutz, um ihn an die genauen Formeln des Brokers anzupassen.
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>
/// Port of the MetaTrader strategy open_close2ampnstochastic_strategy.
/// Replicates the price-action filters combined with a Stochastic crossover and includes the original money management rules.
/// </summary>
public class OpenClose2AmpnStochasticStrategy : Strategy
{
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _maximumRisk;
	private readonly StrategyParam<decimal> _decreaseFactor;
	private readonly StrategyParam<decimal> _minimumVolume;
	private readonly StrategyParam<int> _stochasticLength;
	private readonly StrategyParam<int> _stochasticKLength;
	private readonly StrategyParam<int> _stochasticDLength;
	private readonly StrategyParam<DataType> _candleType;

	private StochasticOscillator _stochastic = null!;

	private decimal? _previousOpen;
	private decimal? _previousClose;

	private decimal _averageEntryPrice;
	private decimal _entryVolume;
	private int _entryDirection;
	private int _lossStreak;

	/// <summary>
	/// Base position size used when portfolio data is unavailable.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Fraction of account value used for risk sizing and the drawdown guard.
	/// </summary>
	public decimal MaximumRisk
	{
		get => _maximumRisk.Value;
		set => _maximumRisk.Value = value;
	}

	/// <summary>
	/// Scaling factor that reduces position size after consecutive losing trades.
	/// </summary>
	public decimal DecreaseFactor
	{
		get => _decreaseFactor.Value;
		set => _decreaseFactor.Value = value;
	}

	/// <summary>
	/// Minimum tradable volume that mirrors the original 0.1 lot floor.
	/// </summary>
	public decimal MinimumVolume
	{
		get => _minimumVolume.Value;
		set => _minimumVolume.Value = value;
	}

	/// <summary>
	/// Stochastic oscillator look-back period.
	/// </summary>
	public int StochasticLength
	{
		get => _stochasticLength.Value;
		set => _stochasticLength.Value = value;
	}

	/// <summary>
	/// %K smoothing period of the Stochastic oscillator.
	/// </summary>
	public int StochasticKLength
	{
		get => _stochasticKLength.Value;
		set => _stochasticKLength.Value = value;
	}

	/// <summary>
	/// %D smoothing period of the Stochastic oscillator.
	/// </summary>
	public int StochasticDLength
	{
		get => _stochasticDLength.Value;
		set => _stochasticDLength.Value = value;
	}

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

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public OpenClose2AmpnStochasticStrategy()
	{
		_baseVolume = Param(nameof(BaseVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Base Volume", "Fallback order volume when risk sizing is unavailable", "Money Management");

		_maximumRisk = Param(nameof(MaximumRisk), 0.3m)
		.SetNotNegative()
		.SetDisplay("Maximum Risk", "Fraction of equity used for sizing and the drawdown guard", "Money Management");

		_decreaseFactor = Param(nameof(DecreaseFactor), 100m)
		.SetNotNegative()
		.SetDisplay("Decrease Factor", "Divisor applied after losing trades to shrink the next position", "Money Management");

		_minimumVolume = Param(nameof(MinimumVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Minimum Volume", "Lowest volume allowed after money management adjustments", "Money Management");

		_stochasticLength = Param(nameof(StochasticLength), 9)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic Length", "Number of periods used by the Stochastic oscillator", "Indicators");

		_stochasticKLength = Param(nameof(StochasticKLength), 3)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %K", "Smoothing applied to the %K line", "Indicators");

		_stochasticDLength = Param(nameof(StochasticDLength), 3)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %D", "Smoothing applied to the %D signal line", "Indicators");

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

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

		_previousOpen = null;
		_previousClose = null;
		_averageEntryPrice = 0m;
		_entryVolume = 0m;
		_entryDirection = 0;
		_lossStreak = 0;
	}

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

		// Build the Stochastic oscillator that mirrors the original (9,3,3) setup.
		_stochastic = new StochasticOscillator
		{
			K = { Length = StochasticKLength },
			D = { Length = StochasticDLength },
		};

		// Subscribe to candle data and bind the indicator values.
		var subscription = SubscribeCandles(CandleType);
		subscription
		.BindEx(_stochastic, ProcessCandle)
		.Start();

		// Draw indicator data if a chart is available.
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _stochastic);
			DrawOwnTrades(area);
		}

		// Enable built-in protection helpers (stop orders, etc.).
		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue stochasticValue)
	{
		// Process signals only once per finished candle.
		if (candle.State != CandleStates.Finished)
			return;

		if (!stochasticValue.IsFinal)
			return;

		var stochastic = (StochasticOscillatorValue)stochasticValue;
		if (stochastic.K is not decimal main || stochastic.D is not decimal signal)
			return;

		// Evaluate the emergency drawdown guard before new signals.
		if (Position != 0m && ApplyRiskGuard(candle.ClosePrice))
		{
			UpdatePreviousPrices(candle);
			return;
		}

		var previousOpen = _previousOpen;
		var previousClose = _previousClose;

		var canTrade = IsFormedAndOnlineAndAllowTrading();

		if (previousOpen is decimal prevOpen && previousClose is decimal prevClose)
		{
			if (Position == 0m && canTrade)
			{
				var longSignal = main > signal && candle.OpenPrice < prevOpen && candle.ClosePrice < prevClose;
				var shortSignal = main < signal && candle.OpenPrice > prevOpen && candle.ClosePrice > prevClose;

				if (longSignal)
				{
					var volume = CalculateTradeVolume(candle.ClosePrice);
					if (volume > 0m)
					{
						BuyMarket(volume);
						LogInfo($"Enter long: main={main:F2}, signal={signal:F2}, open={candle.OpenPrice}, close={candle.ClosePrice}, volume={volume}");
					}
				}
				else if (shortSignal)
				{
					var volume = CalculateTradeVolume(candle.ClosePrice);
					if (volume > 0m)
					{
						SellMarket(volume);
						LogInfo($"Enter short: main={main:F2}, signal={signal:F2}, open={candle.OpenPrice}, close={candle.ClosePrice}, volume={volume}");
					}
				}
			}
			else if (Position > 0m)
			{
				var exitLong = main < signal && candle.OpenPrice > prevOpen && candle.ClosePrice > prevClose;
				if (exitLong)
				{
					ClosePosition(candle.ClosePrice);
					LogInfo($"Exit long: main={main:F2}, signal={signal:F2}");
				}
			}
			else if (Position < 0m)
			{
				var exitShort = main > signal && candle.OpenPrice < prevOpen && candle.ClosePrice < prevClose;
				if (exitShort)
				{
					ClosePosition(candle.ClosePrice);
					LogInfo($"Exit short: main={main:F2}, signal={signal:F2}");
				}
			}
		}

		UpdatePreviousPrices(candle);
	}

	private bool ApplyRiskGuard(decimal closePrice)
	{
		if (MaximumRisk <= 0m)
			return false;

		var floatingPnL = CalculateFloatingPnL(closePrice);
		if (floatingPnL >= 0m)
			return false;

		var marginBase = GetMarginBase();
		if (marginBase <= 0m)
			return false;

		var limit = marginBase * MaximumRisk;
		if (Math.Abs(floatingPnL) < limit)
			return false;

		LogInfo($"Risk guard triggered: floatingPnL={floatingPnL}, limit={limit}. Closing position.");
		ClosePosition(closePrice);
		return true;
	}

	private decimal CalculateTradeVolume(decimal price)
	{
		var volume = BaseVolume;

		// Derive the lot size from account value similar to the original EA.
		var accountValue = Portfolio?.CurrentValue;
		if (accountValue is decimal value && value > 0m && price > 0m && MaximumRisk > 0m)
		{
			var riskVolume = Math.Round(value * MaximumRisk / 1000m, 2, MidpointRounding.AwayFromZero);
			if (riskVolume > 0m)
				volume = riskVolume;
		}

		// Apply loss streak reduction once at least two losses occurred, matching the MT4 script.
		if (DecreaseFactor > 0m && _lossStreak > 1)
		{
			var reduction = volume * _lossStreak / DecreaseFactor;
			volume -= reduction;
		}

		if (volume < MinimumVolume)
			volume = MinimumVolume;

		return AdjustVolume(volume);
	}

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

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

		var minVolume = Security.MinVolume ?? 0m;
		if (minVolume > 0m && volume < minVolume)
			volume = minVolume;

		var maxVolume = Security.MaxVolume;
		if (maxVolume.HasValue && maxVolume.Value > 0m && volume > maxVolume.Value)
			volume = maxVolume.Value;

		return volume;
	}

	private void ClosePosition(decimal closePrice)
	{
		var volume = Math.Abs(Position);
		if (volume <= 0m)
		{
			ResetEntryState();
			return;
		}

		if (Position > 0m)
			SellMarket(volume);
		else
			BuyMarket(volume);

		// Estimate profit using the stored average entry price.
		if (_entryDirection != 0 && _averageEntryPrice > 0m)
		{
			var profit = _entryDirection > 0 ? closePrice - _averageEntryPrice : _averageEntryPrice - closePrice;
			if (profit < 0m)
				_lossStreak++;
			else if (profit > 0m)
				_lossStreak = 0;
		}

		ResetEntryState();
	}

	private void UpdatePreviousPrices(ICandleMessage candle)
	{
		_previousOpen = candle.OpenPrice;
		_previousClose = candle.ClosePrice;
	}

	private decimal CalculateFloatingPnL(decimal price)
	{
		if (Position == 0m)
			return 0m;

		var entryPrice = _averageEntryPrice;
		if (entryPrice == 0m)
			return 0m;

		var priceMove = price - entryPrice;
		var priceStep = Security?.PriceStep ?? 0m;
		var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 0m;

		if (priceStep > 0m && stepPrice > 0m)
		{
			var steps = priceMove / priceStep;
			return steps * stepPrice * Position;
		}

		return priceMove * Position;
	}

	private decimal GetMarginBase()
	{
		if (Portfolio == null)
			return 0m;

		if (Portfolio.BlockedValue is decimal blocked && blocked > 0m)
			return blocked;

		if (Portfolio.CurrentValue is decimal value && value > 0m)
			return value;

		return 0m;
	}

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

		if (trade?.Order == null || trade.Trade == null)
			return;

		var price = trade.Trade.Price;
		var volume = trade.Trade.Volume;

		if (trade.Order.Side == Sides.Buy)
		{
			if (Position > 0m)
			{
				RegisterEntry(price, volume, 1);
			}
			else if (Position == 0m && _entryDirection == -1)
			{
				EvaluateClosedTrade(price);
			}
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			if (Position < 0m)
			{
				RegisterEntry(price, volume, -1);
			}
			else if (Position == 0m && _entryDirection == 1)
			{
				EvaluateClosedTrade(price);
			}
		}
	}

	private void RegisterEntry(decimal price, decimal volume, int direction)
	{
		if (volume <= 0m)
			return;

		if (_entryDirection != direction)
		{
			_entryDirection = direction;
			_averageEntryPrice = price;
			_entryVolume = volume;
			return;
		}

		var totalVolume = _entryVolume + volume;
		if (totalVolume <= 0m)
		{
			ResetEntryState();
			return;
		}

		_averageEntryPrice = (_averageEntryPrice * _entryVolume + price * volume) / totalVolume;
		_entryVolume = totalVolume;
	}

	private void EvaluateClosedTrade(decimal exitPrice)
	{
		if (_entryDirection == 0 || _averageEntryPrice <= 0m)
		{
			ResetEntryState();
			return;
		}

		var profit = _entryDirection > 0 ? exitPrice - _averageEntryPrice : _averageEntryPrice - exitPrice;
		if (profit < 0m)
		{
			_lossStreak++;
		}
		else if (profit > 0m)
		{
			_lossStreak = 0;
		}

		ResetEntryState();
	}

	private void ResetEntryState()
	{
		_averageEntryPrice = 0m;
		_entryVolume = 0m;
		_entryDirection = 0;
	}
}