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
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.
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.
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.
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/targets – StopLossPoints and TakeProfitPoints are expressed in instrument steps and translated into absolute price distances via Security.PriceStep.
Volume control – SizingMode = 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;
}
}
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class exp_fisher_cg_oscillator_strategy(Strategy):
def __init__(self):
super(exp_fisher_cg_oscillator_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._median_prices = []
self._cg_values = []
self._value_buffer = [0.0, 0.0, 0.0, 0.0]
self._value_count = 0
self._previous_fisher = None
self._oscillator_history = []
self._entry_price = None
self._length = 10
@property
def CandleType(self):
return self._candle_type.Value
def OnReseted(self):
super(exp_fisher_cg_oscillator_strategy, self).OnReseted()
self._median_prices = []
self._cg_values = []
self._value_buffer = [0.0, 0.0, 0.0, 0.0]
self._value_count = 0
self._previous_fisher = None
self._oscillator_history = []
self._entry_price = None
def OnStarted2(self, time):
super(exp_fisher_cg_oscillator_strategy, self).OnStarted2(time)
self._median_prices = []
self._cg_values = []
self._value_buffer = [0.0, 0.0, 0.0, 0.0]
self._value_count = 0
self._previous_fisher = None
self._oscillator_history = []
self._entry_price = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._on_process).Start()
def _on_process(self, candle):
if candle.State != CandleStates.Finished:
return
price = (float(candle.HighPrice) + float(candle.LowPrice)) / 2.0
self._median_prices.append(price)
while len(self._median_prices) > self._length:
self._median_prices.pop(0)
if len(self._median_prices) < self._length:
return
num = 0.0
denom = 0.0
weight = 1
for i in range(len(self._median_prices) - 1, -1, -1):
median = self._median_prices[i]
num += weight * median
denom += median
weight += 1
if denom != 0.0:
cg = -num / denom + (self._length + 1.0) / 2.0
else:
cg = 0.0
self._cg_values.append(cg)
while len(self._cg_values) > self._length:
self._cg_values.pop(0)
high = cg
low = cg
for v in self._cg_values:
if v > high:
high = v
if v < low:
low = v
if high != low:
normalized = (cg - low) / (high - low)
else:
normalized = 0.0
limit = min(self._value_count, 3)
shift = limit
while shift > 0:
self._value_buffer[shift] = self._value_buffer[shift - 1]
shift -= 1
self._value_buffer[0] = normalized
if self._value_count < 4:
self._value_count += 1
if self._value_count < 4:
return
value2 = (4.0 * self._value_buffer[0] + 3.0 * self._value_buffer[1] + 2.0 * self._value_buffer[2] + self._value_buffer[3]) / 10.0
x = 1.98 * (value2 - 0.5)
if x > 0.999:
x = 0.999
elif x < -0.999:
x = -0.999
numerator = 1.0 + x
denominator = 1.0 - x
if denominator == 0.0:
denominator = 0.0000001
ratio = numerator / denominator
if ratio <= 0.0:
ratio = 0.0000001
fisher = 0.5 * math.log(ratio)
trigger = self._previous_fisher if self._previous_fisher is not None else fisher
self._previous_fisher = fisher
self._oscillator_history.append((fisher, trigger))
while len(self._oscillator_history) > 10:
self._oscillator_history.pop(0)
if len(self._oscillator_history) < 3:
return
self._handle_risk_management(float(candle.ClosePrice))
current = self._oscillator_history[-1]
previous = self._oscillator_history[-2]
previous_above = previous[0] > previous[1]
previous_below = previous[0] < previous[1]
buy_open = previous_above and current[0] <= current[1]
sell_open = previous_below and current[0] >= current[1]
if previous_above and self.Position < 0:
self.BuyMarket()
self._entry_price = None
if previous_below and self.Position > 0:
self.SellMarket()
self._entry_price = None
if buy_open and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self._entry_price = None
return
self.BuyMarket()
self._entry_price = float(candle.ClosePrice)
elif sell_open and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self._entry_price = None
return
self.SellMarket()
self._entry_price = float(candle.ClosePrice)
def _handle_risk_management(self, close_price):
if self._entry_price is None or self.Position == 0:
return
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
if step <= 0.0:
step = 1.0
stop_distance = 1000 * step
take_distance = 2000 * step
if self.Position > 0:
if close_price <= self._entry_price - stop_distance:
self.SellMarket()
self._entry_price = None
return
if close_price >= self._entry_price + take_distance:
self.SellMarket()
self._entry_price = None
elif self.Position < 0:
if close_price >= self._entry_price + stop_distance:
self.BuyMarket()
self._entry_price = None
return
if close_price <= self._entry_price - take_distance:
self.BuyMarket()
self._entry_price = None
def CreateClone(self):
return exp_fisher_cg_oscillator_strategy()