Стратегия переносит советник Exp_FisherCGOscillator из MetaTrader 5 в высокоуровневый API StockSharp. Она восстанавливает вычисления осциллятора Fisher Center of Gravity, оценивает сигналы на настраиваемой исторической свече и повторяет механику стопов/тейков исходного робота с помощью ордеров StockSharp.
Логика работы
Цепочка индикатора – каждая завершённая свеча передаётся в осциллятор Fisher CG: медианные цены участвуют в расчёте центра тяжести, значения нормализуются по окну Length, затем применяется преобразование Фишера. Линия Trigger – это сдвинутое на один бар значение самого осциллятора.
Извлечение сигналов – стратегия анализирует два исторических значения, задаваемых SignalBar. Лонг открывается, когда более старое значение (SignalBar + 1) расположено выше своей линии триггера, а более новое (SignalBar) пересекает триггер снизу вверх. Шорты формируются по зеркальному условию.
Выходы – выход из лонга происходит сразу, как только старое значение опускается ниже триггера; выход из шорта – когда оно поднимается выше. Это повторяет флаги BUY_Close и SELL_Close оригинального кода. При противоположном сигнале текущая позиция закрывается прежде, чем выполняется разворот.
Обработка по закрытым свечам – расчёты выполняются только на завершённых свечах CandleType, что даёт детерминированные результаты и соответствует проверке "нового бара" в советнике.
Риск-менеджмент и размер позиции
Стоп/тейк – параметры StopLossPoints и TakeProfitPoints задаются в шагах цены и пересчитываются в абсолютные расстояния через Security.PriceStep.
Управление объёмом – при SizingMode = FixedVolume используется постоянный объём FixedVolume. Режим PortfolioShare конвертирует долю капитала DepositShare в количество контрактов, используя последнюю цену закрытия и VolumeStep инструмента.
Одна позиция – стратегия всегда закрывает текущую сделку перед открытием противоположной, избегая хеджирующих позиций.
Параметры
Параметр
Описание
CandleType
Таймфрейм, по которому запрашиваются свечи и рассчитывается индикатор.
Length
Период осциллятора Fisher CG и окно нормализации.
SignalBar
Количество закрытых свечей для считывания сигналов (1 соответствует настройке EA).
AllowLongEntry / AllowShortEntry
Разрешение на открытие лонгов/шортов.
AllowLongExit / AllowShortExit
Разрешение на автоматическое закрытие позиций при обратном сигнале.
StopLossPoints / TakeProfitPoints
Расстояние стопа и тейка в шагах цены. Значение 0 отключает уровень.
FixedVolume
Объём сделки в режиме фиксированного лота.
DepositShare
Доля капитала для расчёта объёма в режиме PortfolioShare.
SizingMode
Выбор между фиксированным объёмом и расчётом от капитала.
Рекомендации по использованию
Подберите CandleType и SignalBar под исходные настройки индикатора (по умолчанию H8 и смещение 1).
Дайте индикатору время на прогрев – пока нет достаточного количества баров, стратегия не торгует.
Стопы и тейки контролируются по цене закрытия свечи, поэтому корректируйте значения в шагах цены под волатильность инструмента.
При использовании PortfolioShare убедитесь, что стратегия получает актуальную оценку портфеля, иначе будет задействован фиксированный объём.
Отличия от оригинального советника
Используются рыночные ордера без параметра Deviation_; проскальзывание обрабатывается средствами StockSharp.
Система управления капиталом упрощена до двух режимов (FixedVolume и PortfolioShare). Опции распределения убытков (LOSSBALANCE, LOSSFREEMARGIN) не реализованы.
Метки времени сигналов (UpSignalTime/DnSignalTime) не применяются – вход выполняется на той же свече после завершения расчётов.
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()