Стратегия BabyShark VWAP
Стратегия сочетает полосы вокруг объёмно-взвешенной средней цены (VWAP) и индикатор RSI, рассчитанный по OBV. Длинные сделки открываются при уходе цены ниже нижней полосы и сигнале перепроданности RSI. Короткие позиции появляются, когда цена поднимается выше верхней полосы и RSI указывает на перекупленность.
Стопы основаны на небольшом процентном убытке, а перед повторным входом выдерживается пауза.
Детали
- Условия входа: Пересечение ценой полос с подтверждением RSI.
- Длинные/Короткие: Оба направления.
- Условия выхода: Возврат к VWAP или срабатывание стопа.
- Стопы: Да.
- Значения по умолчанию:
Length= 60RsiLength= 5HigherLevel= 70LowerLevel= 30Cooldown= 10StopLossPercent= 0.6mCandleType= TimeSpan.FromMinutes(1)
- Фильтры:
- Категория: Средневозврат
- Направление: Оба
- Индикаторы: VWAP, RSI, OBV
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: Внутридневной
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
BAND_DEVIATIONS = 2.0
class baby_shark_vwap_strategy(Strategy):
"""
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.
"""
def __init__(self):
super(baby_shark_vwap_strategy, self).__init__()
self._length = self.Param("Length", 60) \
.SetGreaterThanZero() \
.SetDisplay("Length", "Rolling VWAP window", "VWAP")
self._rsi_length = self.Param("RsiLength", 5) \
.SetGreaterThanZero() \
.SetDisplay("RSI Length", "RSI period applied to OBV", "RSI")
self._higher_level = self.Param("HigherLevel", 70.0) \
.SetDisplay("Higher Level", "OBV RSI level that confirms shorts", "RSI")
self._lower_level = self.Param("LowerLevel", 30.0) \
.SetDisplay("Lower Level", "OBV RSI level that confirms longs", "RSI")
self._cooldown = self.Param("Cooldown", 10) \
.SetNotNegative() \
.SetDisplay("Cooldown", "Candles to wait after a position closes", "Trading")
self._stop_loss_percent = self.Param("StopLossPercent", 0.6) \
.SetNotNegative() \
.SetDisplay("Stop Loss %", "Stop-loss percentage", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._obv_rsi = None
self._reset_state()
@property
def CandleType(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def _reset_state(self):
self._window = []
self._obv = 0.0
self._prev_close = None
self._bar_index = 0
self._last_exit_bar = None
self._was_in_position = False
def OnReseted(self):
super(baby_shark_vwap_strategy, self).OnReseted()
self._obv_rsi = None
self._reset_state()
def OnStarted2(self, time):
super(baby_shark_vwap_strategy, self).OnStarted2(time)
self._reset_state()
self._obv_rsi = RelativeStrengthIndex()
self._obv_rsi.Length = self._rsi_length.Value
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
stop = float(self._stop_loss_percent.Value)
self.StartProtection(Unit(), Unit(Decimal(stop), UnitTypes.Percent) if stop > 0 else Unit(), useMarketOrders=True)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._bar_index += 1
close = float(candle.ClosePrice)
volume = float(candle.TotalVolume)
if self._prev_close is not None:
if close > self._prev_close:
self._obv += volume
elif close < self._prev_close:
self._obv -= volume
self._prev_close = close
obv_rsi_value = process_float(self._obv_rsi, self._obv, candle.ServerTime, True)
typical = (float(candle.HighPrice) + float(candle.LowPrice) + close) / 3.0
self._window.append((typical, volume))
length = self._length.Value
if len(self._window) > length:
self._window.pop(0)
# A position that has disappeared since the last candle was closed, by the stop or by an exit.
if self._was_in_position and self.Position == 0:
self._last_exit_bar = self._bar_index
self._was_in_position = self.Position != 0
if len(self._window) < length or not self._obv_rsi.IsFormed or obv_rsi_value.IsEmpty:
return
obv_rsi = float(to_decimal(obv_rsi_value))
sum_pv = sum(p * v for p, v in self._window)
sum_v = sum(v for _, v in self._window)
if sum_v <= 0:
return
vwap = sum_pv / sum_v
sum_sq = sum(v * (p - vwap) * (p - vwap) for p, v in self._window)
if not self.IsFormedAndOnlineAndAllowTrading():
return
deviation = math.sqrt(max(0.0, sum_sq / sum_v))
upper = vwap + BAND_DEVIATIONS * deviation
lower = vwap - BAND_DEVIATIONS * deviation
if self.Position > 0:
if close >= vwap:
self.SellMarket(self.Position)
self._last_exit_bar = self._bar_index
return
if self.Position < 0:
if close <= vwap:
self.BuyMarket(-self.Position)
self._last_exit_bar = self._bar_index
return
if self._last_exit_bar is not None and self._bar_index - self._last_exit_bar < self._cooldown.Value:
return
if close < lower and obv_rsi < float(self._lower_level.Value):
self.BuyMarket(self.Volume)
self._was_in_position = True
elif close > upper and obv_rsi > float(self._higher_level.Value):
self.SellMarket(self.Volume)
self._was_in_position = True
def CreateClone(self):
return baby_shark_vwap_strategy()