Ver en GitHub

Estrategia JB

Resumen

La estrategia JB procede de un asesor experto de fxDreema que combina filtros de tendencia de largo plazo, confirmación de impulso y rupturas de volatilidad:

  • Filtro de tendencia: exige que el cierre de la vela anterior permanezca por encima (largos) o por debajo (cortos) de una media móvil simple de 100 periodos.
  • Filtro de impulso: confirma la dirección con un Force Index de 100 periodos (positivo para largos, negativo para cortos).
  • Disparador de volatilidad: entra cuando el cierre anterior perfora la banda de Bollinger correspondiente (20 periodos, desviación 2,0).
  • Gestión de posición: aumenta el volumen de la orden con un multiplicador de estilo martingala después de un ciclo perdedor y vuelve al tamaño base después de ciclos rentables.
  • Regla de salida: cierra todas las posiciones abiertas cuando el beneficio no realizado medio por contrato alcanza un objetivo monetario configurable.

Parámetros

Nombre Descripción
SmaPeriod Longitud del filtro de tendencia SMA. Predeterminado: 100.
ForcePeriod Longitud del indicador Force Index. Predeterminado: 100.
BollingerPeriod Longitud de las bandas de Bollinger. Predeterminado: 20.
BollingerDeviation Multiplicador de desviación estándar para las bandas de Bollinger. Predeterminado: 2,0.
BaseVolume Volumen inicial de orden antes de los ajustes de martingala. Predeterminado: 0,1.
LossMultiplier Multiplicador aplicado al siguiente volumen de orden después de un ciclo perdedor. Predeterminado: 1,55.
AverageProfitTarget Beneficio no realizado medio por contrato necesario para cerrar todas las posiciones. Predeterminado: 2,8.
CandleType Tipo de vela utilizado para los cálculos (por defecto, marco temporal de 1 minuto).

Señales

Entrada larga

  1. El cierre de la vela anterior está por debajo o igual a la banda de Bollinger inferior.
  2. El cierre anterior es mayor que la SMA de 100 periodos (tendencia al alza).
  3. El valor del Force Index es positivo.

Entrada corta

  1. El cierre de la vela anterior está por encima o igual a la banda de Bollinger superior.
  2. El cierre anterior es menor que la SMA de 100 periodos (tendencia a la baja).
  3. El valor del Force Index es negativo.

Salidas

  • Cuando el beneficio no realizado medio por contrato de todas las posiciones abiertas alcanza AverageProfitTarget, todas las posiciones se cierran a mercado.
  • Después de cada posición plana, la estrategia ajusta el siguiente volumen de orden: lo multiplica por LossMultiplier tras un ciclo perdedor y lo reinicia a BaseVolume tras un ciclo rentable.

Notas

  • La adaptación de martingala usa el PnL realizado para decidir cuándo se produjo una racha de pérdidas; asegúrese de usar la estrategia solo en instrumentos donde sea aceptable aumentar el volumen.
  • Como las estrategias de StockSharp trabajan con posiciones netas, la cobertura de la versión MQL (cestas largas y cortas simultáneas) se aproxima mediante posiciones agregadas.
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);
	}
}