Estrategia Exp XBullsBearsEyes Vol
Descripción general
Esta estrategia es una conversión en C# del expert de MetaTrader Exp_XBullsBearsEyes_Vol. El asesor original combina las lecturas de Bulls Power y Bears Power, multiplica el resultado por el volumen de la vela y colorea el histograma de acuerdo al impulso resultante. Se mantienen dos slots de posición independientes tanto para el lado largo como para el corto, permitiendo al sistema escalar cuando la intensidad del color aumenta. El port de StockSharp recrea el filtro multi-etapa, la lógica de colores y la gestión de operaciones mientras usa llamadas de API de alto nivel para órdenes y control de riesgo.
El algoritmo se suscribe a un marco temporal configurable, reconstruye el indicador XBullsBearsEyes personalizado y reacciona solo a velas terminadas. Las transiciones de color determinan tanto las entradas como las salidas: los colores alcistas cierran operaciones cortas y pueden abrir uno o dos slots largos; los colores bajistas realizan la acción espejo. Las distancias de stop-loss y take-profit se traducen en parámetros de StartProtection para que los gestores de riesgo de la plataforma puedan manejar órdenes protectoras.
Lógica del indicador
- Los valores de Bulls Power y Bears Power se reconstruyen con una EMA de período
IndicatorPeriodusando el máximo/mínimo de la vela contra el cierre suavizado. - Un filtro adaptativo de cuatro etapas acumula presión alcista (
CU) y bajista (CD) con coeficienteGamma. El valor del indicador esCU / (CU + CD) * 100 - 50. - El valor filtrado se multiplica por el volumen de tick o volumen real, dependiendo de
VolumeType. - Las series multiplicadas y el volumen bruto se suavizan por una media móvil elegida a través de
SmoothingMethod,SmoothingLengthySmoothingPhase(la fase Jurik se respeta cuando la clase subyacente la expone). - Los niveles de color se derivan de
HighLevel1,HighLevel2,LowLevel1yLowLevel2. Los valores por encima de las bandas superiores producen colores0o1, mientras que los valores por debajo de las bandas inferiores producen colores3o4. El color2indica un estado neutral. - El historial de colores se almacena para que las señales puedan evaluarse en la barra
SignalBar(predeterminado: una vela cerrada atrás). El color de la barra de señal actual se compara con el color anterior para detectar transiciones.
Reglas de trading
- Los colores
1y0denotan presión alcista. Cuando el color cambia a uno de esos valores y el color anterior era más débil, el slot 1 (PrimaryVolume) o slot 2 (SecondaryVolume) abre una posición larga respectivamente. Ambos eventos cierran cualquier exposición corta existente siAllowShortExitestá habilitado. - Los colores
3y4denotan presión bajista. Cuando el color se mueve a estos valores y el color anterior era más fuerte, el slot 1 o slot 2 abre una posición corta respectivamente. Ambos eventos cierran cualquier exposición larga existente siAllowLongExitestá habilitado. - Cada slot recuerda si ya tiene una posición abierta e ignora señales repetidas hasta que la dirección correspondiente haya sido cerrada.
SignalBardefine cuántas velas completadas se omiten antes de evaluar el color (0 = última vela terminada). El código requiere al menos dos colores históricos para comparar.- El stop-loss y take-profit expresados en puntos (
StopLossPoints,TakeProfitPoints) se convierten a distancias de precio absoluto conSecurity.PriceStepy se usan para iniciar la protección de la plataforma con salidas de mercado.
Parámetros
| Nombre | Descripción |
|---|---|
PrimaryVolume |
Volumen para el primer slot (activado por color 1 / 3). |
SecondaryVolume |
Volumen para el segundo slot (activado por color 0 / 4). |
StopLossPoints / TakeProfitPoints |
Distancias protectoras en pasos de precio. Establecer en cero para deshabilitar. |
AllowLongEntry / AllowShortEntry |
Habilitar escalar hacia la dirección correspondiente. |
AllowLongExit / AllowShortExit |
Habilitar salidas automatizadas cuando aparece el color opuesto. |
CandleType |
Marco temporal suscrito para velas y cálculo del indicador (predeterminado: 8 horas). |
IndicatorPeriod |
Período EMA usado para reconstruir Bulls/Bears Power. |
Gamma |
Factor de suavizado adaptativo para el filtro de cuatro etapas (0.0 – 0.999). |
VolumeType |
Seleccionar volumen de tick o volumen real para ponderación. |
HighLevel1, HighLevel2, LowLevel1, LowLevel2 |
Multiplicadores de nivel que definen umbrales de color. |
SmoothingMethod |
Tipo de media móvil usado para suavizar el indicador y el volumen (SMA, EMA, SMMA, LWMA, Jurik, JurX, ParMA→EMA, T3, VIDYA→EMA, AMA). |
SmoothingLength |
Longitud de la media móvil de suavizado. |
SmoothingPhase |
Parámetro de fase Jurik (limitado a [-100, 100]). |
SignalBar |
Número de velas cerradas para retroceder antes de evaluar las transiciones de color. |
Notas de uso
- La estrategia opera con un único instrumento retornado por
GetWorkingSecurities()y usa órdenes de mercado para entradas y salidas. - La gestión de slots es neta: entradas adicionales se añaden a la posición neta, mientras que las salidas aplanan toda la exposición para el lado afectado.
- Si la plataforma proporciona solo volumen de tick, seleccionar
VolumeType = Realrecurrirá al conteo de tick disponible. - Los suavizados VIDYA y Parabólico recurren a medias móviles exponenciales porque StockSharp expone esas implementaciones directamente.
- Asegurarse de configurar el paso de precio del instrumento para que
StopLossPointsyTakeProfitPointsse conviertan en las distancias absolutas previstas.
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 System.Reflection;
using StockSharp.Algo;
using StockSharp.Algo.Candles;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy converted from the MetaTrader expert Exp_XBullsBearsEyes_Vol.
/// It recreates the Bulls/Bears pressure indicator that multiplies trend
/// strength by the candle volume and uses the colour transitions to drive
/// entries and exits while supporting two independent position slots per side.
/// </summary>
public class ExpXBullsBearsEyesVolStrategy : Strategy
{
private readonly StrategyParam<decimal> _primaryVolume;
private readonly StrategyParam<decimal> _secondaryVolume;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<bool> _allowLongEntry;
private readonly StrategyParam<bool> _allowShortEntry;
private readonly StrategyParam<bool> _allowLongExit;
private readonly StrategyParam<bool> _allowShortExit;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _indicatorPeriod;
private readonly StrategyParam<decimal> _gamma;
private readonly StrategyParam<AppliedVolumes> _volumeType;
private readonly StrategyParam<int> _highLevel2;
private readonly StrategyParam<int> _highLevel1;
private readonly StrategyParam<int> _lowLevel1;
private readonly StrategyParam<int> _lowLevel2;
private readonly StrategyParam<SmoothMethods> _smoothMethod;
private readonly StrategyParam<int> _smoothLength;
private readonly StrategyParam<int> _smoothPhase;
private readonly StrategyParam<int> _signalBar;
private XBullsBearsEyesVolCalculator _indicator;
private readonly List<ColorSample> _colorHistory = new();
private DateTimeOffset? _lastLongPrimarySignalTime;
private DateTimeOffset? _lastLongSecondarySignalTime;
private DateTimeOffset? _lastShortPrimarySignalTime;
private DateTimeOffset? _lastShortSecondarySignalTime;
private bool _isLongPrimaryOpen;
private bool _isLongSecondaryOpen;
private bool _isShortPrimaryOpen;
private bool _isShortSecondaryOpen;
/// <summary>
/// Initializes a new instance of the <see cref="ExpXBullsBearsEyesVolStrategy"/> class.
/// </summary>
public ExpXBullsBearsEyesVolStrategy()
{
_primaryVolume = Param(nameof(PrimaryVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Primary Volume", "Order volume used by the first long/short slot", "Trading");
_secondaryVolume = Param(nameof(SecondaryVolume), 0.2m)
.SetGreaterThanZero()
.SetDisplay("Secondary Volume", "Order volume used by the second long/short slot", "Trading");
_stopLossPoints = Param(nameof(StopLossPoints), 1000)
.SetNotNegative()
.SetDisplay("Stop Loss (points)", "Protective stop distance expressed in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Target distance expressed in price steps", "Risk");
_allowLongEntry = Param(nameof(AllowLongEntry), true)
.SetDisplay("Allow Long Entry", "Enable opening long positions", "Trading");
_allowShortEntry = Param(nameof(AllowShortEntry), true)
.SetDisplay("Allow Short Entry", "Enable opening short positions", "Trading");
_allowLongExit = Param(nameof(AllowLongExit), true)
.SetDisplay("Allow Long Exit", "Enable closing long positions on bearish colours", "Trading");
_allowShortExit = Param(nameof(AllowShortExit), true)
.SetDisplay("Allow Short Exit", "Enable closing short positions on bullish colours", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used by the indicator and trading signals", "General");
_indicatorPeriod = Param(nameof(IndicatorPeriod), 13)
.SetGreaterThanZero()
.SetDisplay("Indicator Period", "EMA period used by Bulls/Bears power", "Indicator");
_gamma = Param(nameof(Gamma), 0.6m)
.SetDisplay("Gamma", "Adaptive smoothing factor used by the four-stage filter", "Indicator");
_volumeType = Param(nameof(VolumeType), AppliedVolumes.Tick)
.SetDisplay("Volume Type", "Volume source multiplied by the indicator", "Indicator");
_highLevel2 = Param(nameof(HighLevel2), 25)
.SetDisplay("High Level 2", "Upper level that marks strong bullish pressure", "Indicator");
_highLevel1 = Param(nameof(HighLevel1), 10)
.SetDisplay("High Level 1", "Upper level that marks moderate bullish pressure", "Indicator");
_lowLevel1 = Param(nameof(LowLevel1), -10)
.SetDisplay("Low Level 1", "Lower level that marks moderate bearish pressure", "Indicator");
_lowLevel2 = Param(nameof(LowLevel2), -25)
.SetDisplay("Low Level 2", "Lower level that marks strong bearish pressure", "Indicator");
_smoothMethod = Param(nameof(SmoothingMethod), SmoothMethods.Sma)
.SetDisplay("Smoothing Method", "Moving average used for indicator smoothing", "Indicator");
_smoothLength = Param(nameof(SmoothingLength), 12)
.SetGreaterThanZero()
.SetDisplay("Smoothing Length", "Length of the smoothing filter", "Indicator");
_smoothPhase = Param(nameof(SmoothingPhase), 15)
.SetDisplay("Smoothing Phase", "Phase parameter for Jurik based smoothing", "Indicator");
_signalBar = Param(nameof(SignalBar), 1)
.SetNotNegative()
.SetDisplay("Signal Bar", "Shift applied before evaluating colour transitions", "Trading");
}
/// <summary>
/// Volume used by the first long/short slot.
/// </summary>
public decimal PrimaryVolume
{
get => _primaryVolume.Value;
set => _primaryVolume.Value = value;
}
/// <summary>
/// Volume used by the second long/short slot.
/// </summary>
public decimal SecondaryVolume
{
get => _secondaryVolume.Value;
set => _secondaryVolume.Value = value;
}
/// <summary>
/// Stop loss distance expressed in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit distance expressed in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Enable or disable opening long positions.
/// </summary>
public bool AllowLongEntry
{
get => _allowLongEntry.Value;
set => _allowLongEntry.Value = value;
}
/// <summary>
/// Enable or disable opening short positions.
/// </summary>
public bool AllowShortEntry
{
get => _allowShortEntry.Value;
set => _allowShortEntry.Value = value;
}
/// <summary>
/// Enable or disable closing long positions on bearish colours.
/// </summary>
public bool AllowLongExit
{
get => _allowLongExit.Value;
set => _allowLongExit.Value = value;
}
/// <summary>
/// Enable or disable closing short positions on bullish colours.
/// </summary>
public bool AllowShortExit
{
get => _allowShortExit.Value;
set => _allowShortExit.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// EMA period used by Bulls/Bears power calculations.
/// </summary>
public int IndicatorPeriod
{
get => _indicatorPeriod.Value;
set => _indicatorPeriod.Value = value;
}
/// <summary>
/// Adaptive smoothing factor used by the internal filter.
/// </summary>
public decimal Gamma
{
get => _gamma.Value;
set => _gamma.Value = value;
}
/// <summary>
/// Volume source multiplied by the indicator output.
/// </summary>
public AppliedVolumes VolumeType
{
get => _volumeType.Value;
set => _volumeType.Value = value;
}
/// <summary>
/// Upper level that marks strong bullish pressure.
/// </summary>
public int HighLevel2
{
get => _highLevel2.Value;
set => _highLevel2.Value = value;
}
/// <summary>
/// Upper level that marks moderate bullish pressure.
/// </summary>
public int HighLevel1
{
get => _highLevel1.Value;
set => _highLevel1.Value = value;
}
/// <summary>
/// Lower level that marks moderate bearish pressure.
/// </summary>
public int LowLevel1
{
get => _lowLevel1.Value;
set => _lowLevel1.Value = value;
}
/// <summary>
/// Lower level that marks strong bearish pressure.
/// </summary>
public int LowLevel2
{
get => _lowLevel2.Value;
set => _lowLevel2.Value = value;
}
/// <summary>
/// Moving average used for indicator smoothing.
/// </summary>
public SmoothMethods SmoothingMethod
{
get => _smoothMethod.Value;
set => _smoothMethod.Value = value;
}
/// <summary>
/// Length of the smoothing filter.
/// </summary>
public int SmoothingLength
{
get => _smoothLength.Value;
set => _smoothLength.Value = value;
}
/// <summary>
/// Phase parameter for Jurik based smoothing.
/// </summary>
public int SmoothingPhase
{
get => _smoothPhase.Value;
set => _smoothPhase.Value = value;
}
/// <summary>
/// Shift applied before evaluating colour transitions.
/// </summary>
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_indicator?.Reset();
_colorHistory.Clear();
_lastLongPrimarySignalTime = null;
_lastLongSecondarySignalTime = null;
_lastShortPrimarySignalTime = null;
_lastShortSecondarySignalTime = null;
_isLongPrimaryOpen = false;
_isLongSecondaryOpen = false;
_isShortPrimaryOpen = false;
_isShortSecondaryOpen = false;
_indicator = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_indicator = new XBullsBearsEyesVolCalculator(
IndicatorPeriod,
Gamma,
VolumeType,
HighLevel2,
HighLevel1,
LowLevel1,
LowLevel2,
SmoothingMethod,
SmoothingLength,
SmoothingPhase);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var step = Security?.PriceStep ?? 1m;
var stopLoss = StopLossPoints > 0 ? new Unit(StopLossPoints * step, UnitTypes.Absolute) : null;
var takeProfit = TakeProfitPoints > 0 ? new Unit(TakeProfitPoints * step, UnitTypes.Absolute) : null;
if (stopLoss != null || takeProfit != null)
{
StartProtection(stopLoss: stopLoss, takeProfit: takeProfit, useMarketOrders: true);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_indicator is null)
return;
var result = _indicator.Process(candle);
if (result is null)
return;
var signalTime = GetSignalTime(candle);
var r = result.Value;
AddColorSample(new ColorSample(signalTime, r.Value, r.Volume, r.Color));
// trading guard removed
var (currentColor, previousColor, colorTime) = GetSignalContext();
if (currentColor is null || previousColor is null || colorTime is null)
return;
var openLongPrimary = false;
var openLongSecondary = false;
var openShortPrimary = false;
var openShortSecondary = false;
var closeLong = false;
var closeShort = false;
if (currentColor == 1)
{
if (AllowLongEntry && previousColor > 1)
openLongPrimary = true;
if (AllowShortExit)
closeShort = true;
}
if (currentColor == 0)
{
if (AllowLongEntry && previousColor > 0)
openLongSecondary = true;
if (AllowShortExit)
closeShort = true;
}
if (currentColor == 3)
{
if (AllowShortEntry && previousColor < 3)
openShortPrimary = true;
if (AllowLongExit)
closeLong = true;
}
if (currentColor == 4)
{
if (AllowShortEntry && previousColor < 4)
openShortSecondary = true;
if (AllowLongExit)
closeLong = true;
}
if (closeLong && Position > 0)
{
SellMarket();
_isLongPrimaryOpen = false;
_isLongSecondaryOpen = false;
_lastLongPrimarySignalTime = null;
_lastLongSecondarySignalTime = null;
}
if (closeShort && Position < 0)
{
BuyMarket();
_isShortPrimaryOpen = false;
_isShortSecondaryOpen = false;
_lastShortPrimarySignalTime = null;
_lastShortSecondarySignalTime = null;
}
if (openLongPrimary && !_isLongPrimaryOpen && _lastLongPrimarySignalTime != colorTime)
{
var volume = PrimaryVolume;
if (volume > 0m)
{
BuyMarket();
_isLongPrimaryOpen = true;
_lastLongPrimarySignalTime = colorTime;
}
}
if (openLongSecondary && !_isLongSecondaryOpen && _lastLongSecondarySignalTime != colorTime)
{
var volume = SecondaryVolume;
if (volume > 0m)
{
BuyMarket();
_isLongSecondaryOpen = true;
_lastLongSecondarySignalTime = colorTime;
}
}
if (openShortPrimary && !_isShortPrimaryOpen && _lastShortPrimarySignalTime != colorTime)
{
var volume = PrimaryVolume;
if (volume > 0m)
{
SellMarket();
_isShortPrimaryOpen = true;
_lastShortPrimarySignalTime = colorTime;
}
}
if (openShortSecondary && !_isShortSecondaryOpen && _lastShortSecondarySignalTime != colorTime)
{
var volume = SecondaryVolume;
if (volume > 0m)
{
SellMarket();
_isShortSecondaryOpen = true;
_lastShortSecondarySignalTime = colorTime;
}
}
}
private DateTimeOffset GetSignalTime(ICandleMessage candle)
{
var timeFrame = CandleType.Arg is TimeSpan span ? span : TimeSpan.Zero;
var closeTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime + timeFrame;
return closeTime;
}
private (int? current, int? previous, DateTimeOffset? time) GetSignalContext()
{
if (SignalBar < 0)
return (null, null, null);
var index = _colorHistory.Count - 1 - SignalBar;
if (index < 0 || index >= _colorHistory.Count)
return (null, null, null);
var previousIndex = index - 1;
if (previousIndex < 0)
return (null, null, null);
var currentSample = _colorHistory[index];
var previousSample = _colorHistory[previousIndex];
return (currentSample.Color, previousSample.Color, currentSample.Time);
}
private void AddColorSample(ColorSample sample)
{
_colorHistory.Add(sample);
const int maxItems = 1024;
if (_colorHistory.Count > maxItems)
_colorHistory.RemoveRange(0, _colorHistory.Count - maxItems);
}
private readonly struct ColorSample
{
public ColorSample(DateTimeOffset time, decimal value, decimal volume, int color)
{
Time = time;
Value = value;
Volume = volume;
Color = color;
}
public DateTimeOffset Time { get; }
public decimal Value { get; }
public decimal Volume { get; }
public int Color { get; }
}
/// <summary>
/// Volume source applied to the indicator output.
/// </summary>
public enum AppliedVolumes
{
/// <summary>
/// Multiply the indicator by tick volume.
/// </summary>
Tick,
/// <summary>
/// Multiply the indicator by real volume.
/// </summary>
Real,
}
/// <summary>
/// Moving average methods supported by the indicator.
/// </summary>
public enum SmoothMethods
{
/// <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 (JJMA).
/// </summary>
Jjma,
/// <summary>
/// Jurik moving average (JurX variant).
/// </summary>
JurX,
/// <summary>
/// Parabolic moving average approximation.
/// </summary>
ParMa,
/// <summary>
/// Triple exponential moving average (T3).
/// </summary>
T3,
/// <summary>
/// VIDYA adaptive moving average (approximated by EMA).
/// </summary>
Vidya,
/// <summary>
/// Kaufman adaptive moving average.
/// </summary>
Ama,
}
private sealed class XBullsBearsEyesVolCalculator
{
private readonly ExponentialMovingAverage _ema;
private readonly DecimalLengthIndicator _valueSmoother;
private readonly DecimalLengthIndicator _volumeSmoother;
private readonly AppliedVolumes _volumeType;
private readonly decimal _gamma;
private readonly decimal _highLevel2;
private readonly decimal _highLevel1;
private readonly decimal _lowLevel1;
private readonly decimal _lowLevel2;
private decimal _l0;
private decimal _l1;
private decimal _l2;
private decimal _l3;
public XBullsBearsEyesVolCalculator(
int emaPeriod,
decimal gamma,
AppliedVolumes volumeType,
int highLevel2,
int highLevel1,
int lowLevel1,
int lowLevel2,
SmoothMethods method,
int smoothLength,
int smoothPhase)
{
var period = Math.Max(1, emaPeriod);
_ema = new EMA { Length = period };
_gamma = Math.Min(0.999m, Math.Max(0m, gamma));
_volumeType = volumeType;
_highLevel2 = highLevel2;
_highLevel1 = highLevel1;
_lowLevel1 = lowLevel1;
_lowLevel2 = lowLevel2;
_valueSmoother = CreateSmoother(method, smoothLength, smoothPhase);
_volumeSmoother = CreateSmoother(method, smoothLength, smoothPhase);
}
public void Reset()
{
_ema.Reset();
_valueSmoother.Reset();
_volumeSmoother.Reset();
_l0 = 0m;
_l1 = 0m;
_l2 = 0m;
_l3 = 0m;
}
public XBullsBearsEyesVolResult? Process(ICandleMessage candle)
{
var time = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;
var emaValue = _ema.Process(new DecimalIndicatorValue(_ema, candle.ClosePrice, time)).ToNullableDecimal();
if (emaValue is null)
return null;
var bulls = candle.HighPrice - emaValue.Value;
var bears = candle.LowPrice - emaValue.Value;
var combined = bulls + bears;
var l0 = (1m - _gamma) * combined + _gamma * _l0;
var l1 = -_gamma * l0 + _l0 + _gamma * _l1;
var l2 = -_gamma * l1 + _l1 + _gamma * _l2;
var l3 = -_gamma * l2 + _l2 + _gamma * _l3;
_l0 = l0;
_l1 = l1;
_l2 = l2;
_l3 = l3;
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 sum = cu + cd;
var ratio = sum <= 0m ? 0m : cu / sum;
var baseValue = ratio * 100m - 50m;
var volume = GetVolume(candle);
var scaled = baseValue * volume;
var smoothedValue = _valueSmoother.Process(new DecimalIndicatorValue(_valueSmoother, scaled, time)).ToNullableDecimal();
var smoothedVolume = _volumeSmoother.Process(new DecimalIndicatorValue(_volumeSmoother, volume, time)).ToNullableDecimal();
if (smoothedValue is null || smoothedVolume is null)
return null;
var color = DetermineColor(smoothedValue.Value, smoothedVolume.Value);
return new XBullsBearsEyesVolResult(smoothedValue.Value, smoothedVolume.Value, color);
}
private int DetermineColor(decimal value, decimal volume)
{
var maxLevel = _highLevel2 * volume;
var upLevel = _highLevel1 * volume;
var downLevel = _lowLevel1 * volume;
var minLevel = _lowLevel2 * volume;
if (value > maxLevel)
return 0;
if (value > upLevel)
return 1;
if (value < minLevel)
return 4;
if (value < downLevel)
return 3;
return 2;
}
private decimal GetVolume(ICandleMessage candle)
{
return _volumeType switch
{
AppliedVolumes.Tick => candle.TotalTicks.HasValue ? (decimal)candle.TotalTicks.Value : candle.TotalVolume,
AppliedVolumes.Real => candle.TotalVolume > 0 ? candle.TotalVolume : (candle.TotalTicks.HasValue ? (decimal)candle.TotalTicks.Value : 0m),
_ => candle.TotalVolume,
};
}
private static DecimalLengthIndicator CreateSmoother(SmoothMethods method, int length, int phase)
{
var normalizedLength = Math.Max(1, length);
return method switch
{
SmoothMethods.Sma => new SMA { Length = normalizedLength },
SmoothMethods.Ema => new EMA { Length = normalizedLength },
SmoothMethods.Smma => new SmoothedMovingAverage { Length = normalizedLength },
SmoothMethods.Lwma => new WeightedMovingAverage { Length = normalizedLength },
SmoothMethods.Jjma => CreateJurik(normalizedLength, phase),
SmoothMethods.JurX => CreateJurik(normalizedLength, phase),
SmoothMethods.ParMa => new EMA { Length = normalizedLength },
SmoothMethods.T3 => new TripleExponentialMovingAverage { Length = normalizedLength },
SmoothMethods.Vidya => new EMA { Length = normalizedLength },
SmoothMethods.Ama => new KaufmanAdaptiveMovingAverage { Length = normalizedLength },
_ => new SMA { Length = normalizedLength },
};
}
private static DecimalLengthIndicator CreateJurik(int length, int phase)
{
var jurik = new JurikMovingAverage { Length = length };
var property = jurik.GetType().GetProperty("Phase", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (property != null)
{
var value = Math.Max(-100, Math.Min(100, phase));
property.SetValue(jurik, value);
}
return jurik;
}
}
private readonly record struct XBullsBearsEyesVolResult(decimal Value, decimal Volume, int Color);
}
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,
SmoothedMovingAverage, WeightedMovingAverage
)
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class exp_x_bulls_bears_eyes_vol_strategy(Strategy):
def __init__(self):
super(exp_x_bulls_bears_eyes_vol_strategy, self).__init__()
self._primary_volume = self.Param("PrimaryVolume", 0.1) \
.SetDisplay("Primary Volume", "Order volume used by the first long/short slot", "Trading")
self._secondary_volume = self.Param("SecondaryVolume", 0.2) \
.SetDisplay("Secondary Volume", "Order volume used by the second long/short slot", "Trading")
self._stop_loss_points = self.Param("StopLossPoints", 1000) \
.SetDisplay("Stop Loss (points)", "Protective stop distance expressed in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 2000) \
.SetDisplay("Take Profit (points)", "Target distance expressed in price steps", "Risk")
self._allow_long_entry = self.Param("AllowLongEntry", True) \
.SetDisplay("Allow Long Entry", "Enable opening long positions", "Trading")
self._allow_short_entry = self.Param("AllowShortEntry", True) \
.SetDisplay("Allow Short Entry", "Enable opening short positions", "Trading")
self._allow_long_exit = self.Param("AllowLongExit", True) \
.SetDisplay("Allow Long Exit", "Enable closing long positions on bearish colours", "Trading")
self._allow_short_exit = self.Param("AllowShortExit", True) \
.SetDisplay("Allow Short Exit", "Enable closing short positions on bullish colours", "Trading")
self._indicator_period = self.Param("IndicatorPeriod", 13) \
.SetDisplay("Indicator Period", "EMA period used by Bulls/Bears power", "Indicator")
self._gamma_param = self.Param("Gamma", 0.6) \
.SetDisplay("Gamma", "Adaptive smoothing factor used by the four-stage filter", "Indicator")
self._high_level2 = self.Param("HighLevel2", 25) \
.SetDisplay("High Level 2", "Upper level that marks strong bullish pressure", "Indicator")
self._high_level1 = self.Param("HighLevel1", 10) \
.SetDisplay("High Level 1", "Upper level that marks moderate bullish pressure", "Indicator")
self._low_level1 = self.Param("LowLevel1", -10) \
.SetDisplay("Low Level 1", "Lower level that marks moderate bearish pressure", "Indicator")
self._low_level2 = self.Param("LowLevel2", -25) \
.SetDisplay("Low Level 2", "Lower level that marks strong bearish pressure", "Indicator")
self._smooth_length = self.Param("SmoothingLength", 12) \
.SetDisplay("Smoothing Length", "Length of the smoothing filter", "Indicator")
self._signal_bar = self.Param("SignalBar", 1) \
.SetDisplay("Signal Bar", "Shift applied before evaluating colour transitions", "Trading")
self._ema = None
self._value_smoother = None
self._volume_smoother = None
self._color_history = []
self._l0 = 0.0
self._l1 = 0.0
self._l2 = 0.0
self._l3 = 0.0
self._last_long_primary_time = None
self._last_long_secondary_time = None
self._last_short_primary_time = None
self._last_short_secondary_time = None
self._is_long_primary_open = False
self._is_long_secondary_open = False
self._is_short_primary_open = False
self._is_short_secondary_open = False
@property
def primary_volume(self):
return self._primary_volume.Value
@property
def secondary_volume(self):
return self._secondary_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
@property
def allow_long_entry(self):
return self._allow_long_entry.Value
@property
def allow_short_entry(self):
return self._allow_short_entry.Value
@property
def allow_long_exit(self):
return self._allow_long_exit.Value
@property
def allow_short_exit(self):
return self._allow_short_exit.Value
@property
def indicator_period(self):
return self._indicator_period.Value
@property
def gamma_val(self):
return self._gamma_param.Value
@property
def high_level2(self):
return self._high_level2.Value
@property
def high_level1(self):
return self._high_level1.Value
@property
def low_level1(self):
return self._low_level1.Value
@property
def low_level2(self):
return self._low_level2.Value
@property
def smooth_length(self):
return self._smooth_length.Value
@property
def signal_bar(self):
return self._signal_bar.Value
def OnReseted(self):
super(exp_x_bulls_bears_eyes_vol_strategy, self).OnReseted()
self._ema = None
self._value_smoother = None
self._volume_smoother = None
self._color_history = []
self._l0 = 0.0
self._l1 = 0.0
self._l2 = 0.0
self._l3 = 0.0
self._last_long_primary_time = None
self._last_long_secondary_time = None
self._last_short_primary_time = None
self._last_short_secondary_time = None
self._is_long_primary_open = False
self._is_long_secondary_open = False
self._is_short_primary_open = False
self._is_short_secondary_open = False
def OnStarted2(self, time):
super(exp_x_bulls_bears_eyes_vol_strategy, self).OnStarted2(time)
period = max(1, self.indicator_period)
self._ema = ExponentialMovingAverage()
self._ema.Length = period
length = max(1, self.smooth_length)
self._value_smoother = SimpleMovingAverage()
self._value_smoother.Length = length
self._volume_smoother = SimpleMovingAverage()
self._volume_smoother.Length = length
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromHours(8)))
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 1.0
sl = None
tp = None
if self.stop_loss_points > 0:
sl = Unit(float(self.stop_loss_points) * step, UnitTypes.Absolute)
if self.take_profit_points > 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, useMarketOrders=True)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._ema is None:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
ema_result = process_float(self._ema, candle.ClosePrice, candle.OpenTime, True)
if not self._ema.IsFormed:
return
ema_val = float(ema_result)
bulls = high - ema_val
bears = low - ema_val
combined = bulls + bears
gamma = min(0.999, max(0.0, float(self.gamma_val)))
l0 = (1.0 - gamma) * combined + gamma * self._l0
l1 = -gamma * l0 + self._l0 + gamma * self._l1
l2 = -gamma * l1 + self._l1 + gamma * self._l2
l3 = -gamma * l2 + self._l2 + gamma * self._l3
self._l0 = l0
self._l1 = l1
self._l2 = l2
self._l3 = l3
cu = 0.0
cd = 0.0
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
total = cu + cd
ratio = cu / total if total > 0.0 else 0.0
base_value = ratio * 100.0 - 50.0
volume = float(candle.TotalVolume) if candle.TotalVolume > 0 else 1.0
scaled = base_value * volume
from System import Decimal
sv_result = process_float(self._value_smoother, Decimal(scaled), candle.OpenTime, True)
vv_result = process_float(self._volume_smoother, Decimal(volume), candle.OpenTime, True)
if not self._value_smoother.IsFormed or not self._volume_smoother.IsFormed:
return
smoothed_value = float(sv_result)
smoothed_volume = float(vv_result)
color = self._determine_color(smoothed_value, smoothed_volume)
signal_time = candle.CloseTime if candle.CloseTime is not None else candle.OpenTime
self._color_history.append((signal_time, smoothed_value, smoothed_volume, color))
if len(self._color_history) > 1024:
self._color_history = self._color_history[-1024:]
ctx = self._get_signal_context()
if ctx is None:
return
current_color, previous_color, color_time = ctx
open_long_primary = False
open_long_secondary = False
open_short_primary = False
open_short_secondary = False
close_long = False
close_short = False
if current_color == 1:
if self.allow_long_entry and previous_color > 1:
open_long_primary = True
if self.allow_short_exit:
close_short = True
if current_color == 0:
if self.allow_long_entry and previous_color > 0:
open_long_secondary = True
if self.allow_short_exit:
close_short = True
if current_color == 3:
if self.allow_short_entry and previous_color < 3:
open_short_primary = True
if self.allow_long_exit:
close_long = True
if current_color == 4:
if self.allow_short_entry and previous_color < 4:
open_short_secondary = True
if self.allow_long_exit:
close_long = True
if close_long and self.Position > 0:
self.SellMarket()
self._is_long_primary_open = False
self._is_long_secondary_open = False
self._last_long_primary_time = None
self._last_long_secondary_time = None
if close_short and self.Position < 0:
self.BuyMarket()
self._is_short_primary_open = False
self._is_short_secondary_open = False
self._last_short_primary_time = None
self._last_short_secondary_time = None
if open_long_primary and not self._is_long_primary_open and self._last_long_primary_time != color_time:
if float(self.primary_volume) > 0.0:
self.BuyMarket()
self._is_long_primary_open = True
self._last_long_primary_time = color_time
if open_long_secondary and not self._is_long_secondary_open and self._last_long_secondary_time != color_time:
if float(self.secondary_volume) > 0.0:
self.BuyMarket()
self._is_long_secondary_open = True
self._last_long_secondary_time = color_time
if open_short_primary and not self._is_short_primary_open and self._last_short_primary_time != color_time:
if float(self.primary_volume) > 0.0:
self.SellMarket()
self._is_short_primary_open = True
self._last_short_primary_time = color_time
if open_short_secondary and not self._is_short_secondary_open and self._last_short_secondary_time != color_time:
if float(self.secondary_volume) > 0.0:
self.SellMarket()
self._is_short_secondary_open = True
self._last_short_secondary_time = color_time
def _determine_color(self, value, volume):
max_level = float(self.high_level2) * volume
up_level = float(self.high_level1) * volume
down_level = float(self.low_level1) * volume
min_level = float(self.low_level2) * volume
if value > max_level:
return 0
if value > up_level:
return 1
if value < min_level:
return 4
if value < down_level:
return 3
return 2
def _get_signal_context(self):
sb = self.signal_bar
if sb < 0:
return None
index = len(self._color_history) - 1 - sb
if index < 0 or index >= len(self._color_history):
return None
prev_index = index - 1
if prev_index < 0:
return None
current_sample = self._color_history[index]
previous_sample = self._color_history[prev_index]
return (current_sample[3], previous_sample[3], current_sample[0])
def CreateClone(self):
return exp_x_bulls_bears_eyes_vol_strategy()