Auf GitHub ansehen

OzFx Accelerator Stochastic-Strategie

Überblick

  • Konvertierung des MetaTrader-Expertenberaters OzFx (barabashkakvns Edition) zur High-Level-Strategie-API von StockSharp.
  • Kombiniert den Acceleration/Deceleration-Oszillator (AC) mit einem Stochastik-Schwellenwert, um in Trends schichtweise einzusteigen.
  • Entwickelt für diskretionären Forex-Handel, bei dem Orders in Lots dimensioniert werden und der Schutz in Pips ausgedrückt wird.

Handelslogik

  1. Berechne den Acceleration/Deceleration-Oszillator als Differenz zwischen dem Awesome Oscillator und seinem 5-Perioden-SMA.
  2. Abonniere einen stochastischen Oszillator mit konfigurierbaren %K-, %D- und Verlangsamungsperioden.
  3. Wenn eine neue Kerze schließt, bewerte die zwei aktuellsten AC-Werte zusammen mit dem stochastischen Niveau:
    • Long-Setup: %K kreuzt über dem konfigurierten Niveau, der aktuelle AC ist positiv und steigt, während der vorherige Wert negativ war.
    • Short-Setup: %K kreuzt unter dem Niveau, der aktuelle AC ist negativ und fällt, während der vorherige Wert positiv war.
  4. Bei einem gültigen Signal werden bis zu fünf gleich große Marktorders geöffnet. Die erste Schicht spiegelt den ursprünglichen EA wider, indem sie ohne Stop/Ziel gestartet wird, während die verbleibenden Schichten den konfigurierten Stop Loss und gestaffelte Take Profits erben.
  5. Das Exit-Management emuliert das ursprüngliche modok-Flag-Verhalten:
    • Wenn Trailing Stops deaktiviert sind, zieht die Strategie Stops nur nach einem profitablen Exit auf Breakeven nach, und schließt alle Schichten, wenn die Stochastik/AC-Kombination sich gegen die Position dreht.
    • Mit aktivierten Trailing Stops folgt der Stop dem Preis, sobald die Bewegung TrailingStop + TrailingStep übersteigt, und dieselbe Momentum-Umkehr schließt den Stapel.

Positionsskalierung und Ziele

  • Long-Positionen platzieren vier zusätzliche Schichten mit Take Profits bei entry + TakeProfit * i für i = 1..4. Shorts spiegeln dies unterhalb des Preises.
  • Stop Losses (wenn konfiguriert) werden jeder Schicht außer der allerersten angehängt, genau wie das MT5-Skript.
  • Teilweise Take Profits aktualisieren das interne Flag, sodass die nächste Kampagne sofort im Zustand "modok = true" startet und den Breakeven-Schutz für die initiale Schicht freischaltet.

Risikomanagement

  • StopLossPips und TakeProfitPips werden in Pips definiert. Die Strategie konvertiert sie mithilfe der Instrument-Tick-Größe und Stellenpräzision (5 oder 3 Dezimalpaare zählen als Bruchteile von Pips).
  • TrailingStopPips = 0 deaktiviert die Trailing-Logik und ermöglicht nur Breakeven-Anpassungen nach einem Take Profit. Jeder positive Wert aktiviert den oben beschriebenen Trailing-Block.
  • Alle Exits werden mit Marktorders ausgeführt, wenn der Kerzenbereich die gespeicherten Stop- oder Zielniveaus durchbricht, was dem Verhalten des ursprünglichen Experten entspricht, der auf brokerseitige Schutzorders angewiesen war.

Parameter

Name Beschreibung Standard
OrderVolume Lotgröße pro Schicht. 0.1
StopLossPips Abstand für Schutz-Stop-Orders (Pips). 100
TakeProfitPips Basisabstand zwischen gestaffelten Take Profits (Pips). 50
TrailingStopPips Trailing-Stop-Abstand in Pips (0 deaktiviert Trailing). 50
TrailingStepPips Zusätzlicher Abstand vor dem Vorrücken des Trailing Stops. 5
KPeriod Stochastik-%K-Lookback. 5
DPeriod Stochastik-%D-Glättung. 3
SmoothingPeriod Endglättung für %K. 3
StochasticLevel Schwellenwert zur Trennung bullischer/bärischer Regime. 50
CandleType Quell-Kerzenserie für Berechnungen. 4h-Zeitrahmen

