diff --git a/Tests/test_core.cs b/Tests/test_core.cs new file mode 100644 index 00000000..49d67d4b --- /dev/null +++ b/Tests/test_core.cs @@ -0,0 +1,251 @@ +using Xunit; + +namespace QuanTAlib.Tests; + +public class CoreTests +{ + #region CircularBuffer Tests + + [Fact] + public void CircularBuffer_BasicOperations() + { + var buffer = new CircularBuffer(5); + + // Test initial state + Assert.Equal(5, buffer.Capacity); + Assert.Equal(0, buffer.Count); + + // Test adding items + buffer.Add(1.0); + buffer.Add(2.0); + Assert.Equal(2, buffer.Count); + Assert.Equal(1.0, buffer[0]); + Assert.Equal(2.0, buffer[^1]); + + // Test overflow behavior + buffer.Add(3.0); + buffer.Add(4.0); + buffer.Add(5.0); + buffer.Add(6.0); // Should remove oldest item (1.0) + Assert.Equal(5, buffer.Count); + Assert.Equal(2.0, buffer[0]); + Assert.Equal(6.0, buffer[^1]); + } + + [Fact] + public void CircularBuffer_UpdateBehavior() + { + var buffer = new CircularBuffer(3); + + // Add new values + buffer.Add(1.0, isNew: true); + buffer.Add(2.0, isNew: true); + Assert.Equal(2, buffer.Count); + + // Update last value + buffer.Add(2.5, isNew: false); + Assert.Equal(2, buffer.Count); + Assert.Equal(2.5, buffer[^1]); + } + + [Fact] + public void CircularBuffer_MinMaxSumAverage() + { + var buffer = new CircularBuffer(5); + + buffer.Add(1.0); + buffer.Add(2.0); + buffer.Add(3.0); + buffer.Add(4.0); + buffer.Add(5.0); + + Assert.Equal(1.0, buffer.Min()); + Assert.Equal(5.0, buffer.Max()); + Assert.Equal(15.0, buffer.Sum()); + Assert.Equal(3.0, buffer.Average()); + } + + [Fact] + public void CircularBuffer_Enumeration() + { + var buffer = new CircularBuffer(3); + + buffer.Add(1.0); + buffer.Add(2.0); + buffer.Add(3.0); + + var list = buffer.ToList(); + Assert.Equal(3, list.Count); + Assert.Equal(1.0, list[0]); + Assert.Equal(3.0, list[2]); + } + + #endregion + + #region TBar Tests + + [Fact] + public void TBar_Construction() + { + // Default constructor + var bar1 = new TBar(); + Assert.Equal(0, bar1.Open); + Assert.True(bar1.IsNew); + + // Value constructor + var bar2 = new TBar(10.0); + Assert.Equal(10.0, bar2.Open); + Assert.Equal(10.0, bar2.High); + Assert.Equal(10.0, bar2.Low); + Assert.Equal(10.0, bar2.Close); + + // Full constructor + var time = DateTime.Now; + var bar3 = new TBar(time, 10.0, 12.0, 9.0, 11.0, 1000.0, false); + Assert.Equal(time, bar3.Time); + Assert.Equal(10.0, bar3.Open); + Assert.Equal(12.0, bar3.High); + Assert.Equal(9.0, bar3.Low); + Assert.Equal(11.0, bar3.Close); + Assert.Equal(1000.0, bar3.Volume); + Assert.False(bar3.IsNew); + } + + [Fact] + public void TBar_DerivedValues() + { + var bar = new TBar(DateTime.Now, 10.0, 20.0, 5.0, 15.0, 1000.0); + + Assert.Equal(12.5, bar.HL2); // (20 + 5) / 2 + Assert.Equal(12.5, bar.OC2); // (10 + 15) / 2 + Assert.Equal(11.67, bar.OHL3, 2); // (10 + 20 + 5) / 3 + Assert.Equal(13.33, bar.HLC3, 2); // (20 + 5 + 15) / 3 + Assert.Equal(12.5, bar.OHLC4); // (10 + 20 + 5 + 15) / 4 + Assert.Equal(13.75, bar.HLCC4); // (20 + 5 + 15 + 15) / 4 + } + + [Fact] + public void TBarSeries_Operations() + { + var series = new TBarSeries(); + var time = DateTime.Now; + var bar1 = new TBar(time, 10.0, 12.0, 9.0, 11.0, 1000.0); + var bar2 = new TBar(time.AddMinutes(1), 11.0, 13.0, 10.0, 12.0, 1100.0); + + // Test adding bars + series.Add(bar1); + series.Add(bar2); + Assert.Equal(2, series.Count); + + // Test updating last bar + var bar2Update = new TBar(bar2.Time, 11.0, 13.5, 9.5, 12.5, 1200.0, false); + series.Add(bar2Update); + Assert.Equal(2, series.Count); + Assert.Equal(12.5, series.Last.Close); + + // Test derived series + Assert.Equal(11.0, series.Open.Last.Value); + Assert.Equal(13.5, series.High.Last.Value); + Assert.Equal(9.5, series.Low.Last.Value); + Assert.Equal(12.5, series.Close.Last.Value); + Assert.Equal(1200.0, series.Volume.Last.Value); + } + + #endregion + + #region TValue Tests + + [Fact] + public void TValue_Construction() + { + // Default constructor + var value1 = new TValue(); + Assert.Equal(0, value1.Value); + Assert.True(value1.IsNew); + Assert.True(value1.IsHot); + + // Value constructor + var value2 = new TValue(10.0); + Assert.Equal(10.0, value2.Value); + + // Full constructor + var time = DateTime.Now; + var value3 = new TValue(time, 10.0, false, false); + Assert.Equal(time, value3.Time); + Assert.Equal(10.0, value3.Value); + Assert.False(value3.IsNew); + Assert.False(value3.IsHot); + } + + [Fact] + public void TValue_Conversions() + { + var value = new TValue(10.0); + + // Test implicit conversions + double d = value; + Assert.Equal(10.0, d); + + DateTime time = value; + Assert.Equal(value.Time, time); + + // Test implicit conversion from double + TValue newValue = 20.0; + Assert.Equal(20.0, newValue.Value); + } + + [Fact] + public void TSeries_Operations() + { + var series = new TSeries(); + var time = DateTime.Now; + + // Test adding values + series.Add(time, 10.0); + series.Add(time.AddMinutes(1), 20.0); + Assert.Equal(2, series.Count); + + // Test updating last value + series.Add(new TValue(time.AddMinutes(1), 25.0, false)); + Assert.Equal(2, series.Count); + Assert.Equal(25.0, series.Last.Value); + + // Test adding range of values + var values = new[] { 30.0, 40.0, 50.0 }; + foreach (var value in values) + { + series.Add(time.AddMinutes(series.Count + 1), value); + } + Assert.Equal(5, series.Count); + + // Test conversions + var doubleList = (List)series; + Assert.Equal(5, doubleList.Count); + Assert.Equal(50.0, doubleList[^1]); + + var doubleArray = (double[])series; + Assert.Equal(5, doubleArray.Length); + Assert.Equal(50.0, doubleArray[^1]); + } + + [Fact] + public void TSeries_EventHandling() + { + var series = new TSeries(); + var receivedValues = new List(); + var time = DateTime.Now; + + series.Pub += (object sender, in ValueEventArgs args) => receivedValues.Add(args.Tick.Value); + + series.Add(time, 10.0); + series.Add(time.AddMinutes(1), 20.0); + series.Add(time.AddMinutes(2), 30.0); + + Assert.Equal(3, receivedValues.Count); + Assert.Equal(10.0, receivedValues[0]); + Assert.Equal(20.0, receivedValues[1]); + Assert.Equal(30.0, receivedValues[2]); + } + + #endregion +} diff --git a/Tests/test_eventing.cs b/Tests/test_eventing.cs index 5b0d6db4..3564894e 100644 --- a/Tests/test_eventing.cs +++ b/Tests/test_eventing.cs @@ -13,12 +13,13 @@ public class EventingTests // Create a cryptographically secure random number generator using var rng = RandomNumberGenerator.Create(); - // Create an input series to hold our random values + // Create input series to hold our random values var input = new TSeries(); + var barInput = new TBarSeries(); int p = 10; - // Create a list of indicator pairs (direct calculation and event-based) with names - var indicators = new List<(string Name, AbstractBase Direct, AbstractBase EventBased)> + // Create a list of value-based indicator pairs + var valueIndicators = new List<(string Name, AbstractBase Direct, AbstractBase EventBased)> { ("Afirma", new Afirma(p,p,Afirma.WindowType.BlackmanHarris), new Afirma(input, p,p,Afirma.WindowType.BlackmanHarris)), ("Alma", new Alma(p), new Alma(input, p)), @@ -51,19 +52,15 @@ public class EventingTests ("Tema", new Tema(p), new Tema(input, p)), ("Kama", new Kama(2, 30, 6), new Kama(input, 2, 30, 6)), ("Zlema", new Zlema(p), new Zlema(input, p)), - // Added missing averages ("Sinema", new Sinema(p), new Sinema(input, p)), ("Smma", new Smma(p), new Smma(input, p)), ("T3", new T3(p), new T3(input, p)), ("Trima", new Trima(p), new Trima(input, p)), ("Vidya", new Vidya(p), new Vidya(input, p)), - // momentum indicators ("Apo", new Apo(12, 26), new Apo(input, 12, 26)), - // oscillators ("Rsi", new Rsi(p), new Rsi(input, p)), ("Rsx", new Rsx(p), new Rsx(input, p)), ("Cmo", new Cmo(p), new Cmo(input, p)), - // statistics ("Curvature", new Curvature(p), new Curvature(input, p)), ("Entropy", new Entropy(p), new Entropy(input, p)), ("Kurtosis", new Kurtosis(p), new Kurtosis(input, p)), @@ -77,12 +74,12 @@ public class EventingTests ("Stddev", new Stddev(p), new Stddev(input, p)), ("Variance", new Variance(p), new Variance(input, p)), ("Zscore", new Zscore(p), new Zscore(input, p)), - // volatility + // Volatility indicators (value-based) ("Hv", new Hv(p), new Hv(input, p)), ("Jvolty", new Jvolty(p), new Jvolty(input, p)), ("Rv", new Rv(p), new Rv(input, p)), ("Rvi", new Rvi(p), new Rvi(input, p)), - // error classes + // Error classes ("Mae", new Mae(p), new Mae(input, p)), ("Mapd", new Mapd(p), new Mapd(input, p)), ("Mape", new Mape(p), new Mape(input, p)), @@ -101,26 +98,66 @@ public class EventingTests ("Huber", new Huber(p), new Huber(input, p)) }; - // Generate 200 random values and feed them to both direct and event-based indicators + // Create a list of bar-based indicator pairs + var barIndicators = new List<(string Name, AbstractBase Direct, AbstractBase EventBased)> + { + // Volume indicators + ("Adl", new Adl(), new Adl(barInput)), + ("Adosc", new Adosc(3, 10), new Adosc(barInput, 3, 10)), + ("Aobv", new Aobv(), new Aobv(barInput)), + ("Cmf", new Cmf(20), new Cmf(barInput, 20)), + ("Eom", new Eom(14), new Eom(barInput, 14)), + ("Kvo", new Kvo(34, 55), new Kvo(barInput, 34, 55)), + // Volatility indicators (bar-based) + ("Atr", new Atr(14), new Atr(barInput, 14)) + }; + + // Generate 200 random values and feed them to indicators for (int i = 0; i < 200; i++) { + // Generate random value for value-based indicators double randomValue = GetRandomDouble(rng) * 100; input.Add(randomValue); - // Calculate direct indicators - foreach (var (_, direct, _) in indicators) + // Calculate value-based indicators + foreach (var (_, direct, _) in valueIndicators) { direct.Calc(randomValue); } + + // Generate random bar for bar-based indicators + var bar = new TBar( + DateTime.Now, + randomValue, + randomValue + Math.Abs(GetRandomDouble(rng) * 10), + randomValue - Math.Abs(GetRandomDouble(rng) * 10), + randomValue + GetRandomDouble(rng) * 5, + Math.Abs(GetRandomDouble(rng) * 1000), + true + ); + barInput.Add(bar); + + // Calculate bar-based indicators + foreach (var (_, direct, _) in barIndicators) + { + direct.Calc(bar); + } } - // Compare the results of direct and event-based calculations - for (int i = 0; i < indicators.Count; i++) + // Compare the results for value-based indicators + foreach (var (name, direct, eventBased) in valueIndicators) { - var (name, direct, eventBased) = indicators[i]; bool areEqual = (double.IsNaN(direct.Value) && double.IsNaN(eventBased.Value)) || Math.Abs(direct.Value - eventBased.Value) < 1e-9; - Assert.True(areEqual, $"Indicator {name} failed: Expected {direct.Value}, Actual {eventBased.Value}"); + Assert.True(areEqual, $"Value indicator {name} failed: Expected {direct.Value}, Actual {eventBased.Value}"); + } + + // Compare the results for bar-based indicators + foreach (var (name, direct, eventBased) in barIndicators) + { + bool areEqual = (double.IsNaN(direct.Value) && double.IsNaN(eventBased.Value)) || + Math.Abs(direct.Value - eventBased.Value) < 1e-9; + Assert.True(areEqual, $"Bar indicator {name} failed: Expected {direct.Value}, Actual {eventBased.Value}"); } } diff --git a/Tests/test_updates_volume.cs b/Tests/test_updates_volume.cs new file mode 100644 index 00000000..472dd740 --- /dev/null +++ b/Tests/test_updates_volume.cs @@ -0,0 +1,159 @@ +using Xunit; +using System.Security.Cryptography; + +namespace QuanTAlib.Tests; + +public class VolumeUpdateTests +{ + private readonly RandomNumberGenerator rng = RandomNumberGenerator.Create(); + private const int RandomUpdates = 100; + private const int precision = 8; + + private double GetRandomDouble() + { + byte[] bytes = new byte[8]; + rng.GetBytes(bytes); + 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(); + double volume = Math.Abs(GetRandomDouble()) * 1000; // Random positive volume + return new TBar(DateTime.Now, open, high, low, close, volume, IsNew); + } + + [Fact] + public void Adl_Update() + { + var indicator = new Adl(); + TBar r = GetRandomBar(true); + + // First calculation with IsNew: true + double value1 = indicator.Calc(r); + + // Multiple recalculations with IsNew: false should not change the value + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + } + + // Final calculation with IsNew: false should match initial value + double value2 = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + Assert.Equal(value1, value2, precision); + + // New calculation with IsNew: true should update the value + double value3 = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: true)); + Assert.NotEqual(value1, value3, precision); + } + + [Fact] + public void Adosc_Update() + { + var indicator = new Adosc(shortPeriod: 3, longPeriod: 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 Aobv_Update() + { + var indicator = new Aobv(); + TBar r = GetRandomBar(true); + + // First calculation with IsNew: true + double value1 = indicator.Calc(r); + + // Multiple recalculations with IsNew: false should not change the value + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + } + + // Final calculation with IsNew: false should match initial value + double value2 = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + Assert.Equal(value1, value2, precision); + + // New calculation with IsNew: true should update the value + double value3 = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: true)); + Assert.NotEqual(value1, value3, precision); + } + + [Fact] + public void Cmf_Update() + { + var indicator = new Cmf(period: 20); + TBar r = GetRandomBar(true); + + // Generate a sequence of bars for warmup + var warmupBars = new List(); + for (int i = 0; i < indicator.WarmupPeriod; i++) + { + var bar = GetRandomBar(IsNew: true); + warmupBars.Add(bar); + indicator.Calc(bar); + } + + // Calculate initial value after warmup + double initialValue = indicator.Calc(r); + + // Apply random updates + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + + // Reset and replay the same sequence + indicator.Init(); + foreach (var bar in warmupBars) + { + indicator.Calc(bar); + } + 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 Eom_Update() + { + var indicator = new Eom(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 Kvo_Update() + { + var indicator = new Kvo(shortPeriod: 34, longPeriod: 55); + 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/lib/momentum/_list.md b/lib/momentum/_list.md index 398da442..89739c87 100644 --- a/lib/momentum/_list.md +++ b/lib/momentum/_list.md @@ -1,3 +1,5 @@ +# Momentum indicators + ✔️ ADX - Average Directional Movement Index ✔️ ADXR - Average Directional Movement Index Rating ✔️ APO - Absolute Price Oscillator diff --git a/lib/oscillators/_list.md b/lib/oscillators/_list.md index ecda4263..618c07b3 100644 --- a/lib/oscillators/_list.md +++ b/lib/oscillators/_list.md @@ -1,3 +1,5 @@ +# Oscillators indicators + AC - Acceleration Oscillator AO - Awesome Oscillator AROON - Aroon oscillator diff --git a/lib/statistics/_list.md b/lib/statistics/_list.md index b7f77ddc..84fa4b37 100644 --- a/lib/statistics/_list.md +++ b/lib/statistics/_list.md @@ -1,3 +1,5 @@ +# Statistics indicators + BETA - Beta coefficient CORR - Correlation Coefficient ✔️ CURVATURE - Rate of Change in Direction or Slope diff --git a/lib/volatility/_list.md b/lib/volatility/_list.md index 5f62b5cf..1ec6089e 100644 --- a/lib/volatility/_list.md +++ b/lib/volatility/_list.md @@ -1,3 +1,5 @@ +# Volatility indicators + ADR - Average Daily Range AP - Andrew's Pitchfork ✔️ ATR - Average True Range @@ -22,7 +24,7 @@ PSAR - Parabolic Stop and Reverse PV - Parkinson Volatility RSV - Rogers-Satchell Volatility ✔️ RV - Realized Volatility -RVI - Relative Volatility Index +✔️ RVI - Relative Volatility Index STARC - Starc Bands SV - Stochastic Volatility TR - True Range diff --git a/lib/volume/Adl.cs b/lib/volume/Adl.cs new file mode 100644 index 00000000..786233ab --- /dev/null +++ b/lib/volume/Adl.cs @@ -0,0 +1,113 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ADL: Accumulation Distribution Line (Chaikin) +/// A volume-based indicator that measures the cumulative flow of money into and out +/// of a security. It assesses the relationship between price and volume to determine +/// buying/selling pressure. +/// +/// +/// The ADL calculation process: +/// 1. Calculates Money Flow Multiplier (MFM): +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// 2. Calculates Money Flow Volume (MFV): +/// MFV = MFM × Volume +/// 3. ADL is cumulative sum of MFV values +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Cumulative indicator +/// - No upper/lower bounds +/// - Trend confirmation tool +/// - Divergence indicator +/// +/// Formula: +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// MFV = MFM × Volume +/// ADL = Previous ADL + MFV +/// +/// Market Applications: +/// - Trend confirmation +/// - Volume analysis +/// - Price/volume divergence +/// - Support/resistance levels +/// - Market participation +/// +/// Sources: +/// Marc Chaikin - Original development +/// https://www.investopedia.com/terms/a/accumulationdistribution.asp +/// +/// Note: Focuses on the relationship between price and volume +/// + +[SkipLocalsInit] +public sealed class Adl : AbstractBase +{ + private double _cumulativeAdl; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adl() + { + WarmupPeriod = 1; + Name = "ADL"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adl(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _cumulativeAdl = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMoneyFlowMultiplier(double close, double high, double low) + { + double range = high - low; + if (range > 0) + { + return ((close - low) - (high - close)) / range; + } + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate Money Flow Multiplier + double mfm = CalculateMoneyFlowMultiplier(BarInput.Close, BarInput.High, BarInput.Low); + + // Calculate Money Flow Volume + double mfv = mfm * BarInput.Volume; + + // Update cumulative ADL only for new bars + if (BarInput.IsNew) + { + _cumulativeAdl += mfv; + } + + IsHot = _index >= WarmupPeriod; + return _cumulativeAdl; + } +} diff --git a/lib/volume/Adosc.cs b/lib/volume/Adosc.cs new file mode 100644 index 00000000..0d718fd2 --- /dev/null +++ b/lib/volume/Adosc.cs @@ -0,0 +1,138 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ADOSC: Chaikin Accumulation/Distribution Oscillator +/// A momentum indicator that measures the strength of accumulation/distribution by combining +/// price and volume with moving averages. It helps identify potential trend reversals and +/// buying/selling pressure. +/// +/// +/// The ADOSC calculation process: +/// 1. Calculate ADL (Accumulation/Distribution Line) +/// a. Money Flow Multiplier = ((Close - Low) - (High - Close)) / (High - Low) +/// b. Money Flow Volume = MFM × Volume +/// c. ADL = Previous ADL + MFV +/// 2. Calculate two EMAs of ADL values +/// 3. Subtract longer EMA from shorter EMA +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Oscillates around zero +/// - Uses two different time periods +/// - Default periods are 3 and 10 days +/// - Shows momentum of money flow +/// +/// Formula: +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// MFV = MFM × Volume +/// ADL = Previous ADL + MFV +/// ADOSC = EMA(ADL, shortPeriod) - EMA(ADL, longPeriod) +/// +/// Market Applications: +/// - Trend confirmation +/// - Divergence analysis +/// - Volume/price relationship +/// - Support/resistance levels +/// - Market reversals +/// +/// Sources: +/// Marc Chaikin - Original development +/// https://www.investopedia.com/terms/c/chaikinoscillator.asp +/// +/// Note: Positive values indicate buying pressure, while negative values indicate selling pressure +/// + +[SkipLocalsInit] +public sealed class Adosc : AbstractBase +{ + private readonly int _longPeriod; + private double _cumulativeAdl; + private double _shortEma; + private double _longEma; + private readonly double _shortAlpha; + private readonly double _longAlpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adosc(int shortPeriod = 3, int longPeriod = 10) + { + _longPeriod = longPeriod; + WarmupPeriod = longPeriod; // Need longer period for EMA calculation + Name = $"ADOSC({shortPeriod},{_longPeriod})"; + _shortAlpha = 2.0 / (shortPeriod + 1); + _longAlpha = 2.0 / (longPeriod + 1); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adosc(object source, int shortPeriod = 3, int longPeriod = 10) : this(shortPeriod, longPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _cumulativeAdl = 0; + _shortEma = 0; + _longEma = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMoneyFlowMultiplier(double close, double high, double low) + { + double range = high - low; + if (range > 0) + { + return ((close - low) - (high - close)) / range; + } + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate Money Flow Multiplier + double mfm = CalculateMoneyFlowMultiplier(BarInput.Close, BarInput.High, BarInput.Low); + + // Calculate Money Flow Volume + double mfv = mfm * BarInput.Volume; + + // Update cumulative ADL + _cumulativeAdl += mfv; + + // Calculate EMAs + if (_index <= _longPeriod) + { + // Initialize EMAs + _shortEma = _cumulativeAdl; + _longEma = _cumulativeAdl; + return 0; + } + + // Update EMAs + _shortEma = (_shortAlpha * _cumulativeAdl) + ((1 - _shortAlpha) * _shortEma); + _longEma = (_longAlpha * _cumulativeAdl) + ((1 - _longAlpha) * _longEma); + + // Calculate ADOSC + double adosc = _shortEma - _longEma; + + IsHot = _index >= WarmupPeriod; + return adosc; + } +} diff --git a/lib/volume/Aobv.cs b/lib/volume/Aobv.cs new file mode 100644 index 00000000..540ddd72 --- /dev/null +++ b/lib/volume/Aobv.cs @@ -0,0 +1,133 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// AOBV: Archer On-Balance Volume +/// A modified version of the traditional On-Balance Volume (OBV) indicator that uses a more +/// sophisticated method to determine buying and selling pressure. It considers both the +/// closing price and the price range to provide a more nuanced view of volume flow. +/// +/// +/// The AOBV calculation process: +/// 1. Determine price position within the day's range +/// 2. Apply volume based on price position: +/// - If close is in upper 1/3 of range: Add full volume +/// - If close is in middle 1/3 of range: Add/subtract half volume +/// - If close is in lower 1/3 of range: Subtract full volume +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Cumulative indicator +/// - No upper/lower bounds +/// - More nuanced than traditional OBV +/// - Considers price position in range +/// +/// Formula: +/// Range = High - Low +/// UpperThird = High - (Range / 3) +/// LowerThird = Low + (Range / 3) +/// If Close >= UpperThird: +/// AOBV = Previous AOBV + Volume +/// Else if Close <= LowerThird: +/// AOBV = Previous AOBV - Volume +/// Else: +/// If Close > Previous Close: +/// AOBV = Previous AOBV + (Volume / 2) +/// Else: +/// AOBV = Previous AOBV - (Volume / 2) +/// +/// Market Applications: +/// - Trend confirmation +/// - Volume analysis +/// - Price/volume divergence +/// - Support/resistance levels +/// - Market participation +/// +/// Sources: +/// Steve Archer - Original development +/// Technical Analysis of Stock Trends (Edwards, Magee) +/// +/// Note: Provides a more detailed analysis of volume flow than traditional OBV +/// + +[SkipLocalsInit] +public sealed class Aobv : AbstractBase +{ + private double _cumulativeAobv; + private double _prevClose; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Aobv() + { + WarmupPeriod = 1; + Name = "AOBV"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Aobv(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _cumulativeAobv = 0; + _prevClose = 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; + } + + double range = BarInput.High - BarInput.Low; + if (range > 0) + { + double upperThird = BarInput.High - (range / 3); + double lowerThird = BarInput.Low + (range / 3); + + // Determine volume flow based on price position + if (BarInput.Close >= upperThird) + { + _cumulativeAobv += BarInput.Volume; + } + else if (BarInput.Close <= lowerThird) + { + _cumulativeAobv -= BarInput.Volume; + } + else + { + // In middle third, use half volume based on close comparison + _cumulativeAobv += (BarInput.Close > _prevClose) ? + (BarInput.Volume / 2) : -(BarInput.Volume / 2); + } + } + + _prevClose = BarInput.Close; + + IsHot = _index >= WarmupPeriod; + return _cumulativeAobv; + } +} diff --git a/lib/volume/Cmf.cs b/lib/volume/Cmf.cs new file mode 100644 index 00000000..2ac7abf6 --- /dev/null +++ b/lib/volume/Cmf.cs @@ -0,0 +1,129 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// CMF: Chaikin Money Flow +/// A volume-weighted technical indicator that measures the amount of Money Flow Volume (MFV) +/// over a specific period. Unlike ADL which is cumulative, CMF averages the Money Flow +/// Volume over a specified period. +/// +/// +/// The CMF calculation process: +/// 1. Calculates Money Flow Multiplier (MFM): +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// 2. Calculates Money Flow Volume (MFV): +/// MFV = MFM × Volume +/// 3. CMF = Sum(MFV) / Sum(Volume) over N periods +/// +/// Key characteristics: +/// - Oscillator between -1 and +1 +/// - Volume-weighted measure +/// - Non-cumulative indicator +/// - Default period is 20 days +/// +/// Formula: +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// MFV = MFM × Volume +/// CMF = Sum(MFV over N periods) / Sum(Volume over N periods) +/// +/// Market Applications: +/// - Trend confirmation +/// - Volume analysis +/// - Price/volume divergence +/// - Support/resistance levels +/// - Market participation +/// +/// Sources: +/// Marc Chaikin - Original development +/// https://www.investopedia.com/terms/c/chaikinmoneyflow.asp +/// +/// Note: Values above zero indicate buying pressure, while values below zero indicate selling pressure +/// + +[SkipLocalsInit] +public sealed class Cmf : AbstractBase +{ + private readonly int _period; + private readonly double[] _mfv; + private readonly double[] _volume; + private int _position; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cmf(int period = 20) + { + _period = period; + WarmupPeriod = period; + Name = $"CMF({_period})"; + _mfv = new double[period]; + _volume = new double[period]; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cmf(object source, int period = 20) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _position = 0; + Array.Clear(_mfv, 0, _mfv.Length); + Array.Clear(_volume, 0, _volume.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMoneyFlowMultiplier(double close, double high, double low) + { + double range = high - low; + if (range > 0) + { + return ((close - low) - (high - close)) / range; + } + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate Money Flow Multiplier + double mfm = CalculateMoneyFlowMultiplier(BarInput.Close, BarInput.High, BarInput.Low); + + // Calculate Money Flow Volume + double currentMfv = mfm * BarInput.Volume; + + // Update circular buffers + _mfv[_position] = currentMfv; + _volume[_position] = BarInput.Volume; + _position = (_position + 1) % _period; + + // Calculate CMF + double sumMfv = 0; + double sumVolume = 0; + for (int i = 0; i < _period; i++) + { + sumMfv += _mfv[i]; + sumVolume += _volume[i]; + } + + double cmf = Math.Abs(sumVolume) > double.Epsilon ? sumMfv / sumVolume : 0; + IsHot = _index >= WarmupPeriod; + return cmf; + } +} diff --git a/lib/volume/Eom.cs b/lib/volume/Eom.cs new file mode 100644 index 00000000..0c2af40b --- /dev/null +++ b/lib/volume/Eom.cs @@ -0,0 +1,132 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// EOM: Ease of Movement +/// A volume-based technical indicator that relates price change to volume, showing the +/// relationship between price change and volume. It emphasizes days where price changes +/// are accomplished with minimal volume and minimizes days where large volume generates +/// small price changes. +/// +/// +/// The EOM calculation process: +/// 1. Calculate the distance moved: +/// Distance = ((High + Low)/2 - (Prior High + Prior Low)/2) +/// 2. Calculate the Box Ratio: +/// BoxRatio = Volume / (High - Low) +/// 3. Calculate single-period EMV: +/// EMV = Distance / BoxRatio +/// 4. Smooth EMV using simple moving average (optional) +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Oscillates around zero +/// - Shows ease of price movement +/// - Default period is 14 days +/// +/// Formula: +/// Distance = ((H + L)/2 - (pH + pL)/2) +/// BoxRatio = Volume / (High - Low) +/// EMV = Distance / BoxRatio +/// EOM = SMA(EMV, period) +/// +/// Market Applications: +/// - Trend strength analysis +/// - Volume/price relationship +/// - Support/resistance breakouts +/// - Market momentum +/// - Divergence identification +/// +/// Sources: +/// Richard W. Arms Jr. - Original development +/// https://www.investopedia.com/terms/e/easeofmovement.asp +/// +/// Note: Positive values suggest prices are rising with light volume (bullish), +/// while negative values suggest prices are falling with light volume (bearish) +/// + +[SkipLocalsInit] +public sealed class Eom : AbstractBase +{ + private readonly int _period; + private readonly double[] _emv; + private int _position; + private double _prevMidpoint; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Eom(int period = 14) + { + _period = period; + WarmupPeriod = period + 1; // Need one extra period for previous midpoint + Name = $"EOM({_period})"; + _emv = new double[period]; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Eom(object source, int period = 14) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _position = 0; + _prevMidpoint = 0; + Array.Clear(_emv, 0, _emv.Length); + } + + [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); + + double midpoint = (BarInput.High + BarInput.Low) / 2; + double boxRatio = BarInput.Volume / (BarInput.High - BarInput.Low + double.Epsilon); // Avoid division by zero + + // Skip first period to establish previous midpoint + if (_index == 1) + { + _prevMidpoint = midpoint; + return 0; + } + + // Calculate distance moved + double distance = midpoint - _prevMidpoint; + + // Calculate EMV for this period + double emv = distance / boxRatio * 10000; // Multiply by 10000 to make values more readable + + // Store in circular buffer + _emv[_position] = emv; + _position = (_position + 1) % _period; + + // Calculate EOM (simple moving average of EMV) + double sum = 0; + for (int i = 0; i < _period; i++) + { + sum += _emv[i]; + } + double eom = sum / _period; + + // Store current midpoint for next calculation + _prevMidpoint = midpoint; + + IsHot = _index >= WarmupPeriod; + return eom; + } +} diff --git a/lib/volume/Kvo.cs b/lib/volume/Kvo.cs new file mode 100644 index 00000000..4e49c5b4 --- /dev/null +++ b/lib/volume/Kvo.cs @@ -0,0 +1,141 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// KVO: Klinger Volume Oscillator +/// A volume-based technical indicator that compares volume to price movement to identify +/// long-term trends and potential reversals. It helps determine the long-term money flow +/// while remaining sensitive to short-term fluctuations. +/// +/// +/// The KVO calculation process: +/// 1. Calculate Trend: +/// Trend = Current DM > Previous DM ? +1 : -1 +/// 2. Calculate Volume Force (VF): +/// VF = Volume * abs(ROC) * Trend * 100 +/// 3. Calculate two EMAs of VF and their difference: +/// Signal = EMA(VF, shortPeriod) - EMA(VF, longPeriod) +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Oscillates around zero +/// - Uses two different time periods +/// - Default periods are 34 and 55 days +/// - Shows volume force and price direction +/// +/// Formula: +/// DM = (H + L + C) / 3 +/// Trend = DM > Previous DM ? +1 : -1 +/// VF = Volume * abs(ROC) * Trend * 100 +/// KVO = EMA(VF, shortPeriod) - EMA(VF, longPeriod) +/// +/// Market Applications: +/// - Trend confirmation +/// - Divergence analysis +/// - Volume/price relationship +/// - Support/resistance levels +/// - Market reversals +/// +/// Sources: +/// Stephen Klinger - Original development +/// https://www.investopedia.com/terms/k/klingeroscillator.asp +/// +/// Note: Positive values indicate buying pressure, while negative values indicate selling pressure +/// + +[SkipLocalsInit] +public sealed class Kvo : AbstractBase +{ + private readonly int _longPeriod; + private double _prevDm; + private double _shortEma; + private double _longEma; + private readonly double _shortAlpha; + private readonly double _longAlpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Kvo(int shortPeriod = 34, int longPeriod = 55) + { + _longPeriod = longPeriod; + WarmupPeriod = longPeriod + 1; // Need one extra period for previous DM + Name = $"KVO({shortPeriod},{_longPeriod})"; + _shortAlpha = 2.0 / (shortPeriod + 1); + _longAlpha = 2.0 / (longPeriod + 1); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Kvo(object source, int shortPeriod = 34, int longPeriod = 55) : this(shortPeriod, longPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevDm = 0; + _shortEma = 0; + _longEma = 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); + + // Calculate Daily Mean + double dm = (BarInput.High + BarInput.Low + BarInput.Close) / 3; + + // Skip first period to establish previous DM + if (_index == 1) + { + _prevDm = dm; + return 0; + } + + // Calculate Trend + int trend = dm > _prevDm ? 1 : -1; + + // Calculate Rate of Change + double roc = Math.Abs(dm - _prevDm) / _prevDm; + + // Calculate Volume Force + double vf = BarInput.Volume * roc * trend * 100; + + // Calculate EMAs + if (_index <= _longPeriod) + { + // Initialize EMAs + _shortEma = vf; + _longEma = vf; + } + else + { + // Update EMAs + _shortEma = (_shortAlpha * vf) + ((1 - _shortAlpha) * _shortEma); + _longEma = (_longAlpha * vf) + ((1 - _longAlpha) * _longEma); + } + + // Store current DM for next calculation + _prevDm = dm; + + // Calculate KVO + double kvo = _shortEma - _longEma; + + IsHot = _index >= WarmupPeriod; + return kvo; + } +} diff --git a/lib/volume/_list.md b/lib/volume/_list.md index 0b99daa1..86ec2225 100644 --- a/lib/volume/_list.md +++ b/lib/volume/_list.md @@ -1,9 +1,11 @@ -ADL - Chaikin Accumulation Distribution Line -ADOSC - Chaikin Accumulation Distribution Oscillator -AOBV - Archer On-Balance Volume -CMF - Chaikin Money Flow -EOM - Ease of Movement -KVO - Klinger Volume Oscillator +# Volume indicators + +✔️ ADL - Chaikin Accumulation Distribution Line +✔️ ADOSC - Chaikin Accumulation Distribution Oscillator +✔️ AOBV - Archer On-Balance Volume +✔️ CMF - Chaikin Money Flow +✔️ EOM - Ease of Movement +✔️ KVO - Klinger Volume Oscillator MFI - Money Flow Index NVI - Negative Volume Index OBV - On-Balance Volume