View on GitHub

Exp Fisher CG Oscillator Strategy

This strategy ports the Exp_FisherCGOscillator MetaTrader 5 expert advisor to StockSharp's high-level API. It recreates the Fisher Center of Gravity oscillator and its trigger line, evaluates signals on a configurable historical bar, and reproduces the original stop/take workflow with StockSharp orders and risk helpers.

How It Works

  1. Indicator pipeline – each finished candle is passed through the Fisher CG oscillator: median prices feed a center-of-gravity loop, values are normalised over the last Length bars, and a Fisher transform produces the oscillator line. The trigger line is simply the oscillator delayed by one bar.
  2. Signal extraction – the strategy inspects two historical readings defined by SignalBar. It opens a long when the older oscillator value (SignalBar + 1) is above its trigger while the newer value (SignalBar) crosses back above the trigger, signalling a bullish turn. Shorts mirror this logic on the bearish side.
  3. Exit handling – long exits occur as soon as the older oscillator drops below its trigger, while short exits fire when it rises above the trigger, matching the EA's immediate close flags. Opposite entries close the active position before reversing.
  4. Bar-by-bar processing – everything runs on completed candles from CandleType; no intra-bar trades are generated, ensuring deterministic backtests and matching the EA's "new bar" gate.

Risk Management & Position Sizing

  • Stops/targetsStopLossPoints and TakeProfitPoints are expressed in instrument steps and translated into absolute price distances via Security.PriceStep.
  • Volume controlSizingMode = FixedVolume sends the constant FixedVolume. SizingMode = PortfolioShare converts DepositShare of the current portfolio value into contracts using the latest close and VolumeStep.
  • Single position – the strategy always flattens before entering the opposite side, avoiding simultaneous hedged positions.

Parameters

Parameter Description
CandleType Timeframe subscribed for candles and indicator calculations.
Length Fisher CG oscillator period (also used for the normalisation window).
SignalBar Number of closed candles back used to read signals; 1 matches the EA default.
AllowLongEntry / AllowShortEntry Toggle long/short entries.
AllowLongExit / AllowShortExit Toggle automatic exits for long/short positions.
StopLossPoints / TakeProfitPoints Protective stop and target distances in price steps. Set to 0 to disable.
FixedVolume Volume used in fixed sizing mode.
DepositShare Portfolio fraction allocated per trade in PortfolioShare mode.
SizingMode Chooses between fixed volume and share-based position sizing.

Usage Notes

  • Align CandleType and SignalBar with the timeframe used by the original indicator (H8 and bar shift of 1 by default).
  • Allow a short warm-up period so the oscillator has enough history to form; the strategy ignores trades until the indicator is fully initialised.
  • Stops and targets operate on the candle close. Adjust point values to match your instrument's tick size.
  • When PortfolioShare sizing is selected, ensure portfolio valuation is available; otherwise the strategy falls back to the fixed volume.

Differences vs Original EA

  • Orders are sent as market orders without the Deviation_ slippage parameter; StockSharp handles execution with its own slippage settings.
  • Money management is simplified to two sizing modes (FixedVolume and PortfolioShare). The EA's loss-percentage options are intentionally omitted.
  • Pending order timestamps (UpSignalTime/DnSignalTime) are not used. Signals are executed immediately on the processed candle.
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;
			}
		}
	}
}