Auf GitHub ansehen

JB-Strategie

Zusammenfassung

Die JB-Strategie stammt aus einem fxDreema-Expert-Advisor und kombiniert langfristige Trendfilter, Momentum-Bestätigung und Volatilitätsausbrüche:

  • Trendfilter: Der Schlusskurs der vorherigen Kerze muss über (Long) oder unter (Short) einem einfachen gleitenden Durchschnitt über 100 Perioden liegen.
  • Momentum-Filter: Die Richtung wird mit einem Force Index über 100 Perioden bestätigt (positiv für Longs, negativ für Shorts).
  • Volatilitätsauslöser: Einstieg, wenn der vorherige Schlusskurs das entsprechende Bollinger-Band (20 Perioden, Abweichung 2,0) durchbricht.
  • Positionsverwaltung: Erhöht das Ordervolumen nach einem Verlustzyklus mit einem martingalartigen Multiplikator und setzt es nach profitablen Zyklen auf die Basisgröße zurück.
  • Ausstiegsregel: Schließt alle offenen Positionen, sobald der durchschnittliche nicht realisierte Gewinn pro Kontrakt ein konfigurierbares Geldziel erreicht.

Parameter

Name Beschreibung
SmaPeriod Länge des SMA-Trendfilters. Standard: 100.
ForcePeriod Länge des Force-Index-Indikators. Standard: 100.
BollingerPeriod Länge der Bollinger-Bänder. Standard: 20.
BollingerDeviation Standardabweichungsmultiplikator für Bollinger-Bänder. Standard: 2,0.
BaseVolume Anfangsvolumen der Order vor Martingal-Anpassungen. Standard: 0,1.
LossMultiplier Multiplikator für das nächste Ordervolumen nach einem Verlustzyklus. Standard: 1,55.
AverageProfitTarget Durchschnittlicher nicht realisierter Gewinn pro Kontrakt, der zum Schließen aller Positionen erforderlich ist. Standard: 2,8.
CandleType Für Berechnungen verwendeter Kerzentyp (standardmäßig 1-Minuten-Zeitrahmen).

Signale

Long-Einstieg

  1. Der Schlusskurs der vorherigen Kerze liegt unter oder auf dem unteren Bollinger-Band.
  2. Der vorherige Schlusskurs liegt über der SMA über 100 Perioden (Aufwärtstrend).
  3. Der Force-Index-Wert ist positiv.

Short-Einstieg

  1. Der Schlusskurs der vorherigen Kerze liegt über oder auf dem oberen Bollinger-Band.
  2. Der vorherige Schlusskurs liegt unter der SMA über 100 Perioden (Abwärtstrend).
  3. Der Force-Index-Wert ist negativ.

Ausstiege

  • Wenn der durchschnittliche nicht realisierte Gewinn pro Kontrakt über alle offenen Positionen AverageProfitTarget erreicht, werden alle Positionen zum Marktpreis geschlossen.
  • Nach jeder flachen Position passt die Strategie das nächste Ordervolumen an: Multiplikation mit LossMultiplier nach einem Verlustzyklus, Rücksetzung auf BaseVolume nach einem profitablen Zyklus.

Hinweise

  • Die Martingal-Anpassung nutzt realisierten PnL, um Verlustserien zu erkennen; verwenden Sie die Strategie nur auf Instrumenten, bei denen steigendes Volumen akzeptabel ist.
  • Da StockSharp-Strategien mit Nettopositionen arbeiten, wird das Hedging der MQL-Version (gleichzeitige Long- und Short-Körbe) über aggregierte Positionen angenähert.
using System;
using System.Collections.Generic;
using System.Linq;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// JB: Bollinger breakout with long-term SMA, Force Index and martingale sizing.
/// </summary>
public class JbStrategy : Strategy
{
	private readonly StrategyParam<int> _smaPeriod;
	private readonly StrategyParam<int> _forcePeriod;
	private readonly StrategyParam<int> _bollingerPeriod;
	private readonly StrategyParam<decimal> _bollingerDeviation;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _lossMultiplier;
	private readonly StrategyParam<decimal> _averageProfitTarget;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closes = [];
	private decimal? _forceEma;
	private decimal? _lastClose;

	private decimal? _prevClose;
	private decimal? _prevSma;
	private decimal? _prevForce;
	private decimal? _prevLower;
	private decimal? _prevUpper;

	private decimal _nextVolume;
	private decimal _cycleRealizedStart;
	private bool _cycleActive;