Implementierungshinweise

  • Signale, Trailing-Updates und Schutz-Exits werden auf abgeschlossenen Kerzen verarbeitet, um konsistent mit dem EA zu bleiben, der bei neuen Bars auslöst.
  • Der AC-Indikator wird reproduziert, indem der Awesome Oscillator gebunden und sein 5-Perioden-SMA subtrahiert wird; auf Low-Level-Indikatoren-Puffer wird nicht zugegriffen.
  • Die Pip-Konvertierung passt sich automatisch an 4/5-stellige Forex-Symbole an und fällt auf einen angemessenen Standard zurück, wenn Tick-Größen-Metadaten fehlen.
  • Die Strategie führt ein internes Hauptbuch mit Schichteinstiegen, damit Teil-Take-Profits und Stop-Anpassungen mit der Pro-Position-Logik der MetaTrader-Version übereinstimmen.
  • Da StockSharp Exits über Marktorders ausführt, werden Trades geglättet, wenn das Hoch/Tief der Kerze die gespeicherten Stop- oder Zielniveaus durchsticht, anstatt auf brokerseitige Auslöser zu warten.
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>
/// OzFx strategy converted from MetaTrader 5 to the StockSharp high-level API.
/// Stacks multiple entries when the Acceleration/Deceleration oscillator and stochastic agree.
/// Implements layered targets and dynamic protection to mimic the expert advisor behaviour.
/// </summary>
public class OzFxAcceleratorStochasticStrategy : Strategy
{
	private readonly StrategyParam<int> _maxLayers;

	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<int> _kPeriod;
	private readonly StrategyParam<int> _dPeriod;
	private readonly StrategyParam<int> _smoothingPeriod;
	private readonly StrategyParam<decimal> _stochasticLevel;
	private readonly StrategyParam<DataType> _candleType;

	private AwesomeOscillator _ao = null!;
	private SimpleMovingAverage _aoSma = null!;
	private StochasticOscillator _stochastic = null!;

	private decimal? _lastAc;
	private bool _lastExitWasTakeProfit;
	private decimal _pipSize;
	private bool _pipInitialized;

	private readonly List<EntryInfo> _longEntries = new();
	private readonly List<EntryInfo> _shortEntries = new();

	/// <summary>
	/// Defines exit origin to replicate modok flag logic.
	/// </summary>
	private enum ExitReasons
	{
		Manual,
		TakeProfit,
		StopLoss,
	}

	/// <summary>
	/// Stores layered entry metadata (volume, price, protective levels).
	/// </summary>
	private sealed class EntryInfo
	{
		public decimal Volume;
		public decimal EntryPrice;
		public decimal? StopPrice;
		public decimal? TakeProfitPrice;
		public int Layer;
	}

	/// <summary>
	/// Maximum number of layered positions.
	/// </summary>
	public int MaxLayers
	{
		get => _maxLayers.Value;
		set => _maxLayers.Value = value;
	}

	/// <summary>
	/// Order volume for each layer.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

	/// <summary>
	/// Base take profit distance per layer measured in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips. Zero disables trailing mode.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Additional distance in pips before the trailing stop is advanced.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Main stochastic lookback period.
	/// </summary>
	public int KPeriod
	{
		get => _kPeriod.Value;
		set => _kPeriod.Value = value;
	}

	/// <summary>
	/// %D smoothing length.
	/// </summary>
	public int DPeriod
	{
		get => _dPeriod.Value;
		set => _dPeriod.Value = value;
	}

	/// <summary>
	/// Final smoothing applied to %K.
	/// </summary>
	public int SmoothingPeriod
	{
		get => _smoothingPeriod.Value;
		set => _smoothingPeriod.Value = value;
	}

	/// <summary>
	/// Stochastic threshold separating bullish and bearish regimes.
	/// </summary>
	public decimal StochasticLevel
	{
		get => _stochasticLevel.Value;
		set => _stochasticLevel.Value = value;
	}

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

