Yeong RRG
Strategy based on normalized relative strength and momentum ratio (RRG).
The strategy enters long when both JDK RS and JDK RoC are above 100 and exits when both fall below 100.
Details
- Entry Criteria: JDK RS and JDK RoC above 100.
- Long/Short: Long only.
- Exit Criteria: JDK RS and JDK RoC below 100.
- Stops: No.
- Default Values:
Length= 14CandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Relative Strength
- Direction: Long
- Indicators: SMA, ROC, StandardDeviation
- Stops: No
- Complexity: Basic
- Timeframe: Intraday (5m)
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Yeong Relative Rotation Graph strategy.
/// Uses normalized relative strength (price vs SMA) and momentum
/// to classify market into quadrants and trade accordingly.
/// </summary>
public class YeongRrgStrategy : Strategy
{
private readonly StrategyParam<int> _length;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _rsRatioHistory = new();
private readonly List<decimal> _rmRatioHistory = new();
private decimal _prevRsRatio;
public int Length { get => _length.Value; set => _length.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public YeongRrgStrategy()
{
_length = Param(nameof(Length), 20)
.SetGreaterThanZero()
.SetDisplay("Length", "Period for calculations", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_rsRatioHistory.Clear();
_rmRatioHistory.Clear();
_prevRsRatio = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var sma = new SimpleMovingAverage { Length = Length };
_rsRatioHistory.Clear();
_rmRatioHistory.Clear();
_prevRsRatio = 0;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(sma, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal smaVal)
{
if (candle.State != CandleStates.Finished)
return;
if (smaVal <= 0)
return;
// RS ratio: price relative to its SMA (like relative strength vs benchmark)
var rsRatio = (candle.ClosePrice / smaVal) * 100m;
_rsRatioHistory.Add(rsRatio);
if (_rsRatioHistory.Count > Length * 3)
_rsRatioHistory.RemoveAt(0);
// RM ratio: momentum of RS ratio
decimal rmRatio;
if (_prevRsRatio > 0)
rmRatio = rsRatio - _prevRsRatio;
else
rmRatio = 0;
_prevRsRatio = rsRatio;
_rmRatioHistory.Add(rmRatio);
if (_rmRatioHistory.Count > Length * 3)
_rmRatioHistory.RemoveAt(0);
if (_rsRatioHistory.Count < Length || _rmRatioHistory.Count < Length)
return;
// Normalize RS ratio
var rsMean = _rsRatioHistory.Skip(_rsRatioHistory.Count - Length).Average();
var rsStd = StdDev(_rsRatioHistory.Skip(_rsRatioHistory.Count - Length));
if (rsStd == 0) rsStd = 1;
// Normalize RM ratio
var rmMean = _rmRatioHistory.Skip(_rmRatioHistory.Count - Length).Average();
var rmStd = StdDev(_rmRatioHistory.Skip(_rmRatioHistory.Count - Length));
if (rmStd == 0) rmStd = 1;
var jdkRs = 100m + ((rsRatio - rsMean) / rsStd);
var jdkRm = 100m + ((rmRatio - rmMean) / rmStd);
// Quadrant classification
// Green: RS > 100 && RM > 100 (leading)
// Red: RS < 100 && RM < 100 (lagging)
var buySignal = jdkRs > 100m && jdkRm > 100m;
var sellSignal = jdkRs < 100m && jdkRm < 100m;
if (buySignal && Position <= 0)
{
BuyMarket();
}
else if (sellSignal && Position >= 0)
{
SellMarket();
}
}
private static decimal StdDev(IEnumerable<decimal> values)
{
var list = values.ToList();
if (list.Count < 2) return 0;
var mean = list.Average();
var sumSq = list.Sum(v => (v - mean) * (v - mean));
return (decimal)Math.Sqrt((double)(sumSq / list.Count));
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class yeong_rrg_strategy(Strategy):
def __init__(self):
super(yeong_rrg_strategy, self).__init__()
self._length = self.Param("Length", 20) \
.SetDisplay("Length", "Period for calculations", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._rs_ratio_history = []
self._rm_ratio_history = []
self._prev_rs_ratio = 0.0
@property
def length(self):
return self._length.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(yeong_rrg_strategy, self).OnReseted()
self._rs_ratio_history = []
self._rm_ratio_history = []
self._prev_rs_ratio = 0.0
def OnStarted2(self, time):
super(yeong_rrg_strategy, self).OnStarted2(time)
sma = SimpleMovingAverage()
sma.Length = self.length
self._rs_ratio_history = []
self._rm_ratio_history = []
self._prev_rs_ratio = 0.0
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, self.on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def on_process(self, candle, sma_val):
if candle.State != CandleStates.Finished:
return
if sma_val <= 0:
return
rs_ratio = (float(candle.ClosePrice) / float(sma_val)) * 100.0
self._rs_ratio_history.append(rs_ratio)
if len(self._rs_ratio_history) > self.length * 3:
self._rs_ratio_history.pop(0)
if self._prev_rs_ratio > 0:
rm_ratio = rs_ratio - self._prev_rs_ratio
else:
rm_ratio = 0.0
self._prev_rs_ratio = rs_ratio
self._rm_ratio_history.append(rm_ratio)
if len(self._rm_ratio_history) > self.length * 3:
self._rm_ratio_history.pop(0)
if len(self._rs_ratio_history) < self.length or len(self._rm_ratio_history) < self.length:
return
rs_slice = self._rs_ratio_history[-self.length:]
rm_slice = self._rm_ratio_history[-self.length:]
rs_mean = sum(rs_slice) / len(rs_slice)
rs_std = self._std_dev(rs_slice)
if rs_std == 0:
rs_std = 1.0
rm_mean = sum(rm_slice) / len(rm_slice)
rm_std = self._std_dev(rm_slice)
if rm_std == 0:
rm_std = 1.0
jdk_rs = 100.0 + ((rs_ratio - rs_mean) / rs_std)
jdk_rm = 100.0 + ((rm_ratio - rm_mean) / rm_std)
buy_signal = jdk_rs > 100.0 and jdk_rm > 100.0
sell_signal = jdk_rs < 100.0 and jdk_rm < 100.0
if buy_signal and self.Position <= 0:
self.BuyMarket()
elif sell_signal and self.Position >= 0:
self.SellMarket()
def _std_dev(self, values):
if len(values) < 2:
return 0.0
mean = sum(values) / len(values)
sum_sq = 0.0
for v in values:
sum_sq += (v - mean) * (v - mean)
return Math.Sqrt(sum_sq / len(values))
def CreateClone(self):
return yeong_rrg_strategy()