Auf GitHub ansehen

Exp FisherCG-Oszillator-Strategie

Diese Strategie portiert den Exp_FisherCGOscillator MetaTrader 5-Expertenberater auf die High-Level-API von StockSharp. Sie bildet den Fisher Center of Gravity-Oszillator und seine Triggerlinie nach, wertet Signale auf einer konfigurierbaren historischen Bar aus und reproduziert den originalen Stop/Take-Workflow mit StockSharp-Orders und Risikohelferklassen.

Funktionsweise

  1. Indikator-Pipeline – jede abgeschlossene Kerze wird durch den Fisher CG-Oszillator geleitet: Medianpreise speisen eine Center-of-Gravity-Schleife, Werte werden über die letzten Length Bars normalisiert, und eine Fisher-Transformation erzeugt die Oszillatorlinie. Die Triggerlinie ist einfach der um eine Bar verzögerte Oszillator.
  2. Signalextraktion – die Strategie untersucht zwei historische Lesungen, die durch SignalBar definiert werden. Sie eröffnet einen Long, wenn der ältere Oszillatorwert (SignalBar + 1) über seinem Trigger liegt, während der neuere Wert (SignalBar) wieder über den Trigger kreuzt und eine bullische Wende signalisiert. Shorts spiegeln diese Logik auf der bärischen Seite.
  3. Ausstiegsbehandlung – Long-Ausstiege erfolgen, sobald der ältere Oszillator unter seinen Trigger fällt, während Short-Ausstiege ausgelöst werden, wenn er über den Trigger steigt, was den sofortigen Schließindikatoren des EA entspricht. Entgegengesetzte Einstiege schließen die aktive Position vor der Umkehr.
  4. Bar-für-Bar-Verarbeitung – alles läuft auf abgeschlossenen Kerzen aus CandleType; es werden keine Intrabar-Trades generiert, was deterministische Backtests sicherstellt und dem "neue Bar"-Gate des EA entspricht.

Risikomanagement und Positionsgrößenbestimmung

  • Stops/ZieleStopLossPoints und TakeProfitPoints werden in Instrumentschritten ausgedrückt und über Security.PriceStep in absolute Preisabstände übersetzt.
  • VolumenkontrolleSizingMode = FixedVolume sendet das konstante FixedVolume. SizingMode = PortfolioShare konvertiert DepositShare des aktuellen Portfoliowerts in Kontrakte unter Verwendung des letzten Schlusskurses und VolumeStep.
  • Einzelposition – die Strategie flacht immer ab, bevor sie auf die entgegengesetzte Seite eintritt, um gleichzeitig gehedgte Positionen zu vermeiden.

Parameter

Parameter Beschreibung
CandleType Abonnierter Zeitrahmen für Kerzen und Indikatorberechnungen.
Length Fisher CG-Oszillatorperiode (auch für das Normalisierungsfenster verwendet).
SignalBar Anzahl der geschlossenen Kerzen zurück, die zum Lesen von Signalen verwendet werden; 1 entspricht dem EA-Standard.
AllowLongEntry / AllowShortEntry Long-/Short-Einstiege umschalten.
AllowLongExit / AllowShortExit Automatische Ausstiege für Long-/Short-Positionen umschalten.
StopLossPoints / TakeProfitPoints Schutz-Stop- und Zielabstände in Preisschritten. Auf 0 setzen zum Deaktivieren.
FixedVolume Volumen im festen Größenmodus.
DepositShare Portfolioanteil pro Trade im PortfolioShare-Modus.
SizingMode Wählt zwischen festem Volumen und anteilsbasierter Positionsgrößenbestimmung.

Verwendungshinweise

  • Stimmen Sie CandleType und SignalBar auf den vom ursprünglichen Indikator verwendeten Zeitrahmen ab (standardmäßig H8 und Bar-Verschiebung von 1).
  • Erlauben Sie eine kurze Aufwärmphase, damit der Oszillator genug Historie aufbauen kann; die Strategie ignoriert Trades, bis der Indikator vollständig initialisiert ist.
  • Stops und Ziele operieren auf dem Kerzenschluss. Passen Sie Punktwerte an die Tick-Größe Ihres Instruments an.
  • Wenn PortfolioShare-Sizing ausgewählt ist, stellen Sie sicher, dass die Portfoliobewertung verfügbar ist; andernfalls fällt die Strategie auf das feste Volumen zurück.

Unterschiede vs. originalem EA

  • Orders werden als Marktorders ohne den Slippage-Parameter Deviation_ gesendet; StockSharp verarbeitet die Ausführung mit seinen eigenen Slippage-Einstellungen.
  • Geldmanagement ist auf zwei Größenmodi vereinfacht (FixedVolume und PortfolioShare). Die Verlustprozentsatz-Optionen des EA werden absichtlich weggelassen.
  • Ausstehende Order-Zeitstempel (UpSignalTime/DnSignalTime) werden nicht verwendet. Signale werden sofort auf der verarbeiteten Kerze ausgeführt.
namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

using StockSharp.Algo.Candles;

