Exp XBullsBearsEyes Vol Direct 策略
概述
该策略是 MetaTrader 专家顾问 Exp_XBullsBearsEyes_Vol_Direct 的 C# 版本。它重建了由 Bulls Power 与 Bears Power 组成的自定义振荡器,将结果与可配置的成交量源相乘,并应用与原版相同的四阶 Gamma 滤波。交易决策完全 取决于指标的方向缓冲区:策略捕捉平滑直方图的斜率变化,而不是水平突破,从而在动量转折点开仓或 平仓。
与许多移植版本不同,StockSharp 版本保留了对成交量的加权处理以及原始 MQL 代码中使用的四级 Gamma
滤波链。直方图与原始成交量分别采用相同的移动平均类型进行二次平滑,仅当两条序列都完全形成后才会
产生交易信号。策略只处理已完成的蜡烛,可在支持成交量或仅提供 tick 数的市场之间切换,适用性更强。
指标逻辑
- 使用长度为
Period的收盘价指数移动平均线计算 Bulls Power 和 Bears Power。 - 将两个力量值输入四级 Gamma 滤波器 (
L0–L3),得到归一化直方图(范围 -50 至 +50)。 - 将直方图乘以所选的成交量源(tick 数或真实成交量)。
- 使用同一种移动平均方法 (
Method,SmoothingLength,SmoothingPhase) 分别平滑直方图与成交量序列。 - 构造方向缓冲区:当平滑直方图上升时方向为
0,下降时为1,等价于 MetaTrader 版本中的ColorDirectBuffer。
阈值缓冲区(HighLevel/LowLevel)也会在内部计算以保持兼容,但不会影响交易过滤,与原始专家顾问的行为 一致。
交易规则
- 平掉空头:若前一根柱子的方向为多头(
olderColor = 0)。 - 开多头:在允许做多的情况下,若前一柱为多头而当前柱转为空头(
currentColor = 1)且当前无多单。 - 平掉多头:若前一根柱子的方向为空头(
olderColor = 1)。 - 开空头:在允许做空的情况下,若前一柱为空头而当前柱转为多头(
currentColor = 0)且当前无多单。 - 当需要反向时,先平掉对冲头寸,再按
OrderVolume提交新的市价单。
信号读取支持柱子位移 (SignalBar),默认值为 1,与原始 MQL 程序一样会等待蜡烛完全收盘后再执行操作。
参数
| 名称 | 说明 |
|---|---|
CandleType |
策略订阅的蜡烛类型/周期(默认两小时)。 |
Period |
计算 Bulls/Bears Power 的窗口长度。 |
Gamma |
Gamma 滤波器的平滑系数(0…1)。 |
VolumeMode |
成交量来源:tick 数或真实成交量。 |
Method |
直方图与成交量使用的移动平均类型(SMA、EMA、SMMA、LWMA、Jurik;不支持的旧方法会退回到 SMA)。 |
SmoothingLength |
两个平滑阶段的长度。 |
SmoothingPhase |
Jurik 平滑的相位参数(用于兼容)。 |
SignalBar |
读取方向缓冲区时回溯的柱子数。 |
AllowBuyOpen / AllowSellOpen |
是否允许开多/开空。 |
AllowBuyClose / AllowSellClose |
是否允许在反向信号出现时强制平仓。 |
OrderVolume |
新开仓市价单的手数/数量。 |
StopLossPoints |
以最小价格步长表示的止损距离(0 表示禁用)。 |
TakeProfitPoints |
以最小价格步长表示的止盈距离(0 表示禁用)。 |
使用建议
- 策略仅针对
GetWorkingSecurities()返回的单一标的运行,建议用于成交量分布较稳定的市场。 - 对于外汇等仅提供 tick 数的市场,请选择
VolumeMode = Tick;若交易所能提供真实成交量,则可切换到VolumeMode = Real。 - 止损和止盈距离以价格步长计量,策略会自动乘以
PriceStep转换为绝对价格。 - 当平滑直方图保持不变时,方向缓冲区会沿用上一颜色,完全复刻 MetaTrader 的处理方式。
- 默认仅绘制价格蜡烛,如需可视化直方图,可自行扩展图表绘制代码。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy converted from the MetaTrader expert Exp_XBullsBearsEyes_Vol_Direct.
/// It recreates the smoothed Bulls/Bears Power oscillator multiplied by volume
/// and reacts to direction flips detected by the original indicator.
/// </summary>
public class ExpXBullsBearsEyesVolDirectStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _period;
private readonly StrategyParam<decimal> _gamma;
private readonly StrategyParam<VolumeSources> _volumeSource;
private readonly StrategyParam<SmoothingMethods> _smoothingMethod;
private readonly StrategyParam<int> _smoothingLength;
private readonly StrategyParam<int> _smoothingPhase;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<bool> _allowBuyOpen;
private readonly StrategyParam<bool> _allowSellOpen;
private readonly StrategyParam<bool> _allowBuyClose;
private readonly StrategyParam<bool> _allowSellClose;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private ExponentialMovingAverage _ema;
private IIndicator _histogramSmoother;
private IIndicator _volumeSmoother;
private readonly List<int> _directionHistory = new();
private decimal _l0;
private decimal _l1;
private decimal _l2;
private decimal _l3;
private decimal? _previousSmoothedValue;
private int _previousDirection;
/// <summary>
/// Candle type used for calculations and trading signals.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Lookback period of Bulls/Bears Power.
/// </summary>
public int Period
{
get => _period.Value;
set => _period.Value = value;
}
/// <summary>
/// Smoothing factor applied to the adaptive filter (0..1).
/// </summary>
public decimal Gamma
{
get => _gamma.Value;
set => _gamma.Value = value;
}
/// <summary>
/// Volume source used to weight the oscillator.
/// </summary>
public VolumeSources VolumeMode
{
get => _volumeSource.Value;
set => _volumeSource.Value = value;
}
/// <summary>
/// Moving average type for smoothing histogram and volume.
/// </summary>
public SmoothingMethods Method
{
get => _smoothingMethod.Value;
set => _smoothingMethod.Value = value;
}
/// <summary>
/// Length of the smoothing windows.
/// </summary>
public int SmoothingLength
{
get => _smoothingLength.Value;
set => _smoothingLength.Value = value;
}
/// <summary>
/// Jurik phase parameter kept for compatibility.
/// </summary>
public int SmoothingPhase
{
get => _smoothingPhase.Value;
set => _smoothingPhase.Value = value;
}
/// <summary>
/// Bar shift used when reading direction buffers.
/// </summary>
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = value;
}
/// <summary>
/// Allow opening long positions.
/// </summary>
public bool AllowBuyOpen
{
get => _allowBuyOpen.Value;
set => _allowBuyOpen.Value = value;
}
/// <summary>
/// Allow opening short positions.
/// </summary>
public bool AllowSellOpen
{
get => _allowSellOpen.Value;
set => _allowSellOpen.Value = value;
}
/// <summary>
/// Allow closing long positions on bearish signals.
/// </summary>
public bool AllowBuyClose
{
get => _allowBuyClose.Value;
set => _allowBuyClose.Value = value;
}
/// <summary>
/// Allow closing short positions on bullish signals.
/// </summary>
public bool AllowSellClose
{
get => _allowSellClose.Value;
set => _allowSellClose.Value = value;
}
/// <summary>
/// Default order size.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Stop-loss distance measured in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance measured in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="ExpXBullsBearsEyesVolDirectStrategy"/>.
/// </summary>
public ExpXBullsBearsEyesVolDirectStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used by the indicator", "General");
_period = Param(nameof(Period), 13)
.SetGreaterThanZero()
.SetDisplay("Bulls/Bears Period", "Lookback window of Bulls/Bears Power", "Indicator");
_gamma = Param(nameof(Gamma), 0.6m)
.SetDisplay("Gamma", "Adaptive filter smoothing factor", "Indicator");
_volumeSource = Param(nameof(VolumeMode), VolumeSources.Tick)
.SetDisplay("Volume Source", "Volume applied to the histogram", "Indicator");
_smoothingMethod = Param(nameof(Method), SmoothingMethods.Sma)
.SetDisplay("Smoothing Method", "Moving average type for histogram and volume", "Indicator");
_smoothingLength = Param(nameof(SmoothingLength), 12)
.SetGreaterThanZero()
.SetDisplay("Smoothing Length", "Length of the smoothing moving averages", "Indicator");
_smoothingPhase = Param(nameof(SmoothingPhase), 15)
.SetDisplay("Smoothing Phase", "Phase parameter used by Jurik averaging", "Indicator");
_signalBar = Param(nameof(SignalBar), 1)
.SetNotNegative()
.SetDisplay("Signal Bar", "Shift applied when evaluating direction", "Trading");
_allowBuyOpen = Param(nameof(AllowBuyOpen), true)
.SetDisplay("Allow Buy Open", "Enable opening long positions", "Trading");
_allowSellOpen = Param(nameof(AllowSellOpen), true)
.SetDisplay("Allow Sell Open", "Enable opening short positions", "Trading");
_allowBuyClose = Param(nameof(AllowBuyClose), true)
.SetDisplay("Allow Buy Close", "Enable closing longs on bearish flips", "Trading");
_allowSellClose = Param(nameof(AllowSellClose), true)
.SetDisplay("Allow Sell Close", "Enable closing shorts on bullish flips", "Trading");
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Default market order size", "Trading");
_stopLossPoints = Param(nameof(StopLossPoints), 1000)
.SetNotNegative()
.SetDisplay("Stop Loss", "Protective stop in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
.SetNotNegative()
.SetDisplay("Take Profit", "Protective target in price steps", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ema = new EMA
{
Length = Math.Max(1, Period)
};
_histogramSmoother = CreateMovingAverage(Method, SmoothingLength, SmoothingPhase);
_volumeSmoother = CreateMovingAverage(Method, SmoothingLength, SmoothingPhase);
_directionHistory.Clear();
_previousSmoothedValue = null;
_previousDirection = 0;
_l0 = _l1 = _l2 = _l3 = 0m;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
Volume = Math.Max(Security?.MinVolume ?? 1m, OrderVolume);
var priceStep = Security?.PriceStep ?? 0m;
Unit stopLoss = null;
Unit takeProfit = null;
if (StopLossPoints > 0 && priceStep > 0m)
{
stopLoss = new Unit(StopLossPoints * priceStep, UnitTypes.Absolute);
}
if (TakeProfitPoints > 0 && priceStep > 0m)
{
takeProfit = new Unit(TakeProfitPoints * priceStep, UnitTypes.Absolute);
}
if (stopLoss != null || takeProfit != null)
{
StartProtection(takeProfit: takeProfit, stopLoss: stopLoss);
}
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
}
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ema?.Reset();
_histogramSmoother?.Reset();
_volumeSmoother?.Reset();
_directionHistory.Clear();
_previousSmoothedValue = null;
_previousDirection = 0;
_l0 = _l1 = _l2 = _l3 = 0m;
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_ema is null || _histogramSmoother is null || _volumeSmoother is null)
return;
// Feed the EMA with the candle close to emulate iBullsPower/iBearsPower internals.
var emaValue = _ema.Process(new DecimalIndicatorValue(_ema, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
if (!emaValue.IsFinal)
return;
var ema = emaValue.ToDecimal();
// Rebuild Bulls and Bears Power values.
var bulls = candle.HighPrice - ema;
var bears = candle.LowPrice - ema;
// Run the four-stage adaptive smoothing described in the MQL version.
var l0Prev = _l0;
var l1Prev = _l1;
var l2Prev = _l2;
var l3Prev = _l3;
var sum = bulls + bears;
var gamma = Gamma;
_l0 = ((1m - gamma) * sum) + (gamma * l0Prev);
_l1 = (-gamma * _l0) + l0Prev + (gamma * l1Prev);
_l2 = (-gamma * _l1) + l1Prev + (gamma * l2Prev);
_l3 = (-gamma * _l2) + l2Prev + (gamma * l3Prev);
var cu = 0m;
var cd = 0m;
if (_l0 >= _l1)
{
cu = _l0 - _l1;
}
else
{
cd = _l1 - _l0;
}
if (_l1 >= _l2)
{
cu += _l1 - _l2;
}
else
{
cd += _l2 - _l1;
}
if (_l2 >= _l3)
{
cu += _l2 - _l3;
}
else
{
cd += _l3 - _l2;
}
var denom = cu + cd;
var result = denom != 0m ? cu / denom : 0m;
var histogram = (result * 100m) - 50m;
// Apply the requested volume type to the histogram.
var volume = GetVolume(candle);
var scaledHistogram = histogram * volume;
var histogramValue = _histogramSmoother.Process(new DecimalIndicatorValue(_histogramSmoother, scaledHistogram, candle.OpenTime) { IsFinal = true });
var volumeValue = _volumeSmoother.Process(new DecimalIndicatorValue(_volumeSmoother, volume, candle.OpenTime) { IsFinal = true });
if (histogramValue is not DecimalIndicatorValue { IsFinal: true } histogramResult)
return;
if (volumeValue is not DecimalIndicatorValue { IsFinal: true })
return;
var smoothedHistogram = histogramResult.Value;
var direction = CalculateDirection(smoothedHistogram);
UpdateHistory(direction);
// trading guard removed
if (!TryGetColors(out var olderColor, out var currentColor))
return;
HandleSignals(olderColor, currentColor);
}
private int CalculateDirection(decimal currentValue)
{
if (_previousSmoothedValue is not decimal previous)
{
_previousSmoothedValue = currentValue;
_previousDirection = 0;
return _previousDirection;
}
int direction;
if (currentValue > previous)
{
direction = 0;
}
else if (currentValue < previous)
{
direction = 1;
}
else
{
direction = _previousDirection;
}
_previousSmoothedValue = currentValue;
_previousDirection = direction;
return direction;
}
private void UpdateHistory(int direction)
{
_directionHistory.Add(direction);
var maxHistory = Math.Max(4, SignalBar + 3);
if (_directionHistory.Count > maxHistory)
{
_directionHistory.RemoveAt(0);
}
}
private bool TryGetColors(out int olderColor, out int currentColor)
{
olderColor = 0;
currentColor = 0;
var shift = Math.Max(0, SignalBar);
var currentIndex = _directionHistory.Count - 1 - shift;
var olderIndex = currentIndex - 1;
if (currentIndex < 0 || olderIndex < 0)
{
return false;
}
currentColor = _directionHistory[currentIndex];
olderColor = _directionHistory[olderIndex];
return true;
}
private void HandleSignals(int olderColor, int currentColor)
{
// Color 0 is produced when the smoothed histogram rises, color 1 when it declines.
if (olderColor == 0)
{
if (AllowSellClose && Position < 0)
{
BuyMarket(-Position);
}
if (AllowBuyOpen && currentColor == 1 && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
}
}
else if (olderColor == 1)
{
if (AllowBuyClose && Position > 0)
{
SellMarket(Position);
}
if (AllowSellOpen && currentColor == 0 && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
}
}
}
private decimal GetVolume(ICandleMessage candle)
{
return VolumeMode switch
{
VolumeSources.Tick => candle.TotalTicks.HasValue ? (decimal)candle.TotalTicks.Value : candle.TotalVolume,
VolumeSources.Real => candle.TotalVolume,
_ => candle.TotalVolume,
};
}
private static IIndicator CreateMovingAverage(SmoothingMethods method, int length, int phase)
{
var effectiveLength = Math.Max(1, length);
return method switch
{
SmoothingMethods.Sma => new SMA { Length = effectiveLength },
SmoothingMethods.Ema => new EMA { Length = effectiveLength },
SmoothingMethods.Smma => new SmoothedMovingAverage { Length = effectiveLength },
SmoothingMethods.Lwma => new WeightedMovingAverage { Length = effectiveLength },
SmoothingMethods.Jurik => CreateJurik(effectiveLength, phase),
_ => new SMA { Length = effectiveLength },
};
}
private static IIndicator CreateJurik(int length, int phase)
{
var jurik = new JurikMovingAverage
{
Length = Math.Max(1, length)
};
var property = jurik.GetType().GetProperty("Phase");
if (property != null)
{
var value = Math.Max(-100, Math.Min(100, phase));
property.SetValue(jurik, value);
}
return jurik;
}
/// <summary>
/// Supported volume sources.
/// </summary>
public enum VolumeSources
{
/// <summary>
/// Use tick count when available.
/// </summary>
Tick,
/// <summary>
/// Use traded volume (lots/contracts).
/// </summary>
Real
}
/// <summary>
/// Supported smoothing methods.
/// </summary>
public enum SmoothingMethods
{
/// <summary>
/// Simple moving average.
/// </summary>
Sma,
/// <summary>
/// Exponential moving average.
/// </summary>
Ema,
/// <summary>
/// Smoothed moving average (RMA).
/// </summary>
Smma,
/// <summary>
/// Linear weighted moving average.
/// </summary>
Lwma,
/// <summary>
/// Jurik moving average.
/// </summary>
Jurik,
/// <summary>
/// Parabolic, T3, VIDYA and AMA are not available in StockSharp by default.
/// The strategy falls back to SMA for these legacy options.
/// </summary>
Parabolic,
/// <summary>
/// Tillson T3 smoother.
/// </summary>
T3,
/// <summary>
/// Variable index dynamic average.
/// </summary>
Vidya,
/// <summary>
/// Adaptive moving average.
/// </summary>
Ama
}
}
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, UnitTypes, Unit
from StockSharp.Algo.Indicators import (
ExponentialMovingAverage, SimpleMovingAverage
)
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class exp_x_bulls_bears_eyes_vol_direct_strategy(Strategy):
def __init__(self):
super(exp_x_bulls_bears_eyes_vol_direct_strategy, self).__init__()
self._period = self.Param("Period", 13) \
.SetDisplay("Bulls/Bears Period", "Lookback window of Bulls/Bears Power", "Indicator")
self._gamma_param = self.Param("Gamma", 0.6) \
.SetDisplay("Gamma", "Adaptive filter smoothing factor", "Indicator")
self._smooth_length = self.Param("SmoothingLength", 12) \
.SetDisplay("Smoothing Length", "Length of the smoothing moving averages", "Indicator")
self._signal_bar = self.Param("SignalBar", 1) \
.SetDisplay("Signal Bar", "Shift applied when evaluating direction", "Trading")
self._allow_buy_open = self.Param("AllowBuyOpen", True) \
.SetDisplay("Allow Buy Open", "Enable opening long positions", "Trading")
self._allow_sell_open = self.Param("AllowSellOpen", True) \
.SetDisplay("Allow Sell Open", "Enable opening short positions", "Trading")
self._allow_buy_close = self.Param("AllowBuyClose", True) \
.SetDisplay("Allow Buy Close", "Enable closing longs on bearish flips", "Trading")
self._allow_sell_close = self.Param("AllowSellClose", True) \
.SetDisplay("Allow Sell Close", "Enable closing shorts on bullish flips", "Trading")
self._order_volume = self.Param("OrderVolume", 1.0) \
.SetDisplay("Order Volume", "Default market order size", "Trading")
self._stop_loss_points = self.Param("StopLossPoints", 1000) \
.SetDisplay("Stop Loss", "Protective stop in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 2000) \
.SetDisplay("Take Profit", "Protective target in price steps", "Risk")
self._ema = None
self._histogram_smoother = None
self._volume_smoother = None
self._direction_history = []
self._l0 = 0.0
self._l1 = 0.0
self._l2 = 0.0
self._l3 = 0.0
self._previous_smoothed_value = None
self._previous_direction = 0
@property
def period(self):
return self._period.Value
@property
def gamma_val(self):
return self._gamma_param.Value
@property
def smooth_length(self):
return self._smooth_length.Value
@property
def signal_bar(self):
return self._signal_bar.Value
@property
def allow_buy_open(self):
return self._allow_buy_open.Value
@property
def allow_sell_open(self):
return self._allow_sell_open.Value
@property
def allow_buy_close(self):
return self._allow_buy_close.Value
@property
def allow_sell_close(self):
return self._allow_sell_close.Value
@property
def order_volume(self):
return self._order_volume.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
def OnReseted(self):
super(exp_x_bulls_bears_eyes_vol_direct_strategy, self).OnReseted()
self._ema = None
self._histogram_smoother = None
self._volume_smoother = None
self._direction_history = []
self._l0 = 0.0
self._l1 = 0.0
self._l2 = 0.0
self._l3 = 0.0
self._previous_smoothed_value = None
self._previous_direction = 0
def OnStarted2(self, time):
super(exp_x_bulls_bears_eyes_vol_direct_strategy, self).OnStarted2(time)
self._ema = ExponentialMovingAverage()
self._ema.Length = max(1, self.period)
length = max(1, self.smooth_length)
self._histogram_smoother = SimpleMovingAverage()
self._histogram_smoother.Length = length
self._volume_smoother = SimpleMovingAverage()
self._volume_smoother.Length = length
self._direction_history = []
self._previous_smoothed_value = None
self._previous_direction = 0
self._l0 = 0.0
self._l1 = 0.0
self._l2 = 0.0
self._l3 = 0.0
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromHours(2)))
subscription.Bind(self._process_candle)
subscription.Start()
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
sl = None
tp = None
if self.stop_loss_points > 0 and step > 0.0:
sl = Unit(float(self.stop_loss_points) * step, UnitTypes.Absolute)
if self.take_profit_points > 0 and step > 0.0:
tp = Unit(float(self.take_profit_points) * step, UnitTypes.Absolute)
if sl is not None or tp is not None:
self.StartProtection(stopLoss=sl, takeProfit=tp)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._ema is None or self._histogram_smoother is None or self._volume_smoother is None:
return
ema_result = process_float(self._ema, candle.ClosePrice, candle.OpenTime, True)
if not self._ema.IsFormed:
return
ema = float(ema_result)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
bulls = high - ema
bears = low - ema
combined = bulls + bears
gamma = float(self.gamma_val)
l0_prev = self._l0
l1_prev = self._l1
l2_prev = self._l2
l3_prev = self._l3
self._l0 = (1.0 - gamma) * combined + gamma * l0_prev
self._l1 = -gamma * self._l0 + l0_prev + gamma * l1_prev
self._l2 = -gamma * self._l1 + l1_prev + gamma * l2_prev
self._l3 = -gamma * self._l2 + l2_prev + gamma * l3_prev
cu = 0.0
cd = 0.0
if self._l0 >= self._l1:
cu = self._l0 - self._l1
else:
cd = self._l1 - self._l0
if self._l1 >= self._l2:
cu += self._l1 - self._l2
else:
cd += self._l2 - self._l1
if self._l2 >= self._l3:
cu += self._l2 - self._l3
else:
cd += self._l3 - self._l2
denom = cu + cd
result = cu / denom if denom != 0.0 else 0.0
histogram = result * 100.0 - 50.0
volume = float(candle.TotalVolume) if candle.TotalVolume > 0 else 1.0
scaled_histogram = histogram * volume
from System import Decimal
hist_result = process_float(self._histogram_smoother, Decimal(scaled_histogram), candle.OpenTime, True)
vol_result = process_float(self._volume_smoother, Decimal(volume), candle.OpenTime, True)
if not self._histogram_smoother.IsFormed or not self._volume_smoother.IsFormed:
return
smoothed_histogram = float(hist_result)
direction = self._calculate_direction(smoothed_histogram)
self._update_history(direction)
older_color, current_color = self._try_get_colors()
if older_color is None or current_color is None:
return
self._handle_signals(older_color, current_color)
def _calculate_direction(self, current_value):
if self._previous_smoothed_value is None:
self._previous_smoothed_value = current_value
self._previous_direction = 0
return self._previous_direction
if current_value > self._previous_smoothed_value:
direction = 0
elif current_value < self._previous_smoothed_value:
direction = 1
else:
direction = self._previous_direction
self._previous_smoothed_value = current_value
self._previous_direction = direction
return direction
def _update_history(self, direction):
self._direction_history.append(direction)
max_history = max(4, self.signal_bar + 3)
if len(self._direction_history) > max_history:
self._direction_history.pop(0)
def _try_get_colors(self):
shift = max(0, self.signal_bar)
current_index = len(self._direction_history) - 1 - shift
older_index = current_index - 1
if current_index < 0 or older_index < 0:
return None, None
return self._direction_history[older_index], self._direction_history[current_index]
def _handle_signals(self, older_color, current_color):
if older_color == 0:
if self.allow_sell_close and self.Position < 0:
self.BuyMarket()
if self.allow_buy_open and current_color == 1 and self.Position <= 0:
self.BuyMarket()
elif older_color == 1:
if self.allow_buy_close and self.Position > 0:
self.SellMarket()
if self.allow_sell_open and current_color == 0 and self.Position >= 0:
self.SellMarket()
def CreateClone(self):
return exp_x_bulls_bears_eyes_vol_direct_strategy()