Стратегия Exp XBullsBearsEyes Vol
Обзор
Стратегия представляет собой портирование MetaTrader-советника Exp_XBullsBearsEyes_Vol на C#. В оригинале индикатор складывает значения Bulls Power и Bears Power, умножает результат на объём свечи и окрашивает гистограмму в зависимости от силы импульса. Для лонгов и шортов ведётся по два независимых слота, что позволяет наращивать позицию по мере усиления цвета. Версия на StockSharp восстанавливает многокаскадный фильтр, логику окраски и управление позициями, опираясь на высокоуровневые методы API для заявок и риск-менеджмента.
Алгоритм подписывается на выбранный таймфрейм и обрабатывает только закрытые свечи. Он пересчитывает пользовательский индикатор XBullsBearsEyes и реагирует на смену цветов: бычьи оттенки закрывают шорты и могут открыть один или два лонговых слота, медвежьи цвета выполняют зеркальные действия. Стоп-лоссы и тейк-профиты задаются в пунктах и передаются в StartProtection, чтобы защитные приказы сопровождались движком риск-менеджмента.
Логика индикатора
- Bulls Power и Bears Power восстанавливаются по EMA периода
IndicatorPeriod, сравнивая максимумы/минимумы свечи с сглаженной ценой закрытия. - Четырёхступенчатый адаптивный фильтр накапливает бычье (
CU) и медвежье (CD) давление с коэффициентомGamma, итоговая величина равнаCU / (CU + CD) * 100 - 50. - Полученный результат умножается на тиковый либо реальный объём в зависимости от параметра
VolumeType. - Произведение и сам объём дополнительно сглаживаются выбранной средней (
SmoothingMethod,SmoothingLength,SmoothingPhase; для Jurik учитывается фаза при наличии соответствующего свойства). - Уровни
HighLevel1,HighLevel2,LowLevel1,LowLevel2формируют зоны окраски: значения выше верхних порогов дают цвета0или1, ниже нижних — цвета3или4, иначе фиксируется нейтральный цвет2. - История цветов хранится, чтобы оценки проводились по свече с шагом
SignalBar(по умолчанию — одна закрытая свеча назад) и сравнивались с предыдущим значением.
Правила торговли
- Цвета
1и0указывают на бычье давление. Если цвет переходит в одно из этих значений и предыдущий цвет был слабее, слот 1 (PrimaryVolume) или слот 2 (SecondaryVolume) открывает лонг. При активномAllowShortExitтекущие шорты закрываются. - Цвета
3и4сигнализируют о медвежьем давлении. Переход к ним при более высоком предыдущем цвете открывает шорт в слоте 1 или 2 соответственно. При включенномAllowLongExitсуществующие лонги закрываются. - Каждый слот отслеживает собственное состояние и не реагирует повторно, пока позиция не будет закрыта противоположным сигналом.
SignalBarзадаёт число уже завершённых свечей, которые нужно пропустить перед оценкой цвета (0 — последняя закрытая свеча). Для сравнения необходимо минимум два исторических значения.StopLossPointsиTakeProfitPoints, заданные в шагах цены, конвертируются с помощьюSecurity.PriceStepв абсолютные расстояния и используются для запускаStartProtection.
Параметры
| Параметр | Описание |
|---|---|
PrimaryVolume |
Объём первой ступени (цвета 1 / 3). |
SecondaryVolume |
Объём второй ступени (цвета 0 / 4). |
StopLossPoints / TakeProfitPoints |
Расстояние до стопа/тейка в шагах цены, 0 — отключено. |
AllowLongEntry / AllowShortEntry |
Разрешение на открытие позиций соответствующего направления. |
AllowLongExit / AllowShortExit |
Разрешение на автоматическое закрытие при появлении противоположного цвета. |
CandleType |
Таймфрейм для подписки на свечи и расчёта индикатора (по умолчанию 8 часов). |
IndicatorPeriod |
Период EMA для расчёта Bulls/Bears Power. |
Gamma |
Коэффициент адаптивного фильтра (0.0–0.999). |
VolumeType |
Источник объёма — тиковый или реальный. |
HighLevel1, HighLevel2, LowLevel1, LowLevel2 |
Коэффициенты, определяющие границы цветовых зон. |
SmoothingMethod |
Тип сглаживания (SMA, EMA, SMMA, LWMA, Jurik, JurX, ParMA→EMA, T3, VIDYA→EMA, AMA). |
SmoothingLength |
Период сглаживающей средней. |
SmoothingPhase |
Параметр фазы для Jurik (ограничен диапазоном [-100, 100]). |
SignalBar |
Количество закрытых свечей, на которое смещается анализ цвета. |
Рекомендации по использованию
- Стратегия работает с единственным инструментом, возвращаемым
GetWorkingSecurities(), и выставляет рыночные заявки. - Управление слотами ведётся по чистой позиции: дополнительные входы увеличивают текущий объём, а выход полностью закрывает направление.
- При отсутствии данных по реальному объёму выбор
VolumeType = Realавтоматически падёт на тиковые данные. - Для методов VIDYA и Parabolic используется экспоненциальное приближение, так как в StockSharp эти реализации доступны напрямую.
- Убедитесь, что
Security.PriceStepнастроен корректно, иначе пересчётStopLossPointsиTakeProfitPointsв абсолютные значения может отличаться от ожиданий.
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()