/// <summary>
/// Fisher Center of Gravity oscillator crossover strategy.
/// </summary>
public class ExpFisherCgOscillatorStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly List<decimal> _medianPrices = new();
	private readonly List<decimal> _cgValues = new();
	private readonly decimal[] _valueBuffer = new decimal[4];
	private int _valueCount;
	private decimal? _previousFisher;

	private readonly List<(decimal Main, decimal Trigger)> _oscillatorHistory = new();
	private decimal? _entryPrice;
	private int _length = 10;

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

	public ExpFisherCgOscillatorStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame()).SetDisplay("Candle Type", "Timeframe", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_medianPrices.Clear();
		_cgValues.Clear();
		Array.Clear(_valueBuffer);
		_valueCount = 0;
		_previousFisher = null;
		_oscillatorHistory.Clear();
		_entryPrice = null;
	}

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

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

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

		// Calculate Fisher CG oscillator inline
		var price = (candle.HighPrice + candle.LowPrice) / 2m;
		_medianPrices.Add(price);
		while (_medianPrices.Count > _length)
			_medianPrices.RemoveAt(0);

		if (_medianPrices.Count < _length)
			return;

		decimal num = 0m;
		decimal denom = 0m;
		var weight = 1;

		for (var index = _medianPrices.Count - 1; index >= 0; index--)
		{
			var median = _medianPrices[index];
			num += weight * median;
			denom += median;
			weight++;
		}

		decimal cg;
		if (denom != 0m)
			cg = -num / denom + (_length + 1m) / 2m;
		else
			cg = 0m;

		_cgValues.Add(cg);
		while (_cgValues.Count > _length)
			_cgValues.RemoveAt(0);

		var high = cg;
		var low = cg;
		for (var i = 0; i < _cgValues.Count; i++)
		{
			var v = _cgValues[i];
			if (v > high) high = v;
			if (v < low) low = v;
		}

		decimal normalized;
		if (high != low)
			normalized = (cg - low) / (high - low);
		else
			normalized = 0m;

		var limit = Math.Min(_valueCount, 3);
		for (var shift = limit; shift > 0; shift--)
			_valueBuffer[shift] = _valueBuffer[shift - 1];

		_valueBuffer[0] = normalized;
		if (_valueCount < 4)
			_valueCount++;

		if (_valueCount < 4)
			return;

		var value2 = (4m * _valueBuffer[0] + 3m * _valueBuffer[1] + 2m * _valueBuffer[2] + _valueBuffer[3]) / 10m;
		var x = 1.98m * (value2 - 0.5m);
		if (x > 0.999m)
			x = 0.999m;
		else if (x < -0.999m)
			x = -0.999m;

		var numerator = 1m + x;
		var denominator = 1m - x;
		if (denominator == 0m)
			denominator = 0.0000001m;

		var ratio = numerator / denominator;
		if (ratio <= 0m)
			ratio = 0.0000001m;

		var fisher = 0.5m * (decimal)Math.Log((double)ratio);
		var trigger = _previousFisher ?? fisher;
		_previousFisher = fisher;

		// Store history
		_oscillatorHistory.Add((fisher, trigger));
		while (_oscillatorHistory.Count > 10)
			_oscillatorHistory.RemoveAt(0);

		if (_oscillatorHistory.Count < 3)
			return;

		// Handle risk management
		HandleRiskManagement(candle.ClosePrice);

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var current = _oscillatorHistory[^1];
		var previous = _oscillatorHistory[^2];

		var previousAbove = previous.Main > previous.Trigger;
		var previousBelow = previous.Main < previous.Trigger;

		var buyOpen = previousAbove && current.Main <= current.Trigger;
		var sellOpen = previousBelow && current.Main >= current.Trigger;

		var buyClose = previousBelow;
		var sellClose = previousAbove;

		if (sellClose && Position < 0)
		{
			BuyMarket();
			_entryPrice = null;
		}

		if (buyClose && Position > 0)
		{
			SellMarket();
			_entryPrice = null;
		}

		if (buyOpen && Position <= 0)
		{
			if (Position < 0)
			{
				BuyMarket();
				_entryPrice = null;
				return;
			}

			BuyMarket();
			_entryPrice = candle.ClosePrice;
		}
		else if (sellOpen && Position >= 0)
		{
			if (Position > 0)
			{
				SellMarket();
				_entryPrice = null;
				return;
			}

			SellMarket();
			_entryPrice = candle.ClosePrice;
		}
	}

	private void HandleRiskManagement(decimal closePrice)
	{
		if (_entryPrice is null || Position == 0)
			return;

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

		var stopDistance = 1000 * step;
		var takeDistance = 2000 * step;

		if (Position > 0)
		{
			if (closePrice <= _entryPrice.Value - stopDistance)
			{
				SellMarket();
				_entryPrice = null;
				return;
			}
			if (closePrice >= _entryPrice.Value + takeDistance)
			{
				SellMarket();
				_entryPrice = null;
			}
		}
		else if (Position < 0)
		{
			if (closePrice >= _entryPrice.Value + stopDistance)
			{
				BuyMarket();
				_entryPrice = null;
				return;
			}
			if (closePrice <= _entryPrice.Value - takeDistance)
			{
				BuyMarket();
				_entryPrice = null;
			}
		}
	}
}