	/// <summary>
	/// Initializes <see cref="OzFxAcceleratorStochasticStrategy"/>.
	/// </summary>
	public OzFxAcceleratorStochasticStrategy()
	{
		_maxLayers = Param(nameof(MaxLayers), 1)
			.SetGreaterThanZero()
			.SetDisplay("Max Layers", "Maximum number of layered positions", "Risk");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume for each layer", "Trading");

		_stopLossPips = Param(nameof(StopLossPips), 10m)
			.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 5m)
			.SetDisplay("Take Profit (pips)", "Base take profit increment in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetDisplay("Trailing Step (pips)", "Extra move required before advancing the trailing stop", "Risk");

		_kPeriod = Param(nameof(KPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("%K Period", "Stochastic lookback window", "Stochastic");

		_dPeriod = Param(nameof(DPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("%D Period", "Smoothing length for %D", "Stochastic");

		_smoothingPeriod = Param(nameof(SmoothingPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Slowing", "Final smoothing for %K", "Stochastic");

		_stochasticLevel = Param(nameof(StochasticLevel), 50m)
			.SetDisplay("Stochastic Level", "Threshold used to trigger signals", "Stochastic");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle series", "General");
	}

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

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

		_longEntries.Clear();
		_shortEntries.Clear();
		_lastAc = null;
		_lastExitWasTakeProfit = false;
		_pipInitialized = false;
		_pipSize = 0m;
	}

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

		_ao = new AwesomeOscillator
		{
			ShortMa = { Length = 5 },
			LongMa = { Length = 34 },
		};

		_aoSma = new SMA
		{
			Length = 5,
		};

		_stochastic = new StochasticOscillator
		{
			K = { Length = KPeriod },
			D = { Length = DPeriod },
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_ao, _stochastic, ProcessCandle)
			.Start();

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

	/// <summary>
	/// Processes finished candles, updates indicators, and manages entries/exits.
	/// </summary>
	private void ProcessCandle(ICandleMessage candle, IIndicatorValue aoValue, IIndicatorValue stochValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!aoValue.IsFinal || !stochValue.IsFinal)
			return;

		var stoch = (StochasticOscillatorValue)stochValue;
		if (stoch.K is not decimal stochK)
			return;

		var ao = aoValue.GetValue<decimal>();
		var aoSmaValue = _aoSma.Process(new DecimalIndicatorValue(_aoSma, ao, candle.ServerTime) { IsFinal = true });
		if (!_aoSma.IsFormed)
			return;

		var ac = ao - aoSmaValue.GetValue<decimal>();
		var prevAcNullable = _lastAc;
		if (prevAcNullable is not decimal prevAc)
		{
			_lastAc = ac;
			return;
		}

		// indicators checked via BindEx
		if (!_ao.IsFormed || !_stochastic.IsFormed)
		{
			_lastAc = ac;
			return;
		}

		var pip = GetPipSize();
		var stopDistance = StopLossPips > 0m ? StopLossPips * pip : 0m;
		var takeDistance = TakeProfitPips > 0m ? TakeProfitPips * pip : 0m;
		var trailingStopDistance = TrailingStopPips > 0m ? TrailingStopPips * pip : 0m;
		var trailingStepDistance = TrailingStepPips > 0m ? TrailingStepPips * pip : 0m;
		var useTrailing = TrailingStopPips > 0m;

		TryEnterLong(candle, stochK, ac, prevAc, stopDistance, takeDistance);
		TryEnterShort(candle, stochK, ac, prevAc, stopDistance, takeDistance);

		ManageLongPositions(candle, stochK, ac, prevAc, trailingStopDistance, trailingStepDistance, useTrailing);
		ManageShortPositions(candle, stochK, ac, prevAc, trailingStopDistance, trailingStepDistance, useTrailing);

		_lastAc = ac;
	}

	/// <summary>
	/// Opens up to five long layers when momentum turns bullish.
	/// </summary>
	private void TryEnterLong(ICandleMessage candle, decimal stochK, decimal currentAc, decimal previousAc, decimal stopDistance, decimal takeDistance)
	{
		if (_longEntries.Count != 0 || _shortEntries.Count != 0)
			return;

		if (!(stochK > StochasticLevel && currentAc > previousAc))
			return;

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

		var entryPrice = candle.ClosePrice;

		// First layer mirrors the expert advisor: no stop or target until trailing engages.
		BuyMarket();
		_longEntries.Add(new EntryInfo
		{
			Volume = volume,
			EntryPrice = entryPrice,
			StopPrice = null,
			TakeProfitPrice = null,
			Layer = 0,
		});

		for (var i = 1; i < MaxLayers; i++)
		{
			BuyMarket();

			var stopPrice = stopDistance > 0m ? entryPrice - stopDistance : (decimal?)null;
			var takePrice = takeDistance > 0m ? entryPrice + takeDistance * i : (decimal?)null;

			_longEntries.Add(new EntryInfo
			{
				Volume = volume,
				EntryPrice = entryPrice,
				StopPrice = stopPrice,
				TakeProfitPrice = takePrice,
				Layer = i,
			});
		}
	}

	/// <summary>
	/// Opens up to five short layers when momentum turns bearish.
	/// </summary>
	private void TryEnterShort(ICandleMessage candle, decimal stochK, decimal currentAc, decimal previousAc, decimal stopDistance, decimal takeDistance)
	{
		if (_shortEntries.Count != 0 || _longEntries.Count != 0)
			return;

		if (!(stochK < StochasticLevel && currentAc < previousAc))
			return;

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

		var entryPrice = candle.ClosePrice;

		SellMarket();
		_shortEntries.Add(new EntryInfo
		{
			Volume = volume,
			EntryPrice = entryPrice,
			StopPrice = null,
			TakeProfitPrice = null,
			Layer = 0,
		});

		for (var i = 1; i < MaxLayers; i++)
		{
			SellMarket();

			var stopPrice = stopDistance > 0m ? entryPrice + stopDistance : (decimal?)null;
			var takePrice = takeDistance > 0m ? entryPrice - takeDistance * i : (decimal?)null;

			_shortEntries.Add(new EntryInfo
			{
				Volume = volume,
				EntryPrice = entryPrice,
				StopPrice = stopPrice,
				TakeProfitPrice = takePrice,
				Layer = i,
			});
		}
	}

	/// <summary>
	/// Manages open long layers including trailing logic and staged targets.
	/// </summary>
	private void ManageLongPositions(ICandleMessage candle, decimal stochK, decimal currentAc, decimal previousAc, decimal trailingStopDistance, decimal trailingStepDistance, bool useTrailing)
	{
		if (_longEntries.Count == 0)
			return;

		if (Position <= 0m)
		{
			_longEntries.Clear();
			return;
		}

		var closePrice = candle.ClosePrice;
		var highPrice = candle.HighPrice;
		var lowPrice = candle.LowPrice;

		var exitSignal = stochK < 50m && currentAc < previousAc;

		if (useTrailing)
		{
			if (exitSignal)
			{
				CloseAllLong(ExitReasons.Manual);
				return;
			}

			if (trailingStopDistance > 0m)
			{
				for (var i = 0; i < _longEntries.Count; i++)
				{
					var entry = _longEntries[i];
					var profit = closePrice - entry.EntryPrice;
					if (profit > trailingStopDistance + trailingStepDistance)
					{
						var newStop = closePrice - trailingStopDistance;
						if (entry.StopPrice is not decimal existing || newStop > existing)
							entry.StopPrice = newStop;
					}
				}
			}
		}
		else if (_lastExitWasTakeProfit)
		{
			if (exitSignal)
			{
				CloseAllLong(ExitReasons.Manual);
				return;
			}

			for (var i = 0; i < _longEntries.Count; i++)
			{
				var entry = _longEntries[i];
				if (entry.StopPrice is null && closePrice > entry.EntryPrice)
					entry.StopPrice = entry.EntryPrice;
			}
		}

		for (var i = 0; i < _longEntries.Count; i++)
		{
			var entry = _longEntries[i];
			if (entry.StopPrice is decimal stopPrice && lowPrice <= stopPrice)
			{
				CloseAllLong(ExitReasons.StopLoss);
				return;
			}
		}

		var anyTakeProfit = false;
		for (var i = _longEntries.Count - 1; i >= 0; i--)
		{
			var entry = _longEntries[i];
			if (entry.TakeProfitPrice is decimal takePrice && highPrice >= takePrice)
			{
				SellMarket();
				_longEntries.RemoveAt(i);
				anyTakeProfit = true;
			}
		}

		if (anyTakeProfit)
			_lastExitWasTakeProfit = true;
	}

	/// <summary>
	/// Manages open short layers including trailing logic and staged targets.
	/// </summary>
	private void ManageShortPositions(ICandleMessage candle, decimal stochK, decimal currentAc, decimal previousAc, decimal trailingStopDistance, decimal trailingStepDistance, bool useTrailing)
	{
		if (_shortEntries.Count == 0)
			return;

		if (Position >= 0m)
		{
			_shortEntries.Clear();
			return;
		}

		var closePrice = candle.ClosePrice;
		var highPrice = candle.HighPrice;
		var lowPrice = candle.LowPrice;

		var exitSignal = stochK > 50m && currentAc > previousAc;

		if (useTrailing)
		{
			if (exitSignal)
			{
				CloseAllShort(ExitReasons.Manual);
				return;
			}

			if (trailingStopDistance > 0m)
			{
				for (var i = 0; i < _shortEntries.Count; i++)
				{
					var entry = _shortEntries[i];
					var profit = entry.EntryPrice - closePrice;
					if (profit > trailingStopDistance + trailingStepDistance)
					{
						var newStop = closePrice + trailingStopDistance;
						if (entry.StopPrice is not decimal existing || newStop < existing)
							entry.StopPrice = newStop;
					}
				}
			}
		}
		else if (_lastExitWasTakeProfit)
		{
			if (exitSignal)
			{
				CloseAllShort(ExitReasons.Manual);
				return;
			}

			for (var i = 0; i < _shortEntries.Count; i++)
			{
				var entry = _shortEntries[i];
				if (entry.StopPrice is null && closePrice < entry.EntryPrice)
					entry.StopPrice = entry.EntryPrice;
			}
		}

		for (var i = 0; i < _shortEntries.Count; i++)
		{
			var entry = _shortEntries[i];
			if (entry.StopPrice is decimal stopPrice && highPrice >= stopPrice)
			{
				CloseAllShort(ExitReasons.StopLoss);
				return;
			}
		}

		var anyTakeProfit = false;
		for (var i = _shortEntries.Count - 1; i >= 0; i--)
		{
			var entry = _shortEntries[i];
			if (entry.TakeProfitPrice is decimal takePrice && lowPrice <= takePrice)
			{
				BuyMarket();
				_shortEntries.RemoveAt(i);
				anyTakeProfit = true;
			}
		}

		if (anyTakeProfit)
			_lastExitWasTakeProfit = true;
	}

	/// <summary>
	/// Closes all long layers and updates the modok-like flag.
	/// </summary>
	private void CloseAllLong(ExitReasons reason)
	{
		var volume = 0m;
		for (var i = 0; i < _longEntries.Count; i++)
			volume += _longEntries[i].Volume;

		if (volume > 0m && Position > 0m)
			SellMarket();

		_longEntries.Clear();

		if (reason == ExitReasons.TakeProfit)
			_lastExitWasTakeProfit = true;
		else if (reason == ExitReasons.StopLoss)
			_lastExitWasTakeProfit = false;
	}

	/// <summary>
	/// Closes all short layers and updates the modok-like flag.
	/// </summary>
	private void CloseAllShort(ExitReasons reason)
	{
		var volume = 0m;
		for (var i = 0; i < _shortEntries.Count; i++)
			volume += _shortEntries[i].Volume;

		if (volume > 0m && Position < 0m)
			BuyMarket();

		_shortEntries.Clear();

		if (reason == ExitReasons.TakeProfit)
			_lastExitWasTakeProfit = true;
		else if (reason == ExitReasons.StopLoss)
			_lastExitWasTakeProfit = false;
	}

	/// <summary>
	/// Calculates pip value based on the security tick size and decimal digits.
	/// </summary>
	private decimal GetPipSize()
	{
		if (_pipInitialized)
			return _pipSize;

		var security = Security;
		var step = security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 0.0001m;

		var decimals = security?.Decimals ?? 0;
		var adjust = decimals == 3 || decimals == 5 ? 10m : 1m;

		_pipSize = step * adjust;
		if (_pipSize <= 0m)
			_pipSize = step;

		if (_pipSize <= 0m)
			_pipSize = 0.0001m;

		_pipInitialized = true;
		return _pipSize;
	}
}