python wrapper

This commit is contained in:
Miha Kralj
2026-02-28 14:14:35 -08:00
parent 82e0248eb0
commit 83e9511261
521 changed files with 62395 additions and 15669 deletions
+553 -31
View File
@@ -1,22 +1,41 @@
using Xunit;
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
using Skender.Stock.Indicators;
using TALib;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Self-consistency validation: batch == streaming, span == TSeries batch.
/// CRSI validation:
/// - Internal consistency (streaming/batch/span/eventing)
/// - Native Skender GetConnorsRsi cross-validation (batch, streaming, span)
/// - Native Ooples CalculateConnorsRelativeStrengthIndex cross-validation (batch, streaming, span)
/// - External structural cross-validation via RSI components from Skender/TA-Lib/Tulip/Ooples
/// </summary>
public sealed class CrsiValidationTests
public sealed class CrsiValidationTests(ITestOutputHelper output) : IDisposable
{
private const double Tolerance = 1e-10;
private readonly ValidationTestData _data = new();
private readonly ITestOutputHelper _output = output;
private bool _disposed;
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_data.Dispose();
}
[Fact]
public void Streaming_MatchesBatch_DefaultParams()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.01, sigma: 0.2, seed: 1001);
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
var source = _data.Data;
// Streaming
var streaming = new Crsi(3, 2, 100);
var streamVals = new double[source.Count];
for (int i = 0; i < source.Count; i++)
@@ -24,7 +43,6 @@ public sealed class CrsiValidationTests
streamVals[i] = streaming.Update(source[i]).Value;
}
// Batch TSeries
TSeries batchTs = Crsi.Batch(source, 3, 2, 100);
for (int i = 0; i < source.Count; i++)
@@ -36,14 +54,10 @@ public sealed class CrsiValidationTests
[Fact]
public void Span_MatchesBatch_DefaultParams()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.01, sigma: 0.2, seed: 1002);
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
var source = _data.Data;
// Batch TSeries
TSeries batchTs = Crsi.Batch(source, 3, 2, 100);
// Batch Span
var spanOut = new double[source.Count];
Crsi.Batch(source.Values, spanOut, 3, 2, 100);
@@ -56,11 +70,8 @@ public sealed class CrsiValidationTests
[Fact]
public void Eventing_MatchesStreaming()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.01, sigma: 0.2, seed: 1003);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
var source = _data.Data;
// Streaming
var streaming = new Crsi(3, 2, 50);
var streamVals = new double[source.Count];
for (int i = 0; i < source.Count; i++)
@@ -68,7 +79,6 @@ public sealed class CrsiValidationTests
streamVals[i] = streaming.Update(source[i]).Value;
}
// Event-based
var eventTs = new TSeries();
var eventCrsi = new Crsi(eventTs, 3, 2, 50);
var eventVals = new double[source.Count];
@@ -87,11 +97,9 @@ public sealed class CrsiValidationTests
[Fact]
public void Output_AlwaysInRange0To100()
{
var gbm = new GBM(startPrice: 50.0, mu: 0.05, sigma: 0.5, seed: 1004);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
var source = _data.Data;
var crsi = new Crsi(3, 2, 100);
for (int i = 0; i < source.Count; i++)
{
double v = crsi.Update(source[i]).Value;
@@ -102,9 +110,7 @@ public sealed class CrsiValidationTests
[Fact]
public void Reset_ThenReplay_MatchesFreshRun()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 1005);
var bars = gbm.Fetch(150, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
var source = _data.Data;
var crsi1 = new Crsi(3, 2, 30);
for (int i = 0; i < source.Count; i++)
@@ -114,7 +120,6 @@ public sealed class CrsiValidationTests
double finalVal1 = crsi1.Last.Value;
// Reset and replay
crsi1.Reset();
for (int i = 0; i < source.Count; i++)
{
@@ -127,14 +132,11 @@ public sealed class CrsiValidationTests
[Fact]
public void DifferentPeriods_ProduceDistinctResults()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.01, sigma: 0.2, seed: 1006);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
TSeries source = bars.Close;
var source = _data.Data;
TSeries r1 = Crsi.Batch(source, 3, 2, 50);
TSeries r2 = Crsi.Batch(source, 5, 3, 50);
// With different RSI/streak parameters and same data, results should differ
bool anyDiff = false;
for (int i = 0; i < source.Count; i++)
{
@@ -147,4 +149,524 @@ public sealed class CrsiValidationTests
Assert.True(anyDiff, "Different periods should produce different results");
}
}
[Fact]
public void Validate_Skender_StructuralComposite()
{
const int rsiPeriod = 3;
const int streakPeriod = 2;
const int rankPeriod = 100;
double[] close = _data.ClosePrices.ToArray();
double[] streak = ComputeStreak(close);
double[] pctRank = ComputePercentRank(close, rankPeriod);
var closeRsi = _data.SkenderQuotes.GetRsi(rsiPeriod).Select(x => x.Rsi.HasValue ? x.Rsi.Value : double.NaN).ToArray();
var streakQuotes = BuildSyntheticQuotes(_data.SkenderQuotes, streak);
var streakRsi = streakQuotes.GetRsi(streakPeriod).Select(x => x.Rsi.HasValue ? x.Rsi.Value : double.NaN).ToArray();
var expected = ComposeCrsi(closeRsi, streakRsi, pctRank);
var actual = Crsi.Batch(_data.Data, rsiPeriod, streakPeriod, rankPeriod);
ValidationHelper.VerifyData(actual, expected, x => x, skip: 200, tolerance: ValidationHelper.SkenderTolerance);
_output.WriteLine("CRSI validated against Skender structural composite (RSI + RSI(streak) + %Rank).");
}
[Fact]
public void Validate_Talib_StructuralComposite()
{
const int rsiPeriod = 3;
const int streakPeriod = 2;
const int rankPeriod = 100;
double[] close = _data.ClosePrices.ToArray();
double[] streak = ComputeStreak(close);
double[] pctRank = ComputePercentRank(close, rankPeriod);
var closeRsi = ComputeTalibRsiFull(close, rsiPeriod);
var streakRsi = ComputeTalibRsiFull(streak, streakPeriod);
var expected = ComposeCrsi(closeRsi, streakRsi, pctRank);
var actual = Crsi.Batch(_data.Data, rsiPeriod, streakPeriod, rankPeriod);
ValidationHelper.VerifyData(actual, expected, x => x, skip: 200, tolerance: ValidationHelper.TalibTolerance);
_output.WriteLine("CRSI validated against TA-Lib structural composite (RSI + RSI(streak) + %Rank).");
}
[Fact]
public void Validate_Tulip_StructuralComposite()
{
const int rsiPeriod = 3;
const int streakPeriod = 2;
const int rankPeriod = 100;
double[] close = _data.ClosePrices.ToArray();
double[] streak = ComputeStreak(close);
double[] pctRank = ComputePercentRank(close, rankPeriod);
var closeRsi = ComputeTulipRsiFull(close, rsiPeriod);
var streakRsi = ComputeTulipRsiFull(streak, streakPeriod);
var expected = ComposeCrsi(closeRsi, streakRsi, pctRank);
var actual = Crsi.Batch(_data.Data, rsiPeriod, streakPeriod, rankPeriod);
ValidationHelper.VerifyData(actual, expected, x => x, skip: 200, tolerance: ValidationHelper.TulipTolerance);
_output.WriteLine("CRSI validated against Tulip structural composite (RSI + RSI(streak) + %Rank).");
}
[Fact]
public void Validate_Ooples_StructuralComposite()
{
const int rsiPeriod = 3;
const int streakPeriod = 2;
const int rankPeriod = 100;
double[] close = _data.ClosePrices.ToArray();
double[] streak = ComputeStreak(close);
double[] pctRank = ComputePercentRank(close, rankPeriod);
var closeRsi = ComputeOoplesRsiFull(BuildOoplesTickerData(close), rsiPeriod);
var streakRsi = ComputeOoplesRsiFull(BuildOoplesTickerData(streak), streakPeriod);
var expected = ComposeCrsi(closeRsi, streakRsi, pctRank);
var actual = Crsi.Batch(_data.Data, rsiPeriod, streakPeriod, rankPeriod);
ValidationHelper.VerifyData(actual, expected, x => x, skip: 200, tolerance: ValidationHelper.OoplesTolerance);
_output.WriteLine("CRSI validated against Ooples structural composite (RSI + RSI(streak) + %Rank).");
}
[Fact]
public void Validate_Ooples_NativeConnorsRsi_Batch()
{
const int rsiPeriod = 3;
const int streakPeriod = 2;
const int rankPeriod = 100;
var ooplesData = BuildOoplesTickerData(_data.ClosePrices.ToArray());
var expected = ComputeOoplesConnorsRsiFull(ooplesData, rsiPeriod, streakPeriod, rankPeriod);
TSeries actual = Crsi.Batch(_data.Data, rsiPeriod, streakPeriod, rankPeriod);
AssertOoplesNativeComparable(actual.Values.ToArray(), expected, "batch");
_output.WriteLine("CRSI batch structurally validated against Ooples native CalculateConnorsRelativeStrengthIndex.");
}
[Fact]
public void Validate_Ooples_NativeConnorsRsi_Streaming()
{
const int rsiPeriod = 3;
const int streakPeriod = 2;
const int rankPeriod = 100;
var ooplesData = BuildOoplesTickerData(_data.ClosePrices.ToArray());
var expected = ComputeOoplesConnorsRsiFull(ooplesData, rsiPeriod, streakPeriod, rankPeriod);
var crsi = new Crsi(rsiPeriod, streakPeriod, rankPeriod);
var streamVals = new double[_data.Data.Count];
for (int i = 0; i < _data.Data.Count; i++)
{
streamVals[i] = crsi.Update(_data.Data[i]).Value;
}
AssertOoplesNativeComparable(streamVals, expected, "streaming");
_output.WriteLine("CRSI streaming structurally validated against Ooples native CalculateConnorsRelativeStrengthIndex.");
}
[Fact]
public void Validate_Ooples_NativeConnorsRsi_Span()
{
const int rsiPeriod = 3;
const int streakPeriod = 2;
const int rankPeriod = 100;
var ooplesData = BuildOoplesTickerData(_data.ClosePrices.ToArray());
var expected = ComputeOoplesConnorsRsiFull(ooplesData, rsiPeriod, streakPeriod, rankPeriod);
var spanOut = new double[_data.Data.Count];
Crsi.Batch(_data.Data.Values, spanOut, rsiPeriod, streakPeriod, rankPeriod);
AssertOoplesNativeComparable(spanOut, expected, "span");
_output.WriteLine("CRSI span structurally validated against Ooples native CalculateConnorsRelativeStrengthIndex.");
}
[Fact]
public void Validate_Skender_NativeConnorsRsi_Batch()
{
const int rsiPeriod = 3;
const int streakPeriod = 2;
const int rankPeriod = 100;
var skenderResults = _data.SkenderQuotes
.GetConnorsRsi(rsiPeriod, streakPeriod, rankPeriod)
.ToList();
TSeries actual = Crsi.Batch(_data.Data, rsiPeriod, streakPeriod, rankPeriod);
ValidationHelper.VerifyData(
actual,
skenderResults,
x => x.ConnorsRsi,
skip: 200,
tolerance: ValidationHelper.SkenderTolerance);
_output.WriteLine("CRSI batch validated against Skender native GetConnorsRsi.");
}
[Fact]
public void Validate_Skender_NativeConnorsRsi_Streaming()
{
const int rsiPeriod = 3;
const int streakPeriod = 2;
const int rankPeriod = 100;
var skenderResults = _data.SkenderQuotes
.GetConnorsRsi(rsiPeriod, streakPeriod, rankPeriod)
.ToList();
var crsi = new Crsi(rsiPeriod, streakPeriod, rankPeriod);
var streamVals = new double[_data.Data.Count];
for (int i = 0; i < _data.Data.Count; i++)
{
streamVals[i] = crsi.Update(_data.Data[i]).Value;
}
int count = _data.Data.Count;
int start = Math.Max(0, count - 200);
for (int i = start; i < count; i++)
{
double? expected = skenderResults[i].ConnorsRsi;
if (!expected.HasValue)
{
continue;
}
Assert.True(
Math.Abs(streamVals[i] - expected.Value) <= ValidationHelper.SkenderTolerance,
$"Streaming mismatch at i={i}: QuanTAlib={streamVals[i]:G17}, Skender={expected.Value:G17}");
}
_output.WriteLine("CRSI streaming validated against Skender native GetConnorsRsi.");
}
[Fact]
public void Validate_Skender_NativeConnorsRsi_Span()
{
const int rsiPeriod = 3;
const int streakPeriod = 2;
const int rankPeriod = 100;
var skenderResults = _data.SkenderQuotes
.GetConnorsRsi(rsiPeriod, streakPeriod, rankPeriod)
.ToList();
var spanOut = new double[_data.Data.Count];
Crsi.Batch(_data.Data.Values, spanOut, rsiPeriod, streakPeriod, rankPeriod);
ValidationHelper.VerifyData(
spanOut,
skenderResults,
x => x.ConnorsRsi,
skip: 200,
tolerance: ValidationHelper.SkenderTolerance);
_output.WriteLine("CRSI span validated against Skender native GetConnorsRsi.");
}
[Fact]
public void Validate_Skender_NativeConnorsRsi_Components()
{
const int rsiPeriod = 3;
const int streakPeriod = 2;
const int rankPeriod = 100;
var skenderResults = _data.SkenderQuotes
.GetConnorsRsi(rsiPeriod, streakPeriod, rankPeriod)
.ToList();
// Verify all 3 sub-components are populated for converged bars
int count = _data.Data.Count;
int start = Math.Max(0, count - 100);
for (int i = start; i < count; i++)
{
var r = skenderResults[i];
Assert.True(r.Rsi.HasValue, $"Skender Rsi null at {i}");
Assert.True(r.RsiStreak.HasValue, $"Skender RsiStreak null at {i}");
Assert.True(r.PercentRank.HasValue, $"Skender PercentRank null at {i}");
Assert.True(r.ConnorsRsi.HasValue, $"Skender ConnorsRsi null at {i}");
Assert.InRange(r.ConnorsRsi!.Value, 0.0, 100.0);
}
_output.WriteLine("Skender ConnorsRsi components all present and in [0,100] for converged bars.");
}
private static double[] ComputeStreak(ReadOnlySpan<double> close)
{
int n = close.Length;
var streak = new double[n];
int s = 0;
streak[0] = 0.0;
for (int i = 1; i < n; i++)
{
if (close[i] > close[i - 1])
{
s = s >= 0 ? s + 1 : 1;
}
else if (close[i] < close[i - 1])
{
s = s <= 0 ? s - 1 : -1;
}
else
{
s = 0;
}
streak[i] = s;
}
return streak;
}
private static double[] ComputePercentRank(ReadOnlySpan<double> close, int rankPeriod)
{
int n = close.Length;
var pct = new double[n];
var rocBuf = new double[rankPeriod];
int head = 0;
int count = 0;
double prev = double.NaN;
for (int i = 0; i < n; i++)
{
double roc = 0.0;
if (!double.IsNaN(prev) && prev != 0.0)
{
roc = (close[i] - prev) / prev * 100.0;
}
prev = close[i];
// Scan BEFORE writing current roc (compare against historical values only)
int lessCount = 0;
for (int j = 0; j < count; j++)
{
if (rocBuf[j] < roc)
{
lessCount++;
}
}
pct[i] = count > 0 ? (double)lessCount / count * 100.0 : 50.0;
// Store current ROC after rank scan
rocBuf[head] = roc;
head = (head + 1) % rankPeriod;
if (count < rankPeriod)
{
count++;
}
}
return pct;
}
private static double[] ComposeCrsi(double[] priceRsi, double[] streakRsi, double[] pctRank)
{
int n = priceRsi.Length;
var result = new double[n];
for (int i = 0; i < n; i++)
{
double a = priceRsi[i];
double b = streakRsi[i];
double c = pctRank[i];
if (!double.IsFinite(a) || !double.IsFinite(b) || !double.IsFinite(c))
{
result[i] = double.NaN;
continue;
}
double v = (a + b + c) / 3.0;
result[i] = Math.Clamp(v, 0.0, 100.0);
}
return result;
}
private void AssertOoplesNativeComparable(double[] actual, double[] expected, string mode)
{
int count = Math.Min(actual.Length, expected.Length);
int start = Math.Max(0, count - 300);
var a = new List<double>(300);
var b = new List<double>(300);
for (int i = start; i < count; i++)
{
double x = actual[i];
double y = expected[i];
if (double.IsFinite(x) && double.IsFinite(y))
{
Assert.InRange(x, 0.0, 100.0);
Assert.InRange(y, 0.0, 100.0);
a.Add(x);
b.Add(y);
}
}
Assert.True(a.Count >= 150, $"Insufficient overlapping finite values for Ooples {mode} validation.");
double mae = 0.0;
for (int i = 0; i < a.Count; i++)
{
mae += Math.Abs(a[i] - b[i]);
}
mae /= a.Count;
Assert.True(
mae <= 20.0,
$"Ooples {mode} MAE too large for structural agreement: {mae:G17}");
_output.WriteLine($"CRSI {mode} vs Ooples native: finite={a.Count}, MAE={mae:G6}");
}
private static Quote[] BuildSyntheticQuotes(IReadOnlyList<Quote> baseQuotes, double[] values)
{
var quotes = new Quote[values.Length];
for (int i = 0; i < values.Length; i++)
{
decimal v = (decimal)values[i];
quotes[i] = new Quote
{
Date = baseQuotes[i].Date,
Open = v,
High = v,
Low = v,
Close = v,
Volume = baseQuotes[i].Volume
};
}
return quotes;
}
private static double[] ComputeTalibRsiFull(double[] input, int period)
{
var output = new double[input.Length];
var ret = TALib.Functions.Rsi<double>(input, 0..^0, output, out var outRange, period);
Assert.Equal(TALib.Core.RetCode.Success, ret);
var full = Enumerable.Repeat(double.NaN, input.Length).ToArray();
var (offset, length) = outRange.GetOffsetAndLength(output.Length);
for (int i = 0; i < length && (offset + i) < full.Length; i++)
{
full[offset + i] = output[i];
}
return full;
}
private static double[] ComputeTulipRsiFull(double[] input, int period)
{
var indicator = Tulip.Indicators.rsi;
double[][] inputs = { input };
double[] options = { period };
int lookback = indicator.Start(options);
double[][] outputs = { new double[input.Length - lookback] };
indicator.Run(inputs, options, outputs);
var full = Enumerable.Repeat(double.NaN, input.Length).ToArray();
var rsi = outputs[0];
for (int i = 0; i < rsi.Length; i++)
{
full[i + lookback] = rsi[i];
}
return full;
}
private static double[] ComputeOoplesConnorsRsiFull(List<TickerData> data, int rsiPeriod, int streakPeriod, int rankPeriod)
{
var stockData = new StockData(data);
// Ooples uses extension methods declared on static Calculations class.
var method = typeof(Calculations).GetMethods()
.FirstOrDefault(m =>
string.Equals(m.Name, "CalculateConnorsRelativeStrengthIndex", StringComparison.Ordinal) &&
m.GetParameters().Length > 0 &&
m.GetParameters()[0].ParameterType == typeof(StockData));
Assert.NotNull(method);
var parameters = method!.GetParameters();
var args = new object?[parameters.Length];
args[0] = stockData; // extension target
int idx = 0;
int[] periods = [rsiPeriod, streakPeriod, rankPeriod];
for (int i = 1; i < parameters.Length; i++)
{
var p = parameters[i];
if ((p.ParameterType == typeof(int) || p.ParameterType == typeof(int?)) && idx < periods.Length)
{
args[i] = periods[idx++];
}
else if (p.HasDefaultValue)
{
args[i] = p.DefaultValue;
}
else
{
args[i] = Type.Missing;
}
}
var result = method.Invoke(null, args) as StockData;
Assert.NotNull(result);
var outputValues = result!.OutputValues as System.Collections.IDictionary;
Assert.NotNull(outputValues);
Assert.NotEmpty(outputValues!.Keys);
object? firstSeries = outputValues.Values.Cast<object?>().FirstOrDefault(v => v is IEnumerable<double>);
Assert.NotNull(firstSeries);
return ((IEnumerable<double>)firstSeries!).ToArray();
}
private static List<TickerData> BuildOoplesTickerData(double[] values)
{
var list = new List<TickerData>(values.Length);
var start = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc);
for (int i = 0; i < values.Length; i++)
{
double v = values[i];
list.Add(new TickerData
{
Date = start.AddMinutes(i),
Open = v,
High = v,
Low = v,
Close = v,
Volume = 1.0
});
}
return list;
}
private static double[] ComputeOoplesRsiFull(List<TickerData> data, int period)
{
var stockData = new StockData(data);
var result = stockData.CalculateRelativeStrengthIndex(length: period);
return result.OutputValues.Values.First().ToArray();
}
}
+29 -28
View File
@@ -181,36 +181,36 @@ public sealed class Crsi : AbstractBase
roc = (value - s.PrevClose) / s.PrevClose * 100.0;
}
// Circular buffer: slot at RocHead holds the current (overwritten) ROC
// PrevRocSlot saved the old value at RocHead before this bar wrote it (on isNew=true path)
// Circular buffer: slot at RocHead holds the oldest ROC (to be overwritten)
int head = s.RocHead;
int count = s.RocCount;
bool slotWasEmpty = (count < _rankPeriod);
// Save old slot content (used by next rollback)
s.PrevRocSlot = _rocBuf[head];
// Percent rank: count how many HISTORICAL entries in buffer are strictly < current roc
// BEFORE writing current roc to buffer (Connors/Alvarez: "percentage of values the current return is greater than")
int lessCount = 0;
for (int i = 0; i < count; i++)
{
if (_rocBuf[i] < roc)
{
lessCount++;
}
}
double pctRank = count > 0 ? (double)lessCount / count * 100.0 : 50.0;
// Now store current ROC into circular buffer (after rank scan)
_rocBuf[head] = roc;
s.RocHead = (head + 1) % _rankPeriod;
if (slotWasEmpty)
if (count < _rankPeriod)
{
count++;
}
s.RocCount = count;
// Percent rank: count how many entries in buffer are <= current roc
int lessOrEqual = 0;
for (int i = 0; i < count; i++)
{
if (_rocBuf[i] <= roc)
{
lessOrEqual++;
}
}
double pctRank = count > 0 ? (double)lessOrEqual / count * 100.0 : 50.0;
// Update prev close and streak in state
s.PrevClose = value;
s.Streak = streak;
@@ -417,24 +417,25 @@ public sealed class Crsi : AbstractBase
prevClose = v;
bool wasEmpty = rocCount < rankPeriod;
rocBuf[rocHead] = roc;
rocHead = (rocHead + 1) % rankPeriod;
if (wasEmpty)
{
rocCount++;
}
int lessOrEqual = 0;
// Scan BEFORE writing current roc to buffer (compare against historical values)
int lessCount = 0;
for (int j = 0; j < rocCount; j++)
{
if (rocBuf[j] <= roc)
if (rocBuf[j] < roc)
{
lessOrEqual++;
lessCount++;
}
}
double pctRank = rocCount > 0 ? (double)lessOrEqual / rocCount * 100.0 : 50.0;
double pctRank = rocCount > 0 ? (double)lessCount / rocCount * 100.0 : 50.0;
// Now store current ROC into circular buffer (after rank scan)
rocBuf[rocHead] = roc;
rocHead = (rocHead + 1) % rankPeriod;
if (rocCount < rankPeriod)
{
rocCount++;
}
double crsi = (priceRsiOut[i] + streakRsiOut[i] + pctRank) / 3.0;
output[i] = Math.Max(0.0, Math.Min(100.0, crsi));
}
+10 -8
View File
@@ -91,19 +91,21 @@ crsi(series float source, simple int rsiPeriod, simple int streakPeriod, simple
if not na(source)
prevSrc := source
// Count how many HISTORICAL ROC values are strictly < current ROC
// BEFORE storing current roc (Connors/Alvarez: "percentage of values the current return is greater than")
int lessCount = 0
for i = 0 to rocCount - 1
float val = array.get(rocBuf, i)
if not na(val) and val < roc
lessCount += 1
float pctRank = rocCount > 0 ? (float(lessCount) / float(rocCount)) * 100.0 : 50.0
// Store current ROC after rank scan
if na(array.get(rocBuf, rocHead))
rocCount := math.min(rocCount + 1, rankPeriod)
array.set(rocBuf, rocHead, roc)
rocHead := (rocHead + 1) % rankPeriod
// Count how many historical ROC values <= current ROC
int lessEqual = 0
for i = 0 to rocCount - 1
float val = array.get(rocBuf, i)
if not na(val) and val <= roc
lessEqual += 1
float pctRank = rocCount > 0 ? (float(lessEqual) / float(rocCount)) * 100.0 : 50.0
// Connors RSI = average of three components
float result = (priceRsi + streakRsi + pctRank) / 3.0
math.max(0.0, math.min(100.0, result))