Adl, Adosc, Aobv, Cmf

This commit is contained in:
Miha
2024-10-28 15:29:29 -07:00
parent 6b79f8158c
commit 45c6f08e1e
14 changed files with 1266 additions and 23 deletions
+251
View File
@@ -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<double>)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<double>();
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
}
+53 -16
View File
@@ -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}");
}
}
+159
View File
@@ -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<TBar>();
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);
}
}
+2
View File
@@ -1,3 +1,5 @@
# Momentum indicators
✔️ ADX - Average Directional Movement Index
✔️ ADXR - Average Directional Movement Index Rating
✔️ APO - Absolute Price Oscillator
+2
View File
@@ -1,3 +1,5 @@
# Oscillators indicators
AC - Acceleration Oscillator
AO - Awesome Oscillator
AROON - Aroon oscillator
+2
View File
@@ -1,3 +1,5 @@
# Statistics indicators
BETA - Beta coefficient
CORR - Correlation Coefficient
✔️ CURVATURE - Rate of Change in Direction or Slope
+3 -1
View File
@@ -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
+113
View File
@@ -0,0 +1,113 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
[SkipLocalsInit]
public sealed class Adl : AbstractBase
{
private double _cumulativeAdl;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Adl()
{
WarmupPeriod = 1;
Name = "ADL";
Init();
}
/// <param name="source">The data source object that publishes updates.</param>
[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;
}
}
+138
View File
@@ -0,0 +1,138 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
[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();
}
/// <param name="source">The data source object that publishes updates.</param>
[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;
}
}
+133
View File
@@ -0,0 +1,133 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
[SkipLocalsInit]
public sealed class Aobv : AbstractBase
{
private double _cumulativeAobv;
private double _prevClose;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Aobv()
{
WarmupPeriod = 1;
Name = "AOBV";
Init();
}
/// <param name="source">The data source object that publishes updates.</param>
[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;
}
}
+129
View File
@@ -0,0 +1,129 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
[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();
}
/// <param name="source">The data source object that publishes updates.</param>
[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;
}
}
+132
View File
@@ -0,0 +1,132 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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)
/// </remarks>
[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();
}
/// <param name="source">The data source object that publishes updates.</param>
[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;
}
}
+141
View File
@@ -0,0 +1,141 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
[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();
}
/// <param name="source">The data source object that publishes updates.</param>
[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;
}
}
+8 -6
View File
@@ -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