Exp XBullsBearsEyes Vol Strategy
Overview
This strategy is a C# conversion of the MetaTrader expert Exp_XBullsBearsEyes_Vol. The original advisor combines Bulls Power and Bears Power readings, multiplies the result by candle volume and colours the histogram according to the resulting momentum. Two independent position slots are maintained for both the long and the short side, allowing the system to scale in when the colour intensity increases. The StockSharp port recreates the multi-stage filter, colour logic and trade management while using high-level API calls for orders and risk control.
The algorithm subscribes to a configurable timeframe, rebuilds the custom XBullsBearsEyes indicator and reacts only to finished candles. Colour transitions determine both the entries and the exits: bullish colours close short trades and can open one or two long slots; bearish colours perform the mirror action. Stop-loss and take-profit distances are translated into StartProtection parameters so that platform risk managers can handle protective orders.
Indicator logic
- Bulls Power and Bears Power values are rebuilt with an EMA of period
IndicatorPeriodusing the candle high/low against the smoothed close. - A four-stage adaptive filter accumulates bullish (
CU) and bearish (CD) pressure with coefficientGamma. The indicator value isCU / (CU + CD) * 100 - 50. - The filtered value is multiplied by either tick volume or real volume, depending on
VolumeType. - The multiplied series and the raw volume are both smoothed by a moving average chosen through
SmoothingMethod,SmoothingLengthandSmoothingPhase(Jurik phase is honoured when the underlying class exposes it). - Colour levels are derived from
HighLevel1,HighLevel2,LowLevel1andLowLevel2. Values above the upper bands produce colours0or1, while values below the lower bands produce colours3or4. Colour2indicates a neutral state. - Colour history is stored so that signals can be evaluated on bar
SignalBar(default: one closed candle back). The colour from the current signal bar is compared to the previous colour to detect transitions.
Trading rules
- Colours
1and0denote bullish pressure. When the colour changes into one of those values and the previous colour was weaker, slot 1 (PrimaryVolume) or slot 2 (SecondaryVolume) opens a long position respectively. Both events close any existing short exposure ifAllowShortExitis enabled. - Colours
3and4denote bearish pressure. When the colour moves into these values and the previous colour was stronger, slot 1 or slot 2 opens a short position respectively. Both events close any existing long exposure ifAllowLongExitis enabled. - Each slot remembers whether it already has an open position and ignores repeated signals until the corresponding direction has been closed.
SignalBardefines how many completed candles are skipped before evaluating the colour (0 = latest finished candle). The code requires at least two historical colours to compare.- Stop-loss and take-profit expressed in points (
StopLossPoints,TakeProfitPoints) are converted to absolute price distances withSecurity.PriceStepand used to start platform protection with market exits.
Parameters
| Name | Description |
|---|---|
PrimaryVolume |
Volume for the first slot (triggered by colour 1 / 3). |
SecondaryVolume |
Volume for the second slot (triggered by colour 0 / 4). |
StopLossPoints / TakeProfitPoints |
Protective distances in price steps. Set to zero to disable. |
AllowLongEntry / AllowShortEntry |
Enable scaling into the corresponding direction. |
AllowLongExit / AllowShortExit |
Enable automated exits when the opposite colour appears. |
CandleType |
Timeframe subscribed for candles and indicator calculation (default: 8 hours). |
IndicatorPeriod |
EMA period used to rebuild Bulls/Bears Power. |
Gamma |
Adaptive smoothing factor for the four-stage filter (0.0 – 0.999). |
VolumeType |
Select tick volume or real volume for weighting. |
HighLevel1, HighLevel2, LowLevel1, LowLevel2 |
Level multipliers that define colour thresholds. |
SmoothingMethod |
Moving average type used to smooth the indicator and the volume (SMA, EMA, SMMA, LWMA, Jurik, JurX, ParMA→EMA, T3, VIDYA→EMA, AMA). |
SmoothingLength |
Length of the smoothing moving average. |
SmoothingPhase |
Jurik phase parameter (clamped to [-100, 100]). |
SignalBar |
Number of closed candles to step back before evaluating colour transitions. |
Usage notes
- The strategy operates on a single security returned by
GetWorkingSecurities()and uses market orders for entries and exits. - Slot management is netted: additional entries add to the net position, while exits flatten the entire exposure for the affected side.
- If the platform provides only tick volume, selecting
VolumeType = Realwill fall back to the available tick count. - VIDYA and Parabolic smoothing fall back to exponential moving averages because StockSharp exposes those implementations directly.
- Make sure to configure the instrument price step so that
StopLossPointsandTakeProfitPointsconvert into the intended absolute distances.
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()