	public int SmaPeriod { get => _smaPeriod.Value; set => _smaPeriod.Value = value; }
	public int ForcePeriod { get => _forcePeriod.Value; set => _forcePeriod.Value = value; }
	public int BollingerPeriod { get => _bollingerPeriod.Value; set => _bollingerPeriod.Value = value; }
	public decimal BollingerDeviation { get => _bollingerDeviation.Value; set => _bollingerDeviation.Value = value; }
	public decimal BaseVolume { get => _baseVolume.Value; set => _baseVolume.Value = value; }
	public decimal LossMultiplier { get => _lossMultiplier.Value; set => _lossMultiplier.Value = value; }
	public decimal AverageProfitTarget { get => _averageProfitTarget.Value; set => _averageProfitTarget.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public JbStrategy()
	{
		_smaPeriod = Param(nameof(SmaPeriod), 100).SetGreaterThanZero();
		_forcePeriod = Param(nameof(ForcePeriod), 100).SetGreaterThanZero();
		_bollingerPeriod = Param(nameof(BollingerPeriod), 20).SetGreaterThanZero();
		_bollingerDeviation = Param(nameof(BollingerDeviation), 2m).SetGreaterThanZero();
		_baseVolume = Param(nameof(BaseVolume), 0.1m).SetGreaterThanZero();
		_lossMultiplier = Param(nameof(LossMultiplier), 1.55m).SetGreaterThanZero();
		_averageProfitTarget = Param(nameof(AverageProfitTarget), 2.8m).SetNotNegative();
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame());
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	protected override void OnReseted()
	{
		base.OnReseted();
		_closes.Clear();
		_forceEma = null;
		_lastClose = null;
		_prevClose = null;
		_prevSma = null;
		_prevForce = null;
		_prevLower = null;
		_prevUpper = null;
		_nextVolume = BaseVolume;
		_cycleRealizedStart = 0m;
		_cycleActive = false;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_nextVolume = NormalizeVolume(BaseVolume);
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (Position != 0m)
		{
			var perContract = Math.Abs(Position) > 0m
				? PnLManager.UnrealizedPnL / Math.Abs(Position)
				: 0m;

			if (perContract >= AverageProfitTarget)
			{
				if (Position > 0m)
					SellMarket(Math.Abs(Position));
				else
					BuyMarket(Math.Abs(Position));

				UpdateIndicators(candle);
				return;
			}
		}
		else if (_prevClose is decimal prevClose &&
			_prevSma is decimal prevSma &&
			_prevForce is decimal prevForce &&
			_prevLower is decimal prevLower &&
			_prevUpper is decimal prevUpper)
		{
			var signal = GetSignal(prevClose, prevSma, prevForce, prevLower, prevUpper);

			if (signal > 0)
				Enter(Sides.Buy);
			else if (signal < 0)
				Enter(Sides.Sell);
		}

		UpdateIndicators(candle);
	}

	private void UpdateIndicators(ICandleMessage candle)
	{
		if (_lastClose is decimal last)
		{
			var rawForce = (candle.ClosePrice - last) * candle.TotalVolume;
			_forceEma = Ema(_forceEma, rawForce, ForcePeriod);
		}

		_lastClose = candle.ClosePrice;
		_closes.Add(candle.ClosePrice);

		var keep = Math.Max(SmaPeriod, BollingerPeriod);
		if (_closes.Count > keep)
			_closes.RemoveRange(0, _closes.Count - keep);

		if (_closes.Count < SmaPeriod || _closes.Count < BollingerPeriod || _forceEma is null)
			return;

		var sma = _closes.Skip(_closes.Count - SmaPeriod).Average();
		var bb = _closes.Skip(_closes.Count - BollingerPeriod).ToArray();
		var mean = bb.Average();
		var variance = bb.Select(v => (v - mean) * (v - mean)).Average();
		var std = (decimal)Math.Sqrt((double)variance);

		_prevClose = candle.ClosePrice;
		_prevSma = sma;
		_prevForce = _forceEma.Value;
		_prevLower = mean - BollingerDeviation * std;
		_prevUpper = mean + BollingerDeviation * std;
	}

	private void Enter(Sides side)
	{
		var volume = NormalizeVolume(_nextVolume);
		if (volume <= 0m)
			return;

		_cycleRealizedStart = PnLManager.RealizedPnL;
		_cycleActive = true;

		if (side == Sides.Buy)
			BuyMarket(volume);
		else
			SellMarket(volume);
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (!_cycleActive || Position != 0m)
			return;

		var cyclePnL = PnLManager.RealizedPnL - _cycleRealizedStart;
		_nextVolume = cyclePnL < 0m
			? NormalizeVolume(_nextVolume * LossMultiplier)
			: NormalizeVolume(BaseVolume);
		_cycleActive = false;
	}

	internal static int GetSignal(decimal previousClose, decimal sma, decimal force, decimal lowerBand, decimal upperBand)
	{
		if (previousClose <= lowerBand && previousClose > sma && force > 0m)
			return 1;

		if (previousClose >= upperBand && previousClose < sma && force < 0m)
			return -1;

		return 0;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (Security?.MaxVolume is decimal max && max > 0m)
			volume = Math.Min(volume, max);
		if (Security?.MinVolume is decimal min && min > 0m)
			volume = Math.Max(volume, min);
		if (Security?.VolumeStep is decimal step && step > 0m)
			volume = Math.Floor(volume / step) * step;

		return volume;
	}

	private static decimal Ema(decimal? previous, decimal value, int period)
	{
		if (previous is null)
			return value;

		var alpha = 2m / (period + 1m);
		return previous.Value + alpha * (value - previous.Value);
	}
}