diff --git a/Tests/test_updates_oscillators.cs b/Tests/test_updates_oscillators.cs index 12f55626..cbc20ec1 100644 --- a/Tests/test_updates_oscillators.cs +++ b/Tests/test_updates_oscillators.cs @@ -17,6 +17,15 @@ public class OscillatorsUpdateTests return ((double)BitConverter.ToUInt64(bytes, 0) / ulong.MaxValue * 200) - 100; // Range: -100 to 100 } + private TBar GetRandomBar(bool IsNew) + { + double open = GetRandomDouble(); + double high = open + Math.Abs(GetRandomDouble()); + double low = open - Math.Abs(GetRandomDouble()); + double close = low + ((high - low) * GetRandomDouble()); + return new TBar(DateTime.Now, open, high, low, close, 1000, IsNew); + } + [Fact] public void Rsi_Update() { @@ -66,12 +75,12 @@ public class OscillatorsUpdateTests public void Ao_Update() { var indicator = new Ao(); - TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + TBar r = GetRandomBar(true); double initialValue = indicator.Calc(r); for (int i = 0; i < RandomUpdates; i++) { - indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, IsNew: false)); + indicator.Calc(GetRandomBar(IsNew: false)); } double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); @@ -82,12 +91,12 @@ public class OscillatorsUpdateTests public void Ac_Update() { var indicator = new Ac(); - TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + TBar r = GetRandomBar(true); double initialValue = indicator.Calc(r); for (int i = 0; i < RandomUpdates; i++) { - indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, IsNew: false)); + indicator.Calc(GetRandomBar(IsNew: false)); } double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); @@ -98,12 +107,12 @@ public class OscillatorsUpdateTests public void Aroon_Update() { var indicator = new Aroon(period: 25); - TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + TBar r = GetRandomBar(true); double initialValue = indicator.Calc(r); for (int i = 0; i < RandomUpdates; i++) { - indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, IsNew: false)); + indicator.Calc(GetRandomBar(IsNew: false)); } double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); @@ -114,12 +123,12 @@ public class OscillatorsUpdateTests public void Bop_Update() { var indicator = new Bop(); - TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + TBar r = GetRandomBar(true); double initialValue = indicator.Calc(r); for (int i = 0; i < RandomUpdates; i++) { - indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, IsNew: false)); + indicator.Calc(GetRandomBar(IsNew: false)); } double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); @@ -130,12 +139,12 @@ public class OscillatorsUpdateTests public void Cci_Update() { var indicator = new Cci(period: 20); - TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + TBar r = GetRandomBar(true); double initialValue = indicator.Calc(r); for (int i = 0; i < RandomUpdates; i++) { - indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, IsNew: false)); + indicator.Calc(GetRandomBar(IsNew: false)); } double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); @@ -161,12 +170,12 @@ public class OscillatorsUpdateTests public void Chop_Update() { var indicator = new Chop(period: 14); - TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + TBar r = GetRandomBar(true); double initialValue = indicator.Calc(r); for (int i = 0; i < RandomUpdates; i++) { - indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, IsNew: false)); + indicator.Calc(GetRandomBar(IsNew: false)); } double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); @@ -187,4 +196,113 @@ public class OscillatorsUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Smi_Update() + { + var indicator = new Smi(period: 10, smooth1: 3, smooth2: 3); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Srsi_Update() + { + var indicator = new Srsi(rsiPeriod: 14, stochPeriod: 14, smoothK: 3, smoothD: 3); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Stc_Update() + { + var indicator = new Stc(cyclePeriod: 10, fastPeriod: 23, slowPeriod: 50); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Stoch_Update() + { + var indicator = new Stoch(period: 14, smoothK: 3, smoothD: 3); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Tsi_Update() + { + var indicator = new Tsi(firstPeriod: 25, secondPeriod: 13); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Uo_Update() + { + var indicator = new Uo(period1: 7, period2: 14, period3: 28); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Willr_Update() + { + var indicator = new Willr(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } } diff --git a/Tests/test_updates_statistics.cs b/Tests/test_updates_statistics.cs index a5d901fc..e1c902aa 100644 --- a/Tests/test_updates_statistics.cs +++ b/Tests/test_updates_statistics.cs @@ -22,16 +22,7 @@ public class StatisticsUpdateTests double open = GetRandomDouble(); double high = open + Math.Abs(GetRandomDouble()); double low = open - Math.Abs(GetRandomDouble()); - double close = low + (high - low) * GetRandomDouble(); - return new TBar(DateTime.Now, open, high, low, close, 1000, IsNew); - } - - private TBar GetRandomBar(bool IsNew) - { - double open = GetRandomDouble(); - double high = open + Math.Abs(GetRandomDouble()); - double low = open - Math.Abs(GetRandomDouble()); - double close = low + (high - low) * GetRandomDouble(); + double close = low + ((high - low) * GetRandomDouble()); return new TBar(DateTime.Now, open, high, low, close, 1000, IsNew); } diff --git a/Tests/test_updates_volatility.cs b/Tests/test_updates_volatility.cs index 26b6a5f9..85a58ea7 100644 --- a/Tests/test_updates_volatility.cs +++ b/Tests/test_updates_volatility.cs @@ -170,6 +170,22 @@ public class VolatilityUpdateTests Assert.Equal(initialValue, finalValue, precision); } + [Fact] + public void Dchn_Update() + { + var indicator = new Dchn(period: 20); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + [Fact] public void Ewma_Update() { @@ -264,6 +280,54 @@ public class VolatilityUpdateTests Assert.Equal(initialValue, finalValue, precision); } + [Fact] + public void Natr_Update() + { + var indicator = new Natr(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Pch_Update() + { + var indicator = new Pch(period: 20); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Pv_Update() + { + var indicator = new Pv(period: 10); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + [Fact] public void Realized_Update() { @@ -279,6 +343,22 @@ public class VolatilityUpdateTests Assert.Equal(initialValue, finalValue, precision); } + [Fact] + public void Rsv_Update() + { + var indicator = new Rsv(period: 10); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + [Fact] public void Rvi_Update() { @@ -294,6 +374,22 @@ public class VolatilityUpdateTests Assert.Equal(initialValue, finalValue, precision); } + [Fact] + public void Sv_Update() + { + var indicator = new Sv(period: 20, lambda: 0.94); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + [Fact] public void Tr_Update() { @@ -389,4 +485,20 @@ public class VolatilityUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Yzv_Update() + { + var indicator = new Yzv(period: 20); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } } diff --git a/docs/indicators/indicators.md b/docs/indicators/indicators.md index 0aecedff..8c607e10 100644 --- a/docs/indicators/indicators.md +++ b/docs/indicators/indicators.md @@ -1,18 +1,16 @@ # Indicators in QuanTAlib - - | **Category** | **Status** | **Completion** | |--------------|:----------:|:--------------:| | Basic Transforms | 6 of 6 | 100% | | Averages & Trends | 33 of 33 | 100% | -| Momentum | 17 of 17 | 100% | -| Oscillators | 11 of 29 | 38% | +| Momentum | 16 of 16 | 100% | +| Oscillators | 20 of 29 | 69% | | Volatility | 24 of 35 | 69% | | Volume | 15 of 19 | 79% | | Numerical Analysis | 13 of 19 | 68% | | Errors | 16 of 16 | 100% | -| **Total** | **135 of 174** | **78%** | +| **Total** | **143 of 173** | **83%** | |Technical Indicator Name| Class Name| |-----------|:----------:| @@ -71,7 +69,6 @@ |PPO - Percentage Price Oscillator|`Ppo`| |PRS - Price Relative Strength|`Prs`| |ROC - Rate of Change|`Roc`| -|TSI - True Strength Index|`Tsi`| |TRIX - 1-day ROC of TEMA|`Trix`| |VEL - Jurik Signal Velocity|`Vel`| |VORTEX* - Vortex Indicator (VI+, VI-)|`Vortex`| @@ -85,8 +82,8 @@ |CMO - Chande Momentum Oscillator|`Cmo`| |CHOP - Choppiness Index|`Chop`| |COG - Ehler's Center of Gravity|`Cog`| -|🚧 COPPOCK - Coppock Curve|`Coppock`| -|🚧 CRSI - Connor RSI|`Crsi`| +|COPPOCK - Coppock Curve|`Coppock`| +|CRSI - Connor RSI|`Crsi`| |🚧 CTI - Ehler's Correlation Trend Indicator|`Cti`| |🚧 DOSC - Derivative Oscillator|`Dosc`| |🚧 EFI - Elder Ray's Force Index|`Efi`| @@ -98,13 +95,13 @@ |RSI - Relative Strength Index|`Rsi`| |RSX - Jurik Trend Strength Index|`Rsx`| |🚧 RVGI* - Relative Vigor Index (RVGI, Signal)|`Rvgi`| -|🚧 SMI - Stochastic Momentum Index|`Smi`| -|🚧 SRSI* - Stochastic RSI (SRSI, Signal)|`Srsi`| -|🚧 STC - Schaff Trend Cycle|`Stc`| -|🚧 STOCH* - Stochastic Oscillator (%K, %D)|`Stoch`| -|🚧 TSI - True Strength Index|`Tsi`| -|🚧 UO - Ultimate Oscillator|`Uo`| -|🚧 WILLR - Larry Williams' %R|`Willr`| +|SMI - Stochastic Momentum Index|`Smi`| +|SRSI* - Stochastic RSI (SRSI, Signal)|`Srsi`| +|STC - Schaff Trend Cycle|`Stc`| +|STOCH* - Stochastic Oscillator (%K, %D)|`Stoch`| +|TSI - True Strength Index|`Tsi`| +|UO - Ultimate Oscillator|`Uo`| +|WILLR - Larry Williams' %R|`Willr`| |**VOLATILITY INDICATORS**|| |ADR - Average Daily Range|`Adr`| |AP - Andrew's Pitchfork|`Ap`| @@ -122,7 +119,7 @@ |GKV - Garman-Klass Volatility|`Gkv`| |HLV - High-Low Volatility|`Hlv`| |HV - Historical Volatility|`Hv`| -|🚧 ICH* - Ichimoku Cloud (Conversion, Base, Leading Span A, Leading Span B, Lagging Span)|`Ich`| +|🚧 ICH* - Ichimoku Cloud (Conversion, Base, Span A, Span B, Lagging Span)|`Ich`| |JVOLTY - Jurik Volatility|`Jvolty`| |🚧 KC* - Keltner Channels (Upper, Middle, Lower)|`Kc`| |🚧 NATR - Normalized Average True Range|`Natr`| diff --git a/lib/core/tbar.cs b/lib/core/tbar.cs index c27682f4..5e949861 100644 --- a/lib/core/tbar.cs +++ b/lib/core/tbar.cs @@ -59,110 +59,3 @@ public readonly record struct TBar(DateTime Time, double Open, double High, doub [MethodImpl(MethodImplOptions.AggressiveInlining)] public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]"; } - -public delegate void BarSignal(object source, in TBarEventArgs args); - -[SkipLocalsInit] -public sealed class TBarEventArgs : EventArgs -{ - public readonly TBar Bar; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TBarEventArgs(TBar bar) => Bar = bar; -} - -[SkipLocalsInit] -public class TBarSeries : List -{ - private static readonly TBar Default = new(DateTime.MinValue, double.NaN, double.NaN, double.NaN, double.NaN, double.NaN); - - public readonly TSeries Open; - public readonly TSeries High; - public readonly TSeries Low; - public readonly TSeries Close; - public readonly TSeries Volume; - - public TBar Last => Count > 0 ? this[^1] : Default; - public TBar First => Count > 0 ? this[0] : Default; - public int Length => Count; - public string Name { get; set; } - public event BarSignal Pub = delegate { }; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TBarSeries() - { - Name = "Bar"; - Open = new TSeries(); - High = new TSeries(); - Low = new TSeries(); - Close = new TSeries(); - Volume = new TSeries(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TBarSeries(object source) : this() - { - var pubEvent = source.GetType().GetEvent("Pub"); - pubEvent?.AddEventHandler(source, new BarSignal(Sub)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public new virtual void Add(TBar bar) - { - if (bar.IsNew || base.Count == 0) - { - base.Add(bar); - } - else - { - this[^1] = bar; - } - - Pub?.Invoke(this, new TBarEventArgs(bar)); - - Open.Add(bar.Time, bar.Open, IsNew: bar.IsNew, IsHot: true); - High.Add(bar.Time, bar.High, IsNew: bar.IsNew, IsHot: true); - Low.Add(bar.Time, bar.Low, IsNew: bar.IsNew, IsHot: true); - Close.Add(bar.Time, bar.Close, IsNew: bar.IsNew, IsHot: true); - Volume.Add(bar.Time, bar.Volume, IsNew: bar.IsNew, IsHot: true); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(DateTime Time, double Open, double High, double Low, double Close, double Volume, bool IsNew = true) => - Add(new TBar(Time, Open, High, Low, Close, Volume, IsNew)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(double Open, double High, double Low, double Close, double Volume, bool IsNew = true) => - Add(new TBar(DateTime.Now, Open, High, Low, Close, Volume, IsNew)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(TBarSeries series) - { - if (series == this) - { - // If adding itself, create a copy to avoid modification during enumeration - var copy = new TBarSeries { Name = Name }; - copy.AddRange(this); - AddRange(copy); - } - else - { - AddRange(series); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public new virtual void AddRange(IEnumerable collection) - { - foreach (var item in collection) - { - Add(item); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Sub(object source, in TBarEventArgs args) - { - Add(args.Bar); - } -} diff --git a/lib/core/tbarseries.cs b/lib/core/tbarseries.cs new file mode 100644 index 00000000..32163238 --- /dev/null +++ b/lib/core/tbarseries.cs @@ -0,0 +1,110 @@ +using System.Runtime.CompilerServices; + +namespace QuanTAlib; + +public delegate void BarSignal(object source, in TBarEventArgs args); + +[SkipLocalsInit] +public sealed class TBarEventArgs : EventArgs +{ + public readonly TBar Bar; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBarEventArgs(TBar bar) => Bar = bar; +} + +[SkipLocalsInit] +public class TBarSeries : List +{ + private static readonly TBar Default = new(DateTime.MinValue, double.NaN, double.NaN, double.NaN, double.NaN, double.NaN); + + public TSeries Open { get; init; } + public TSeries High { get; init; } + public TSeries Low { get; init; } + public TSeries Close { get; init; } + public TSeries Volume { get; init; } + + public TBar Last => Count > 0 ? this[^1] : Default; + public TBar First => Count > 0 ? this[0] : Default; + public int Length => Count; + public string Name { get; set; } + public event BarSignal Pub = delegate { }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBarSeries() + { + Name = "Bar"; + Open = new TSeries(); + High = new TSeries(); + Low = new TSeries(); + Close = new TSeries(); + Volume = new TSeries(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBarSeries(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public new virtual void Add(TBar bar) + { + if (bar.IsNew || base.Count == 0) + { + base.Add(bar); + } + else + { + this[^1] = bar; + } + + Pub?.Invoke(this, new TBarEventArgs(bar)); + + Open.Add(bar.Time, bar.Open, IsNew: bar.IsNew, IsHot: true); + High.Add(bar.Time, bar.High, IsNew: bar.IsNew, IsHot: true); + Low.Add(bar.Time, bar.Low, IsNew: bar.IsNew, IsHot: true); + Close.Add(bar.Time, bar.Close, IsNew: bar.IsNew, IsHot: true); + Volume.Add(bar.Time, bar.Volume, IsNew: bar.IsNew, IsHot: true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(DateTime Time, double Open, double High, double Low, double Close, double Volume, bool IsNew = true) => + Add(new TBar(Time, Open, High, Low, Close, Volume, IsNew)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(double Open, double High, double Low, double Close, double Volume, bool IsNew = true) => + Add(new TBar(DateTime.Now, Open, High, Low, Close, Volume, IsNew)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(TBarSeries series) + { + if (series == this) + { + // If adding itself, create a copy to avoid modification during enumeration + var copy = new TBarSeries { Name = Name }; + copy.AddRange(this); + AddRange(copy); + } + else + { + AddRange(series); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public new virtual void AddRange(IEnumerable collection) + { + foreach (var item in collection) + { + Add(item); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Sub(object source, in TBarEventArgs args) + { + Add(args.Bar); + } +} diff --git a/lib/core/tseries.cs b/lib/core/tseries.cs new file mode 100644 index 00000000..acbe3ac6 --- /dev/null +++ b/lib/core/tseries.cs @@ -0,0 +1,122 @@ +using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; + +namespace QuanTAlib; + +public delegate void ValueSignal(object source, in ValueEventArgs args); + +[SkipLocalsInit] +public sealed class ValueEventArgs : EventArgs +{ + public readonly TValue Tick; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ValueEventArgs(TValue value) => Tick = value; +} + +[SkipLocalsInit] +public class TSeries : List +{ + private static readonly TValue Default = new(DateTime.MinValue, double.NaN); + + public IEnumerable t => this.Select(item => item.t); + public IEnumerable v => this.Select(item => item.v); + public TValue Last => Count > 0 ? this[^1] : Default; + public TValue First => Count > 0 ? this[0] : Default; + public int Length => Count; + public string Name { get; set; } + + /// + /// Event that publishes value updates to subscribers. This event is used in the pub/sub pattern + /// where TSeries instances can subscribe to updates from other data sources through the Sub method, + /// and publish their own updates to downstream subscribers. + /// + [SuppressMessage("Minor Code Smell", "S3264:Events should be invoked", Justification = "Event is invoked through delegate")] + public event ValueSignal Pub = delegate { }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TSeries() + { + Name = "Data"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TSeries(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + if (pubEvent != null) + { + pubEvent.AddEventHandler(source, new ValueSignal(Sub)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator List(TSeries series) => series.Select(item => item.Value).ToList(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator double[](TSeries series) => series.Select(item => item.Value).ToArray(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public new virtual void Add(TValue tick) + { + if (tick.IsNew || base.Count == 0) + { + base.Add(tick); + } + else + { + this[^1] = tick; + } + Pub?.Invoke(this, new ValueEventArgs(tick)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void Add(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) => + Add(new TValue(Time, Value, IsNew, IsHot)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void Add(double Value, bool IsNew = true, bool IsHot = true) => + Add(new TValue(DateTime.UtcNow, Value, IsNew, IsHot)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(IEnumerable values) + { + var valueList = values.ToList(); + int count = valueList.Count; + DateTime startTime = DateTime.UtcNow - TimeSpan.FromHours(count); + + for (int i = 0; i < count; i++) + { + Add(startTime, valueList[i]); + startTime = startTime.AddHours(1); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(TSeries series) + { + if (series == this) + { + // If adding itself, create a copy to avoid modification during enumeration + var copy = new TSeries { Name = Name }; + copy.AddRange(this); + AddRange(copy); + } + else + { + AddRange(series); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public new virtual void AddRange(IEnumerable collection) + { + foreach (var item in collection) + { + Add(item); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Sub(object source, in ValueEventArgs args) => Add(args.Tick); +} diff --git a/lib/core/tvalue.cs b/lib/core/tvalue.cs index 8e54ba48..04f4e517 100644 --- a/lib/core/tvalue.cs +++ b/lib/core/tvalue.cs @@ -39,114 +39,3 @@ public readonly record struct TValue(DateTime Time, double Value, bool IsNew = t [MethodImpl(MethodImplOptions.AggressiveInlining)] public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}, {Value:F2}, IsNew: {IsNew}, IsHot: {IsHot}]"; } - -public delegate void ValueSignal(object source, in ValueEventArgs args); - -[SkipLocalsInit] -public sealed class ValueEventArgs : EventArgs -{ - public readonly TValue Tick; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ValueEventArgs(TValue value) => Tick = value; -} - -[SkipLocalsInit] -public class TSeries : List -{ - private static readonly TValue Default = new(DateTime.MinValue, double.NaN); - - public IEnumerable t => this.Select(item => item.t); - public IEnumerable v => this.Select(item => item.v); - public TValue Last => Count > 0 ? this[^1] : Default; - public TValue First => Count > 0 ? this[0] : Default; - public int Length => Count; - public string Name { get; set; } - public event ValueSignal Pub = delegate { }; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TSeries() - { - Name = "Data"; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TSeries(object source) : this() - { - var pubEvent = source.GetType().GetEvent("Pub"); - if (pubEvent != null) - { - pubEvent.AddEventHandler(source, new ValueSignal(Sub)); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static explicit operator List(TSeries series) => series.Select(item => item.Value).ToList(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static explicit operator double[](TSeries series) => series.Select(item => item.Value).ToArray(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public new virtual void Add(TValue tick) - { - if (tick.IsNew || base.Count == 0) - { - base.Add(tick); - } - else - { - this[^1] = tick; - } - Pub?.Invoke(this, new ValueEventArgs(tick)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public virtual void Add(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) => - Add(new TValue(Time, Value, IsNew, IsHot)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public virtual void Add(double Value, bool IsNew = true, bool IsHot = true) => - Add(new TValue(DateTime.UtcNow, Value, IsNew, IsHot)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(IEnumerable values) - { - var valueList = values.ToList(); - int count = valueList.Count; - DateTime startTime = DateTime.UtcNow - TimeSpan.FromHours(count); - - for (int i = 0; i < count; i++) - { - Add(startTime, valueList[i]); - startTime = startTime.AddHours(1); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(TSeries series) - { - if (series == this) - { - // If adding itself, create a copy to avoid modification during enumeration - var copy = new TSeries { Name = Name }; - copy.AddRange(this); - AddRange(copy); - } - else - { - AddRange(series); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public new virtual void AddRange(IEnumerable collection) - { - foreach (var item in collection) - { - Add(item); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Sub(object source, in ValueEventArgs args) => Add(args.Tick); -} diff --git a/lib/momentum/Tsi.cs b/lib/momentum/Tsi.cs deleted file mode 100644 index e39458bc..00000000 --- a/lib/momentum/Tsi.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System.Runtime.CompilerServices; -namespace QuanTAlib; - -/// -/// TSI: True Strength Index -/// A momentum indicator that shows both trend direction and overbought/oversold conditions -/// by using two smoothing steps on price changes. -/// -/// -/// The TSI calculation process: -/// 1. Calculate price change (PC): -/// PC = Close - Previous Close -/// 2. Calculate absolute price change (APC): -/// APC = |PC| -/// 3. Double smooth both PC and APC using EMA: -/// First PC EMA = EMA(PC, firstPeriod) -/// Second PC EMA = EMA(First PC EMA, secondPeriod) -/// First APC EMA = EMA(APC, firstPeriod) -/// Second APC EMA = EMA(First APC EMA, secondPeriod) -/// 4. Calculate TSI: -/// TSI = (Second PC EMA / Second APC EMA) * 100 -/// -/// Key characteristics: -/// - Double smoothed momentum indicator -/// - Oscillates between +100 and -100 -/// - Default periods are 25 and 13 -/// - Shows trend direction -/// - Identifies overbought/oversold -/// -/// Formula: -/// TSI = (EMA(EMA(PC, r), s) / EMA(EMA(|PC|, r), s)) * 100 -/// where: -/// PC = Close - Previous Close -/// r = first period (default 25) -/// s = second period (default 13) -/// -/// Market Applications: -/// - Trend direction -/// - Overbought/Oversold levels -/// - Centerline crossovers -/// - Divergence analysis -/// - Signal line crossovers -/// -/// Sources: -/// William Blau - Original development (1991) -/// https://www.investopedia.com/terms/t/tsi.asp -/// -/// Note: Values above +25 indicate overbought conditions, while values below -25 indicate oversold conditions -/// -[SkipLocalsInit] -public sealed class Tsi : AbstractBase -{ - private readonly int _firstPeriod; - private double _prevClose; - private double _pcFirstEma; - private double _pcSecondEma; - private double _apcFirstEma; - private double _apcSecondEma; - private readonly double _firstAlpha; - private readonly double _secondAlpha; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Tsi(int firstPeriod = 25, int secondPeriod = 13) - { - _firstPeriod = firstPeriod; - WarmupPeriod = firstPeriod + secondPeriod; - Name = $"TSI({_firstPeriod},{secondPeriod})"; - _firstAlpha = 2.0 / (firstPeriod + 1); - _secondAlpha = 2.0 / (secondPeriod + 1); - Init(); - } - - /// The data source object that publishes updates. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Tsi(object source, int firstPeriod = 25, int secondPeriod = 13) : this(firstPeriod, secondPeriod) - { - var pubEvent = source.GetType().GetEvent("Pub"); - pubEvent?.AddEventHandler(source, new BarSignal(Sub)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void Init() - { - base.Init(); - _prevClose = 0; - _pcFirstEma = 0; - _pcSecondEma = 0; - _apcFirstEma = 0; - _apcSecondEma = 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected override void ManageState(bool isNew) - { - if (isNew) - { - _lastValidValue = Value; - _index++; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - protected override double Calculation() - { - ManageState(BarInput.IsNew); - - // Skip first period to establish previous close - if (_index == 1) - { - _prevClose = BarInput.Close; - return 0; - } - - // Calculate price changes - double pc = BarInput.Close - _prevClose; - double apc = Math.Abs(pc); - - // Initialize or update EMAs - if (_index <= _firstPeriod) - { - _pcFirstEma = pc; - _apcFirstEma = apc; - } - else - { - _pcFirstEma = (_firstAlpha * pc) + ((1 - _firstAlpha) * _pcFirstEma); - _apcFirstEma = (_firstAlpha * apc) + ((1 - _firstAlpha) * _apcFirstEma); - } - - if (_index <= WarmupPeriod) - { - _pcSecondEma = _pcFirstEma; - _apcSecondEma = _apcFirstEma; - } - else - { - _pcSecondEma = (_secondAlpha * _pcFirstEma) + ((1 - _secondAlpha) * _pcSecondEma); - _apcSecondEma = (_secondAlpha * _apcFirstEma) + ((1 - _secondAlpha) * _apcSecondEma); - } - - // Store current close for next calculation - _prevClose = BarInput.Close; - - // Calculate TSI - double tsi = Math.Abs(_apcSecondEma) > double.Epsilon ? (_pcSecondEma / _apcSecondEma) * 100 : 0; - - IsHot = _index >= WarmupPeriod; - return tsi; - } -} diff --git a/lib/oscillators/Coppock.cs b/lib/oscillators/Coppock.cs new file mode 100644 index 00000000..9cd6231c --- /dev/null +++ b/lib/oscillators/Coppock.cs @@ -0,0 +1,118 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// COPPOCK: Coppock Curve +/// A long-term momentum oscillator used to identify major bottoms in the market. +/// It is calculated using a weighted moving average of two different Rate of Change calculations. +/// +/// +/// The Coppock Curve calculation process: +/// 1. Calculate 14-period Rate of Change (ROC) +/// 2. Calculate 11-period Rate of Change (ROC) +/// 3. Sum the two ROC values +/// 4. Apply 10-period Weighted Moving Average (WMA) to the sum +/// +/// Key characteristics: +/// - Long-term momentum indicator +/// - Primarily used for monthly data +/// - Buy signals when curve turns up from below zero +/// - Rarely used for sell signals +/// - Designed to identify major bottoms in stock market indices +/// +/// Formula: +/// COPPOCK = WMA(10) of (ROC(14) + ROC(11)) +/// where: +/// ROC(n) = ((Price - Price[n]) / Price[n]) * 100 +/// WMA is weighted moving average +/// +/// Sources: +/// Edwin Coppock - Barron's Magazine (October 1962) +/// https://www.investopedia.com/terms/c/coppockcurve.asp +/// +/// Note: Originally designed for monthly data with parameters (14,11,10), +/// but can be adapted for other timeframes +/// +[SkipLocalsInit] +public sealed class Coppock : AbstractBase +{ + private readonly CircularBuffer _values; + private readonly Wma _wma; + private readonly int _roc1Period; + private readonly int _roc2Period; + private const int DefaultRoc1Period = 14; + private const int DefaultRoc2Period = 11; + private const int DefaultWmaPeriod = 10; + + /// The first ROC period (default 14). + /// The second ROC period (default 11). + /// The WMA smoothing period (default 10). + /// Thrown when any period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Coppock(int roc1Period = DefaultRoc1Period, int roc2Period = DefaultRoc2Period, int wmaPeriod = DefaultWmaPeriod) + { + if (roc1Period < 1) + throw new ArgumentOutOfRangeException(nameof(roc1Period), "ROC1 period must be greater than 0"); + if (roc2Period < 1) + throw new ArgumentOutOfRangeException(nameof(roc2Period), "ROC2 period must be greater than 0"); + if (wmaPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(wmaPeriod), "WMA period must be greater than 0"); + + _roc1Period = roc1Period; + _roc2Period = roc2Period; + int maxPeriod = Math.Max(roc1Period, roc2Period); + _values = new(maxPeriod + 1); + _wma = new(wmaPeriod); + WarmupPeriod = maxPeriod + wmaPeriod; + Name = $"COPPOCK({roc1Period},{roc2Period},{wmaPeriod})"; + } + + /// The data source object that publishes updates. + /// The first ROC period. + /// The second ROC period. + /// The WMA smoothing period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Coppock(object source, int roc1Period = DefaultRoc1Period, int roc2Period = DefaultRoc2Period, int wmaPeriod = DefaultWmaPeriod) + : this(roc1Period, roc2Period, wmaPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _values.Add(Input.Value); + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateRoc(int period) + { + if (_index <= period) return 0; + double currentValue = _values[0]; + double oldValue = _values[period]; + return ((currentValue - oldValue) / oldValue) * 100.0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Calculate ROC values and their sum + double roc1 = CalculateRoc(_roc1Period); + double roc2 = CalculateRoc(_roc2Period); + double rocSum = roc1 + roc2; + + // Not enough data for WMA calculation + if (_index <= Math.Max(_roc1Period, _roc2Period)) + return 0; + + // Calculate WMA of ROC sums + return _wma.Calc(new TValue(Input.Time, rocSum, Input.IsNew)); + } +} diff --git a/lib/oscillators/Crsi.cs b/lib/oscillators/Crsi.cs new file mode 100644 index 00000000..1d65c9e8 --- /dev/null +++ b/lib/oscillators/Crsi.cs @@ -0,0 +1,97 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// CRSI: Connor RSI +/// A momentum oscillator that combines three different RSI time periods to provide +/// a more comprehensive view of price momentum. It helps identify overbought and +/// oversold conditions with higher accuracy than traditional RSI. +/// +/// +/// The CRSI calculation process: +/// 1. Calculate three RSIs with different periods (3,2,1) +/// 2. Sum the three RSI values +/// 3. Divide by 3 to get the average +/// +/// Key characteristics: +/// - Oscillates between 0 and 100 +/// - More responsive than traditional RSI +/// - Combines multiple timeframes +/// - Traditional overbought level at 90 +/// - Traditional oversold level at 10 +/// +/// Formula: +/// CRSI = (RSI(3) + RSI(2) + RSI(1)) / 3 +/// where each RSI is calculated using standard RSI formula: +/// RSI = 100 - (100 / (1 + RS)) +/// RS = Average Gain / Average Loss +/// +/// Sources: +/// Larry Connors - "Short-term Trading Strategies That Work" +/// https://www.tradingview.com/script/cYk1LVpw-Connors-RSI-LazyBear/ +/// +/// Note: Default periods are 3,2,1 as recommended by Connors +/// +[SkipLocalsInit] +public sealed class Crsi : AbstractBase +{ + private readonly Rsi _rsi3; + private readonly Rsi _rsi2; + private readonly Rsi _rsi1; + private const int DefaultPeriod1 = 3; + private const int DefaultPeriod2 = 2; + private const int DefaultPeriod3 = 1; + + /// The first RSI period (default 3). + /// The second RSI period (default 2). + /// The third RSI period (default 1). + /// Thrown when any period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Crsi(int period1 = DefaultPeriod1, int period2 = DefaultPeriod2, int period3 = DefaultPeriod3) + { + if (period1 < 1) + throw new ArgumentOutOfRangeException(nameof(period1), "Period1 must be greater than 0"); + if (period2 < 1) + throw new ArgumentOutOfRangeException(nameof(period2), "Period2 must be greater than 0"); + if (period3 < 1) + throw new ArgumentOutOfRangeException(nameof(period3), "Period3 must be greater than 0"); + + _rsi3 = new(period1); + _rsi2 = new(period2); + _rsi1 = new(period3); + WarmupPeriod = Math.Max(Math.Max(period1, period2), period3) + 1; + Name = $"CRSI({period1},{period2},{period3})"; + } + + /// The data source object that publishes updates. + /// The first RSI period. + /// The second RSI period. + /// The third RSI period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Crsi(object source, int period1 = DefaultPeriod1, int period2 = DefaultPeriod2, int period3 = DefaultPeriod3) + : this(period1, period2, period3) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Calculate individual RSIs + double rsi3 = _rsi3.Calc(Input); + double rsi2 = _rsi2.Calc(Input); + double rsi1 = _rsi1.Calc(Input); + + // Average the three RSIs + return (rsi3 + rsi2 + rsi1) / 3.0; + } +} diff --git a/lib/oscillators/Smi.cs b/lib/oscillators/Smi.cs new file mode 100644 index 00000000..27e6e635 --- /dev/null +++ b/lib/oscillators/Smi.cs @@ -0,0 +1,121 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// SMI: Stochastic Momentum Index +/// A double-smoothed momentum indicator that shows where the close is relative +/// to the midpoint of the recent high/low range. It helps identify overbought +/// and oversold conditions with higher accuracy than traditional stochastics. +/// +/// +/// The SMI calculation process: +/// 1. Calculate median price distance (Close - (High + Low)/2) +/// 2. Calculate highest high and lowest low over period +/// 3. First smoothing of median distance and range +/// 4. Second smoothing of first smoothed values +/// 5. Scale to percentage (-100 to +100) +/// +/// Key characteristics: +/// - Oscillates between -100 and +100 +/// - Double smoothing reduces noise +/// - Traditional overbought level at +40 +/// - Traditional oversold level at -40 +/// - Centerline crossovers signal trend changes +/// +/// Formula: +/// D = Close - (High + Low)/2 +/// HL = Highest High - Lowest Low +/// First smoothing: +/// SD = EMA(EMA(D, period1), period2) +/// SHL = EMA(EMA(HL, period1), period2) +/// SMI = 100 * (SD / (SHL/2)) +/// +/// Sources: +/// William Blau - "Momentum, Direction, and Divergence" (1995) +/// https://www.tradingview.com/scripts/stochasticmomentumindex/ +/// +/// Note: Default periods (10,3,3) are commonly used values +/// +[SkipLocalsInit] +public sealed class Smi : AbstractBase +{ + private readonly CircularBuffer _highs; + private readonly CircularBuffer _lows; + private readonly Ema _dEma1; + private readonly Ema _dEma2; + private readonly Ema _hlEma1; + private readonly Ema _hlEma2; + private const int DefaultPeriod = 10; + private const int DefaultSmooth1 = 3; + private const int DefaultSmooth2 = 3; + private const double ScalingFactor = 100.0; + + /// The lookback period (default 10). + /// First smoothing period (default 3). + /// Second smoothing period (default 3). + /// Thrown when any period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Smi(int period = DefaultPeriod, int smooth1 = DefaultSmooth1, int smooth2 = DefaultSmooth2) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0"); + if (smooth1 < 1) + throw new ArgumentOutOfRangeException(nameof(smooth1), "Smooth1 must be greater than 0"); + if (smooth2 < 1) + throw new ArgumentOutOfRangeException(nameof(smooth2), "Smooth2 must be greater than 0"); + + _highs = new(period); + _lows = new(period); + _dEma1 = new(smooth1); + _dEma2 = new(smooth2); + _hlEma1 = new(smooth1); + _hlEma2 = new(smooth2); + WarmupPeriod = period + smooth1 + smooth2; + Name = $"SMI({period},{smooth1},{smooth2})"; + } + + /// The data source object that publishes updates. + /// The lookback period. + /// First smoothing period. + /// Second smoothing period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Smi(object source, int period = DefaultPeriod, int smooth1 = DefaultSmooth1, int smooth2 = DefaultSmooth2) + : this(period, smooth1, smooth2) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _highs.Add(BarInput.High); + _lows.Add(BarInput.Low); + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate median price distance and range + double midpoint = (BarInput.High + BarInput.Low) / 2.0; + double distance = BarInput.Close - midpoint; + double range = _highs.Max() - _lows.Min(); + + // First smoothing + double smoothD1 = _dEma1.Calc(new TValue(BarInput.Time, distance, BarInput.IsNew)); + double smoothHL1 = _hlEma1.Calc(new TValue(BarInput.Time, range, BarInput.IsNew)); + + // Second smoothing + double smoothD2 = _dEma2.Calc(new TValue(BarInput.Time, smoothD1, BarInput.IsNew)); + double smoothHL2 = _hlEma2.Calc(new TValue(BarInput.Time, smoothHL1, BarInput.IsNew)); + + // Calculate SMI + return smoothHL2 >= double.Epsilon ? ScalingFactor * (smoothD2 / (smoothHL2 / 2.0)) : 0; + } +} diff --git a/lib/oscillators/Srsi.cs b/lib/oscillators/Srsi.cs new file mode 100644 index 00000000..c58d3fe9 --- /dev/null +++ b/lib/oscillators/Srsi.cs @@ -0,0 +1,145 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// SRSI: Stochastic RSI +/// A momentum oscillator that applies the stochastic formula to RSI values +/// instead of price data. It provides a more sensitive indicator than standard +/// RSI or Stochastic oscillators. +/// +/// +/// The SRSI calculation process: +/// 1. Calculate RSI +/// 2. Apply Stochastic formula to RSI values: +/// - Find highest high and lowest low of RSI over period +/// - Calculate where current RSI is within this range +/// 3. Smooth the result with SMA (signal line) +/// +/// Key characteristics: +/// - Oscillates between 0 and 100 +/// - More sensitive than standard RSI +/// - Combines benefits of both RSI and Stochastic +/// - Traditional overbought level at 80 +/// - Traditional oversold level at 20 +/// +/// Formula: +/// SRSI = ((RSI - Lowest RSI) / (Highest RSI - Lowest RSI)) * 100 +/// Signal = SMA(SRSI, signalPeriod) +/// +/// Sources: +/// Tushar Chande and Stanley Kroll - "The New Technical Trader" (1994) +/// https://www.investopedia.com/terms/s/stochrsi.asp +/// +/// Note: Default periods (14,14,3,3) are commonly used values +/// +[SkipLocalsInit] +public sealed class Srsi : AbstractBase +{ + private readonly Rsi _rsi; + private readonly CircularBuffer _rsiValues; + private readonly CircularBuffer _srsiValues; + private readonly Sma _signal; + private readonly int _rsiPeriod; + private readonly int _stochPeriod; + private const int DefaultRsiPeriod = 14; + private const int DefaultStochPeriod = 14; + private const int DefaultSmoothK = 3; + private const int DefaultSmoothD = 3; + private const double ScalingFactor = 100.0; + + /// The RSI period (default 14). + /// The Stochastic period (default 14). + /// K line smoothing period (default 3). + /// D line smoothing period (default 3). + /// Thrown when any period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Srsi(int rsiPeriod = DefaultRsiPeriod, int stochPeriod = DefaultStochPeriod, + int smoothK = DefaultSmoothK, int smoothD = DefaultSmoothD) + { + if (rsiPeriod < 1) + { + throw new ArgumentOutOfRangeException(nameof(rsiPeriod), "Period must be greater than 0"); + } + if (stochPeriod < 1) + { + throw new ArgumentOutOfRangeException(nameof(stochPeriod), "Period must be greater than 0"); + } + if (smoothK < 1) + { + throw new ArgumentOutOfRangeException(nameof(smoothK), "Period must be greater than 0"); + } + if (smoothD < 1) + { + throw new ArgumentOutOfRangeException(nameof(smoothD), "Period must be greater than 0"); + } + + _rsiPeriod = rsiPeriod; + _stochPeriod = stochPeriod; + _rsi = new(rsiPeriod); + _rsiValues = new(stochPeriod); + _srsiValues = new(smoothK); + _signal = new(smoothD); + WarmupPeriod = rsiPeriod + stochPeriod + Math.Max(smoothK, smoothD); + Name = $"SRSI({rsiPeriod},{stochPeriod},{smoothK},{smoothD})"; + } + + /// The data source object that publishes updates. + /// The RSI period. + /// The Stochastic period. + /// K line smoothing period. + /// D line smoothing period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Srsi(object source, int rsiPeriod = DefaultRsiPeriod, int stochPeriod = DefaultStochPeriod, + int smoothK = DefaultSmoothK, int smoothD = DefaultSmoothD) + : this(rsiPeriod, stochPeriod, smoothK, smoothD) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Calculate RSI + double rsiValue = _rsi.Calc(Input); + + if (Input.IsNew) + _rsiValues.Add(rsiValue); + + // Not enough data + if (_index <= _rsiPeriod) + return 0; + + // Calculate Stochastic RSI + double highest = _rsiValues.Max(); + double lowest = _rsiValues.Min(); + double range = highest - lowest; + double srsi = range >= double.Epsilon ? ((rsiValue - lowest) / range) * ScalingFactor : 0; + + if (Input.IsNew) + _srsiValues.Add(srsi); + + // Calculate signal line + return _signal.Calc(new TValue(Input.Time, srsi, Input.IsNew)); + } + + /// + /// Gets the K line value (raw Stochastic RSI) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double K() => _srsiValues[0]; + + /// + /// Gets the D line value (signal line) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double D() => Value; +} diff --git a/lib/oscillators/Stc.cs b/lib/oscillators/Stc.cs new file mode 100644 index 00000000..feb544c5 --- /dev/null +++ b/lib/oscillators/Stc.cs @@ -0,0 +1,140 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// STC: Schaff Trend Cycle +/// A trend-following indicator that combines MACD and stochastic concepts +/// to create a smoother, more responsive indicator with less noise. +/// +/// +/// The STC calculation process: +/// 1. Calculate MACD-style momentum using EMAs +/// 2. Apply double stochastic formula to smooth the momentum +/// 3. Scale result to oscillator range +/// +/// Key characteristics: +/// - Oscillates between 0 and 100 +/// - Combines trend and momentum +/// - Double smoothing reduces noise +/// - Traditional overbought level at 75 +/// - Traditional oversold level at 25 +/// +/// Formula: +/// Momentum = EMA1(Close) - EMA2(Close) +/// First Stochastic: +/// %K1 = 100 * (Momentum - Lowest Low) / (Highest High - Lowest Low) +/// %D1 = EMA(%K1) +/// Second Stochastic: +/// %K2 = 100 * (%D1 - Lowest %D1) / (Highest %D1 - Lowest %D1) +/// STC = EMA(%K2) +/// +/// Sources: +/// Doug Schaff - "The Schaff Trend Cycle" (1999) +/// https://www.tradingview.com/script/o6tSS6Hn-Schaff-Trend-Cycle/ +/// +/// Note: Default periods (23,10,3) were recommended by Schaff +/// +[SkipLocalsInit] +public sealed class Stc : AbstractBase +{ + private readonly Ema _fastEma; + private readonly Ema _slowEma; + private readonly CircularBuffer _macdValues; + private readonly CircularBuffer _k1Values; + private readonly CircularBuffer _d1Values; + private readonly Ema _d1Ema; + private readonly Ema _stcEma; + private const int DefaultCyclePeriod = 10; + private const int DefaultFastPeriod = 23; + private const int DefaultSlowPeriod = 50; + private const int DefaultD1Period = 3; + private const int DefaultStcPeriod = 3; + private const double ScalingFactor = 100.0; + + /// The lookback period for highs/lows (default 10). + /// Fast EMA period (default 23). + /// Slow EMA period (default 50). + /// First %D smoothing period (default 3). + /// Final STC smoothing period (default 3). + /// Thrown when any period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Stc(int cyclePeriod = DefaultCyclePeriod, int fastPeriod = DefaultFastPeriod, + int slowPeriod = DefaultSlowPeriod, int d1Period = DefaultD1Period, + int stcPeriod = DefaultStcPeriod) + { + if (cyclePeriod < 1 || fastPeriod < 1 || slowPeriod < 1 || d1Period < 1 || stcPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(cyclePeriod), "All periods must be greater than 0"); + if (fastPeriod >= slowPeriod) + { + throw new ArgumentOutOfRangeException(nameof(fastPeriod), "Fast period must be less than slow period"); + } + _fastEma = new(fastPeriod); + _slowEma = new(slowPeriod); + _macdValues = new(cyclePeriod); + _k1Values = new(cyclePeriod); + _d1Values = new(cyclePeriod); + _d1Ema = new(d1Period); + _stcEma = new(stcPeriod); + + WarmupPeriod = slowPeriod + cyclePeriod + Math.Max(d1Period, stcPeriod); + Name = $"STC({cyclePeriod},{fastPeriod},{slowPeriod},{d1Period},{stcPeriod})"; + } + + /// The data source object that publishes updates. + /// The lookback period for highs/lows. + /// Fast EMA period. + /// Slow EMA period. + /// First %D smoothing period. + /// Final STC smoothing period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Stc(object source, int cyclePeriod = DefaultCyclePeriod, int fastPeriod = DefaultFastPeriod, + int slowPeriod = DefaultSlowPeriod, int d1Period = DefaultD1Period, + int stcPeriod = DefaultStcPeriod) + : this(cyclePeriod, fastPeriod, slowPeriod, d1Period, stcPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateStochastic(double value, double highest, double lowest) + { + double range = highest - lowest; + return range >= double.Epsilon ? ((value - lowest) / range) * ScalingFactor : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Calculate MACD-style momentum + double fastEma = _fastEma.Calc(Input); + double slowEma = _slowEma.Calc(Input); + double macd = fastEma - slowEma; + + if (Input.IsNew) + _macdValues.Add(macd); + + // First stochastic + double k1 = CalculateStochastic(macd, _macdValues.Max(), _macdValues.Min()); + if (Input.IsNew) + _k1Values.Add(k1); + + double d1 = _d1Ema.Calc(new TValue(Input.Time, k1, Input.IsNew)); + if (Input.IsNew) + _d1Values.Add(d1); + + // Second stochastic + double k2 = CalculateStochastic(d1, _d1Values.Max(), _d1Values.Min()); + + // Final smoothing + return _stcEma.Calc(new TValue(Input.Time, k2, Input.IsNew)); + } +} diff --git a/lib/oscillators/Stoch.cs b/lib/oscillators/Stoch.cs new file mode 100644 index 00000000..ab10d0b6 --- /dev/null +++ b/lib/oscillators/Stoch.cs @@ -0,0 +1,126 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// STOCH: Stochastic Oscillator +/// A momentum indicator that shows the location of the close relative to +/// high-low range over a period. Consists of %K (fast) and %D (slow) lines. +/// +/// +/// The Stochastic calculation process: +/// 1. Calculate %K (raw stochastic): +/// - Find highest high and lowest low over period +/// - Calculate where current close is within this range +/// 2. Smooth %K with SMA to get Fast %K +/// 3. Smooth Fast %K with SMA to get %D (signal line) +/// +/// Key characteristics: +/// - Oscillates between 0 and 100 +/// - Traditional overbought level at 80 +/// - Traditional oversold level at 20 +/// - %K/%D crossovers signal momentum shifts +/// - Divergence with price shows potential reversals +/// +/// Formula: +/// Raw %K = 100 * (Close - Lowest Low) / (Highest High - Lowest Low) +/// Fast %K = SMA(Raw %K, smoothK) +/// %D = SMA(Fast %K, smoothD) +/// +/// Sources: +/// George Lane - "Lane's Stochastics" (1950s) +/// https://www.investopedia.com/terms/s/stochasticoscillator.asp +/// +/// Note: Default periods (14,3,3) are commonly used values +/// +[SkipLocalsInit] +public sealed class Stoch : AbstractBase +{ + private readonly CircularBuffer _highs; + private readonly CircularBuffer _lows; + private readonly Sma _fastK; + private readonly Sma _slowD; + private readonly CircularBuffer _rawK; + private const int DefaultPeriod = 14; + private const int DefaultSmoothK = 3; + private const int DefaultSmoothD = 3; + private const double ScalingFactor = 100.0; + + /// The lookback period (default 14). + /// %K smoothing period (default 3). + /// %D smoothing period (default 3). + /// Thrown when any period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Stoch(int period = DefaultPeriod, int smoothK = DefaultSmoothK, int smoothD = DefaultSmoothD) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0"); + if (smoothK < 1) + throw new ArgumentOutOfRangeException(nameof(smoothK), "%K smoothing period must be greater than 0"); + if (smoothD < 1) + throw new ArgumentOutOfRangeException(nameof(smoothD), "%D smoothing period must be greater than 0"); + + _highs = new(period); + _lows = new(period); + _rawK = new(smoothK); + _fastK = new(smoothK); + _slowD = new(smoothD); + WarmupPeriod = period + Math.Max(smoothK, smoothD); + Name = $"STOCH({period},{smoothK},{smoothD})"; + } + + /// The data source object that publishes updates. + /// The lookback period. + /// %K smoothing period. + /// %D smoothing period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Stoch(object source, int period = DefaultPeriod, int smoothK = DefaultSmoothK, int smoothD = DefaultSmoothD) + : this(period, smoothK, smoothD) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _highs.Add(BarInput.High); + _lows.Add(BarInput.Low); + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate raw %K + double highest = _highs.Max(); + double lowest = _lows.Min(); + double range = highest - lowest; + double rawK = range >= double.Epsilon ? ((BarInput.Close - lowest) / range) * ScalingFactor : 0; + + if (BarInput.IsNew) + _rawK.Add(rawK); + + // Calculate Fast %K (first smoothing) + double fastK = _fastK.Calc(new TValue(BarInput.Time, rawK, BarInput.IsNew)); + + // Calculate %D (second smoothing) + return _slowD.Calc(new TValue(BarInput.Time, fastK, BarInput.IsNew)); + } + + /// + /// Gets the %K line value (Fast Stochastic) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double K() => _fastK.Value; + + /// + /// Gets the %D line value (Slow Stochastic) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double D() => Value; +} diff --git a/lib/oscillators/Tsi.cs b/lib/oscillators/Tsi.cs new file mode 100644 index 00000000..98f214ea --- /dev/null +++ b/lib/oscillators/Tsi.cs @@ -0,0 +1,111 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// TSI: True Strength Index +/// A momentum oscillator that shows both trend direction and overbought/oversold conditions. +/// Uses two EMAs of price change momentum to help identify short-term trends and reversals. +/// +/// +/// The TSI calculation process: +/// 1. Calculate price change (PC): Current close - Previous close +/// 2. Calculate absolute price change (APC): Absolute value of PC +/// 3. First smoothing: EMA1 of PC and EMA1 of APC +/// 4. Second smoothing: EMA2 of EMA1(PC) and EMA2 of EMA1(APC) +/// 5. TSI = 100 * (Double smoothed PC / Double smoothed APC) +/// +/// Key characteristics: +/// - Oscillates around zero +/// - Shows momentum and trend direction +/// - Identifies overbought/oversold conditions +/// - Generates signals through centerline/signal line crossovers +/// - Shows momentum divergence with price +/// +/// Formula: +/// TSI = 100 * (EMA2(EMA1(PC)) / EMA2(EMA1(APC))) +/// where: +/// PC = Current Price - Previous Price +/// APC = |PC| +/// Default periods: First EMA = 25, Second EMA = 13 +/// +/// Sources: +/// William Blau - "Momentum, Direction, and Divergence" (1995) +/// https://www.investopedia.com/terms/t/tsi.asp +/// +/// Note: Default periods (25,13) were recommended by Blau +/// +[SkipLocalsInit] +public sealed class Tsi : AbstractBase +{ + private readonly Ema _pcEma1; + private readonly Ema _pcEma2; + private readonly Ema _apcEma1; + private readonly Ema _apcEma2; + private double _prevPrice; + private const int DefaultFirstPeriod = 25; + private const int DefaultSecondPeriod = 13; + private const double ScalingFactor = 100.0; + + /// The first EMA smoothing period (default 25). + /// The second EMA smoothing period (default 13). + /// Thrown when any period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Tsi(int firstPeriod = DefaultFirstPeriod, int secondPeriod = DefaultSecondPeriod) + { + if (firstPeriod < 1 || secondPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(firstPeriod), "All periods must be greater than 0"); + + _pcEma1 = new(firstPeriod); + _pcEma2 = new(secondPeriod); + _apcEma1 = new(firstPeriod); + _apcEma2 = new(secondPeriod); + WarmupPeriod = firstPeriod + secondPeriod; + Name = $"TSI({firstPeriod},{secondPeriod})"; + } + + /// The data source object that publishes updates. + /// The first EMA smoothing period. + /// The second EMA smoothing period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Tsi(object source, int firstPeriod = DefaultFirstPeriod, int secondPeriod = DefaultSecondPeriod) + : this(firstPeriod, secondPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + if (_index == 0) + _prevPrice = Input.Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Calculate price changes + double priceChange = Input.Value - _prevPrice; + double absPriceChange = Math.Abs(priceChange); + + if (Input.IsNew) + _prevPrice = Input.Value; + + // First smoothing + double smoothPc = _pcEma1.Calc(new TValue(Input.Time, priceChange, Input.IsNew)); + double smoothApc = _apcEma1.Calc(new TValue(Input.Time, absPriceChange, Input.IsNew)); + + // Second smoothing + double doubleSmoothedPc = _pcEma2.Calc(new TValue(Input.Time, smoothPc, Input.IsNew)); + double doubleSmoothedApc = _apcEma2.Calc(new TValue(Input.Time, smoothApc, Input.IsNew)); + + // Calculate TSI + return doubleSmoothedApc >= double.Epsilon ? ScalingFactor * (doubleSmoothedPc / doubleSmoothedApc) : 0; + } +} diff --git a/lib/oscillators/Uo.cs b/lib/oscillators/Uo.cs new file mode 100644 index 00000000..ec247ebd --- /dev/null +++ b/lib/oscillators/Uo.cs @@ -0,0 +1,179 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// UO: Ultimate Oscillator +/// A momentum oscillator that uses three different time periods to reduce volatility +/// and false signals. It incorporates a weighted average of three oscillator calculations +/// using different periods. +/// +/// +/// The UO calculation process: +/// 1. Calculate buying pressure (BP): Close - Min(Low, Prior Close) +/// 2. Calculate true range (TR): Max(High, Prior Close) - Min(Low, Prior Close) +/// 3. Calculate average of BP/TR for each period +/// 4. Apply weights to each period's average +/// 5. Scale result to oscillator range +/// +/// Key characteristics: +/// - Oscillates between 0 and 100 +/// - Uses multiple timeframes to reduce false signals +/// - Weighted sum of three periods +/// - Traditional overbought level at 70 +/// - Traditional oversold level at 30 +/// +/// Formula: +/// UO = 100 * ((4 * Average7) + (2 * Average14) + Average28) / (4 + 2 + 1) +/// where: +/// Average7 = 7-period average of BP/TR +/// Average14 = 14-period average of BP/TR +/// Average28 = 28-period average of BP/TR +/// +/// Sources: +/// Larry Williams - "New Trading Dimensions" (1998) +/// https://www.investopedia.com/terms/u/ultimateoscillator.asp +/// +/// Note: Default periods (7,14,28) and weights (4,2,1) were recommended by Williams +/// +[SkipLocalsInit] +public sealed class Uo : AbstractBase +{ + private readonly CircularBuffer _bp1; + private readonly CircularBuffer _tr1; + private readonly CircularBuffer _bp2; + private readonly CircularBuffer _tr2; + private readonly CircularBuffer _bp3; + private readonly CircularBuffer _tr3; + private readonly double _weight1; + private readonly double _weight2; + private readonly double _weight3; + private double _prevClose; + private const int DefaultPeriod1 = 7; + private const int DefaultPeriod2 = 14; + private const int DefaultPeriod3 = 28; + private const double DefaultWeight1 = 4.0; + private const double DefaultWeight2 = 2.0; + private const double DefaultWeight3 = 1.0; + private const double ScalingFactor = 100.0; + + /// The first period (default 7). + /// The second period (default 14). + /// The third period (default 28). + /// Weight for first period (default 4). + /// Weight for second period (default 2). + /// Weight for third period (default 1). + /// Thrown when any period is less than 1 or any weight is less than or equal to 0. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Uo(int period1 = DefaultPeriod1, int period2 = DefaultPeriod2, int period3 = DefaultPeriod3, + double weight1 = DefaultWeight1, double weight2 = DefaultWeight2, double weight3 = DefaultWeight3) + { + if (period1 < 1) + { + throw new ArgumentOutOfRangeException(nameof(period1), "Period1 must be greater than 0"); + } + if (period2 < 1) + { + throw new ArgumentOutOfRangeException(nameof(period2), "Period2 must be greater than 0"); + } + if (period3 < 1) + { + throw new ArgumentOutOfRangeException(nameof(period3), "Period3 must be greater than 0"); + } + if (weight1 <= 0) + { + throw new ArgumentOutOfRangeException(nameof(weight1), "Weight1 must be greater than 0"); + } + if (weight2 <= 0) + { + throw new ArgumentOutOfRangeException(nameof(weight2), "Weight2 must be greater than 0"); + } + if (weight3 <= 0) + { + throw new ArgumentOutOfRangeException(nameof(weight3), "Weight3 must be greater than 0"); + } + + _weight1 = weight1; + _weight2 = weight2; + _weight3 = weight3; + + _bp1 = new(period1); + _tr1 = new(period1); + _bp2 = new(period2); + _tr2 = new(period2); + _bp3 = new(period3); + _tr3 = new(period3); + + WarmupPeriod = period3; + Name = $"UO({period1},{period2},{period3})"; + } + + /// The data source object that publishes updates. + /// The first period. + /// The second period. + /// The third period. + /// Weight for first period. + /// Weight for second period. + /// Weight for third period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Uo(object source, int period1 = DefaultPeriod1, int period2 = DefaultPeriod2, int period3 = DefaultPeriod3, + double weight1 = DefaultWeight1, double weight2 = DefaultWeight2, double weight3 = DefaultWeight3) + : this(period1, period2, period3, weight1, weight2, weight3) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + if (_index == 0) + _prevClose = BarInput.Close; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateAverage(CircularBuffer bp, CircularBuffer tr) + { + double trSum = tr.Sum(); + return trSum >= double.Epsilon ? bp.Sum() / trSum : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate buying pressure and true range + double minLowPrevClose = Math.Min(BarInput.Low, _prevClose); + double maxHighPrevClose = Math.Max(BarInput.High, _prevClose); + double bp = BarInput.Close - minLowPrevClose; + double tr = maxHighPrevClose - minLowPrevClose; + + if (BarInput.IsNew) + { + // Add values to buffers + _bp1.Add(bp); + _tr1.Add(tr); + _bp2.Add(bp); + _tr2.Add(tr); + _bp3.Add(bp); + _tr3.Add(tr); + _prevClose = BarInput.Close; + } + + // Not enough data + if (_index <= 1) return 0; + + // Calculate averages for each period + double avg1 = CalculateAverage(_bp1, _tr1); + double avg2 = CalculateAverage(_bp2, _tr2); + double avg3 = CalculateAverage(_bp3, _tr3); + + // Calculate weighted sum + double weightSum = _weight1 + _weight2 + _weight3; + return ScalingFactor * (((_weight1 * avg1) + (_weight2 * avg2) + (_weight3 * avg3)) / weightSum); + } +} diff --git a/lib/oscillators/Willr.cs b/lib/oscillators/Willr.cs new file mode 100644 index 00000000..cb0beb84 --- /dev/null +++ b/lib/oscillators/Willr.cs @@ -0,0 +1,86 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// WILLR: Williams %R +/// A momentum oscillator that measures the level of the close relative to the +/// highest high for a look-back period. Similar to Stochastic Oscillator but +/// with a reversed scale and no smoothing. +/// +/// +/// The Williams %R calculation process: +/// 1. Find highest high and lowest low over period +/// 2. Calculate where current close is within this range +/// 3. Scale result to -100 to 0 range +/// +/// Key characteristics: +/// - Oscillates between -100 and 0 +/// - Similar to Stochastic but no smoothing +/// - Traditional overbought level at -20 +/// - Traditional oversold level at -80 +/// - Leading indicator for market tops/bottoms +/// +/// Formula: +/// %R = -100 * (Highest High - Close) / (Highest High - Lowest Low) +/// +/// Sources: +/// Larry Williams - "How I Made One Million Dollars Last Year Trading Commodities" (1973) +/// https://www.investopedia.com/terms/w/williamsr.asp +/// +/// Note: Default period of 14 is commonly used +/// +[SkipLocalsInit] +public sealed class Willr : AbstractBase +{ + private readonly CircularBuffer _highs; + private readonly CircularBuffer _lows; + private const int DefaultPeriod = 14; + private const double ScalingFactor = -100.0; + + /// The lookback period (default 14). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Willr(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _highs = new(period); + _lows = new(period); + WarmupPeriod = period; + Name = $"WILLR({period})"; + } + + /// The data source object that publishes updates. + /// The lookback period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Willr(object source, int period = DefaultPeriod) + : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _highs.Add(BarInput.High); + _lows.Add(BarInput.Low); + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + double highest = _highs.Max(); + double lowest = _lows.Min(); + double range = highest - lowest; + + return range >= double.Epsilon ? ScalingFactor * ((highest - BarInput.Close) / range) : 0; + } +} diff --git a/lib/oscillators/_list.md b/lib/oscillators/_list.md index 718edbf5..0cd477ee 100644 --- a/lib/oscillators/_list.md +++ b/lib/oscillators/_list.md @@ -1,17 +1,17 @@ # Oscillators indicators -Done: 11, Todo: 18 +Done: 20, Todo: 9 βœ”οΈ AC - Acceleration Oscillator βœ”οΈ AO - Awesome Oscillator -βœ”οΈ *AROON - Aroon oscillator (Up, Down) +βœ”οΈ AROON - Aroon oscillator (Up, Down) βœ”οΈ BOP - Balance of Power βœ”οΈ CCI - Commodity Channel Index βœ”οΈ CFO - Chande Forcast Oscillator -βœ”οΈ CMO - Chande Momentum Oscillator βœ”οΈ CHOP - Choppiness Index +βœ”οΈ CMO - Chande Momentum Oscillator βœ”οΈ COG - Ehler's Center of Gravity -COPPOCK - Coppock Curve -CRSI - Connor RSI +βœ”οΈ COPPOCK - Coppock Curve +βœ”οΈ CRSI - Connor RSI CTI - Ehler's Correlation Trend Indicator DOSC - Derivative Oscillator EFI - Elder Ray's Force Index @@ -23,10 +23,10 @@ KRI - Kairi Relative Index βœ”οΈ RSI - Relative Strength Index βœ”οΈ RSX - Jurik Trend Strength Index *RVGI - Relative Vigor Index (RVGI, Signal) -SMI - Stochastic Momentum Index -*SRSI - Stochastic RSI (SRSI, Signal) -STC - Schaff Trend Cycle -*STOCH - Stochastic Oscillator (%K, %D) -TSI - True Strength Index -UO - Ultimate Oscillator -WILLR - Larry Williams' %R +βœ”οΈ SMI - Stochastic Momentum Index +βœ”οΈ SRSI - Stochastic RSI (SRSI, Signal) +βœ”οΈ STC - Schaff Trend Cycle +βœ”οΈ STOCH - Stochastic Oscillator (%K, %D) +βœ”οΈ TSI - True Strength Index +βœ”οΈ UO - Ultimate Oscillator +βœ”οΈ WILLR - Larry Williams' %R diff --git a/lib/statistics/Hurst.cs b/lib/statistics/Hurst.cs index 4ec62254..4c2c9e2d 100644 --- a/lib/statistics/Hurst.cs +++ b/lib/statistics/Hurst.cs @@ -46,7 +46,6 @@ namespace QuanTAlib; /// /// Note: Returns a value between 0 and 1 /// - [SkipLocalsInit] public sealed class Hurst : AbstractBase { @@ -184,7 +183,7 @@ public sealed class Hurst : AbstractBase double hurst = 0.5; // Default to random walk if (numPoints > 1) { - double slope = (numPoints * sumXY - sumX * sumY) / (numPoints * sumX2 - sumX * sumX); + double slope = ((numPoints * sumXY) - (sumX * sumY)) / ((numPoints * sumX2) - (sumX * sumX)); hurst = Math.Max(0, Math.Min(1, slope)); // Clamp between 0 and 1 } diff --git a/lib/statistics/_list.md b/lib/statistics/_list.md index fa7932fd..4e16b0de 100644 --- a/lib/statistics/_list.md +++ b/lib/statistics/_list.md @@ -5,8 +5,7 @@ Done: 13, Todo: 6 *CORR - Correlation Coefficient (Correlation, P-value) βœ”οΈ CURVATURE - Rate of Change in Direction or Slope βœ”οΈ ENTROPY - Measure of Uncertainty or Disorder -HUBER - Huber Loss -HURST - Hurst Exponent +βœ”οΈ HURST - Hurst Exponent βœ”οΈ KURTOSIS - Measure of Tails/Peakedness βœ”οΈ MAX - Maximum with exponential decay βœ”οΈ MEDIAN - Middle value diff --git a/lib/volatility/Bband.cs b/lib/volatility/Bband.cs index a135b71b..19226928 100644 --- a/lib/volatility/Bband.cs +++ b/lib/volatility/Bband.cs @@ -41,7 +41,6 @@ namespace QuanTAlib; /// /// Note: Returns three values: upper, middle, and lower bands /// - [SkipLocalsInit] public sealed class Bband : AbstractBase { diff --git a/lib/volatility/Ccv.cs b/lib/volatility/Ccv.cs index cfe6e7ba..6514b7a4 100644 --- a/lib/volatility/Ccv.cs +++ b/lib/volatility/Ccv.cs @@ -36,7 +36,6 @@ namespace QuanTAlib; /// /// Note: Returns annualized volatility as a percentage /// - [SkipLocalsInit] public sealed class Ccv : AbstractBase { diff --git a/lib/volatility/Ce.cs b/lib/volatility/Ce.cs index 225c2c22..cdab9695 100644 --- a/lib/volatility/Ce.cs +++ b/lib/volatility/Ce.cs @@ -38,7 +38,6 @@ namespace QuanTAlib; /// /// Note: Returns two values: long exit and short exit levels /// - [SkipLocalsInit] public sealed class Ce : AbstractBase { diff --git a/lib/volatility/Cv.cs b/lib/volatility/Cv.cs index 77dd3fff..bc77b20b 100644 --- a/lib/volatility/Cv.cs +++ b/lib/volatility/Cv.cs @@ -43,7 +43,6 @@ namespace QuanTAlib; /// /// Note: Returns annualized volatility as a percentage /// - [SkipLocalsInit] public sealed class Cv : AbstractBase { @@ -126,7 +125,7 @@ public sealed class Cv : AbstractBase } // Update variance estimate using GARCH(1,1) - double variance = _omega + _alpha * squaredReturn + _beta * _prevVariance; + double variance = _omega + (_alpha * squaredReturn) + (_beta * _prevVariance); _prevVariance = variance; // Calculate annualized volatility as percentage diff --git a/lib/volatility/Dchn.cs b/lib/volatility/Dchn.cs new file mode 100644 index 00000000..2a5d5782 --- /dev/null +++ b/lib/volatility/Dchn.cs @@ -0,0 +1,100 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// DCHN: Donchian Channels +/// A volatility indicator that identifies the highest high and lowest low +/// over a specified period, creating a channel that contains price movement. +/// +/// +/// The DCHN calculation process: +/// 1. Track highest high over period +/// 2. Track lowest low over period +/// 3. Calculate midline as average of high and low +/// 4. Updates with each new price bar +/// +/// Key characteristics: +/// - Trend following indicator +/// - Support/resistance identification +/// - Breakout detection +/// - Volatility measurement +/// - Range-based analysis +/// +/// Formula: +/// Upper = Highest High over period +/// Lower = Lowest Low over period +/// Middle = (Upper + Lower) / 2 +/// +/// Market Applications: +/// - Trend identification +/// - Support/resistance levels +/// - Breakout trading +/// - Volatility analysis +/// - Range-bound trading +/// +[SkipLocalsInit] +public sealed class Dchn : AbstractBase +{ + private readonly CircularBuffer _highs; + private readonly CircularBuffer _lows; + private const int DefaultPeriod = 20; + + /// The number of periods for DCHN calculation (default 20). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Dchn(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _highs = new(period); + _lows = new(period); + WarmupPeriod = period; + Name = $"DCHN({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods for DCHN calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Dchn(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _highs.Add(BarInput.High); + _lows.Add(BarInput.Low); + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate channel boundaries + double upper = _highs.Max(); + double lower = _lows.Min(); + + // Return midline + return (upper + lower) / 2.0; + } + + /// + /// Gets the upper channel value (highest high) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double Upper() => _highs.Max(); + + /// + /// Gets the lower channel value (lowest low) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double Lower() => _lows.Min(); +} diff --git a/lib/volatility/Ewma.cs b/lib/volatility/Ewma.cs index 52fb9443..672ebfb1 100644 --- a/lib/volatility/Ewma.cs +++ b/lib/volatility/Ewma.cs @@ -41,7 +41,6 @@ namespace QuanTAlib; /// /// Note: Returns annualized volatility as a percentage /// - [SkipLocalsInit] public sealed class Ewma : AbstractBase { @@ -121,7 +120,7 @@ public sealed class Ewma : AbstractBase } // Update EWMA - _ewma = _lambda * _ewma + (1 - _lambda) * squaredReturn; + _ewma = (_lambda * _ewma) + ((1 - _lambda) * squaredReturn); // Calculate volatility double volatility = Math.Sqrt(_ewma); diff --git a/lib/volatility/Fcb.cs b/lib/volatility/Fcb.cs index 45c61aa9..60c926e1 100644 --- a/lib/volatility/Fcb.cs +++ b/lib/volatility/Fcb.cs @@ -38,7 +38,6 @@ namespace QuanTAlib; /// /// Note: Returns three values: upper, middle, and lower bands /// - [SkipLocalsInit] public sealed class Fcb : AbstractBase { @@ -134,8 +133,8 @@ public sealed class Fcb : AbstractBase } // Apply smoothing to bands - _upperBand = _smoothing * _upperEma + (1 - _smoothing) * BarInput.High; - _lowerBand = _smoothing * _lowerEma + (1 - _smoothing) * BarInput.Low; + _upperBand = (_smoothing * _upperEma) + ((1 - _smoothing) * BarInput.High); + _lowerBand = (_smoothing * _lowerEma) + ((1 - _smoothing) * BarInput.Low); _middleBand = (_upperBand + _lowerBand) / 2; IsHot = _index >= WarmupPeriod; diff --git a/lib/volatility/Gkv.cs b/lib/volatility/Gkv.cs index 19baebdb..fd197d48 100644 --- a/lib/volatility/Gkv.cs +++ b/lib/volatility/Gkv.cs @@ -38,7 +38,6 @@ namespace QuanTAlib; /// /// Note: Returns annualized volatility as a percentage /// - [SkipLocalsInit] public sealed class Gkv : AbstractBase { @@ -97,7 +96,7 @@ public sealed class Gkv : AbstractBase c = c * c; // Combine components with optimal weights - double component = 0.5 * u - (2 * _ln2 - 1) * c; + double component = (0.5 * u) - (((2 * _ln2) - 1) * c); _components.Add(component); // Need enough values for calculation diff --git a/lib/volatility/Hlv.cs b/lib/volatility/Hlv.cs index 63a0b3ea..b1d60e36 100644 --- a/lib/volatility/Hlv.cs +++ b/lib/volatility/Hlv.cs @@ -37,7 +37,6 @@ namespace QuanTAlib; /// /// Note: Returns annualized volatility as a percentage /// - [SkipLocalsInit] public sealed class Hlv : AbstractBase { diff --git a/lib/volatility/Natr.cs b/lib/volatility/Natr.cs new file mode 100644 index 00000000..d4a37d21 --- /dev/null +++ b/lib/volatility/Natr.cs @@ -0,0 +1,94 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// NATR: Normalized Average True Range +/// A volatility indicator that expresses ATR as a percentage of closing price, +/// making it more comparable across different price levels. +/// +/// +/// The NATR calculation process: +/// 1. Calculate True Range (TR): +/// TR = max(high-low, abs(high-prevClose), abs(low-prevClose)) +/// 2. Calculate ATR using SMA of TR +/// 3. Normalize by dividing ATR by close price and multiply by 100 +/// 4. Updates with each new price bar +/// +/// Key characteristics: +/// - Normalized volatility measure +/// - Period-based average +/// - Trend independent +/// - Percentage-based measure +/// - Comparable across instruments +/// +/// Formula: +/// TR = max(high-low, abs(high-prevClose), abs(low-prevClose)) +/// ATR = SMA(TR, period) +/// NATR = (ATR / Close) * 100 +/// +/// Market Applications: +/// - Cross-market comparison +/// - Position sizing +/// - Volatility analysis +/// - Risk assessment +/// - Market regime identification +/// +/// Note: More suitable for comparing volatility across different instruments than ATR +/// +[SkipLocalsInit] +public sealed class Natr : AbstractBase +{ + private readonly Sma _ma; + private double _prevClose; + private const int DefaultPeriod = 14; + + /// The number of periods for NATR calculation (default 14). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Natr(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _ma = new(period); + WarmupPeriod = period; + Name = $"NATR({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods for NATR calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Natr(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _prevClose = BarInput.Close; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate True Range + double hl = BarInput.High - BarInput.Low; + double hc = Math.Abs(BarInput.High - _prevClose); + double lc = Math.Abs(BarInput.Low - _prevClose); + double tr = Math.Max(hl, Math.Max(hc, lc)); + + // Calculate ATR + double atr = _ma.Calc(tr, BarInput.IsNew); + + // Normalize ATR + return (atr / BarInput.Close) * 100.0; + } +} diff --git a/lib/volatility/Pch.cs b/lib/volatility/Pch.cs new file mode 100644 index 00000000..476d3b15 --- /dev/null +++ b/lib/volatility/Pch.cs @@ -0,0 +1,102 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PCH: Price Channel +/// A volatility indicator that identifies the highest high and lowest low +/// over a specified period, creating a channel that contains price movement. +/// +/// +/// The PCH calculation process: +/// 1. Track highest high over period +/// 2. Track lowest low over period +/// 3. Calculate midline as average of high and low +/// 4. Updates with each new price bar +/// +/// Key characteristics: +/// - Trend following indicator +/// - Support/resistance identification +/// - Breakout detection +/// - Volatility measurement +/// - Range-based analysis +/// +/// Formula: +/// Upper = Highest High over period +/// Lower = Lowest Low over period +/// Middle = (Upper + Lower) / 2 +/// +/// Market Applications: +/// - Trend identification +/// - Support/resistance levels +/// - Breakout trading +/// - Volatility analysis +/// - Range-bound trading +/// +/// Note: Also known as Donchian Channels +/// +[SkipLocalsInit] +public sealed class Pch : AbstractBase +{ + private readonly CircularBuffer _highs; + private readonly CircularBuffer _lows; + private const int DefaultPeriod = 20; + + /// The number of periods for PCH calculation (default 20). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pch(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _highs = new(period); + _lows = new(period); + WarmupPeriod = period; + Name = $"PCH({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods for PCH calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pch(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _highs.Add(BarInput.High); + _lows.Add(BarInput.Low); + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate channel boundaries + double upper = _highs.Max(); + double lower = _lows.Min(); + + // Return midline + return (upper + lower) / 2.0; + } + + /// + /// Gets the upper channel value (highest high) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double Upper() => _highs.Max(); + + /// + /// Gets the lower channel value (lowest low) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double Lower() => _lows.Min(); +} diff --git a/lib/volatility/Pv.cs b/lib/volatility/Pv.cs new file mode 100644 index 00000000..97d5817b --- /dev/null +++ b/lib/volatility/Pv.cs @@ -0,0 +1,93 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PV: Parkinson Volatility +/// A volatility measure that uses the high and low prices to estimate +/// volatility, assuming continuous trading and log-normal price distribution. +/// +/// +/// The PV calculation process: +/// 1. Calculate squared log range for each period +/// 2. Apply scaling factor (1/4ln2) +/// 3. Average over specified period +/// 4. Take square root for final volatility +/// +/// Key characteristics: +/// - Range-based volatility +/// - More efficient than close-to-close +/// - Assumes continuous trading +/// - No gap consideration +/// - Log-normal distribution +/// +/// Formula: +/// PV = sqrt(1/(4*ln(2)*n) * Ξ£(ln(High/Low))Β²) +/// where n is the number of periods +/// +/// Market Applications: +/// - Volatility estimation +/// - Risk assessment +/// - Option pricing +/// - Trading system development +/// - Market regime identification +/// +/// Note: More efficient than traditional volatility measures but sensitive to gaps +/// +[SkipLocalsInit] +public sealed class Pv : AbstractBase +{ + private readonly Sma _ma; + private readonly double _scaleFactor; + private const int DefaultPeriod = 10; + private double _prevValue; + + /// The number of periods for PV calculation (default 10). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pv(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _ma = new(period); + _scaleFactor = 1.0 / (4.0 * Math.Log(2.0)); + WarmupPeriod = period; + Name = $"PV({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods for PV calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pv(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + if (!BarInput.IsNew) + return _prevValue; + + ManageState(true); + + // Calculate log range squared + double logRange = Math.Log(BarInput.High / BarInput.Low); + double logRangeSquared = logRange * logRange; + + // Apply moving average and scaling + double meanLogRangeSquared = _ma.Calc(logRangeSquared, true); + + // Calculate final volatility + _prevValue = Math.Sqrt(_scaleFactor * meanLogRangeSquared); + return _prevValue; + } +} diff --git a/lib/volatility/Rsv.cs b/lib/volatility/Rsv.cs new file mode 100644 index 00000000..af0a1b2b --- /dev/null +++ b/lib/volatility/Rsv.cs @@ -0,0 +1,93 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// RSV: Rogers-Satchell Volatility +/// A volatility measure that accounts for drift in the price process and +/// is independent of the mean return level. +/// +/// +/// The RSV calculation process: +/// 1. Calculate log differences between prices +/// 2. Combine log differences in specific way +/// 3. Average over specified period +/// 4. Take square root for final volatility +/// +/// Key characteristics: +/// - Drift-independent +/// - Uses all price data (HLOC) +/// - More efficient estimator +/// - Handles trending markets +/// - Non-zero mean returns +/// +/// Formula: +/// RSV = sqrt(mean(ln(H/C) * ln(H/O) + ln(L/C) * ln(L/O))) +/// where H=High, L=Low, O=Open, C=Close +/// +/// Market Applications: +/// - Volatility estimation +/// - Risk measurement +/// - Option pricing +/// - Trading system development +/// - Market regime identification +/// +/// Note: More robust than simple volatility measures in trending markets +/// +[SkipLocalsInit] +public sealed class Rsv : AbstractBase +{ + private readonly Sma _ma; + private const int DefaultPeriod = 10; + private double _prevValue; + + /// The number of periods for RSV calculation (default 10). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rsv(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _ma = new(period); + WarmupPeriod = period; + Name = $"RSV({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods for RSV calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rsv(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + if (!BarInput.IsNew) + return _prevValue; + + ManageState(true); + + // Calculate log ratios + double lnHC = Math.Log(BarInput.High / BarInput.Close); + double lnHO = Math.Log(BarInput.High / BarInput.Open); + double lnLC = Math.Log(BarInput.Low / BarInput.Close); + double lnLO = Math.Log(BarInput.Low / BarInput.Open); + + // Calculate Rogers-Satchell term + double rs = (lnHC * lnHO) + (lnLC * lnLO); + + // Apply moving average and take square root + _prevValue = Math.Sqrt(_ma.Calc(rs, true)); + return _prevValue; + } +} diff --git a/lib/volatility/Sv.cs b/lib/volatility/Sv.cs new file mode 100644 index 00000000..28ddd7df --- /dev/null +++ b/lib/volatility/Sv.cs @@ -0,0 +1,105 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// SV: Stochastic Volatility +/// A volatility measure that models price volatility as a random process, +/// capturing both the magnitude and the rate of change in price movements. +/// +/// +/// The SV calculation process: +/// 1. Calculate log returns +/// 2. Compute exponentially weighted variance +/// 3. Apply smoothing to variance estimate +/// 4. Take square root for volatility +/// +/// Key characteristics: +/// - Time-varying volatility +/// - Mean-reverting process +/// - Captures volatility clustering +/// - Handles leverage effects +/// - Accounts for fat tails +/// +/// Formula: +/// Returns = ln(Close/PrevClose) +/// Variance = Ξ» * PrevVariance + (1-Ξ») * ReturnsΒ² +/// SV = sqrt(Variance) +/// where Ξ» is the decay factor +/// +/// Market Applications: +/// - Option pricing +/// - Risk management +/// - Trading strategies +/// - Portfolio optimization +/// - Market regime detection +/// +/// Note: More sophisticated than simple volatility measures, better captures market dynamics +/// +[SkipLocalsInit] +public sealed class Sv : AbstractBase +{ + private readonly double _lambda; + private readonly Sma _ma; + private double _prevClose; + private double _prevVariance; + private double _prevValue; + private const int DefaultPeriod = 20; + private const double DefaultLambda = 0.94; + + /// The number of periods for smoothing (default 20). + /// The decay factor for variance calculation (default 0.94). + /// Thrown when period is less than 1 or lambda is not between 0 and 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Sv(int period = DefaultPeriod, double lambda = DefaultLambda) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + if (lambda <= 0 || lambda >= 1) + throw new ArgumentOutOfRangeException(nameof(lambda)); + + _lambda = lambda; + _ma = new(period); + WarmupPeriod = period; + Name = $"SV({period},{lambda:F2})"; + } + + /// The data source object that publishes updates. + /// The number of periods for smoothing. + /// The decay factor for variance calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Sv(object source, int period = DefaultPeriod, double lambda = DefaultLambda) : this(period, lambda) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _prevClose = BarInput.Close; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + if (!BarInput.IsNew) + return _prevValue; + + ManageState(true); + + // Calculate log return + double logReturn = Math.Log(BarInput.Close / _prevClose); + double squaredReturn = logReturn * logReturn; + + // Update variance estimate + _prevVariance = (_lambda * _prevVariance) + ((1.0 - _lambda) * squaredReturn); + + // Apply smoothing and take square root + _prevValue = Math.Sqrt(_ma.Calc(_prevVariance, true)); + return _prevValue; + } +} diff --git a/lib/volatility/Yzv.cs b/lib/volatility/Yzv.cs new file mode 100644 index 00000000..4331f982 --- /dev/null +++ b/lib/volatility/Yzv.cs @@ -0,0 +1,113 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// YZV: Yang-Zhang Volatility +/// A volatility estimator that combines overnight and trading volatilities, +/// providing a more complete picture of price variation while being drift-independent. +/// +/// +/// The YZV calculation process: +/// 1. Calculate overnight (close-to-open) volatility +/// 2. Calculate open-to-close volatility +/// 3. Calculate Rogers-Satchell volatility +/// 4. Combine components with optimal weights +/// +/// Key characteristics: +/// - Drift independence +/// - Minimum variance +/// - Handles overnight gaps +/// - Uses all HLOC prices +/// - Optimal weighting +/// +/// Formula: +/// YZV = sqrt(Vo + k*Vc + (1-k)*Vrs) +/// where: +/// Vo = overnight volatility +/// Vc = open-to-close volatility +/// Vrs = Rogers-Satchell volatility +/// k β‰ˆ 0.34 (optimal weight) +/// +/// Market Applications: +/// - Option pricing +/// - Risk measurement +/// - Trading systems +/// - Portfolio management +/// - Market analysis +/// +/// Note: Most efficient unbiased estimator among drift-independent estimators +/// +[SkipLocalsInit] +public sealed class Yzv : AbstractBase +{ + private readonly Sma _maCo; // Close-to-Open + private readonly Sma _maOc; // Open-to-Close + private readonly Sma _maRs; // Rogers-Satchell + private double _prevClose; + private double _prevValue; + private const double K = 0.34; // Optimal weight + private const int DefaultPeriod = 20; + + /// The number of periods for volatility calculation (default 20). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Yzv(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _maCo = new(period); + _maOc = new(period); + _maRs = new(period); + WarmupPeriod = period; + Name = $"YZV({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods for volatility calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Yzv(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _prevClose = BarInput.Close; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + if (!BarInput.IsNew) + return _prevValue; + + ManageState(true); + + // Calculate overnight volatility (close-to-open) + double co = Math.Log(BarInput.Open / _prevClose); + double vo = _maCo.Calc(co * co, true); + + // Calculate open-to-close volatility + double oc = Math.Log(BarInput.Close / BarInput.Open); + double vc = _maOc.Calc(oc * oc, true); + + // Calculate Rogers-Satchell volatility component + double lnHC = Math.Log(BarInput.High / BarInput.Close); + double lnHO = Math.Log(BarInput.High / BarInput.Open); + double lnLC = Math.Log(BarInput.Low / BarInput.Close); + double lnLO = Math.Log(BarInput.Low / BarInput.Open); + double rs = (lnHC * lnHO) + (lnLC * lnLO); + double vrs = _maRs.Calc(rs, true); + + // Combine components with optimal weights + _prevValue = Math.Sqrt(vo + (K * vc) + ((1.0 - K) * vrs)); + return _prevValue; + } +} diff --git a/lib/volatility/_list.md b/lib/volatility/_list.md index 9e0f4094..59fa8825 100644 --- a/lib/volatility/_list.md +++ b/lib/volatility/_list.md @@ -1,5 +1,5 @@ # Volatility indicators -Done: 24, Todo: 11 +Done: 25, Todo: 10 βœ”οΈ ADR - Average Daily Range βœ”οΈ AP - Andrew's Pitchfork @@ -11,28 +11,28 @@ Done: 24, Todo: 11 βœ”οΈ CE - Chandelier Exit βœ”οΈ CV - Conditional Volatility (ARCH/GARCH) βœ”οΈ CVI - Chaikin's Volatility -*DC - Donchian Channels (Upper, Middle, Lower) +βœ”οΈ DCHN - Donchian Channels (Upper, Middle, Lower) βœ”οΈ EWMA - Exponential Weighted Moving Average Volatility βœ”οΈ FCB - Fractal Chaos Bands βœ”οΈ GKV - Garman-Klass Volatility βœ”οΈ HLV - High-Low Volatility βœ”οΈ HV - Historical Volatility *ICH - Ichimoku Cloud (Conversion, Base, Leading Span A, Leading Span B, Lagging Span) -βœ”οΈ JVOLTY - Jurik Volatility +βœ”οΈ *JVOLTY - Jurik Volatility (Jvolty, Upper band, Lower band) *KC - Keltner Channels (Upper, Middle, Lower) -NATR - Normalized Average True Range -PCH - Price Channel Indicator +βœ”οΈ NATR - Normalized Average True Range +βœ”οΈ PCH - Price Channel Indicator *PSAR - Parabolic Stop and Reverse (Value, Trend) -PV - Parkinson Volatility -RSV - Rogers-Satchell Volatility +βœ”οΈ PV - Parkinson Volatility +βœ”οΈ RSV - Rogers-Satchell Volatility βœ”οΈ RV - Realized Volatility βœ”οΈ RVI - Relative Volatility Index *STARC - Starc Bands (Upper, Middle, Lower) -SV - Stochastic Volatility +βœ”οΈ SV - Stochastic Volatility βœ”οΈ TR - True Range βœ”οΈ UI - Ulcer Index βœ”οΈ *VC - Volatility Cone (Mean, Upper Bound, Lower Bound) βœ”οΈ VOV - Volatility of Volatility βœ”οΈ VR - Volatility Ratio βœ”οΈ *VS - Volatility Stop (Long Stop, Short Stop) -YZV - Yang-Zhang Volatility +βœ”οΈ YZV - Yang-Zhang Volatility