Auf GitHub ansehen

BabyShark VWAP-Strategie

Diese Strategie kombiniert ein volumengewichtetes Durchschnittspreis-Band (VWAP) mit einem OBV-basierten RSI-Filter. Long-Trades entstehen, wenn der Preis unter das untere Abweichungsband fällt und der RSI überverkauft signalisiert. Short-Trades werden ausgelöst, wenn der Preis über das obere Band steigt und der RSI überkauft ist.

Stops verwenden einen kleinen prozentualen Verlust, und Positionen warten eine Abkühlungsperiode vor dem Wiedereinstieg ab.

Details

  • Einstiegskriterien: Preis kreuzt Abweichungsbänder mit RSI-Bestätigung.
  • Long/Short: Beide Richtungen.
  • Ausstiegskriterien: Rückkehr zum VWAP oder Stop-Loss.
  • Stops: Ja.
  • Standardwerte:
    • Length = 60
    • RsiLength = 5
    • HigherLevel = 70
    • LowerLevel = 30
    • Cooldown = 10
    • StopLossPercent = 0.6m
    • CandleType = TimeSpan.FromMinutes(1)
  • Filter:
    • Kategorie: Mean Reversion
    • Richtung: Beide
    • Indikatoren: VWAP, RSI, OBV
    • Stops: Ja
    • Komplexität: Mittel
    • Zeitrahmen: Intraday
    • Saisonalität: Nein
    • Neuronale Netze: Nein
    • Divergenz: Nein
    • Risikolevel: Mittel
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// BabyShark VWAP strategy.
/// A rolling VWAP of the typical price over Length candles carries bands two volume-weighted standard deviations away, and an
/// RSI of On-Balance Volume confirms extremes. A close below the lower band with OBV RSI below LowerLevel goes long and a close
/// above the upper band with OBV RSI above HigherLevel goes short. A long closes when price returns to the VWAP from below and
/// a short when it returns from above; a StopLossPercent stop protects both. After a position is closed no new entry is
/// taken for Cooldown candles.
/// </summary>
public class BabySharkVwapStrategy : Strategy
{
	private const decimal _bandDeviations = 2m;

	private readonly StrategyParam<int> _length;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<decimal> _higherLevel;
	private readonly StrategyParam<decimal> _lowerLevel;
	private readonly StrategyParam<int> _cooldown;
	private readonly StrategyParam<decimal> _stopLossPercent;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Queue<(decimal price, decimal volume)> _window = new();
	private RelativeStrengthIndex _obvRsi;
	private decimal _obv;
	private decimal? _prevClose;
	private int _barIndex;
	private int? _lastExitBar;
	private bool _wasInPosition;

	/// <summary>
	/// Rolling VWAP window.
	/// </summary>
	public int Length
	{
		get => _length.Value;
		set => _length.Value = value;
	}

	/// <summary>
	/// RSI period applied to OBV.
	/// </summary>
	public int RsiLength
	{
		get => _rsiLength.Value;
		set => _rsiLength.Value = value;
	}

	/// <summary>
	/// OBV RSI level that confirms shorts.
	/// </summary>
	public decimal HigherLevel
	{
		get => _higherLevel.Value;
		set => _higherLevel.Value = value;
	}

	/// <summary>
	/// OBV RSI level that confirms longs.
	/// </summary>
	public decimal LowerLevel
	{
		get => _lowerLevel.Value;
		set => _lowerLevel.Value = value;
	}

	/// <summary>
	/// Candles to wait after a position closes.
	/// </summary>
	public int Cooldown
	{
		get => _cooldown.Value;
		set => _cooldown.Value = value;
	}

	/// <summary>
	/// Stop-loss percentage.
	/// </summary>
	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}

	/// <summary>
	/// Candle type.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Constructor.
	/// </summary>
	public BabySharkVwapStrategy()
	{
		_length = Param(nameof(Length), 60)
			.SetGreaterThanZero()
			.SetDisplay("Length", "Rolling VWAP window", "VWAP");

		_rsiLength = Param(nameof(RsiLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("RSI Length", "RSI period applied to OBV", "RSI");

		_higherLevel = Param(nameof(HigherLevel), 70m)
			.SetDisplay("Higher Level", "OBV RSI level that confirms shorts", "RSI");

		_lowerLevel = Param(nameof(LowerLevel), 30m)
			.SetDisplay("Lower Level", "OBV RSI level that confirms longs", "RSI");

		_cooldown = Param(nameof(Cooldown), 10)
			.SetNotNegative()
			.SetDisplay("Cooldown", "Candles to wait after a position closes", "Trading");

		_stopLossPercent = Param(nameof(StopLossPercent), 0.6m)
			.SetNotNegative()
			.SetDisplay("Stop Loss %", "Stop-loss percentage", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_obvRsi = null;
		ResetState();
	}

	private void ResetState()
	{
		_window.Clear();
		_obv = 0m;
		_prevClose = null;
		_barIndex = 0;
		_lastExitBar = null;
		_wasInPosition = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		ResetState();

		_obvRsi = new RelativeStrengthIndex { Length = RsiLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		StartProtection(new Unit(), StopLossPercent > 0m ? new Unit(StopLossPercent, UnitTypes.Percent) : new Unit(), useMarketOrders: true);

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

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

		_barIndex++;

		var close = candle.ClosePrice;
		var volume = candle.TotalVolume;

		if (_prevClose is decimal pc)
			_obv += close > pc ? volume : close < pc ? -volume : 0m;
		_prevClose = close;

		var obvRsiValue = _obvRsi.Process(_obv, candle.ServerTime, true);

		var typical = (candle.HighPrice + candle.LowPrice + close) / 3m;
		_window.Enqueue((typical, volume));
		if (_window.Count > Length)
			_window.Dequeue();

		// A position that has disappeared since the last candle was closed, by the stop or by an exit.
		if (_wasInPosition && Position == 0)
			_lastExitBar = _barIndex;
		_wasInPosition = Position != 0;

		if (_window.Count < Length || !_obvRsi.IsFormed || obvRsiValue.IsEmpty)
			return;

		var obvRsi = obvRsiValue.ToDecimal();

		var sumPv = 0m;
		var sumV = 0m;
		foreach (var (price, v) in _window)
		{
			sumPv += price * v;
			sumV += v;
		}

		if (sumV <= 0m)
			return;

		var vwap = sumPv / sumV;
		var sumSq = 0m;
		foreach (var (price, v) in _window)
			sumSq += v * (price - vwap) * (price - vwap);

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var variance = sumSq / sumV;
		var deviation = (decimal)Math.Sqrt((double)variance);
		var upper = vwap + _bandDeviations * deviation;
		var lower = vwap - _bandDeviations * deviation;

		if (Position > 0)
		{
			if (close >= vwap)
			{
				SellMarket(Position);
				_lastExitBar = _barIndex;
			}

			return;
		}

		if (Position < 0)
		{
			if (close <= vwap)
			{
				BuyMarket(-Position);
				_lastExitBar = _barIndex;
			}

			return;
		}

		if (_lastExitBar is int last && _barIndex - last < Cooldown)
			return;

		if (close < lower && obvRsi < LowerLevel)
		{
			BuyMarket(Volume);
			_wasInPosition = true;
		}
		else if (close > upper && obvRsi > HigherLevel)
		{
			SellMarket(Volume);
			_wasInPosition = true;
		}
	}
}