Merge branch 'dev'

This commit is contained in:
Miha
2024-10-30 07:49:28 -07:00
26 changed files with 2345 additions and 34 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}");
}
}
+125
View File
@@ -88,4 +88,129 @@ public class MomentumUpdateTests
Assert.Equal(initialValue, finalValue, precision);
}
[Fact]
public void Dmx_Update()
{
var indicator = new Dmx(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 Pmo_Update()
{
var indicator = new Pmo(period1: 35, period2: 20);
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 Po_Update()
{
var indicator = new Po(fastPeriod: 10, slowPeriod: 21);
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 Ppo_Update()
{
var indicator = new Ppo(fastPeriod: 12, slowPeriod: 26);
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 Prs_Update()
{
var indicator = new Prs();
indicator.SetBenchmark(ReferenceValue);
double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true));
for (int i = 0; i < RandomUpdates; i++)
{
indicator.SetBenchmark(GetRandomDouble() + 100); // Ensure positive benchmark
indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false));
}
indicator.SetBenchmark(ReferenceValue);
double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false));
Assert.Equal(initialValue, finalValue, precision);
}
[Fact]
public void Roc_Update()
{
var indicator = new Roc(period: 12);
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() + 100, IsNew: false)); // Ensure positive prices
}
double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false));
Assert.Equal(initialValue, finalValue, precision);
}
[Fact]
public void Mom_Update()
{
var indicator = new Mom(period: 10);
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 Vel_Update()
{
var indicator = new Vel(period: 10);
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);
}
}
+32
View File
@@ -61,4 +61,36 @@ public class OscillatorsUpdateTests
Assert.Equal(initialValue, finalValue, precision);
}
[Fact]
public void Ao_Update()
{
var indicator = new Ao();
TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: 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));
}
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 Ac_Update()
{
var indicator = new Ac();
TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: 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));
}
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);
}
}
+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);
}
}
+174
View File
@@ -0,0 +1,174 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// DMX: Enhanced Directional Movement Index using JMA smoothing
/// An improvement over the traditional DMI indicator that uses Jurik Moving Average (JMA)
/// for smoothing instead of Wilder's moving average. This enhancement provides better
/// noise reduction while maintaining responsiveness to significant price movements.
/// </summary>
/// <remarks>
/// The DMX calculation process:
/// 1. Calculate True Range (TR)
/// 2. Calculate +DM (Positive Directional Movement)
/// 3. Calculate -DM (Negative Directional Movement)
/// 4. Smooth TR, +DM, and -DM using JMA instead of Wilder's smoothing
/// 5. Calculate +DI and -DI as percentages
///
/// Key improvements over DMI:
/// - Uses JMA's adaptive volatility-based smoothing
/// - Better noise reduction in the directional movement signals
/// - Maintains responsiveness to significant price movements
/// - Reduced lag through JMA's phase-shifting
///
/// Formula:
/// TR = max(high-low, abs(high-prevClose), abs(low-prevClose))
/// +DM = if(high-prevHigh > prevLow-low) then max(high-prevHigh, 0) else 0
/// -DM = if(prevLow-low > high-prevHigh) then max(prevLow-low, 0) else 0
/// +DI = 100 * JMA(+DM) / JMA(TR)
/// -DI = 100 * JMA(-DM) / JMA(TR)
///
/// Sources:
/// Original DMI by J. Welles Wilder Jr. - "New Concepts in Technical Trading Systems" (1978)
/// Enhanced with JMA smoothing by Mark Jurik
/// </remarks>
[SkipLocalsInit]
public sealed class Dmx : AbstractBarBase
{
private readonly Jma _smoothedTr;
private readonly Jma _smoothedPlusDm;
private readonly Jma _smoothedMinusDm;
private double _prevHigh, _prevLow, _prevClose;
private double _p_prevHigh, _p_prevLow, _p_prevClose;
private double _plusDi, _minusDi;
private const double ScalingFactor = 100.0;
private const int DefaultPeriod = 10;
private const int DefaultPhase = 100;
private const double DefaultFactor = 0.25;
/// <summary>
/// Gets the most recent +DI value
/// </summary>
public double PlusDI => _plusDi;
/// <summary>
/// Gets the most recent -DI value
/// </summary>
public double MinusDI => _minusDi;
/// <param name="period">The number of periods used in the DMX calculation (default 14).</param>
/// <param name="phase">The phase for the JMA smoothing (default 0).</param>
/// <param name="factor">The factor for the JMA smoothing (default 0.45).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Dmx(int period = DefaultPeriod, int phase = DefaultPhase, double factor = DefaultFactor)
{
if (period < 1)
throw new ArgumentOutOfRangeException(nameof(period));
_smoothedTr = new(period, phase, factor);
_smoothedPlusDm = new(period, phase, factor);
_smoothedMinusDm = new(period, phase, factor);
_index = 0;
WarmupPeriod = period * 2; // JMA needs more warmup periods than RMA
Name = $"DMX({period})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of periods used in the DMX calculation.</param>
/// <param name="phase">The phase for the JMA smoothing.</param>
/// <param name="factor">The factor for the JMA smoothing.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Dmx(object source, int period, int phase = DefaultPhase, double factor = DefaultFactor) : this(period, phase, factor)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
_p_prevHigh = _prevHigh;
_p_prevLow = _prevLow;
_p_prevClose = _prevClose;
}
else
{
_prevHigh = _p_prevHigh;
_prevLow = _p_prevLow;
_prevClose = _p_prevClose;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static double CalculateTrueRange(double high, double low, double prevClose)
{
double hl = high - low;
double hpc = Math.Abs(high - prevClose);
double lpc = Math.Abs(low - prevClose);
return Math.Max(hl, Math.Max(hpc, lpc));
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static (double plusDm, double minusDm) CalculateDirectionalMovement(
double high, double low, double prevHigh, double prevLow)
{
double upMove = high - prevHigh;
double downMove = prevLow - low;
double plusDm = 0.0;
double minusDm = 0.0;
if (upMove > downMove && upMove > 0)
plusDm = upMove;
else if (downMove > upMove && downMove > 0)
minusDm = downMove;
return (plusDm, minusDm);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
if (_index == 1)
{
_prevHigh = Input.High;
_prevLow = Input.Low;
_prevClose = Input.Close;
return 0.0;
}
// Calculate True Range and Directional Movement
double tr = CalculateTrueRange(Input.High, Input.Low, _prevClose);
var (plusDm, minusDm) = CalculateDirectionalMovement(
Input.High, Input.Low, _prevHigh, _prevLow);
// Update previous values
_prevHigh = Input.High;
_prevLow = Input.Low;
_prevClose = Input.Close;
// Smooth the indicators using JMA
_smoothedTr.Calc(tr, Input.IsNew);
_smoothedPlusDm.Calc(plusDm, Input.IsNew);
_smoothedMinusDm.Calc(minusDm, Input.IsNew);
// Calculate +DI and -DI
double smoothedTr = _smoothedTr.Value;
if (smoothedTr > 0)
{
_plusDi = ScalingFactor * _smoothedPlusDm.Value / smoothedTr;
_minusDi = ScalingFactor * _smoothedMinusDm.Value / smoothedTr;
return _plusDi - _minusDi; // Return the difference as main value
}
_plusDi = 0.0;
_minusDi = 0.0;
return 0.0;
}
}
+75
View File
@@ -0,0 +1,75 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// Mom: Momentum
/// A basic momentum indicator that measures the change in price over a specified
/// period, helping identify the strength and speed of price movements.
/// </summary>
/// <remarks>
/// The Momentum calculation process:
/// 1. Store historical prices in a circular buffer
/// 2. Calculate absolute difference between current and historical price
/// 3. No scaling factor applied to maintain raw price difference
///
/// Key characteristics:
/// - Basic momentum measurement
/// - Shows absolute price changes
/// - Zero line crossovers signal trend changes
/// - Foundation for other momentum indicators
///
/// Formula:
/// Mom = Price - PriceN
/// where PriceN is the price N periods ago
///
/// Sources:
/// Technical Analysis of Financial Markets by John J. Murphy
/// Technical Analysis Using Multiple Timeframes by Brian Shannon
/// </remarks>
[SkipLocalsInit]
public sealed class Mom : AbstractBase
{
private readonly CircularBuffer _priceBuffer;
private const int DefaultPeriod = 10;
/// <param name="period">The lookback period for momentum calculation (default 10).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Mom(int period = DefaultPeriod)
{
if (period < 1)
throw new ArgumentOutOfRangeException(nameof(period));
_priceBuffer = new(period + 1);
WarmupPeriod = period;
Name = $"MOM({period})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The lookback period for momentum calculation.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Mom(object source, int period) : this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
_priceBuffer.Add(Input.Value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
if (_priceBuffer.Count < _priceBuffer.Capacity)
return 0.0;
return Input.Value - _priceBuffer[0];
}
}
+102
View File
@@ -0,0 +1,102 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// PMO: Price Momentum Oscillator
/// A momentum indicator that uses exponential moving averages of ROC (Rate of Change)
/// to identify overbought and oversold conditions in price movements.
/// </summary>
/// <remarks>
/// The PMO calculation process:
/// 1. Calculate ROC (Rate of Change) of closing prices
/// 2. Apply a first smoothing EMA to the ROC values
/// 3. Apply a second smoothing EMA to the result
/// 4. Multiply by a scaling factor for better visualization
///
/// Key characteristics:
/// - Double-smoothed momentum indicator
/// - Helps identify overbought/oversold conditions
/// - Useful for trend confirmation and divergence analysis
/// - More responsive than traditional momentum oscillators
///
/// Formula:
/// ROC = (Close - PrevClose) / PrevClose
/// Signal1 = EMA(ROC, Period1)
/// PMO = EMA(Signal1, Period2) * ScalingFactor
///
/// Sources:
/// Developed by Carl Swenlin
/// Technical Analysis of Stocks and Commodities magazine
/// </remarks>
[SkipLocalsInit]
public sealed class Pmo : AbstractBase
{
private readonly Ema _smoothing1;
private readonly Ema _smoothing2;
private double _prevClose;
private double _p_prevClose;
private const double ScalingFactor = 100.0;
private const int DefaultPeriod1 = 35;
private const int DefaultPeriod2 = 20;
/// <param name="period1">The first smoothing period (default 35).</param>
/// <param name="period2">The second smoothing period (default 20).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when either period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Pmo(int period1 = DefaultPeriod1, int period2 = DefaultPeriod2)
{
if (period1 < 1 || period2 < 1)
throw new ArgumentOutOfRangeException(nameof(period1));
_smoothing1 = new(period1);
_smoothing2 = new(period2);
_index = 0;
WarmupPeriod = period1 + period2;
Name = $"PMO({period1},{period2})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period1">The first smoothing period.</param>
/// <param name="period2">The second smoothing period.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Pmo(object source, int period1, int period2) : this(period1, period2)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
_p_prevClose = _prevClose;
}
else
{
_prevClose = _p_prevClose;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
if (_index == 1)
{
_prevClose = Input.Value;
return 0.0;
}
// Calculate Rate of Change
double roc = (Input.Value - _prevClose) / _prevClose;
_prevClose = Input.Value;
// Apply double smoothing
double signal1 = _smoothing1.Calc(roc, Input.IsNew);
return _smoothing2.Calc(signal1, Input.IsNew) * ScalingFactor;
}
}
+80
View File
@@ -0,0 +1,80 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// PO: Price Oscillator
/// A momentum indicator that measures the difference between two moving averages
/// of different periods to identify price momentum and potential trend changes.
/// </summary>
/// <remarks>
/// The PO calculation process:
/// 1. Calculate fast EMA of closing prices
/// 2. Calculate slow EMA of closing prices
/// 3. Calculate the difference between fast and slow EMAs
/// 4. Multiply by a scaling factor for better visualization
///
/// Key characteristics:
/// - Measures momentum through moving average differences
/// - Helps identify trend direction and potential reversals
/// - Zero line crossovers signal trend changes
/// - Similar to MACD but more customizable periods
///
/// Formula:
/// FastMA = EMA(Close, FastPeriod)
/// SlowMA = EMA(Close, SlowPeriod)
/// PO = (FastMA - SlowMA) * ScalingFactor
///
/// Sources:
/// Technical Analysis of Financial Markets by John J. Murphy
/// </remarks>
[SkipLocalsInit]
public sealed class Po : AbstractBase
{
private readonly Ema _fastEma;
private readonly Ema _slowEma;
private const double ScalingFactor = 1.0;
private const int DefaultFastPeriod = 10;
private const int DefaultSlowPeriod = 21;
/// <param name="fastPeriod">The fast EMA period (default 10).</param>
/// <param name="slowPeriod">The slow EMA period (default 21).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when either period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Po(int fastPeriod = DefaultFastPeriod, int slowPeriod = DefaultSlowPeriod)
{
if (fastPeriod < 1 || slowPeriod < 1)
throw new ArgumentOutOfRangeException(nameof(fastPeriod));
if (fastPeriod >= slowPeriod)
throw new ArgumentException("Fast period must be less than slow period");
_fastEma = new(fastPeriod);
_slowEma = new(slowPeriod);
WarmupPeriod = slowPeriod;
Name = $"PO({fastPeriod},{slowPeriod})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="fastPeriod">The fast EMA period.</param>
/// <param name="slowPeriod">The slow EMA period.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Po(object source, int fastPeriod, int slowPeriod) : this(fastPeriod, slowPeriod)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
// No state management needed for this indicator
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
double fastEma = _fastEma.Calc(Input.Value, Input.IsNew);
double slowEma = _slowEma.Calc(Input.Value, Input.IsNew);
return (fastEma - slowEma) * ScalingFactor;
}
}
+85
View File
@@ -0,0 +1,85 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// PPO: Percentage Price Oscillator
/// A momentum indicator that shows the percentage difference between two moving averages
/// of different periods, helping identify price momentum and potential trend changes.
/// </summary>
/// <remarks>
/// The PPO calculation process:
/// 1. Calculate fast EMA of closing prices
/// 2. Calculate slow EMA of closing prices
/// 3. Calculate the percentage difference between fast and slow EMAs
/// 4. Multiply by a scaling factor for better visualization
///
/// Key characteristics:
/// - Measures momentum through percentage differences
/// - Normalized for comparison across different price levels
/// - Zero line crossovers signal trend changes
/// - Similar to MACD but expressed as a percentage
///
/// Formula:
/// FastMA = EMA(Close, FastPeriod)
/// SlowMA = EMA(Close, SlowPeriod)
/// PPO = ((FastMA - SlowMA) / SlowMA) * 100
///
/// Sources:
/// Technical Analysis of Financial Markets by John J. Murphy
/// StockCharts.com Technical Indicators
/// </remarks>
[SkipLocalsInit]
public sealed class Ppo : AbstractBase
{
private readonly Ema _fastEma;
private readonly Ema _slowEma;
private const double ScalingFactor = 100.0;
private const int DefaultFastPeriod = 12;
private const int DefaultSlowPeriod = 26;
/// <param name="fastPeriod">The fast EMA period (default 12).</param>
/// <param name="slowPeriod">The slow EMA period (default 26).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when either period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Ppo(int fastPeriod = DefaultFastPeriod, int slowPeriod = DefaultSlowPeriod)
{
if (fastPeriod < 1 || slowPeriod < 1)
throw new ArgumentOutOfRangeException(nameof(fastPeriod));
if (fastPeriod >= slowPeriod)
throw new ArgumentException("Fast period must be less than slow period");
_fastEma = new(fastPeriod);
_slowEma = new(slowPeriod);
WarmupPeriod = slowPeriod;
Name = $"PPO({fastPeriod},{slowPeriod})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="fastPeriod">The fast EMA period.</param>
/// <param name="slowPeriod">The slow EMA period.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Ppo(object source, int fastPeriod, int slowPeriod) : this(fastPeriod, slowPeriod)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
// No state management needed for this indicator
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
double fastEma = _fastEma.Calc(Input.Value, Input.IsNew);
double slowEma = _slowEma.Calc(Input.Value, Input.IsNew);
if (Math.Abs(slowEma) <= double.Epsilon)
return 0.0;
return ((fastEma - slowEma) / slowEma) * ScalingFactor;
}
}
+84
View File
@@ -0,0 +1,84 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// PRS: Price Relative Strength
/// A momentum indicator that compares the performance of a security against a benchmark,
/// helping identify which is showing stronger relative momentum.
/// </summary>
/// <remarks>
/// The PRS calculation process:
/// 1. Take the current price of the security
/// 2. Take the current price of the benchmark
/// 3. Calculate the ratio between them
/// 4. Multiply by a scaling factor for better visualization
///
/// Key characteristics:
/// - Measures relative performance against a benchmark
/// - Helps identify market leaders and laggards
/// - Rising PRS indicates outperformance
/// - Falling PRS indicates underperformance
///
/// Formula:
/// PRS = (Price / Benchmark) * 100
///
/// Sources:
/// Technical Analysis of Financial Markets by John J. Murphy
/// StockCharts.com Technical Indicators
/// </remarks>
[SkipLocalsInit]
public sealed class Prs : AbstractBase
{
private const double ScalingFactor = 100.0;
private double _benchmark;
private double _p_benchmark;
/// <summary>
/// Initializes a new instance of the PRS indicator
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Prs()
{
WarmupPeriod = 1;
Name = "PRS";
}
/// <param name="source">The data source object that publishes updates.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Prs(object source) : this()
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
/// <summary>
/// Sets the current benchmark value
/// </summary>
/// <param name="benchmark">The benchmark value to compare against</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetBenchmark(double benchmark)
{
_benchmark = benchmark;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
_p_benchmark = _benchmark;
else
_benchmark = _p_benchmark;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
if (_benchmark <= double.Epsilon)
return 0.0;
return (Input.Value / _benchmark) * ScalingFactor;
}
}
+80
View File
@@ -0,0 +1,80 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// ROC: Rate of Change
/// A momentum indicator that measures the percentage change in price over a specified
/// period, helping identify the speed and strength of price movements.
/// </summary>
/// <remarks>
/// The ROC calculation process:
/// 1. Store historical prices in a circular buffer
/// 2. Calculate percentage change between current and historical price
/// 3. Multiply by scaling factor for better visualization
///
/// Key characteristics:
/// - Pure momentum indicator
/// - Oscillates around zero line
/// - Helps identify overbought/oversold conditions
/// - Useful for divergence analysis
///
/// Formula:
/// ROC = ((Price - PriceN) / PriceN) * 100
/// where PriceN is the price N periods ago
///
/// Sources:
/// Technical Analysis of Financial Markets by John J. Murphy
/// Technical Analysis of Stock Trends by Robert D. Edwards and John Magee
/// </remarks>
[SkipLocalsInit]
public sealed class Roc : AbstractBase
{
private readonly CircularBuffer _priceBuffer;
private const double ScalingFactor = 100.0;
private const int DefaultPeriod = 12;
/// <param name="period">The lookback period for ROC calculation (default 12).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Roc(int period = DefaultPeriod)
{
if (period < 1)
throw new ArgumentOutOfRangeException(nameof(period));
_priceBuffer = new(period + 1);
WarmupPeriod = period;
Name = $"ROC({period})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The lookback period for ROC calculation.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Roc(object source, int period) : this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
_priceBuffer.Add(Input.Value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
if (_priceBuffer.Count < _priceBuffer.Capacity)
return 0.0;
double oldPrice = _priceBuffer[0];
if (oldPrice <= double.Epsilon)
return 0.0;
return ((Input.Value - oldPrice) / oldPrice) * ScalingFactor;
}
}
+89
View File
@@ -0,0 +1,89 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// Vel: Velocity
/// An enhanced momentum indicator that applies Jurik Moving Average (JMA) smoothing
/// to the basic momentum calculation, providing better noise reduction while
/// maintaining responsiveness to significant price movements.
/// </summary>
/// <remarks>
/// The Velocity calculation process:
/// 1. Calculate basic momentum (price difference)
/// 2. Apply JMA smoothing to the momentum values
/// 3. No scaling factor applied to maintain price-based units
///
/// Key characteristics:
/// - Enhanced momentum measurement with JMA smoothing
/// - Better noise reduction than basic momentum
/// - Maintains responsiveness to significant moves
/// - Reduced lag through JMA's phase-shifting
///
/// Formula:
/// Mom = Price - PriceN
/// Vel = JMA(Mom, period)
///
/// Sources:
/// Enhanced with JMA smoothing by Mark Jurik
/// Technical Analysis of Financial Markets by John J. Murphy
/// </remarks>
[SkipLocalsInit]
public sealed class Vel : AbstractBase
{
private readonly CircularBuffer _priceBuffer;
private readonly Jma _smoothing;
private const int DefaultPeriod = 10;
private const int DefaultPhase = 100;
private const double DefaultFactor = 0.25;
/// <param name="period">The lookback period for velocity calculation (default 10).</param>
/// <param name="phase">The phase for the JMA smoothing (default 0).</param>
/// <param name="power">The power factor for the JMA smoothing (default 2.0).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vel(int period = DefaultPeriod, int phase = DefaultPhase, double factor = DefaultFactor)
{
if (period < 1)
throw new ArgumentOutOfRangeException(nameof(period));
_priceBuffer = new(period + 1);
_smoothing = new(period, phase, factor);
WarmupPeriod = period * 2; // JMA needs more warmup periods
Name = $"VEL({period})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The lookback period for velocity calculation.</param>
/// <param name="phase">The phase for the JMA smoothing.</param>
/// <param name="power">The power factor for the JMA smoothing.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vel(object source, int period, int phase = DefaultPhase, double power = DefaultFactor)
: this(period, phase, power)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
_priceBuffer.Add(Input.Value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
if (_priceBuffer.Count < _priceBuffer.Capacity)
return 0.0;
// Calculate basic momentum
double momentum = Input.Value - _priceBuffer[0];
// Apply JMA smoothing
return _smoothing.Calc(momentum, Input.IsNew);
}
}
+11 -9
View File
@@ -1,16 +1,18 @@
# Momentum indicators
✔️ ADX - Average Directional Movement Index
✔️ ADXR - Average Directional Movement Index Rating
✔️ APO - Absolute Price Oscillator
DMI - Directional Movement Index
DMX - Jurik Directional Movement Index
✔️ DMI - Directional Movement Index
✔️ DMX - Jurik Directional Movement Index
DPO - Detrended Price Oscillator
MACD - Moving Average Convergence/Divergence
MOM - Momentum
PMO - Price Momentum Oscillator
PO - Price Oscillator
PPO - Percentage Price Oscillator
PRS - Price Relative Strength
ROC - Rate of Change
✔️ MOM - Momentum
✔️ PMO - Price Momentum Oscillator
✔️ PO - Price Oscillator
✔️ PPO - Percentage Price Oscillator
✔️ PRS - Price Relative Strength
✔️ ROC - Rate of Change
TRIX - 1-day ROC of TEMA
VEL - Jurik Signal Velocity
✔️ VEL - Jurik Signal Velocity
VORTEX - Vortex Indicator
+71
View File
@@ -0,0 +1,71 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// AC: Acceleration/Deceleration Oscillator
/// A momentum indicator that measures the acceleration and deceleration of the current driving force.
/// It is derived from the Awesome Oscillator (AO) and helps identify potential trend reversals.
/// </summary>
/// <remarks>
/// The AC calculation process:
/// 1. Calculate the Awesome Oscillator (AO)
/// 2. Calculate a 5-period simple moving average of the AO
/// 3. Subtract the 5-period SMA from the current AO value
///
/// Key characteristics:
/// - Oscillates above and below zero
/// - Measures the acceleration/deceleration of market driving force
/// - Positive values indicate increasing momentum
/// - Negative values indicate decreasing momentum
/// - Can be used to identify potential trend reversals
///
/// Formula:
/// AC = AO - SMA(AO, 5)
///
/// Sources:
/// Bill Williams - "Trading Chaos" (1995)
/// https://www.investopedia.com/terms/a/ac.asp
/// </remarks>
[SkipLocalsInit]
public sealed class Ac : AbstractBase
{
private readonly Ao _ao;
private readonly Sma _sma5;
/// <param name="source">The data source object that publishes updates.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Ac(object source) : this()
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Ac()
{
_ao = new Ao();
_sma5 = new Sma(5);
WarmupPeriod = 39; // AO requires 34 periods + 5 for AC's SMA
Name = "AC";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override double Calculation()
{
ManageState(BarInput.IsNew);
var ao = _ao.Calc(BarInput, BarInput.IsNew);
_sma5.Calc(ao, BarInput.IsNew);
return ao - _sma5.Value;
}
}
+71
View File
@@ -0,0 +1,71 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// AO: Awesome Oscillator
/// A momentum indicator that reflects the precise changes in the market driving force.
/// It is used to affirm trends or to anticipate possible reversals.
/// </summary>
/// <remarks>
/// The AO calculation process:
/// 1. Calculates the 5-period simple moving average of the HL2 (High+Low)/2 values.
/// 2. Calculates the 34-period simple moving average of the HL2 (High+Low)/2 values.
/// 3. Subtracts the 34-period SMA from the 5-period SMA.
///
/// Key characteristics:
/// - Oscillates above and below zero
/// - Positive values indicate bullish momentum
/// - Negative values indicate bearish momentum
/// - Crosses above zero suggest buying opportunities
/// - Crosses below zero suggest selling opportunities
///
/// Formula:
/// AO = SMA(HL2, 5) - SMA(HL2, 34)
///
/// Sources:
/// Bill Williams - "Trading Chaos" (1995)
/// https://www.investopedia.com/terms/a/awesomeoscillator.asp
/// </remarks>
[SkipLocalsInit]
public sealed class Ao : AbstractBase
{
private readonly Sma _sma5;
private readonly Sma _sma34;
/// <param name="source">The data source object that publishes updates.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Ao(object source) : this()
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Ao()
{
_sma5 = new Sma(5);
_sma34 = new Sma(34);
WarmupPeriod = 34;
Name = "AO";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override double Calculation()
{
ManageState(BarInput.IsNew);
_sma5.Calc(BarInput.HL2, BarInput.IsNew);
_sma34.Calc(BarInput.HL2, BarInput.IsNew);
return _sma5.Value - _sma34.Value;
}
}
+4 -2
View File
@@ -1,5 +1,7 @@
AC - Acceleration Oscillator
AO - Awesome Oscillator
# Oscillators indicators
✔️ AC - Acceleration Oscillator
✔️ AO - Awesome Oscillator
AROON - Aroon oscillator
BOP - Balance of Power
CCI - Commodity Channel Index
+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