mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +00:00
Remove multiple Pine Script indicators: SSFDSP, STARCHANNEL, STBANDS, STC, UBANDS, UCHANNEL, VWAPBANDS, and VWAPSD. These indicators were deleted to streamline the library and remove unused or redundant code.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class KriIndicatorTests
|
||||
{
|
||||
[Fact] public void KriIndicator_Constructor_SetsDefaults() { var i = new KriIndicator(); Assert.Equal(14, i.Period); Assert.Equal(SourceType.Close, i.Source); Assert.True(i.ShowColdValues); Assert.Equal("KRI - Kairi Relative Index", i.Name); Assert.True(i.SeparateWindow); }
|
||||
[Fact] public void KriIndicator_MinHistoryDepths_EqualsZero() { Assert.Equal(0, KriIndicator.MinHistoryDepths); IWatchlistIndicator w = new KriIndicator(); Assert.Equal(0, w.MinHistoryDepths); }
|
||||
[Fact] public void KriIndicator_ShortName_IncludesParameters() { var i = new KriIndicator { Period = 20 }; i.Initialize(); Assert.Contains("KRI", i.ShortName, StringComparison.Ordinal); Assert.Contains("20", i.ShortName, StringComparison.Ordinal); }
|
||||
[Fact] public void KriIndicator_SourceCodeLink_IsValid() { var i = new KriIndicator(); Assert.Contains("github.com", i.SourceCodeLink, StringComparison.Ordinal); Assert.Contains("Kri.Quantower.cs", i.SourceCodeLink, StringComparison.Ordinal); }
|
||||
[Fact] public void KriIndicator_Initialize_CreatesInternalKri() { var i = new KriIndicator { Period = 10 }; i.Initialize(); Assert.Single(i.LinesSeries); }
|
||||
[Fact]
|
||||
public void KriIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new KriIndicator { Period = 5 }; indicator.Initialize();
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++) { indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i); indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); }
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
[Fact]
|
||||
public void KriIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new KriIndicator { Period = 5 }; indicator.Initialize();
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
}
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
[Fact] public void KriIndicator_Parameters_CanBeChanged() { var i = new KriIndicator { Period = 20 }; i.Initialize(); Assert.Equal(20, i.Period); }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class KriIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 5000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput(sortIndex: 2)]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Kri _kri = null!;
|
||||
private readonly LineSeries _kriLine;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"KRI ({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/kri/Kri.Quantower.cs";
|
||||
|
||||
public KriIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "KRI - Kairi Relative Index";
|
||||
Description = "Percentage deviation of price from its SMA";
|
||||
|
||||
_kriLine = new LineSeries("KRI", Color.Yellow, 2, LineStyle.Solid);
|
||||
AddLineSeries(_kriLine);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_kri = new Kri(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var priceSelector = Source.GetPriceSelector();
|
||||
var item = HistoricalData[0, SeekOriginHistory.End];
|
||||
double price = priceSelector(item);
|
||||
|
||||
TValue input = new(item.TimeLeft, price);
|
||||
TValue result = _kri.Update(input, args.IsNewBar());
|
||||
|
||||
if (!_kri.IsHot && !ShowColdValues)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_kriLine.SetValue(result.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class KriTests
|
||||
{
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
// ───── A) Constructor validation ─────
|
||||
[Fact] public void Constructor_DefaultPeriod_IsValid() { var k = new Kri(); Assert.Equal(14, k.Period); Assert.Equal("Kri(14)", k.Name); }
|
||||
[Fact] public void Constructor_InvalidPeriod_Throws() { var ex = Assert.Throws<ArgumentException>(() => new Kri(period: 0)); Assert.Equal("period", ex.ParamName); }
|
||||
[Fact] public void Constructor_NegativePeriod_Throws() { var ex = Assert.Throws<ArgumentException>(() => new Kri(period: -5)); Assert.Equal("period", ex.ParamName); }
|
||||
[Fact] public void Constructor_CustomPeriod_SetsCorrectly() { var k = new Kri(period: 20); Assert.Equal(20, k.Period); Assert.Equal("Kri(20)", k.Name); }
|
||||
|
||||
// ───── B) Basic calculation ─────
|
||||
[Fact] public void Update_ReturnsTValue() { var k = new Kri(5); Assert.IsType<TValue>(k.Update(new TValue(DateTime.UtcNow, 100.0))); }
|
||||
[Fact] public void Update_Last_IsAccessible() { var k = new Kri(5); k.Update(new TValue(DateTime.UtcNow, 100.0)); Assert.True(double.IsFinite(k.Last.Value)); }
|
||||
[Fact]
|
||||
public void Update_PriceAboveSMA_PositiveKRI()
|
||||
{
|
||||
var k = new Kri(5);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
k.Update(new TValue(DateTime.UtcNow, 100.0 + i * 2));
|
||||
}
|
||||
Assert.True(k.Last.Value > 0, "Price above SMA should produce positive KRI");
|
||||
}
|
||||
[Fact]
|
||||
public void Update_PriceBelowSMA_NegativeKRI()
|
||||
{
|
||||
var k = new Kri(5);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
k.Update(new TValue(DateTime.UtcNow, 200.0 - i * 2));
|
||||
}
|
||||
Assert.True(k.Last.Value < 0, "Price below SMA should produce negative KRI");
|
||||
}
|
||||
|
||||
// ───── C) State + bar correction ─────
|
||||
[Fact]
|
||||
public void Update_IsNew_False_RollsBack()
|
||||
{
|
||||
var k = new Kri(5);
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
k.Update(new TValue(DateTime.UtcNow, 100.0 + i), isNew: true);
|
||||
}
|
||||
k.Update(new TValue(DateTime.UtcNow, 105.0), isNew: false);
|
||||
var c1 = k.Last;
|
||||
k.Update(new TValue(DateTime.UtcNow, 105.0), isNew: false);
|
||||
Assert.Equal(c1.Value, k.Last.Value, Tolerance);
|
||||
}
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_Restore()
|
||||
{
|
||||
var k = new Kri(5);
|
||||
double[] data = new double[15];
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
data[i] = 100 + i * 2;
|
||||
}
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
k.Update(new TValue(DateTime.UtcNow, data[i]), isNew: true);
|
||||
}
|
||||
var baseline = k.Last.Value;
|
||||
k.Update(new TValue(DateTime.UtcNow, 999.0), isNew: false);
|
||||
k.Update(new TValue(DateTime.UtcNow, 888.0), isNew: false);
|
||||
k.Update(new TValue(DateTime.UtcNow, data[^1]), isNew: false);
|
||||
Assert.Equal(baseline, k.Last.Value, Tolerance);
|
||||
}
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var k = new Kri(5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
k.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
k.Reset();
|
||||
Assert.False(k.IsHot);
|
||||
Assert.Equal(0.0, k.Last.Value);
|
||||
}
|
||||
|
||||
// ───── D) Warmup/convergence ─────
|
||||
[Fact]
|
||||
public void IsHot_FlipsAfterPeriod()
|
||||
{
|
||||
int period = 10;
|
||||
var k = new Kri(period);
|
||||
for (int i = 0; i < period - 1; i++)
|
||||
{
|
||||
k.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
Assert.False(k.IsHot);
|
||||
}
|
||||
k.Update(new TValue(DateTime.UtcNow, 110.0));
|
||||
Assert.True(k.IsHot);
|
||||
}
|
||||
[Fact] public void WarmupPeriod_MatchesPeriod() { Assert.Equal(14, new Kri(14).WarmupPeriod); }
|
||||
|
||||
// ───── E) Robustness ─────
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValid()
|
||||
{
|
||||
var k = new Kri(5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
k.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
k.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsFinite(k.Last.Value));
|
||||
}
|
||||
[Fact]
|
||||
public void Update_Infinity_UsesLastValid()
|
||||
{
|
||||
var k = new Kri(5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
k.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
k.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(k.Last.Value));
|
||||
}
|
||||
[Fact]
|
||||
public void Update_BatchNaN_RemainsFinite()
|
||||
{
|
||||
var k = new Kri(5);
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
k.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
}
|
||||
Assert.True(double.IsFinite(k.Last.Value));
|
||||
}
|
||||
|
||||
// ───── F) Consistency (4 modes match) ─────
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResults()
|
||||
{
|
||||
int period = 10;
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
var streaming = new Kri(period);
|
||||
var streamResults = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(source[i]).Value;
|
||||
}
|
||||
|
||||
TSeries batchSeries = Kri.Batch(source, period);
|
||||
var spanOutput = new double[source.Count];
|
||||
Kri.Batch(source.Values, spanOutput, period);
|
||||
|
||||
var eventSource = new TSeries();
|
||||
var eventIndicator = new Kri(eventSource, period);
|
||||
var eventResults = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
eventSource.Add(source[i]);
|
||||
eventResults[i] = eventIndicator.Last.Value;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchSeries.Values[i], Tolerance);
|
||||
Assert.Equal(streamResults[i], spanOutput[i], Tolerance);
|
||||
Assert.Equal(streamResults[i], eventResults[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
// ───── G) Span API tests ─────
|
||||
[Fact] public void Batch_Span_MismatchedLength_Throws() { var ex = Assert.Throws<ArgumentException>(() => Kri.Batch(new double[10], new double[5], 5)); Assert.Equal("output", ex.ParamName); }
|
||||
[Fact] public void Batch_Span_InvalidPeriod_Throws() { var ex = Assert.Throws<ArgumentException>(() => Kri.Batch(new double[10], new double[10], 0)); Assert.Equal("period", ex.ParamName); }
|
||||
[Fact] public void Batch_Span_Empty_NoException() { Kri.Batch(ReadOnlySpan<double>.Empty, Span<double>.Empty, 5); Assert.True(true); }
|
||||
[Fact]
|
||||
public void Batch_Span_NaN_Handled()
|
||||
{
|
||||
double[] src = [100, double.NaN, 102, 103, 104, 105, 106, 107, 108, 109];
|
||||
var output = new double[src.Length];
|
||||
Kri.Batch(src, output, 5);
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
}
|
||||
}
|
||||
|
||||
// ───── H) Chainability ─────
|
||||
[Fact]
|
||||
public void Pub_Fires_OnUpdate()
|
||||
{
|
||||
var k = new Kri(5); int f = 0;
|
||||
k.Pub += (object? _, in TValueEventArgs _) => f++;
|
||||
k.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(1, f);
|
||||
}
|
||||
[Fact]
|
||||
public void EventBased_Chaining_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var k = new Kri(source, 5);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.True(double.IsFinite(k.Last.Value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// KRI: Kairi Relative Index
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Percentage deviation of the current price from its Simple Moving Average:
|
||||
/// <c>KRI = 100 × (source − SMA) / SMA</c>
|
||||
///
|
||||
/// Uses a circular buffer with running sum for O(1) per-bar updates.
|
||||
/// Positive KRI indicates price is above its average (bullish);
|
||||
/// negative indicates price is below (bearish).
|
||||
///
|
||||
/// References:
|
||||
/// Japanese technical analysis tradition
|
||||
/// PineScript reference: kri.pine
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Kri : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly RingBuffer _buffer;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double Sum,
|
||||
double LastValid,
|
||||
int Count);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
/// <summary>
|
||||
/// Creates Kairi Relative Index with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">SMA lookback period (must be > 0)</param>
|
||||
public Kri(int period = 14)
|
||||
{
|
||||
if (period <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
}
|
||||
|
||||
_period = period;
|
||||
_buffer = new RingBuffer(period);
|
||||
Name = $"Kri({period})";
|
||||
WarmupPeriod = period;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates KRI with specified source and period.
|
||||
/// </summary>
|
||||
public Kri(ITValuePublisher source, int period = 14) : this(period)
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsHot => _buffer.IsFull;
|
||||
|
||||
/// <summary>Period of the indicator.</summary>
|
||||
public int Period => _period;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
}
|
||||
|
||||
double value = input.Value;
|
||||
|
||||
if (!double.IsFinite(value))
|
||||
{
|
||||
value = double.IsFinite(_state.LastValid) ? _state.LastValid : 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state.LastValid = value;
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
if (_buffer.IsFull)
|
||||
{
|
||||
_state.Sum -= _buffer[0];
|
||||
}
|
||||
_state.Sum += value;
|
||||
_buffer.Add(value);
|
||||
_state.Count = _buffer.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
_buffer.UpdateNewest(value);
|
||||
// Recompute sum from buffer to avoid drift from mismatched eviction state
|
||||
double sum = 0;
|
||||
for (int j = 0; j < _buffer.Count; j++)
|
||||
{
|
||||
sum += _buffer[j];
|
||||
}
|
||||
_state.Sum = sum;
|
||||
_state.Count = _buffer.Count;
|
||||
}
|
||||
|
||||
double sma = _state.Sum / Math.Max(1, _state.Count);
|
||||
double kri = sma != 0.0 ? 100.0 * (value - sma) / sma : 0.0;
|
||||
|
||||
Last = new TValue(input.Time, kri);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
Batch(source.Values, CollectionsMarshal.AsSpan(v), _period);
|
||||
source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
TimeSpan interval = step ?? TimeSpan.FromTicks(1);
|
||||
DateTime baseTime = DateTime.UtcNow - (interval * (source.Length - 1));
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(baseTime + (interval * i), source[i]), isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>Calculates KRI for entire series.</summary>
|
||||
public static TSeries Batch(TSeries source, int period = 14)
|
||||
{
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
Batch(source.Values, CollectionsMarshal.AsSpan(v), period);
|
||||
source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>Batch KRI via circular buffer with running sum.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period = 14)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
||||
}
|
||||
if (period <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var buffer = new RingBuffer(period);
|
||||
double sum = 0.0;
|
||||
double lastValid = 0.0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (!double.IsFinite(val)) { val = lastValid; } else { lastValid = val; }
|
||||
|
||||
if (buffer.IsFull) { sum -= buffer[0]; }
|
||||
sum += val;
|
||||
buffer.Add(val);
|
||||
|
||||
double sma = sum / Math.Max(1, buffer.Count);
|
||||
output[i] = sma != 0.0 ? 100.0 * (val - sma) / sma : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates a KRI indicator, processes source, returns results with indicator.</summary>
|
||||
public static (TSeries Results, Kri Indicator) Calculate(TSeries source, int period = 14)
|
||||
{
|
||||
var indicator = new Kri(period);
|
||||
return (indicator.Update(source), indicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
# KRI: Kairi Relative Index
|
||||
|
||||
> "The simplest measure of overextension is the oldest: how far has price strayed from its average? The Japanese knew this before anyone had a computer." -- Anonymous
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Category** | Oscillator |
|
||||
| **Inputs** | Source (close) |
|
||||
| **Parameters** | `period` (default 14) |
|
||||
| **Outputs** | Single series (percentage deviation from SMA) |
|
||||
| **Output range** | Unbounded (centered around 0) |
|
||||
| **Warmup** | `period` bars |
|
||||
|
||||
### Key takeaways
|
||||
|
||||
- Measures the percentage deviation of price from its Simple Moving Average: $\text{KRI} = 100 \times (P - \text{SMA}) / \text{SMA}$.
|
||||
- Positive KRI means price is above its average (bullish bias); negative means below (bearish bias).
|
||||
- Functionally equivalent to a percentage-normalized price-SMA deviation; simpler than RSI but less bounded.
|
||||
- Uses a circular buffer with running sum for O(1) per-bar updates.
|
||||
- Extreme KRI readings suggest overextension and potential mean reversion.
|
||||
|
||||
## Historical Context
|
||||
|
||||
The Kairi Relative Index originates from the Japanese technical analysis tradition, predating the widespread adoption of Western oscillators. The concept is straightforward: express the distance between the current price and its moving average as a percentage of the average itself.
|
||||
|
||||
Before RSI, MACD, and stochastic oscillators became standard, Japanese traders relied on simple deviation measures to gauge overextension. KRI is the percentage form of what is sometimes called the "price oscillator" or "detrended price." The logic is that prices tend to oscillate around their moving averages, and extreme deviations create gravitational pull back toward the mean.
|
||||
|
||||
KRI never achieved the fame of RSI or MACD in Western markets, partly because it lacks the elegant bounded range that makes those indicators visually convenient. But its simplicity is also its strength: no smoothing layers, no signal lines, no arbitrary scaling. Just raw deviation from the average, expressed as a percentage.
|
||||
|
||||
## What It Measures and Why It Matters
|
||||
|
||||
KRI answers one question: how far, in percentage terms, has price moved from its moving average? A KRI of +5 means price is 5% above its SMA. A KRI of $-3$ means price is 3% below.
|
||||
|
||||
This makes KRI a mean-reversion detector. When KRI reaches extreme positive values, price is overextended above its average and statistically more likely to pull back. When KRI reaches extreme negative values, price is overextended below and more likely to bounce.
|
||||
|
||||
The indicator is instrument-specific. What constitutes "extreme" depends on the asset's typical volatility. A KRI of +2 might be extreme for a low-volatility bond ETF but unremarkable for a high-beta tech stock. Traders must calibrate their thresholds per instrument and timeframe.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Core Formula
|
||||
|
||||
$$
|
||||
\text{SMA}_t = \frac{1}{N} \sum_{i=0}^{N-1} P_{t-i}
|
||||
$$
|
||||
|
||||
$$
|
||||
\text{KRI}_t = 100 \times \frac{P_t - \text{SMA}_t}{\text{SMA}_t}
|
||||
$$
|
||||
|
||||
where:
|
||||
|
||||
- $P_t$ = current price (close)
|
||||
- $N$ = lookback period (default 14)
|
||||
- $\text{SMA}_t$ = Simple Moving Average over $N$ bars
|
||||
|
||||
### Special Case
|
||||
|
||||
$$
|
||||
\text{If } \text{SMA}_t = 0: \quad \text{KRI}_t = 0
|
||||
$$
|
||||
|
||||
Division by zero returns $0$ rather than NaN.
|
||||
|
||||
### Parameter Mapping
|
||||
|
||||
| Parameter | Symbol | Default | Constraint |
|
||||
|-----------|--------|---------|------------|
|
||||
| `period` | $N$ | 14 | $N \geq 1$ |
|
||||
|
||||
### Warmup Period
|
||||
|
||||
$$
|
||||
W = N
|
||||
$$
|
||||
|
||||
The circular buffer must fill with $N$ values before the SMA is computed over the full window.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Circular Buffer with Running Sum
|
||||
|
||||
A single `RingBuffer` of capacity $N$ stores source values. A running sum tracks the total, enabling O(1) SMA computation: subtract the oldest value (about to be evicted), add the newest, divide by count.
|
||||
|
||||
### 2. State Management
|
||||
|
||||
A `record struct State` holds `Sum`, `LastValid`, and `Count`. The `_state` / `_p_state` pattern supports bar correction.
|
||||
|
||||
### 3. Batch Path
|
||||
|
||||
`Batch(ReadOnlySpan, Span, int)` mirrors the streaming logic using a local `RingBuffer` and running sum, producing identical results without instantiating a full indicator.
|
||||
|
||||
### 4. Edge Cases
|
||||
|
||||
| Condition | Behavior |
|
||||
|-----------|----------|
|
||||
| `period <= 0` | `ArgumentException` with `nameof(period)` |
|
||||
| `NaN` / `Infinity` input | Substitutes last valid value |
|
||||
| SMA = 0 | Returns $0$ (avoids division by zero) |
|
||||
| During warmup | SMA computed over available bars (partial window) |
|
||||
|
||||
## Interpretation and Signals
|
||||
|
||||
### Signal Zones
|
||||
|
||||
| Zone | Condition | Interpretation |
|
||||
|------|-----------|----------------|
|
||||
| Overbought | KRI >> 0 (instrument-specific) | Price far above average; overextended upside |
|
||||
| Neutral | KRI near 0 | Price tracking its average closely |
|
||||
| Oversold | KRI << 0 (instrument-specific) | Price far below average; overextended downside |
|
||||
|
||||
### Signal Patterns
|
||||
|
||||
- **Mean reversion**: Extreme KRI readings (e.g., beyond $\pm 2\sigma$ of its own distribution) suggest a pullback toward the SMA.
|
||||
- **Trend confirmation**: Persistently positive KRI confirms an uptrend. Persistently negative KRI confirms a downtrend.
|
||||
- **Zero-line crossover**: KRI crossing from negative to positive means price has reclaimed its SMA. From positive to negative means it has broken below.
|
||||
- **Divergence**: Price making new highs while KRI peaks decline suggests weakening momentum relative to the average.
|
||||
|
||||
### Practical Notes
|
||||
|
||||
KRI thresholds must be calibrated per instrument. For major equity indices, KRI beyond $\pm 5$ is often noteworthy. For cryptocurrencies, $\pm 15$ might be routine. Look at the historical distribution of KRI for the specific asset to determine meaningful extreme levels. Bollinger Bands around KRI itself (KRI $\pm 2\sigma$ of KRI) can automate threshold detection.
|
||||
|
||||
## Related Indicators
|
||||
|
||||
- [**Pgo**](../pgo/Pgo.md): Pretty Good Oscillator, normalizes deviation from SMA by ATR instead of the SMA value itself.
|
||||
- [**Fisher**](../fisher/Fisher.md): Fisher Transform, normalizes price position into a bounded Gaussian distribution.
|
||||
- [**Inertia**](../inertia/Inertia.md): Regression-based trend strength, related but uses slope rather than deviation.
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Batch | Streaming | Span | Notes |
|
||||
|---------|:-----:|:---------:|:----:|-------|
|
||||
| **TA-Lib** | -- | -- | -- | No direct KRI function |
|
||||
| **Skender** | -- | -- | -- | Not available |
|
||||
| **Tulip** | -- | -- | -- | Not available |
|
||||
| **Ooples** | -- | -- | -- | Not available |
|
||||
|
||||
Validated via internal consistency across all four API modes (batch, streaming, span, eventing).
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Key Optimizations
|
||||
|
||||
- **O(1) streaming**: Running sum avoids window re-summation each bar.
|
||||
- **Zero allocation**: Pre-allocated `RingBuffer` and `record struct State`.
|
||||
- **Aggressive inlining**: `[MethodImpl(AggressiveInlining)]` on `Update` and `Batch`.
|
||||
- **Single buffer**: Unlike ER's dual-buffer design, KRI needs only one circular buffer.
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count per bar |
|
||||
|-----------|---------------|
|
||||
| SUB | 1 (remove oldest from sum) |
|
||||
| ADD | 1 (add newest to sum) |
|
||||
| DIV | 1 (sum / count = SMA) |
|
||||
| SUB | 1 (value - SMA) |
|
||||
| MUL | 1 (100 * deviation) |
|
||||
| DIV | 1 (deviation / SMA) |
|
||||
| NaN check | 1 |
|
||||
| **Total** | **~7 ops** |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Unbounded output**: KRI has no fixed range like RSI's $[0, 100]$. A KRI of $+20$ is unremarkable for volatile assets but alarming for stable ones. Always calibrate thresholds per instrument.
|
||||
2. **SMA lag**: KRI inherits the SMA's lag. A 14-period SMA lags about 7 bars, which means KRI reacts to deviations from a lagged average, not the current trend center.
|
||||
3. **Not a standalone signal**: Extreme KRI readings do not guarantee reversal. In strong trends, KRI can remain extreme for extended periods (trending above the average persistently).
|
||||
4. **Percentage scaling hides absolute moves**: A KRI of $+5\%$ on a $\$10$ stock is $\$0.50$; on a $\$500$ stock it's $\$25$. The indicator normalizes magnitude but loses absolute context.
|
||||
5. **Division by zero on zero-price assets**: If SMA = 0 (theoretically possible with synthetic series), KRI returns 0. In practice this only matters in testing.
|
||||
6. **Partial warmup**: Before the buffer is full, SMA is computed over fewer bars. Values during the warmup phase are less reliable.
|
||||
|
||||
## References
|
||||
|
||||
- Japanese Technical Analysis tradition, various sources.
|
||||
- Achelis, S. B. *Technical Analysis from A to Z*. McGraw-Hill, 2000.
|
||||
- Colby, R. W. *The Encyclopedia of Technical Market Indicators*, 2nd ed. McGraw-Hill, 2003.
|
||||
@@ -0,0 +1,45 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Kairi Relative Index (KRI)", "KRI", overlay=false, precision=4)
|
||||
|
||||
//@function Kairi Relative Index: percentage deviation of source from its SMA
|
||||
//@param source Series to analyze
|
||||
//@param period SMA lookback period
|
||||
//@returns 100 * (source - SMA) / SMA
|
||||
//@optimized O(1) per bar via circular buffer with running sum
|
||||
kri(series float source, simple int period) =>
|
||||
if period <= 0
|
||||
runtime.error("Period must be greater than 0")
|
||||
|
||||
var array<float> buffer = array.new_float(period, na)
|
||||
var int head = 0
|
||||
var float sum = 0.0
|
||||
var int count = 0
|
||||
|
||||
float oldest = array.get(buffer, head)
|
||||
if not na(oldest)
|
||||
sum -= oldest
|
||||
else
|
||||
count += 1
|
||||
|
||||
float current = nz(source)
|
||||
sum += current
|
||||
array.set(buffer, head, current)
|
||||
head := (head + 1) % period
|
||||
|
||||
float sma = sum / math.max(1, count)
|
||||
sma != 0.0 ? 100.0 * (current - sma) / sma : 0.0
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_source = input.source(close, "Source")
|
||||
i_period = input.int(14, "Period", minval=1, maxval=5000, tooltip="SMA lookback period")
|
||||
|
||||
// Calculation
|
||||
float result = kri(i_source, i_period)
|
||||
|
||||
// Plot
|
||||
plot(result, "KRI", color=color.yellow, linewidth=2)
|
||||
hline(0, "Zero", color=color.gray, linestyle=hline.style_dotted)
|
||||
Reference in New Issue
Block a user