mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 11:38:05 +00:00
Add unit tests for various moving average indicators
- Implement tests for HMA (Hull Moving Average) indicator to verify default settings, history depth calculations, and value computations during updates. - Create tests for KAMA (Kaufman Adaptive Moving Average) indicator, ensuring correct defaults, history depth, and value calculations. - Add tests for SMA (Simple Moving Average) indicator, checking default values, history depth, and value computations. - Develop tests for T3 (Tillson T3 Moving Average) indicator, validating defaults, history depth, and value calculations. - Implement tests for TEMA (Triple Exponential Moving Average) indicator, ensuring correct defaults and value computations. - Create tests for TRIMA (Triangular Moving Average) indicator, verifying defaults, history depth, and value calculations. - Add tests for WMA (Weighted Moving Average) indicator, checking default values, history depth, and value computations.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
- [**QuanTAlib**](/)
|
||||
|
||||
- **Trends**
|
||||
- [Overview](trends/)
|
||||
- [ALMA - Arnaud Legoux MA](trends/alma/Alma.md)
|
||||
- [DEMA - Double Exponential MA](trends/dema/Dema.md)
|
||||
- [EMA - Exponential MA](trends/ema/Ema.md)
|
||||
- [HMA - Hull MA](trends/hma/Hma.md)
|
||||
- [SMA - Simple MA](trends/sma/Sma.md)
|
||||
- [T3 - Tillson T3 MA](trends/t3/T3.md)
|
||||
- [TEMA - Triple Exponential MA](trends/tema/Tema.md)
|
||||
- [TRIMA - Triangular MA](trends/trima/Trima.md)
|
||||
- [WMA - Weighted MA](trends/wma/Wma.md)
|
||||
@@ -1,65 +0,0 @@
|
||||
# Averages
|
||||
|
||||
| Indicator | Name |
|
||||
| --------- | ------------------------------ |
|
||||
| ALMA | Arnaud Legoux MA |
|
||||
| BESSEL | Bessel Filter |
|
||||
| BILATERAL | Bilateral Filter |
|
||||
| BLMA | Blackman Window MA |
|
||||
| BPF | Ehlers Bandpass Filter |
|
||||
| BUTTER | Butterworth Filter |
|
||||
| BWMA | Bessel-Weighted MA |
|
||||
| CHEBY1 | Chebyshev Type I Filter |
|
||||
| CHEBY2 | Chebyshev Type II Filter |
|
||||
| CONV | Convolution MA with any kernel |
|
||||
| [DEMA](dema/Dema.cs) | Double Exponential MA |
|
||||
| DSMA | Deviation-Scaled MA |
|
||||
| DWMA | Double Weighted MA |
|
||||
| ELLIPTIC | Elliptic (Cauer) Filter |
|
||||
| [EMA](ema/Ema.cs) | Exponential MA |
|
||||
| EPMA | Endpoint MA |
|
||||
| FRAMA | Fractal Adaptive MA |
|
||||
| GAUSS | Gaussian Filter |
|
||||
| GWMA | Gaussian-Weighted MA |
|
||||
| HAMMA | Hamming Window MA |
|
||||
| HANN | Hann FIR Filter |
|
||||
| HANMA | Hanning Window MA |
|
||||
| HEMA | Hull Exponential MA |
|
||||
| [HMA](hma/Hma.cs) | Hull MA |
|
||||
| HP | Hodrick-Prescott Filter |
|
||||
| HPF | Ehlers Highpass Filter |
|
||||
| HTIT | Hilbert Transform Instantaneous Trend |
|
||||
| HWMA | Holt Weighted MA |
|
||||
| JMA | Jurik MA |
|
||||
| KAMA | Kaufman Adaptive MA |
|
||||
| KF | Kalman Filter |
|
||||
| LOESS | LOESS/LOWESS Smoothing |
|
||||
| LSMA | Least Squares MA |
|
||||
| LTMA | Linear Trend MA |
|
||||
| MAMA | MESA Adaptive MA |
|
||||
| MEDIAN | Median Filter |
|
||||
| MGDI | McGinley Dynamic Indicator |
|
||||
| MMA | Modified MA |
|
||||
| NOTCH | Notch Filter |
|
||||
| PWMA | Pascal Weighted MA |
|
||||
| QEMA | Quadruple Exponential MA |
|
||||
| REMA | Regularized Exponential MA |
|
||||
| RGMA | Recursive Gaussian MA |
|
||||
| RMA | wildeR MA (SMMA, MMA) |
|
||||
| SGF | Savitzky-Golay Filter |
|
||||
| SGMA | Savitzky-Golay MA |
|
||||
| SINEMA | Sine-weighted MA |
|
||||
| [SMA](sma/Sma.cs) | Simple MA |
|
||||
| SSF | Ehlers Super Smooth Filter |
|
||||
| [T3](t3/T3.cs) | Tillson T3 MA |
|
||||
| [TEMA](tema/Tema.cs) | Triple Exponential MA |
|
||||
| [TRIMA](trima/Trima.cs) | Triangular MA |
|
||||
| USF | Ehlers Ultrasmooth Filter |
|
||||
| VAMA | Volatility Adjusted MA |
|
||||
| VIDYA | Variable Index Dynamic Average |
|
||||
| WIENER | Wiener Filter |
|
||||
| [WMA](wma/Wma.cs) | Weighted MA |
|
||||
| YZVAMA | Yang-Zhang Volatility Adjusted MA |
|
||||
| ZLDEMA | Zero-Lag Double Exponential MA |
|
||||
| ZLEMA | Zero-Lag Exponential MA |
|
||||
| ZLTEMA | Zero-Lag Triple Exponential MA |
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>QuanTAlib Documentation</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="description" content="Quantitative Technical Analysis Library in C#">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
window.$docsify = {
|
||||
name: 'QuanTAlib',
|
||||
repo: 'https://github.com/mihakralj/QuanTAlib',
|
||||
loadSidebar: true,
|
||||
subMaxLevel: 2,
|
||||
auto2top: true
|
||||
}
|
||||
</script>
|
||||
<!-- Docsify v4 -->
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
|
||||
<!-- MathJax -->
|
||||
<script src="//cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify-latex@0"></script>
|
||||
<!-- Prism for C# syntax highlighting -->
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-csharp.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,67 @@
|
||||
# Trends
|
||||
|
||||
Trend indicators help identify the direction and strength of a market trend. Moving averages are the most common type of trend indicator, smoothing out price data to create a clearer picture of the underlying direction.
|
||||
|
||||
| Indicator | Full Name | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| [ALMA](trends/alma/Alma.md) | Arnaud Legoux MA | Uses Gaussian distribution weights to balance smoothness and responsiveness. |
|
||||
| BESSEL | Bessel Filter | |
|
||||
| BILATERAL | Bilateral Filter | |
|
||||
| BLMA | Blackman Window MA | |
|
||||
| BPF | Ehlers Bandpass Filter | |
|
||||
| BUTTER | Butterworth Filter | |
|
||||
| BWMA | Bessel-Weighted MA | |
|
||||
| CHEBY1 | Chebyshev Type I Filter | |
|
||||
| CHEBY2 | Chebyshev Type II Filter | |
|
||||
| CONV | Convolution MA with any kernel | |
|
||||
| [DEMA](trends/dema/Dema.md) | Double Exponential Moving Average | Reduces lag by placing more weight on recent data than a standard EMA. |
|
||||
| DSMA | Deviation-Scaled MA | |
|
||||
| DWMA | Double Weighted MA | |
|
||||
| ELLIPTIC | Elliptic (Cauer) Filter | |
|
||||
| [EMA](trends/ema/Ema.md) | Exponential Moving Average | Weighted average giving more importance to recent price data. |
|
||||
| EPMA | Endpoint MA | |
|
||||
| FRAMA | Fractal Adaptive MA | |
|
||||
| GAUSS | Gaussian Filter | |
|
||||
| GWMA | Gaussian-Weighted MA | |
|
||||
| HAMMA | Hamming Window MA | |
|
||||
| HANN | Hann FIR Filter | |
|
||||
| HANMA | Hanning Window MA | |
|
||||
| HEMA | Hull Exponential MA | |
|
||||
| [HMA](trends/hma/Hma.md) | Hull Moving Average | Developed by Alan Hull to reduce lag while improving smoothing. |
|
||||
| HP | Hodrick-Prescott Filter | |
|
||||
| HPF | Ehlers Highpass Filter | |
|
||||
| HTIT | Hilbert Transform Instantaneous Trend | |
|
||||
| HWMA | Holt Weighted MA | |
|
||||
| JMA | Jurik MA | |
|
||||
| [KAMA](trends/kama/Kama.md) | Kaufman Adaptive MA | Adapts to market volatility by adjusting its smoothing factor based on an Efficiency Ratio. |
|
||||
| KF | Kalman Filter | |
|
||||
| LOESS | LOESS/LOWESS Smoothing | |
|
||||
| LSMA | Least Squares MA | |
|
||||
| LTMA | Linear Trend MA | |
|
||||
| MAMA | MESA Adaptive MA | |
|
||||
| MEDIAN | Median Filter | |
|
||||
| MGDI | McGinley Dynamic Indicator | |
|
||||
| MMA | Modified MA | |
|
||||
| NOTCH | Notch Filter | |
|
||||
| PWMA | Pascal Weighted MA | |
|
||||
| QEMA | Quadruple Exponential MA | |
|
||||
| REMA | Regularized Exponential MA | |
|
||||
| RGMA | Recursive Gaussian MA | |
|
||||
| RMA | wildeR MA (SMMA, MMA) | |
|
||||
| SGF | Savitzky-Golay Filter | |
|
||||
| SGMA | Savitzky-Golay MA | |
|
||||
| SINEMA | Sine-weighted MA | |
|
||||
| [SMA](trends/sma/Sma.md) | Simple Moving Average | The unweighted mean of the previous n data. |
|
||||
| SSF | Ehlers Super Smooth Filter | |
|
||||
| [T3](trends/t3/T3.md) | Tillson T3 Moving Average | A smooth moving average that uses a smoothing factor to reduce lag. |
|
||||
| [TEMA](trends/tema/Tema.md) | Triple Exponential Moving Average | Designed to smooth price fluctuations and filter out volatility. |
|
||||
| [TRIMA](trends/trima/Trima.md) | Triangular Moving Average | A double-smoothed SMA that gives more weight to the middle of the data window. |
|
||||
| USF | Ehlers Ultrasmooth Filter | |
|
||||
| VAMA | Volatility Adjusted MA | |
|
||||
| VIDYA | Variable Index Dynamic Average | |
|
||||
| WIENER | Wiener Filter | |
|
||||
| [WMA](trends/wma/Wma.md) | Weighted Moving Average | Assigns a heavier weighting to more current data points since they are more relevant. |
|
||||
| YZVAMA | Yang-Zhang Volatility Adjusted MA | |
|
||||
| ZLDEMA | Zero-Lag Double Exponential MA | |
|
||||
| ZLEMA | Zero-Lag Exponential MA | |
|
||||
| ZLTEMA | Zero-Lag Triple Exponential MA | |
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class AlmaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 9;
|
||||
|
||||
[InputParameter("Offset", sortIndex: 2, 0.0, 1.0, 0.01, 2)]
|
||||
public double Offset { get; set; } = 0.85;
|
||||
|
||||
[InputParameter("Sigma", sortIndex: 3, 0.1, 100.0, 0.1, 1)]
|
||||
public double Sigma { get; set; } = 6.0;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Alma? ma;
|
||||
protected LineSeries? Series;
|
||||
protected string? SourceName;
|
||||
private int _warmupBarIndex = -1;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"ALMA {Period}:{SourceName}";
|
||||
|
||||
public AlmaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
SourceName = Source.ToString();
|
||||
Name = "ALMA - Arnaud Legoux Moving Average";
|
||||
Description = "Arnaud Legoux Moving Average";
|
||||
Series = new(name: $"ALMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
ma = new Alma(Period, Offset, Sigma);
|
||||
SourceName = Source.ToString();
|
||||
_warmupBarIndex = -1;
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TValue input = this.GetInputValue(args, Source);
|
||||
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
|
||||
TValue result = ma!.Update(input, isNew);
|
||||
Series!.SetValue(result.Value);
|
||||
Series!.SetMarker(0, Color.Transparent);
|
||||
|
||||
if (_warmupBarIndex < 0 && ma!.IsHot)
|
||||
_warmupBarIndex = Count;
|
||||
}
|
||||
|
||||
public override void OnPaintChart(PaintChartEventArgs args)
|
||||
{
|
||||
base.OnPaintChart(args);
|
||||
int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count;
|
||||
this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AlmaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Alma_Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Alma(0));
|
||||
Assert.Throws<ArgumentException>(() => new Alma(10, sigma: 0));
|
||||
|
||||
var alma = new Alma(10);
|
||||
Assert.NotNull(alma);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Alma_Calc_ReturnsValue()
|
||||
{
|
||||
var alma = new Alma(10);
|
||||
TValue result = alma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(result.Value > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Alma_IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
var alma = new Alma(5);
|
||||
|
||||
Assert.False(alma.IsHot);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
alma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.False(alma.IsHot);
|
||||
}
|
||||
|
||||
alma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(alma.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Alma_StreamingMatchesBatch()
|
||||
{
|
||||
var almaStreaming = new Alma(10);
|
||||
var almaBatch = new Alma(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
var series = new TSeries();
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var streamingResults = new TSeries();
|
||||
foreach (var item in series)
|
||||
{
|
||||
streamingResults.Add(almaStreaming.Update(item));
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResults = almaBatch.Update(series);
|
||||
|
||||
Assert.Equal(streamingResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < streamingResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i].Value, batchResults[i].Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Alma_StaticCalculate_MatchesInstance()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
var instanceResults = new Alma(10).Update(series);
|
||||
var staticResults = Alma.Calculate(series, 10);
|
||||
|
||||
for (int i = 0; i < instanceResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(instanceResults[i].Value, staticResults[i].Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Alma_SpanCalculate_MatchesSeries()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
var seriesResults = Alma.Calculate(series, 10);
|
||||
|
||||
double[] input = series.Values.ToArray();
|
||||
double[] output = new double[input.Length];
|
||||
|
||||
Alma.Calculate(input.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
Assert.Equal(seriesResults[i].Value, output[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Alma_Update_IsNewFalse_CorrectsValue()
|
||||
{
|
||||
var alma = new Alma(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
|
||||
// Feed initial data
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
alma.Update(new TValue(bar.Time, bar.Close), isNew: true);
|
||||
}
|
||||
|
||||
// Update with isNew=false (correction)
|
||||
var newBar = gbm.Next(isNew: true);
|
||||
alma.Update(new TValue(newBar.Time, newBar.Close), isNew: true);
|
||||
|
||||
double valueAfterCommit = alma.Last.Value;
|
||||
|
||||
// Now update the SAME bar with a different value
|
||||
alma.Update(new TValue(newBar.Time, newBar.Close + 10.0), isNew: false);
|
||||
|
||||
double valueAfterCorrection = alma.Last.Value;
|
||||
|
||||
Assert.NotEqual(valueAfterCommit, valueAfterCorrection);
|
||||
|
||||
// Now restore original value
|
||||
alma.Update(new TValue(newBar.Time, newBar.Close), isNew: false);
|
||||
|
||||
Assert.Equal(valueAfterCommit, alma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Alma_NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var alma = new Alma(5);
|
||||
|
||||
alma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
alma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
var resultAfterNaN = alma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
Assert.NotEqual(0, resultAfterNaN.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Alma_Reset_ClearsState()
|
||||
{
|
||||
var alma = new Alma(10);
|
||||
alma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
alma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
Assert.True(alma.Last.Value > 0);
|
||||
|
||||
alma.Reset();
|
||||
|
||||
Assert.Equal(0, alma.Last.Value);
|
||||
Assert.False(alma.IsHot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Skender.Stock.Indicators;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AlmaValidationTests
|
||||
{
|
||||
// Note: ALMA is not available in TA-Lib or Tulip, so validation is limited to Skender.Stock.Indicators.
|
||||
|
||||
private readonly TBarSeries _bars;
|
||||
private readonly TSeries _data;
|
||||
private readonly List<Quote> _skenderQuotes;
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public AlmaValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
|
||||
// 1. Generate 1000 records using GBM feed
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
_bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// 2. Extract Close TSeries
|
||||
_data = _bars.Close;
|
||||
|
||||
// 3. Prepare data for Skender (List<Quote>)
|
||||
_skenderQuotes = new List<Quote>();
|
||||
for (int i = 0; i < _bars.Count; i++)
|
||||
{
|
||||
_skenderQuotes.Add(new Quote
|
||||
{
|
||||
Date = new DateTime(_bars.Open.Times[i], DateTimeKind.Utc),
|
||||
Open = (decimal)_bars.Open[i].Value,
|
||||
High = (decimal)_bars.High[i].Value,
|
||||
Low = (decimal)_bars.Low[i].Value,
|
||||
Close = (decimal)_bars.Close[i].Value,
|
||||
Volume = (decimal)_bars.Volume[i].Value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Batch()
|
||||
{
|
||||
int[] periods = { 9, 14, 20, 50 };
|
||||
double offset = 0.85;
|
||||
double sigma = 6.0;
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib ALMA (batch TSeries)
|
||||
var alma = new global::QuanTAlib.Alma(period, offset, sigma);
|
||||
var qResult = alma.Update(_data);
|
||||
|
||||
// Calculate Skender ALMA
|
||||
var sResult = _skenderQuotes.GetAlma(period, offset, sigma).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData_Skender(qResult, sResult);
|
||||
}
|
||||
_output.WriteLine("ALMA Batch(TSeries) validated successfully against Skender");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Streaming()
|
||||
{
|
||||
int[] periods = { 9, 14, 20, 50 };
|
||||
double offset = 0.85;
|
||||
double sigma = 6.0;
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib ALMA (streaming)
|
||||
var alma = new global::QuanTAlib.Alma(period, offset, sigma);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _data)
|
||||
{
|
||||
qResults.Add(alma.Update(item).Value);
|
||||
}
|
||||
|
||||
// Calculate Skender ALMA
|
||||
var sResult = _skenderQuotes.GetAlma(period, offset, sigma).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData_Skender_Streaming(qResults, sResult);
|
||||
}
|
||||
_output.WriteLine("ALMA Streaming validated successfully against Skender");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Span()
|
||||
{
|
||||
int[] periods = { 9, 14, 20, 50 };
|
||||
double offset = 0.85;
|
||||
double sigma = 6.0;
|
||||
|
||||
// Prepare data for Span API
|
||||
double[] sourceData = _data.Select(x => x.Value).ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib ALMA (Span API)
|
||||
double[] qOutput = new double[sourceData.Length];
|
||||
global::QuanTAlib.Alma.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period, offset, sigma);
|
||||
|
||||
// Calculate Skender ALMA
|
||||
var sResult = _skenderQuotes.GetAlma(period, offset, sigma).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData_Skender_Span(qOutput, sResult);
|
||||
}
|
||||
_output.WriteLine("ALMA Span validated successfully against Skender");
|
||||
}
|
||||
|
||||
private static void VerifyData_Skender(TSeries qSeries, List<AlmaResult> sSeries)
|
||||
{
|
||||
Assert.Equal(qSeries.Count, sSeries.Count);
|
||||
|
||||
int count = qSeries.Count;
|
||||
int skip = count - 100;
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qSeries[i].Value;
|
||||
double? sValue = sSeries[i].Alma;
|
||||
|
||||
if (!sValue.HasValue) continue;
|
||||
|
||||
Assert.Equal(sValue.Value, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
private static void VerifyData_Skender_Streaming(List<double> qResults, List<AlmaResult> sSeries)
|
||||
{
|
||||
Assert.Equal(qResults.Count, sSeries.Count);
|
||||
|
||||
int count = qResults.Count;
|
||||
int skip = count - 100;
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qResults[i];
|
||||
double? sValue = sSeries[i].Alma;
|
||||
|
||||
if (!sValue.HasValue) continue;
|
||||
|
||||
Assert.Equal(sValue.Value, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
private static void VerifyData_Skender_Span(double[] qOutput, List<AlmaResult> sSeries)
|
||||
{
|
||||
Assert.Equal(qOutput.Length, sSeries.Count);
|
||||
|
||||
int count = qOutput.Length;
|
||||
int skip = count - 100;
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qOutput[i];
|
||||
double? sValue = sSeries[i].Alma;
|
||||
|
||||
if (!sValue.HasValue) continue;
|
||||
|
||||
Assert.Equal(sValue.Value, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// ALMA: Arnaud Legoux Moving Average
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ALMA uses a Gaussian distribution to determine weights for the moving average.
|
||||
/// It allows for adjusting smoothness and responsiveness via Offset and Sigma parameters.
|
||||
///
|
||||
/// Formula:
|
||||
/// Weights are calculated using the Gaussian function:
|
||||
/// W_i = exp( - (i - offset)^2 / (2 * sigma^2) )
|
||||
/// where:
|
||||
/// offset = floor(period * offset_param)
|
||||
/// sigma = period / sigma_param
|
||||
///
|
||||
/// The final ALMA is the weighted sum of the price window divided by the sum of weights.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Alma : ITValuePublisher
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly double[] _weights;
|
||||
private readonly double _weightSum;
|
||||
private readonly RingBuffer _buffer;
|
||||
private double _lastValidValue;
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
public event Action<TValue>? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Current ALMA value.
|
||||
/// </summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the ALMA has enough data to produce valid results (buffer is full).
|
||||
/// </summary>
|
||||
public bool IsHot => _buffer.IsFull;
|
||||
|
||||
/// <summary>
|
||||
/// Creates ALMA with specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="period">Window size (must be > 0)</param>
|
||||
/// <param name="offset">Gaussian offset (0-1, default 0.85). Closer to 1 makes it more responsive.</param>
|
||||
/// <param name="sigma">Standard deviation (default 6). Higher values make it sharper.</param>
|
||||
public Alma(int period, double offset = 0.85, double sigma = 6.0)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
if (sigma <= 0)
|
||||
throw new ArgumentException("Sigma must be greater than 0", nameof(sigma));
|
||||
|
||||
_period = period;
|
||||
_buffer = new RingBuffer(period);
|
||||
_weights = new double[period];
|
||||
Name = $"Alma({period}, {offset:F2}, {sigma:F2})";
|
||||
|
||||
// Precompute weights
|
||||
double m = offset * (period - 1);
|
||||
double s = period / sigma;
|
||||
double s2 = 2 * s * s;
|
||||
double sum = 0;
|
||||
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
double v = i - m;
|
||||
_weights[i] = Math.Exp(-(v * v) / s2);
|
||||
sum += _weights[i];
|
||||
}
|
||||
|
||||
_weightSum = sum;
|
||||
}
|
||||
|
||||
public Alma(ITValuePublisher source, int period, double offset = 0.85, double sigma = 6.0)
|
||||
: this(period, offset, sigma)
|
||||
{
|
||||
source.Pub += (item) => Update(item);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetValidValue(double input)
|
||||
{
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
_lastValidValue = input;
|
||||
return input;
|
||||
}
|
||||
return _lastValidValue;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
double val = GetValidValue(input.Value);
|
||||
_buffer.Add(val, isNew);
|
||||
|
||||
double result = 0;
|
||||
if (_buffer.Count > 0)
|
||||
{
|
||||
result = CalculateWeightedSum();
|
||||
}
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
Pub?.Invoke(Last);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Calculate(source.Values, vSpan, _period);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Restore state
|
||||
_buffer.Clear();
|
||||
_lastValidValue = 0;
|
||||
|
||||
// Replay last part to restore buffer state
|
||||
int startIndex = Math.Max(0, len - _period);
|
||||
for (int i = startIndex; i < len; i++)
|
||||
{
|
||||
Update(source[i]);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double CalculateWeightedSum()
|
||||
{
|
||||
// If buffer is not full, we only use the most recent 'count' weights?
|
||||
// Standard ALMA usually waits for full period, or re-normalizes weights.
|
||||
// Here we'll re-normalize based on how many items we have.
|
||||
// But to match standard behavior, we usually just run on what we have.
|
||||
// However, the weights are designed for a specific period.
|
||||
// Using a partial window with full-period weights might be weird.
|
||||
// Let's stick to the standard: use the weights corresponding to the filled positions.
|
||||
// Since RingBuffer adds new items at 'head', and we want to apply weights
|
||||
// such that weights[period-1] applies to the newest item, etc.
|
||||
|
||||
// RingBuffer: [Oldest ... Newest]
|
||||
// Weights: [0 ... period-1]
|
||||
// We want: Sum(Buffer[i] * Weights[i]) / Sum(Weights)
|
||||
|
||||
// BUT: If buffer is not full, say count=5, period=10.
|
||||
// We have 5 items. Should we use weights[0..4] or weights[5..9]?
|
||||
// Usually, moving averages grow.
|
||||
// Let's assume we use the last 'count' weights, normalized.
|
||||
|
||||
ReadOnlySpan<double> bufferSpan = _buffer.GetSpan();
|
||||
int count = bufferSpan.Length;
|
||||
|
||||
// If not full, we need to handle it carefully.
|
||||
// For simplicity and performance, let's just iterate.
|
||||
// Optimization: If full, use SIMD.
|
||||
|
||||
if (count < _period)
|
||||
{
|
||||
double sum = 0;
|
||||
double wSum = 0;
|
||||
// Map weights to buffer:
|
||||
// Buffer[0] (oldest) -> Weights[period - count] ??
|
||||
// Actually, standard is: Weights are fixed.
|
||||
// Let's align newest with newest.
|
||||
// Buffer[count-1] (newest) <-> Weights[period-1]
|
||||
// Buffer[0] (oldest) <-> Weights[period-count]
|
||||
|
||||
int weightOffset = _period - count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
double w = _weights[weightOffset + i];
|
||||
sum += bufferSpan[i] * w;
|
||||
wSum += w;
|
||||
}
|
||||
return wSum > 0 ? sum / wSum : 0;
|
||||
}
|
||||
|
||||
// Full buffer
|
||||
return CalculateWeightedSumSimd(bufferSpan);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double CalculateWeightedSumSimd(ReadOnlySpan<double> buffer)
|
||||
{
|
||||
double sum = 0;
|
||||
int i = 0;
|
||||
int len = _period;
|
||||
|
||||
if (Avx2.IsSupported && len >= Vector256<double>.Count)
|
||||
{
|
||||
var vSum = Vector256<double>.Zero;
|
||||
ref double bufRef = ref MemoryMarshal.GetReference(buffer);
|
||||
ref double wRef = ref MemoryMarshal.GetReference(_weights.AsSpan());
|
||||
|
||||
for (; i <= len - Vector256<double>.Count; i += Vector256<double>.Count)
|
||||
{
|
||||
var vBuf = Vector256.LoadUnsafe(ref Unsafe.Add(ref bufRef, i));
|
||||
var vW = Vector256.LoadUnsafe(ref Unsafe.Add(ref wRef, i));
|
||||
vSum = Avx.Add(vSum, Avx.Multiply(vBuf, vW));
|
||||
}
|
||||
|
||||
// Horizontal sum
|
||||
vSum = Avx.Add(vSum, Avx2.Permute4x64(vSum.AsUInt64(), 0b_01_00_11_10).AsDouble());
|
||||
vSum = Avx.Add(vSum, Avx2.Permute4x64(vSum.AsUInt64(), 0b_00_00_00_01).AsDouble());
|
||||
sum = vSum.GetElement(0);
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
for (; i < len; i++)
|
||||
{
|
||||
sum += buffer[i] * _weights[i];
|
||||
}
|
||||
|
||||
return sum / _weightSum;
|
||||
}
|
||||
|
||||
public static TSeries Calculate(TSeries source, int period, double offset = 0.85, double sigma = 6.0)
|
||||
{
|
||||
var alma = new Alma(period, offset, sigma);
|
||||
return alma.Update(source);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, double offset = 0.85, double sigma = 6.0)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
if (source.Length != output.Length)
|
||||
throw new ArgumentException("Source and output must have the same length");
|
||||
|
||||
// Precompute weights
|
||||
double[] weights = new double[period];
|
||||
double m = offset * (period - 1);
|
||||
double s = period / sigma;
|
||||
double s2 = 2 * s * s;
|
||||
double weightSum = 0;
|
||||
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
double v = i - m;
|
||||
weights[i] = Math.Exp(-(v * v) / s2);
|
||||
weightSum += weights[i];
|
||||
}
|
||||
|
||||
// Buffer for sliding window
|
||||
// Use stackalloc for small periods
|
||||
Span<double> buffer = period <= 256 ? stackalloc double[period] : new double[period];
|
||||
int bufferIdx = 0;
|
||||
int count = 0;
|
||||
double lastValid = 0;
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
lastValid = val;
|
||||
else
|
||||
val = lastValid;
|
||||
|
||||
// Add to circular buffer
|
||||
buffer[bufferIdx] = val;
|
||||
bufferIdx = (bufferIdx + 1) % period;
|
||||
if (count < period) count++;
|
||||
|
||||
// Calculate weighted sum
|
||||
// We need to iterate buffer from oldest to newest to match weights[0..period-1]
|
||||
// Oldest is at: (bufferIdx - count + period) % period
|
||||
// But wait, the buffer wraps.
|
||||
// Let's just iterate 0..count-1 and map to buffer index.
|
||||
|
||||
double sum = 0;
|
||||
double currentWeightSum = 0;
|
||||
|
||||
int startIdx = (bufferIdx - count + period) % period;
|
||||
int weightOffset = period - count; // Align weights to end
|
||||
|
||||
// Optimization: If full, we can use SIMD if we unwrap the buffer or handle wrapping.
|
||||
// For simplicity in static method (and since we can't easily unwrap stackalloc),
|
||||
// we'll use scalar loop with modulo.
|
||||
// Or better: copy to a temporary linear buffer? No, that's too much copying.
|
||||
|
||||
// Actually, for full period, we can do two loops (part1, part2) to avoid modulo in loop.
|
||||
|
||||
if (count == period)
|
||||
{
|
||||
// Buffer is full. startIdx is bufferIdx (which is the oldest, since we just wrote to bufferIdx-1)
|
||||
// Wait, bufferIdx points to the NEXT write position.
|
||||
// So bufferIdx is the Oldest.
|
||||
|
||||
// Part 1: bufferIdx to End
|
||||
int part1Len = period - bufferIdx;
|
||||
for (int j = 0; j < part1Len; j++)
|
||||
{
|
||||
sum += buffer[bufferIdx + j] * weights[j];
|
||||
}
|
||||
|
||||
// Part 2: 0 to bufferIdx
|
||||
for (int j = 0; j < bufferIdx; j++)
|
||||
{
|
||||
sum += buffer[j] * weights[part1Len + j];
|
||||
}
|
||||
|
||||
output[i] = sum / weightSum;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Partial buffer
|
||||
for (int j = 0; j < count; j++)
|
||||
{
|
||||
int idx = (startIdx + j) % period;
|
||||
double w = weights[weightOffset + j];
|
||||
sum += buffer[idx] * w;
|
||||
currentWeightSum += w;
|
||||
}
|
||||
output[i] = currentWeightSum > 0 ? sum / currentWeightSum : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_lastValidValue = 0;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
# ALMA: Arnaud Legoux Moving Average
|
||||
|
||||
## Overview and Purpose
|
||||
|
||||
The Arnaud Legoux Moving Average (ALMA) is a technical indicator that attempts to bridge the gap between responsiveness and smoothness. It uses a Gaussian distribution to determine the weights of the moving average, allowing the user to shift the peak of the weight distribution (offset) and control the width of the distribution (sigma).
|
||||
|
||||
ALMA is designed to reduce lag while maintaining smoothness, making it superior to traditional moving averages like SMA or EMA in many trend-following applications.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
* **Gaussian Weighting:** Weights are distributed according to a bell curve.
|
||||
* **Offset Control:** Allows shifting the focus of the average. An offset of 0.5 is a symmetric filter (like SMA/WMA), while an offset closer to 1.0 makes it more responsive to recent prices.
|
||||
* **Sigma Control:** Controls the "sharpness" of the filter. Higher sigma values include more data points in the calculation, making it smoother but potentially introducing more lag.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| Period | 9 | The window size for the moving average. |
|
||||
| Offset | 0.85 | The center of the Gaussian distribution (0.0 to 1.0). |
|
||||
| Sigma | 6.0 | The standard deviation of the Gaussian distribution. |
|
||||
|
||||
## Formula
|
||||
|
||||
The weight for the $i$-th element in the window (where $i=0$ is the oldest) is calculated as:
|
||||
|
||||
$$W_i = \exp\left(-\frac{(i - \text{offset\_idx})^2}{2\sigma_{idx}^2}\right)$$
|
||||
|
||||
Where:
|
||||
|
||||
* $\text{offset\_idx} = \lfloor \text{Period} \times \text{Offset} \rfloor$
|
||||
* $\sigma_{idx} = \text{Period} / \text{Sigma}$
|
||||
|
||||
The ALMA value is the weighted sum:
|
||||
|
||||
$$ALMA = \frac{\sum_{i=0}^{n-1} P_i \times W_i}{\sum_{i=0}^{n-1} W_i}$$
|
||||
|
||||
## C# Implementation
|
||||
|
||||
### Standard Usage
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Initialize with period 9, offset 0.85, sigma 6
|
||||
var alma = new Alma(9, offset: 0.85, sigma: 6.0);
|
||||
|
||||
// Update with new value
|
||||
TValue result = alma.Update(new TValue(time, price));
|
||||
Console.WriteLine($"ALMA: {result.Value}");
|
||||
```
|
||||
|
||||
### Zero-Allocation Span API
|
||||
|
||||
```csharp
|
||||
double[] prices = ...;
|
||||
double[] output = new double[prices.Length];
|
||||
|
||||
// Calculate ALMA for the entire array
|
||||
Alma.Calculate(prices.AsSpan(), output.AsSpan(), period: 9, offset: 0.85, sigma: 6.0);
|
||||
```
|
||||
|
||||
### Bar Correction
|
||||
|
||||
```csharp
|
||||
var alma = new Alma(9);
|
||||
|
||||
// Update with initial tick
|
||||
alma.Update(new TValue(time, 100), isNew: true);
|
||||
|
||||
// Update with correction (same bar)
|
||||
alma.Update(new TValue(time, 101), isNew: false);
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
* **Trend Following:** Like other moving averages, ALMA helps identify the trend direction.
|
||||
* **Crossovers:** Price crossing ALMA or two ALMAs crossing each other can signal trend changes.
|
||||
* **Support/Resistance:** ALMA often acts as dynamic support/resistance.
|
||||
|
||||
## References
|
||||
|
||||
* Arnaud Legoux and Dimitris Kouzis-Loukas (2009).
|
||||
@@ -23,7 +23,7 @@ public class DemaIndicator : Indicator, IWatchlistIndicator
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"DEMA {Period}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/averages/dema/Dema.Quantower.cs";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/dema/Dema.Quantower.cs";
|
||||
|
||||
public DemaIndicator()
|
||||
{
|
||||
@@ -23,7 +23,7 @@ public class HmaIndicator : Indicator, IWatchlistIndicator
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"HMA {Period}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/averages/hma/Hma.cs";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/hma/Hma.cs";
|
||||
|
||||
public HmaIndicator()
|
||||
{
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class KamaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 10;
|
||||
|
||||
[InputParameter("Fast Period", sortIndex: 2, 1, 1000, 1, 0)]
|
||||
public int FastPeriod { get; set; } = 2;
|
||||
|
||||
[InputParameter("Slow Period", sortIndex: 3, 1, 1000, 1, 0)]
|
||||
public int SlowPeriod { get; set; } = 30;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Kama? ma;
|
||||
protected LineSeries? Series;
|
||||
protected string? SourceName;
|
||||
private int _warmupBarIndex = -1;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"KAMA {Period}:{SourceName}";
|
||||
|
||||
public KamaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
SourceName = Source.ToString();
|
||||
Name = "KAMA - Kaufman Adaptive Moving Average";
|
||||
Description = "Kaufman Adaptive Moving Average";
|
||||
Series = new(name: $"KAMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
ma = new Kama(Period, FastPeriod, SlowPeriod);
|
||||
SourceName = Source.ToString();
|
||||
_warmupBarIndex = -1;
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TValue input = this.GetInputValue(args, Source);
|
||||
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
|
||||
TValue result = ma!.Update(input, isNew);
|
||||
Series!.SetValue(result.Value);
|
||||
Series!.SetMarker(0, Color.Transparent);
|
||||
|
||||
if (_warmupBarIndex < 0 && ma!.IsHot)
|
||||
_warmupBarIndex = Count;
|
||||
}
|
||||
|
||||
public override void OnPaintChart(PaintChartEventArgs args)
|
||||
{
|
||||
base.OnPaintChart(args);
|
||||
int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count;
|
||||
this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class KamaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Kama_Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Kama(0));
|
||||
Assert.Throws<ArgumentException>(() => new Kama(10, fastPeriod: 0));
|
||||
Assert.Throws<ArgumentException>(() => new Kama(10, slowPeriod: 0));
|
||||
Assert.Throws<ArgumentException>(() => new Kama(10, fastPeriod: 10, slowPeriod: 5));
|
||||
|
||||
var kama = new Kama(10);
|
||||
Assert.NotNull(kama);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kama_Calc_ReturnsValue()
|
||||
{
|
||||
var kama = new Kama(10);
|
||||
TValue result = kama.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(result.Value > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kama_IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
// Buffer size is period + 1
|
||||
var kama = new Kama(5);
|
||||
|
||||
Assert.False(kama.IsHot);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
kama.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.False(kama.IsHot);
|
||||
}
|
||||
|
||||
kama.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(kama.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kama_StreamingMatchesBatch()
|
||||
{
|
||||
var kamaStreaming = new Kama(10);
|
||||
var kamaBatch = new Kama(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
var series = new TSeries();
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var streamingResults = new TSeries();
|
||||
foreach (var item in series)
|
||||
{
|
||||
streamingResults.Add(kamaStreaming.Update(item));
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResults = kamaBatch.Update(series);
|
||||
|
||||
Assert.Equal(streamingResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < streamingResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i].Value, batchResults[i].Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kama_StaticCalculate_MatchesInstance()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
var instanceResults = new Kama(10).Update(series);
|
||||
var staticResults = new double[series.Count];
|
||||
Kama.Calculate(series.Values.ToArray().AsSpan(), staticResults.AsSpan(), 10);
|
||||
|
||||
for (int i = 0; i < instanceResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(instanceResults[i].Value, staticResults[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kama_Update_IsNewFalse_CorrectsValue()
|
||||
{
|
||||
var kama = new Kama(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
|
||||
// Feed initial data
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
kama.Update(new TValue(bar.Time, bar.Close), isNew: true);
|
||||
}
|
||||
|
||||
// Update with isNew=false (correction)
|
||||
var newBar = gbm.Next(isNew: true);
|
||||
kama.Update(new TValue(newBar.Time, newBar.Close), isNew: true);
|
||||
|
||||
double valueAfterCommit = kama.Last.Value;
|
||||
|
||||
// Now update the SAME bar with a different value
|
||||
kama.Update(new TValue(newBar.Time, newBar.Close + 10.0), isNew: false);
|
||||
|
||||
double valueAfterCorrection = kama.Last.Value;
|
||||
|
||||
Assert.NotEqual(valueAfterCommit, valueAfterCorrection);
|
||||
|
||||
// Now restore original value
|
||||
kama.Update(new TValue(newBar.Time, newBar.Close), isNew: false);
|
||||
|
||||
Assert.Equal(valueAfterCommit, kama.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kama_NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var kama = new Kama(5);
|
||||
|
||||
kama.Update(new TValue(DateTime.UtcNow, 100));
|
||||
kama.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
var resultAfterNaN = kama.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
Assert.NotEqual(0, resultAfterNaN.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kama_Reset_ClearsState()
|
||||
{
|
||||
var kama = new Kama(10);
|
||||
kama.Update(new TValue(DateTime.UtcNow, 100));
|
||||
kama.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
Assert.True(kama.Last.Value > 0);
|
||||
|
||||
kama.Reset();
|
||||
|
||||
Assert.Equal(0, kama.Last.Value);
|
||||
Assert.False(kama.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kama_FlatLine_ReturnsSameValue()
|
||||
{
|
||||
var kama = new Kama(10);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
kama.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
|
||||
Assert.Equal(100, kama.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Skender.Stock.Indicators;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class KamaValidationTests
|
||||
{
|
||||
private readonly TBarSeries _bars;
|
||||
private readonly TSeries _data;
|
||||
private readonly List<Quote> _skenderQuotes;
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public KamaValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
|
||||
// 1. Generate 1000 records using GBM feed
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
_bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// 2. Extract Close TSeries
|
||||
_data = _bars.Close;
|
||||
|
||||
// 3. Prepare data for Skender (List<Quote>)
|
||||
_skenderQuotes = new List<Quote>();
|
||||
for (int i = 0; i < _bars.Count; i++)
|
||||
{
|
||||
_skenderQuotes.Add(new Quote
|
||||
{
|
||||
Date = new DateTime(_bars.Open.Times[i], DateTimeKind.Utc),
|
||||
Open = (decimal)_bars.Open[i].Value,
|
||||
High = (decimal)_bars.High[i].Value,
|
||||
Low = (decimal)_bars.Low[i].Value,
|
||||
Close = (decimal)_bars.Close[i].Value,
|
||||
Volume = (decimal)_bars.Volume[i].Value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Batch()
|
||||
{
|
||||
int[] periods = { 10, 14, 20 };
|
||||
int fastPeriod = 2;
|
||||
int slowPeriod = 30;
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib KAMA (batch TSeries)
|
||||
var kama = new global::QuanTAlib.Kama(period, fastPeriod, slowPeriod);
|
||||
var qResult = kama.Update(_data);
|
||||
|
||||
// Calculate Skender KAMA
|
||||
var sResult = _skenderQuotes.GetKama(period, fastPeriod, slowPeriod).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData_Skender(qResult, sResult);
|
||||
}
|
||||
_output.WriteLine("KAMA Batch(TSeries) validated successfully against Skender");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Streaming()
|
||||
{
|
||||
int[] periods = { 10, 14, 20 };
|
||||
int fastPeriod = 2;
|
||||
int slowPeriod = 30;
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib KAMA (streaming)
|
||||
var kama = new global::QuanTAlib.Kama(period, fastPeriod, slowPeriod);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _data)
|
||||
{
|
||||
qResults.Add(kama.Update(item).Value);
|
||||
}
|
||||
|
||||
// Calculate Skender KAMA
|
||||
var sResult = _skenderQuotes.GetKama(period, fastPeriod, slowPeriod).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData_Skender_Streaming(qResults, sResult);
|
||||
}
|
||||
_output.WriteLine("KAMA Streaming validated successfully against Skender");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Span()
|
||||
{
|
||||
int[] periods = { 10, 14, 20 };
|
||||
int fastPeriod = 2;
|
||||
int slowPeriod = 30;
|
||||
|
||||
// Prepare data for Span API
|
||||
double[] sourceData = _data.Select(x => x.Value).ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib KAMA (Span API)
|
||||
double[] qOutput = new double[sourceData.Length];
|
||||
global::QuanTAlib.Kama.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period, fastPeriod, slowPeriod);
|
||||
|
||||
// Calculate Skender KAMA
|
||||
var sResult = _skenderQuotes.GetKama(period, fastPeriod, slowPeriod).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData_Skender_Span(qOutput, sResult);
|
||||
}
|
||||
_output.WriteLine("KAMA Span validated successfully against Skender");
|
||||
}
|
||||
|
||||
private static void VerifyData_Skender(TSeries qSeries, List<KamaResult> sSeries)
|
||||
{
|
||||
Assert.Equal(qSeries.Count, sSeries.Count);
|
||||
|
||||
int count = qSeries.Count;
|
||||
int skip = count - 100;
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qSeries[i].Value;
|
||||
double? sValue = (double?)sSeries[i].Kama;
|
||||
|
||||
if (!sValue.HasValue) continue;
|
||||
|
||||
Assert.Equal(sValue.Value, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
private static void VerifyData_Skender_Streaming(List<double> qResults, List<KamaResult> sSeries)
|
||||
{
|
||||
Assert.Equal(qResults.Count, sSeries.Count);
|
||||
|
||||
int count = qResults.Count;
|
||||
int skip = count - 100;
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qResults[i];
|
||||
double? sValue = (double?)sSeries[i].Kama;
|
||||
|
||||
if (!sValue.HasValue) continue;
|
||||
|
||||
Assert.Equal(sValue.Value, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
private static void VerifyData_Skender_Span(double[] qOutput, List<KamaResult> sSeries)
|
||||
{
|
||||
Assert.Equal(qOutput.Length, sSeries.Count);
|
||||
|
||||
int count = qOutput.Length;
|
||||
int skip = count - 100;
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qOutput[i];
|
||||
double? sValue = (double?)sSeries[i].Kama;
|
||||
|
||||
if (!sValue.HasValue) continue;
|
||||
|
||||
Assert.Equal(sValue.Value, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// KAMA: Kaufman's Adaptive Moving Average
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// KAMA adapts to market volatility by adjusting its smoothing factor based on an Efficiency Ratio (ER).
|
||||
/// ER is calculated as the ratio of the absolute price change over a period to the sum of absolute price changes (volatility).
|
||||
///
|
||||
/// Formula:
|
||||
/// ER = Change / Volatility
|
||||
/// Change = Abs(Price - Price[period])
|
||||
/// Volatility = Sum(Abs(Price[i] - Price[i-1]), period)
|
||||
/// SC = (ER * (fast_alpha - slow_alpha) + slow_alpha)^2
|
||||
/// KAMA = KAMA[prev] + SC * (Price - KAMA[prev])
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Kama : ITValuePublisher
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly double _fastAlpha;
|
||||
private readonly double _slowAlpha;
|
||||
private readonly RingBuffer _buffer;
|
||||
private double _kama;
|
||||
private double _p_kama;
|
||||
private double _volatilitySum;
|
||||
private double _p_volatilitySum;
|
||||
private double _lastDiffOut;
|
||||
private double _lastValidValue;
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
public event Action<TValue>? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Current KAMA value.
|
||||
/// </summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the KAMA has enough data to produce valid results.
|
||||
/// </summary>
|
||||
public bool IsHot => _buffer.IsFull;
|
||||
|
||||
/// <summary>
|
||||
/// Creates KAMA with specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="period">Lookback period for Efficiency Ratio (default 10).</param>
|
||||
/// <param name="fastPeriod">Fast EMA period for SC calculation (default 2).</param>
|
||||
/// <param name="slowPeriod">Slow EMA period for SC calculation (default 30).</param>
|
||||
public Kama(int period = 10, int fastPeriod = 2, int slowPeriod = 30)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
if (fastPeriod <= 0)
|
||||
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
|
||||
if (slowPeriod <= 0)
|
||||
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
|
||||
if (fastPeriod >= slowPeriod)
|
||||
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
|
||||
|
||||
_period = period;
|
||||
// Buffer needs to hold period + 1 values to calculate Change over 'period' bars
|
||||
// Change = Price[0] - Price[period]
|
||||
_buffer = new RingBuffer(period + 1);
|
||||
|
||||
_fastAlpha = 2.0 / (fastPeriod + 1);
|
||||
_slowAlpha = 2.0 / (slowPeriod + 1);
|
||||
|
||||
Name = $"Kama({period}, {fastPeriod}, {slowPeriod})";
|
||||
_kama = double.NaN;
|
||||
}
|
||||
|
||||
public Kama(ITValuePublisher source, int period = 10, int fastPeriod = 2, int slowPeriod = 30)
|
||||
: this(period, fastPeriod, slowPeriod)
|
||||
{
|
||||
source.Pub += (item) => Update(item);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetValidValue(double input)
|
||||
{
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
_lastValidValue = input;
|
||||
return input;
|
||||
}
|
||||
return _lastValidValue;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
double val = GetValidValue(input.Value);
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_p_kama = _kama;
|
||||
_p_volatilitySum = _volatilitySum;
|
||||
|
||||
double removed = _buffer.Add(val);
|
||||
|
||||
if (_buffer.IsFull)
|
||||
{
|
||||
// removed is the value that fell off (Price[period+1] relative to new state?)
|
||||
// No, removed is the value that was at index 0 (oldest).
|
||||
// The new oldest is at index 0.
|
||||
// diff_out was abs(removed - new_oldest).
|
||||
double diff_out = Math.Abs(removed - _buffer[0]);
|
||||
_lastDiffOut = diff_out;
|
||||
|
||||
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]);
|
||||
_volatilitySum += diff_in - diff_out;
|
||||
}
|
||||
else if (_buffer.Count >= 2)
|
||||
{
|
||||
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]);
|
||||
_volatilitySum += diff_in;
|
||||
_lastDiffOut = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore state
|
||||
_kama = _p_kama;
|
||||
_buffer.UpdateNewest(val);
|
||||
|
||||
if (_buffer.IsFull)
|
||||
{
|
||||
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]);
|
||||
_volatilitySum = _p_volatilitySum + diff_in - _lastDiffOut;
|
||||
}
|
||||
else if (_buffer.Count >= 2)
|
||||
{
|
||||
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]);
|
||||
_volatilitySum = _p_volatilitySum + diff_in;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate KAMA
|
||||
if (double.IsNaN(_kama))
|
||||
{
|
||||
_kama = val;
|
||||
_p_kama = val; // Ensure p_kama is initialized
|
||||
}
|
||||
else
|
||||
{
|
||||
double change = Math.Abs(_buffer[^1] - _buffer[0]);
|
||||
double volatility = _volatilitySum;
|
||||
|
||||
// Avoid division by zero
|
||||
double er = (volatility > double.Epsilon) ? change / volatility : 0.0;
|
||||
// Cap ER at 1.0 just in case floating point errors push it slightly over
|
||||
if (er > 1.0) er = 1.0;
|
||||
|
||||
double sc = er * (_fastAlpha - _slowAlpha) + _slowAlpha;
|
||||
sc = sc * sc;
|
||||
|
||||
_kama = _p_kama + sc * (val - _p_kama);
|
||||
}
|
||||
|
||||
Last = new TValue(input.Time, _kama);
|
||||
Pub?.Invoke(Last);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
|
||||
// Use static Calculate for performance
|
||||
var outputSpan = new double[len];
|
||||
Calculate(source.Values, outputSpan, _period,
|
||||
(int)(2.0/_fastAlpha - 1), (int)(2.0/_slowAlpha - 1)); // Reverse calc periods from alphas?
|
||||
// Actually better to pass alphas or periods.
|
||||
// The static method signature should match constructor params.
|
||||
|
||||
// Wait, I need to pass periods to static method.
|
||||
// fastPeriod = 2/fastAlpha - 1.
|
||||
int fastPeriod = (int)Math.Round(2.0 / _fastAlpha - 1);
|
||||
int slowPeriod = (int)Math.Round(2.0 / _slowAlpha - 1);
|
||||
|
||||
Calculate(source.Values, outputSpan, _period, fastPeriod, slowPeriod);
|
||||
|
||||
for(int i=0; i<len; i++)
|
||||
{
|
||||
t.Add(source.Times[i]);
|
||||
v.Add(outputSpan[i]);
|
||||
}
|
||||
|
||||
// Restore state by replaying last few bars
|
||||
// This is expensive but necessary to sync the object state
|
||||
Reset();
|
||||
int startIndex = Math.Max(0, len - _period - 1);
|
||||
for (int i = startIndex; i < len; i++)
|
||||
{
|
||||
Update(source[i]);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, int fastPeriod = 2, int slowPeriod = 30)
|
||||
{
|
||||
if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
if (source.Length != output.Length) throw new ArgumentException("Source and output must have the same length");
|
||||
|
||||
double fastAlpha = 2.0 / (fastPeriod + 1);
|
||||
double slowAlpha = 2.0 / (slowPeriod + 1);
|
||||
|
||||
// We need a buffer for price history to calculate ER
|
||||
// Size period + 1
|
||||
int bufSize = period + 1;
|
||||
Span<double> buffer = bufSize <= 256 ? stackalloc double[bufSize] : new double[bufSize];
|
||||
int bufferIdx = 0;
|
||||
int count = 0;
|
||||
|
||||
double volatilitySum = 0;
|
||||
double kama = 0;
|
||||
bool kamaInitialized = false;
|
||||
double lastValid = 0;
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
lastValid = val;
|
||||
else
|
||||
val = lastValid;
|
||||
|
||||
// Add to buffer
|
||||
double removed = buffer[bufferIdx];
|
||||
buffer[bufferIdx] = val;
|
||||
|
||||
// Update volatility
|
||||
if (count >= 1)
|
||||
{
|
||||
// diff_in = abs(val - prev)
|
||||
// prev is at bufferIdx-1 (circular)
|
||||
int prevIdx = (bufferIdx - 1 + bufSize) % bufSize;
|
||||
double diff_in = Math.Abs(val - buffer[prevIdx]);
|
||||
|
||||
volatilitySum += diff_in;
|
||||
|
||||
if (count == bufSize)
|
||||
{
|
||||
// diff_out = abs(removed - new_oldest)
|
||||
// new_oldest is at (bufferIdx + 1) % bufSize
|
||||
int oldestIdx = (bufferIdx + 1) % bufSize;
|
||||
double diff_out = Math.Abs(removed - buffer[oldestIdx]);
|
||||
volatilitySum -= diff_out;
|
||||
}
|
||||
}
|
||||
|
||||
bufferIdx = (bufferIdx + 1) % bufSize;
|
||||
if (count < bufSize) count++;
|
||||
|
||||
if (!kamaInitialized)
|
||||
{
|
||||
kama = val;
|
||||
kamaInitialized = true;
|
||||
output[i] = kama;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Calculate ER
|
||||
// Change = abs(current - oldest)
|
||||
// current = val
|
||||
// oldest:
|
||||
// if full, oldest is at bufferIdx (which is the next write pos, so it holds the oldest)
|
||||
// Wait, bufferIdx points to where we WILL write next.
|
||||
// So buffer[bufferIdx] is the oldest value (the one that will be overwritten next).
|
||||
// So Change = abs(val - buffer[bufferIdx])
|
||||
|
||||
double change = 0;
|
||||
if (count == bufSize)
|
||||
{
|
||||
change = Math.Abs(val - buffer[bufferIdx]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If not full, oldest is at 0?
|
||||
// No, we fill 0, 1, 2...
|
||||
// Oldest is at 0.
|
||||
// But bufferIdx wraps.
|
||||
// If count < bufSize, we haven't wrapped yet (except maybe once if count==bufSize?)
|
||||
// If count < bufSize, bufferIdx is the index of next write.
|
||||
// Oldest is at 0.
|
||||
change = Math.Abs(val - buffer[0]);
|
||||
}
|
||||
|
||||
double er = (volatilitySum > double.Epsilon) ? change / volatilitySum : 0.0;
|
||||
if (er > 1.0) er = 1.0;
|
||||
|
||||
double sc = er * (fastAlpha - slowAlpha) + slowAlpha;
|
||||
sc = sc * sc;
|
||||
|
||||
kama = kama + sc * (val - kama);
|
||||
output[i] = kama;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_kama = double.NaN;
|
||||
_p_kama = double.NaN;
|
||||
_volatilitySum = 0;
|
||||
_p_volatilitySum = 0;
|
||||
_lastDiffOut = 0;
|
||||
_lastValidValue = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# KAMA: Kaufman's Adaptive Moving Average
|
||||
|
||||
## Overview and Purpose
|
||||
|
||||
Kaufman's Adaptive Moving Average (KAMA) is an intelligent technical indicator that automatically adjusts its sensitivity based on market conditions. Developed by Perry Kaufman, KAMA solves the fundamental problem of traditional moving averages: their inability to adapt to changing market volatility.
|
||||
|
||||
KAMA becomes more responsive during trending markets (high efficiency) and more stable during sideways or choppy conditions (low efficiency). This self-adjusting behavior makes it valuable for traders who need a single moving average that can effectively handle different market environments without manual parameter changes.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
* **Efficiency Ratio (ER):** Measures the directional movement relative to volatility.
|
||||
* **Market Adaptation:** Automatically adjusts sensitivity based on current price behavior.
|
||||
* **Non-linear Response:** Uses a squared smoothing constant to emphasize differences between trending and non-trending states.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| Period | 10 | The lookback window for the Efficiency Ratio. |
|
||||
| Fast Period | 2 | The effective EMA period when the market is trending (ER = 1). |
|
||||
| Slow Period | 30 | The effective EMA period when the market is choppy (ER = 0). |
|
||||
|
||||
## Formula
|
||||
|
||||
1. **Efficiency Ratio (ER):**
|
||||
$$ER = \frac{\text{Change}}{\text{Volatility}}$$
|
||||
$$\text{Change} = |P_t - P_{t-n}|$$
|
||||
$$\text{Volatility} = \sum_{i=0}^{n-1} |P_{t-i} - P_{t-i-1}|$$
|
||||
|
||||
2. **Smoothing Constant (SC):**
|
||||
$$SC = \left(ER \times (\alpha_{fast} - \alpha_{slow}) + \alpha_{slow}\right)^2$$
|
||||
$$\alpha_{fast} = \frac{2}{\text{FastPeriod} + 1}$$
|
||||
$$\alpha_{slow} = \frac{2}{\text{SlowPeriod} + 1}$$
|
||||
|
||||
3. **KAMA:**
|
||||
$$KAMA_t = KAMA_{t-1} + SC \times (P_t - KAMA_{t-1})$$
|
||||
|
||||
## C# Implementation
|
||||
|
||||
### Standard Usage
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Initialize with period 10, fast 2, slow 30
|
||||
var kama = new Kama(10, fastPeriod: 2, slowPeriod: 30);
|
||||
|
||||
// Update with new value
|
||||
TValue result = kama.Update(new TValue(time, price));
|
||||
Console.WriteLine($"KAMA: {result.Value}");
|
||||
```
|
||||
|
||||
### Zero-Allocation Span API
|
||||
|
||||
```csharp
|
||||
double[] prices = ...;
|
||||
double[] output = new double[prices.Length];
|
||||
|
||||
// Calculate KAMA for the entire array
|
||||
Kama.Calculate(prices.AsSpan(), output.AsSpan(), period: 10, fastPeriod: 2, slowPeriod: 30);
|
||||
```
|
||||
|
||||
### Bar Correction
|
||||
|
||||
```csharp
|
||||
var kama = new Kama(10);
|
||||
|
||||
// Update with initial tick
|
||||
kama.Update(new TValue(time, 100), isNew: true);
|
||||
|
||||
// Update with correction (same bar)
|
||||
kama.Update(new TValue(time, 101), isNew: false);
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
* **Trend Identification:** When price is consistently above KAMA, it indicates an uptrend. Below indicates a downtrend.
|
||||
* **Trend Strength:** A steep KAMA slope suggests a strong trend. A flat KAMA suggests consolidation.
|
||||
* **Support/Resistance:** KAMA often acts as dynamic support or resistance, especially during pullbacks in a trend.
|
||||
* **Filter:** KAMA filters out minor fluctuations during sideways markets while remaining responsive to genuine breakouts.
|
||||
|
||||
## References
|
||||
|
||||
* Kaufman, P. (1995). *Smarter Trading*. McGraw-Hill.
|
||||
* Kaufman, P. (2013). *Trading Systems and Methods*, 5th Edition. Wiley Trading.
|
||||
@@ -23,7 +23,7 @@ public class SmaIndicator : Indicator, IWatchlistIndicator
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"SMA {Period}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/averages/sma/Sma.Quantower.cs";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/sma/Sma.Quantower.cs";
|
||||
|
||||
public SmaIndicator()
|
||||
{
|
||||
@@ -23,7 +23,7 @@ public class TemaIndicator : Indicator, IWatchlistIndicator
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"TEMA {Period}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/averages/tema/Tema.Quantower.cs";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/tema/Tema.Quantower.cs";
|
||||
|
||||
public TemaIndicator()
|
||||
{
|
||||
@@ -23,7 +23,7 @@ public class TrimaIndicator : Indicator, IWatchlistIndicator
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"TRIMA {Period}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/averages/trima/Trima.Quantower.cs";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/trima/Trima.Quantower.cs";
|
||||
|
||||
public TrimaIndicator()
|
||||
{
|
||||
@@ -23,7 +23,7 @@ public class WmaIndicator : Indicator, IWatchlistIndicator
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"WMA {Period}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/averages/wma/Wma.Quantower.cs";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/wma/Wma.Quantower.cs";
|
||||
|
||||
public WmaIndicator()
|
||||
{
|
||||
@@ -47,7 +47,7 @@ public sealed class Wma : ITValuePublisher
|
||||
if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_period = period;
|
||||
_divisor = period * (period + 1) * 0.5;
|
||||
_divisor = (double)period * (period + 1) * 0.5;
|
||||
_buffer = new RingBuffer(period);
|
||||
Name = $"Wma({period})";
|
||||
}
|
||||
@@ -133,7 +133,7 @@ public sealed class Wma : ITValuePublisher
|
||||
_buffer.UpdateNewest(val);
|
||||
}
|
||||
|
||||
double currentDivisor = _buffer.IsFull ? _divisor : _buffer.Count * (_buffer.Count + 1) * 0.5;
|
||||
double currentDivisor = _buffer.IsFull ? _divisor : (double)_buffer.Count * (_buffer.Count + 1) * 0.5;
|
||||
Last = new TValue(input.Time, _wsum / currentDivisor);
|
||||
Pub?.Invoke(Last);
|
||||
return Last;
|
||||
@@ -226,7 +226,7 @@ public sealed class Wma : ITValuePublisher
|
||||
private static void CalculateScalarCore(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
int len = source.Length;
|
||||
double divisor = period * (period + 1) * 0.5;
|
||||
double divisor = (double)period * (period + 1) * 0.5;
|
||||
double sum = 0;
|
||||
double wsum = 0;
|
||||
double lastValid = 0;
|
||||
@@ -248,7 +248,7 @@ public sealed class Wma : ITValuePublisher
|
||||
wsum += (i + 1) * val;
|
||||
buffer[i] = val;
|
||||
|
||||
double currentDivisor = (i + 1) * (i + 2) * 0.5;
|
||||
double currentDivisor = (double)(i + 1) * (i + 2) * 0.5;
|
||||
output[i] = wsum / currentDivisor;
|
||||
}
|
||||
|
||||
@@ -304,7 +304,7 @@ public sealed class Wma : ITValuePublisher
|
||||
ref double srcRef = ref MemoryMarshal.GetReference(source);
|
||||
ref double outRef = ref MemoryMarshal.GetReference(output);
|
||||
|
||||
double divisor = period * (period + 1) * 0.5;
|
||||
double divisor = (double)period * (period + 1) * 0.5;
|
||||
double invDivisor = 1.0 / divisor;
|
||||
|
||||
int warmupEnd = Math.Min(period, len);
|
||||
@@ -315,7 +315,7 @@ public sealed class Wma : ITValuePublisher
|
||||
double val = Unsafe.Add(ref srcRef, i);
|
||||
sum += val;
|
||||
wsum += (i + 1) * val;
|
||||
double currentDivisor = (i + 1) * (i + 2) * 0.5;
|
||||
double currentDivisor = (double)(i + 1) * (i + 2) * 0.5;
|
||||
Unsafe.Add(ref outRef, i) = wsum / currentDivisor;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user