Color Schaff RVI Trend Cycle 策略
该策略在 StockSharp 高级 API 中实现 Color Schaff RVI Trend Cycle 指标。该指标对快速和慢速 RVI 的差值应用双重随机过程并对结果进行平滑处理。
参数
FastRviLength– 快速 RVI 的周期(默认 23)。SlowRviLength– 慢速 RVI 的周期(默认 50)。CycleLength– 随机循环长度(默认 10)。HighLevel– 判定多头条件的上阈值(默认 60)。LowLevel– 判定空头条件的下阈值(默认 -60)。CandleType– 策略使用的 K 线类型(默认 4 小时)。
交易逻辑
- 计算快速和慢速 RVI。
- 基于二者差值构建 Schaff Trend Cycle。
- 当 STC 值高于上阈值并且上升时 买入。
- 当 STC 值低于下阈值并且下降时 卖出。
说明
- 策略只处理已完成的 K 线。
- 启动时开启仓位保护。
- 本示例仅供学习参考,不构成投资建议。
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()