Exp Fisher CG Oscillator 策略
本策略将 Exp_FisherCGOscillator MetaTrader 5 智能交易程序迁移到 StockSharp 高阶 API。它重建 Fisher Center of Gravity 振荡器及其触发线,根据可配置的历史 K 线读取信号,并使用 StockSharp 订单复现原有的止损/止盈流程。
工作原理
- 指标流水线——每根收盘 K 线都会进入 Fisher CG 振荡器:先对最高价与最低价计算中值,送入中心重力算法;随后在最近
Length根 K 线上进行归一化,并应用 Fisher 变换得到主振荡曲线。Trigger线等于主值向后平移一根 K 线。 - 信号判断——策略读取由
SignalBar指定的两根历史数据。当更早的振荡值 (SignalBar + 1) 位于触发线之上,同时较新的值 (SignalBar) 再次上穿触发线时,判定为多头拐点;空头逻辑完全对称。 - 离场管理——一旦较早的振荡值跌破触发线就立即平掉多单;若其上破触发线则平掉空单,对应原始 EA 中的
BUY_Close/SELL_Close标志。若出现反向信号,策略会先平仓再考虑反向开仓。 - 逐根收盘处理——所有计算都基于
CandleType的收盘 K 线完成,避免盘中噪音,同时契合 EA 中的“新 K 线”判断。
风险管理与仓位控制
- 止损/止盈——
StopLossPoints与TakeProfitPoints以价格步长表示,会通过Security.PriceStep换算成绝对价格距离。 - 仓位模式——
SizingMode = FixedVolume时直接使用固定手数FixedVolume;SizingMode = PortfolioShare则按照组合当前价值的DepositShare比例,结合最新收盘价与VolumeStep计算手数。 - 单一持仓——策略始终在开反向仓位前平掉原有仓位,避免同向对冲。
参数
| 参数 | 说明 |
|---|---|
CandleType |
订阅并计算指标所使用的 K 线周期。 |
Length |
Fisher CG 振荡器的周期,也是归一化窗口长度。 |
SignalBar |
读取信号时向前回看的已收盘 K 线数量,1 即原脚本默认值。 |
AllowLongEntry / AllowShortEntry |
是否允许开多/开空。 |
AllowLongExit / AllowShortExit |
是否允许在相反信号下平多/平空。 |
StopLossPoints / TakeProfitPoints |
以价格步长表示的止损、止盈距离,设为 0 可关闭。 |
FixedVolume |
固定仓位模式下的下单手数。 |
DepositShare |
在 PortfolioShare 模式下,每次交易使用的资产比例。 |
SizingMode |
在固定手数与按资金比例之间切换。 |
使用提示
- 请将
CandleType与SignalBar设置为与原 EA 相同的周期与偏移(默认 8 小时周期,偏移 1)。 - 指标需要一定历史数据才能形成,初始化阶段不会触发交易。
- 止损与止盈基于 K 线收盘价触发,请根据品种的最小报价单位调节点数参数。
- 若选择
PortfolioShare模式,请确保组合估值可用;否则策略会退回到固定手数模式。
与原 EA 的差异
- 所有交易均以市价单执行,不再使用
Deviation_滑点参数;滑点由 StockSharp 自行处理。 - 资金管理简化为
FixedVolume与PortfolioShare两种模式,原脚本的亏损份额分配选项未实现。 - 不再使用
UpSignalTime/DnSignalTime挂单时间,信号在当前收盘 K 线处理完成后立即执行。
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()