Exp XBullsBearsEyes Vol Strategie
Überblick
Diese Strategie ist eine C#-Konvertierung des MetaTrader-Experts Exp_XBullsBearsEyes_Vol. Der ursprüngliche Advisor kombiniert Bulls Power und Bears Power Werte, multipliziert das Ergebnis mit dem Kerzenvolumen und färbt das Histogramm entsprechend des resultierenden Momentums. Zwei unabhängige Positionsslots werden sowohl für die Long- als auch für die Short-Seite geführt, was dem System ermöglicht, zu skalieren, wenn die Farbintensität zunimmt. Der StockSharp-Port recreiert den mehrstufigen Filter, die Farblogik und das Trade-Management, während er High-Level-API-Aufrufe für Orders und Risikokontrolle verwendet.
Der Algorithmus abonniert einen konfigurierbaren Zeitrahmen, baut den benutzerdefinierten XBullsBearsEyes-Indikator neu auf und reagiert nur auf abgeschlossene Kerzen. Farbübergänge bestimmen sowohl die Einstiege als auch die Ausstiege: Bullische Farben schließen Short-Trades und können einen oder zwei Long-Slots öffnen; Bärische Farben führen die Spiegelaktion durch. Stop-Loss- und Take-Profit-Abstände werden in StartProtection-Parameter übersetzt, damit Platform-Risikomanager Schutzorders verwalten können.
Indikatorlogik
- Bulls Power und Bears Power Werte werden mit einer EMA der Periode
IndicatorPeriodneu aufgebaut, indem das Kerzenhoch/-tief gegen den geglätteten Schlusskurs verglichen wird. - Ein vierstufiger adaptiver Filter akkumuliert bullischen (
CU) und bärischen (CD) Druck mit dem KoeffizientenGamma. Der Indikatorwert istCU / (CU + CD) * 100 - 50. - Der gefilterte Wert wird je nach
VolumeTypeentweder mit Tick-Volumen oder realem Volumen multipliziert. - Die multiplizierte Serie und das Rohvolumen werden beide durch einen gleitenden Durchschnitt geglättet, der durch
SmoothingMethod,SmoothingLengthundSmoothingPhasegewählt wird (die Jurik-Phase wird berücksichtigt, wenn die zugrunde liegende Klasse sie verfügbar macht). - Farbniveaus werden aus
HighLevel1,HighLevel2,LowLevel1undLowLevel2abgeleitet. Werte über den oberen Bändern produzieren Farben0oder1, Werte unter den unteren Bändern produzieren Farben3oder4. Farbe2zeigt einen neutralen Zustand an. - Der Farbverlauf wird gespeichert, damit Signale auf der Kerze
SignalBarausgewertet werden können (Standard: eine geschlossene Kerze zurück). Die Farbe der aktuellen Signalkerze wird mit der vorherigen Farbe verglichen, um Übergänge zu erkennen.
Handelsregeln
- Farben
1und0kennzeichnen bullischen Druck. Wenn sich die Farbe in einen dieser Werte ändert und die vorherige Farbe schwächer war, öffnet Slot 1 (PrimaryVolume) bzw. Slot 2 (SecondaryVolume) eine Long-Position. Beide Ereignisse schließen bestehende Short-Exposure, wennAllowShortExitaktiviert ist. - Farben
3und4kennzeichnen bärischen Druck. Wenn die Farbe sich in diese Werte bewegt und die vorherige Farbe stärker war, öffnet Slot 1 oder Slot 2 jeweils eine Short-Position. Beide Ereignisse schließen bestehende Long-Exposure, wennAllowLongExitaktiviert ist. - Jeder Slot merkt sich, ob er bereits eine offene Position hat, und ignoriert wiederholte Signale, bis die entsprechende Richtung geschlossen wurde.
SignalBardefiniert, wie viele abgeschlossene Kerzen vor der Farbauswertung übersprungen werden (0 = letzte abgeschlossene Kerze). Der Code benötigt mindestens zwei historische Farben zum Vergleich.- Stop-Loss und Take-Profit in Punkten (
StopLossPoints,TakeProfitPoints) werden mitSecurity.PriceStepin absolute Preisabstände umgerechnet und zum Starten des Plattformschutzes mit Marktausstiegen verwendet.
Parameter
| Name | Beschreibung |
|---|---|
PrimaryVolume |
Volumen für den ersten Slot (ausgelöst durch Farbe 1 / 3). |
SecondaryVolume |
Volumen für den zweiten Slot (ausgelöst durch Farbe 0 / 4). |
StopLossPoints / TakeProfitPoints |
Schutzabstände in Preisschritten. Auf null setzen zum Deaktivieren. |
AllowLongEntry / AllowShortEntry |
Skalierung in die entsprechende Richtung aktivieren. |
AllowLongExit / AllowShortExit |
Automatische Ausstiege aktivieren, wenn die entgegengesetzte Farbe erscheint. |
CandleType |
Für Kerzen und Indikatorberechnung abonnierter Zeitrahmen (Standard: 8 Stunden). |
IndicatorPeriod |
EMA-Periode zum Neuaufbau von Bulls/Bears Power. |
Gamma |
Adaptiver Glättungsfaktor für den vierstufigen Filter (0.0 – 0.999). |
VolumeType |
Tick-Volumen oder reales Volumen zur Gewichtung auswählen. |
HighLevel1, HighLevel2, LowLevel1, LowLevel2 |
Niveaumultiplikatoren, die Farbschwellen definieren. |
SmoothingMethod |
Gleitender Durchschnittstyp zur Glättung des Indikators und des Volumens (SMA, EMA, SMMA, LWMA, Jurik, JurX, ParMA→EMA, T3, VIDYA→EMA, AMA). |
SmoothingLength |
Länge des glättenden gleitenden Durchschnitts. |
SmoothingPhase |
Jurik-Phaseparameter (begrenzt auf [-100, 100]). |
SignalBar |
Anzahl geschlossener Kerzen, die vor der Auswertung von Farbübergängen übersprungen werden. |
Verwendungshinweise
- Die Strategie operiert mit einem einzelnen Instrument, das von
GetWorkingSecurities()zurückgegeben wird, und verwendet Marktorders für Ein- und Ausstiege. - Slot-Management ist nettiert: Zusätzliche Einstiege erhöhen die Nettoposition, während Ausstiege die gesamte Exposure für die betroffene Seite glätten.
- Wenn die Plattform nur Tick-Volumen bereitstellt, fällt
VolumeType = Realauf die verfügbare Tick-Anzahl zurück. - VIDYA- und Parabolische Glättung fallen auf exponentielle gleitende Durchschnitte zurück, weil StockSharp diese Implementierungen direkt bereitstellt.
- Den Instrumentenpreisschritt korrekt konfigurieren, damit
StopLossPointsundTakeProfitPointsin die beabsichtigten absoluten Abstände konvertiert werden.
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()