Стратегия Color Schaff RVI Trend Cycle
Данная стратегия реализует индикатор Color Schaff RVI Trend Cycle на высокоуровневом API StockSharp. Индикатор применяет двойной стохастический расчёт к разнице между быстрым и медленным RVI и дополнительно сглаживает результат.
Параметры
FastRviLength– период быстрого RVI (по умолчанию 23).SlowRviLength– период медленного RVI (по умолчанию 50).CycleLength– длина стохастического цикла (по умолчанию 10).HighLevel– верхний порог для определения бычьих условий (по умолчанию 60).LowLevel– нижний порог для определения медвежьих условий (по умолчанию -60).CandleType– тип обрабатываемых свечей (по умолчанию таймфрейм 4 часа).
Логика торговли
- Вычисляются быстрый и медленный RVI.
- На основе их разницы строится Schaff Trend Cycle.
- Покупка, когда значение STC выше верхнего уровня и растёт.
- Продажа, когда значение STC ниже нижнего уровня и снижается.
Примечания
- Стратегия обрабатывает только завершённые свечи.
- При старте включается защита позиций.
- Пример приведён исключительно в учебных целях и не является инвестиционной рекомендацией.
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Trading strategy based on a Schaff-style cycle built from fast and slow RVI averages.
/// </summary>
public class ColorSchaffRviTrendCycleStrategy : Strategy
{
private readonly StrategyParam<int> _fastRviLength;
private readonly StrategyParam<int> _slowRviLength;
private readonly StrategyParam<int> _cycleLength;
private readonly StrategyParam<int> _highLevel;
private readonly StrategyParam<int> _lowLevel;
private readonly StrategyParam<int> _signalCooldownBars;
private readonly StrategyParam<DataType> _candleType;
private readonly List<ICandleMessage> _recentCandles = [];
private readonly Queue<decimal> _fastWindow = [];
private readonly Queue<decimal> _slowWindow = [];
private readonly List<decimal> _macd = [];
private readonly List<decimal> _st = [];
private decimal _fastSum;
private decimal _slowSum;
private bool _stReady;
private bool _stcReady;
private decimal _prevSt;
private decimal _prevStc;
private int _cooldownRemaining;
/// <summary>
/// Fast RVI smoothing length.
/// </summary>
public int FastRviLength
{
get => _fastRviLength.Value;
set => _fastRviLength.Value = value;
}
/// <summary>
/// Slow RVI smoothing length.
/// </summary>
public int SlowRviLength
{
get => _slowRviLength.Value;
set => _slowRviLength.Value = value;
}
/// <summary>
/// Cycle length for stochastic calculations.
/// </summary>
public int CycleLength
{
get => _cycleLength.Value;
set => _cycleLength.Value = value;
}
/// <summary>
/// Upper threshold for the cycle.
/// </summary>
public int HighLevel
{
get => _highLevel.Value;
set => _highLevel.Value = value;
}
/// <summary>
/// Lower threshold for the cycle.
/// </summary>
public int LowLevel
{
get => _lowLevel.Value;
set => _lowLevel.Value = value;
}
/// <summary>
/// Bars to wait between reversals.
/// </summary>
public int SignalCooldownBars
{
get => _signalCooldownBars.Value;
set => _signalCooldownBars.Value = value;
}
/// <summary>
/// Candle type used for processing.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="ColorSchaffRviTrendCycleStrategy"/>.
/// </summary>
public ColorSchaffRviTrendCycleStrategy()
{
_fastRviLength = Param(nameof(FastRviLength), 23)
.SetGreaterThanZero()
.SetDisplay("Fast RVI Length", "Smoothing length for fast RVI", "General");
_slowRviLength = Param(nameof(SlowRviLength), 50)
.SetGreaterThanZero()
.SetDisplay("Slow RVI Length", "Smoothing length for slow RVI", "General");
_cycleLength = Param(nameof(CycleLength), 10)
.SetGreaterThanZero()
.SetDisplay("Cycle", "Length of the stochastic cycle", "General");
_highLevel = Param(nameof(HighLevel), 60)
.SetDisplay("High Level", "Upper threshold", "General");
_lowLevel = Param(nameof(LowLevel), -60)
.SetDisplay("Low Level", "Lower threshold", "General");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 6)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait between reversals", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_recentCandles.Clear();
_fastWindow.Clear();
_slowWindow.Clear();
_macd.Clear();
_st.Clear();
_fastSum = 0m;
_slowSum = 0m;
_stReady = false;
_stcReady = false;
_prevSt = 0m;
_prevStc = 0m;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_recentCandles.Clear();
_fastWindow.Clear();
_slowWindow.Clear();
_macd.Clear();
_st.Clear();
_fastSum = 0m;
_slowSum = 0m;
_stReady = false;
_stcReady = false;
_prevSt = 0m;
_prevStc = 0m;
_cooldownRemaining = 0;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
_recentCandles.Add(candle);
if (_recentCandles.Count > 4)
_recentCandles.RemoveAt(0);
if (_recentCandles.Count < 4)
return;
var rawRvi = CalculateRawRvi();
UpdateWindow(_fastWindow, ref _fastSum, rawRvi, FastRviLength);
UpdateWindow(_slowWindow, ref _slowSum, rawRvi, SlowRviLength);
if (_fastWindow.Count < FastRviLength || _slowWindow.Count < SlowRviLength)
return;
var fast = _fastSum / _fastWindow.Count;
var slow = _slowSum / _slowWindow.Count;
var macd = fast - slow;
AddValue(_macd, macd, CycleLength);
if (_macd.Count < CycleLength)
return;
GetMinMax(_macd, out var minMacd, out var maxMacd);
var st = maxMacd == minMacd ? _prevSt : (macd - minMacd) / (maxMacd - minMacd) * 100m;
if (_stReady)
st = 0.5m * (st - _prevSt) + _prevSt;
else
_stReady = true;
_prevSt = st;
AddValue(_st, st, CycleLength);
GetMinMax(_st, out var minSt, out var maxSt);
var previousStc = _prevStc;
var stc = maxSt == minSt ? previousStc : (st - minSt) / (maxSt - minSt) * 200m - 100m;
if (_stcReady)
stc = 0.5m * (stc - previousStc) + previousStc;
else
_stcReady = true;
_prevStc = stc;
var delta = stc - previousStc;
var longEntry = previousStc <= HighLevel && stc > HighLevel && delta > 0m;
var shortEntry = previousStc >= LowLevel && stc < LowLevel && delta < 0m;
var longExit = Position > 0 && stc < 0m;
var shortExit = Position < 0 && stc > 0m;
if (longExit)
{
SellMarket(Position);
_cooldownRemaining = SignalCooldownBars;
}
else if (shortExit)
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
else if (_cooldownRemaining == 0 && longEntry && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
else if (_cooldownRemaining == 0 && shortEntry && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
}
private decimal CalculateRawRvi()
{
var c0 = _recentCandles[0];
var c1 = _recentCandles[1];
var c2 = _recentCandles[2];
var c3 = _recentCandles[3];
var valueUp = ((c0.ClosePrice - c0.OpenPrice) +
2m * (c1.ClosePrice - c1.OpenPrice) +
2m * (c2.ClosePrice - c2.OpenPrice) +
(c3.ClosePrice - c3.OpenPrice)) / 6m;
var valueDn = ((c0.HighPrice - c0.LowPrice) +
2m * (c1.HighPrice - c1.LowPrice) +
2m * (c2.HighPrice - c2.LowPrice) +
(c3.HighPrice - c3.LowPrice)) / 6m;
return valueDn == 0m ? valueUp : valueUp / valueDn;
}
private static void UpdateWindow(Queue<decimal> window, ref decimal sum, decimal value, int length)
{
window.Enqueue(value);
sum += value;
while (window.Count > length)
sum -= window.Dequeue();
}
private static void AddValue(List<decimal> values, decimal value, int limit)
{
values.Add(value);
if (values.Count > limit)
values.RemoveAt(0);
}
private static void GetMinMax(List<decimal> values, out decimal min, out decimal max)
{
min = values[0];
max = values[0];
for (var i = 1; i < values.Count; i++)
{
var value = values[i];
if (value < min)
min = value;
if (value > max)
max = value;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import Math, TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from collections import deque
class color_schaff_rvi_trend_cycle_strategy(Strategy):
def __init__(self):
super(color_schaff_rvi_trend_cycle_strategy, self).__init__()
self._fast_rvi_length = self.Param("FastRviLength", 23) \
.SetDisplay("Fast RVI Length", "Smoothing length for fast RVI", "General")
self._slow_rvi_length = self.Param("SlowRviLength", 50) \
.SetDisplay("Slow RVI Length", "Smoothing length for slow RVI", "General")
self._cycle_length = self.Param("CycleLength", 10) \
.SetDisplay("Cycle", "Length of the stochastic cycle", "General")
self._high_level = self.Param("HighLevel", 60) \
.SetDisplay("High Level", "Upper threshold", "General")
self._low_level = self.Param("LowLevel", -60) \
.SetDisplay("Low Level", "Lower threshold", "General")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 6) \
.SetDisplay("Signal Cooldown", "Bars to wait between reversals", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._recent_candles = []
self._fast_window = deque()
self._slow_window = deque()
self._macd_history = []
self._st_history = []
self._fast_sum = 0.0
self._slow_sum = 0.0
self._st_ready = False
self._stc_ready = False
self._prev_st = 0.0
self._prev_stc = 0.0
self._cooldown_remaining = 0
@property
def FastRviLength(self):
return self._fast_rvi_length.Value
@FastRviLength.setter
def FastRviLength(self, value):
self._fast_rvi_length.Value = value
@property
def SlowRviLength(self):
return self._slow_rvi_length.Value
@SlowRviLength.setter
def SlowRviLength(self, value):
self._slow_rvi_length.Value = value
@property
def CycleLength(self):
return self._cycle_length.Value
@CycleLength.setter
def CycleLength(self, value):
self._cycle_length.Value = value
@property
def HighLevel(self):
return self._high_level.Value
@HighLevel.setter
def HighLevel(self, value):
self._high_level.Value = value
@property
def LowLevel(self):
return self._low_level.Value
@LowLevel.setter
def LowLevel(self, value):
self._low_level.Value = value
@property
def SignalCooldownBars(self):
return self._signal_cooldown_bars.Value
@SignalCooldownBars.setter
def SignalCooldownBars(self, value):
self._signal_cooldown_bars.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(color_schaff_rvi_trend_cycle_strategy, self).OnStarted2(time)
self._recent_candles = []
self._fast_window = deque()
self._slow_window = deque()
self._macd_history = []
self._st_history = []
self._fast_sum = 0.0
self._slow_sum = 0.0
self._st_ready = False
self._stc_ready = False
self._prev_st = 0.0
self._prev_stc = 0.0
self._cooldown_remaining = 0
self.SubscribeCandles(self.CandleType) \
.Bind(self.ProcessCandle) \
.Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
self._recent_candles.append(candle)
if len(self._recent_candles) > 4:
self._recent_candles.pop(0)
if len(self._recent_candles) < 4:
return
raw_rvi = self._calculate_raw_rvi()
fast_len = self.FastRviLength
slow_len = self.SlowRviLength
cycle = self.CycleLength
self._fast_window.append(raw_rvi)
self._fast_sum += raw_rvi
while len(self._fast_window) > fast_len:
self._fast_sum -= self._fast_window.popleft()
self._slow_window.append(raw_rvi)
self._slow_sum += raw_rvi
while len(self._slow_window) > slow_len:
self._slow_sum -= self._slow_window.popleft()
if len(self._fast_window) < fast_len or len(self._slow_window) < slow_len:
return
fast = self._fast_sum / len(self._fast_window)
slow = self._slow_sum / len(self._slow_window)
macd = fast - slow
self._add_value(self._macd_history, macd, cycle)
if len(self._macd_history) < cycle:
return
min_macd, max_macd = self._get_min_max(self._macd_history)
if max_macd == min_macd:
st = self._prev_st
else:
st = (macd - min_macd) / (max_macd - min_macd) * 100.0
if self._st_ready:
st = 0.5 * (st - self._prev_st) + self._prev_st
else:
self._st_ready = True
self._prev_st = st
self._add_value(self._st_history, st, cycle)
min_st, max_st = self._get_min_max(self._st_history)
previous_stc = self._prev_stc
if max_st == min_st:
stc = previous_stc
else:
stc = (st - min_st) / (max_st - min_st) * 200.0 - 100.0
if self._stc_ready:
stc = 0.5 * (stc - previous_stc) + previous_stc
else:
self._stc_ready = True
self._prev_stc = stc
delta = stc - previous_stc
high = float(self.HighLevel)
low = float(self.LowLevel)
long_entry = previous_stc <= high and stc > high and delta > 0
short_entry = previous_stc >= low and stc < low and delta < 0
long_exit = self.Position > 0 and stc < 0
short_exit = self.Position < 0 and stc > 0
if long_exit:
self.SellMarket(self.Position)
self._cooldown_remaining = self.SignalCooldownBars
elif short_exit:
self.BuyMarket(abs(self.Position))
self._cooldown_remaining = self.SignalCooldownBars
elif self._cooldown_remaining == 0 and long_entry and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._cooldown_remaining = self.SignalCooldownBars
elif self._cooldown_remaining == 0 and short_entry and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._cooldown_remaining = self.SignalCooldownBars
def _calculate_raw_rvi(self):
c0 = self._recent_candles[0]
c1 = self._recent_candles[1]
c2 = self._recent_candles[2]
c3 = self._recent_candles[3]
value_up = ((float(c0.ClosePrice) - float(c0.OpenPrice)) +
2.0 * (float(c1.ClosePrice) - float(c1.OpenPrice)) +
2.0 * (float(c2.ClosePrice) - float(c2.OpenPrice)) +
(float(c3.ClosePrice) - float(c3.OpenPrice))) / 6.0
value_dn = ((float(c0.HighPrice) - float(c0.LowPrice)) +
2.0 * (float(c1.HighPrice) - float(c1.LowPrice)) +
2.0 * (float(c2.HighPrice) - float(c2.LowPrice)) +
(float(c3.HighPrice) - float(c3.LowPrice))) / 6.0
if value_dn == 0:
return value_up
return value_up / value_dn
def _add_value(self, values, value, limit):
values.append(value)
if len(values) > limit:
values.pop(0)
def _get_min_max(self, values):
min_val = values[0]
max_val = values[0]
for i in range(1, len(values)):
val = values[i]
if val < min_val:
min_val = val
if val > max_val:
max_val = val
return min_val, max_val
def OnReseted(self):
super(color_schaff_rvi_trend_cycle_strategy, self).OnReseted()
self._recent_candles = []
self._fast_window = deque()
self._slow_window = deque()
self._macd_history = []
self._st_history = []
self._fast_sum = 0.0
self._slow_sum = 0.0
self._st_ready = False
self._stc_ready = False
self._prev_st = 0.0
self._prev_stc = 0.0
self._cooldown_remaining = 0
def CreateClone(self):
return color_schaff_rvi_trend_cycle_strategy()