From ed5e5c8209c4acb45686446da88728b8c38c34c3 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Mon, 8 Dec 2025 11:00:58 -0800 Subject: [PATCH] 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. --- .clinerules/good-indicator.md | 156 ++++++ .github/copilot-instructions.md | 6 +- .gitignore | 9 +- QuanTAlib.sln | 2 +- README.md | 4 +- lib/_sidebar.md | 13 + lib/averages/_index.md | 65 --- lib/index.html | 30 + lib/trends/_index.md | 67 +++ lib/trends/alma/Alma.Quantower.cs | 70 +++ lib/trends/alma/Alma.Tests.cs | 179 ++++++ lib/trends/alma/Alma.Validation.Tests.cs | 172 ++++++ lib/trends/alma/Alma.cs | 347 ++++++++++++ lib/trends/alma/Alma.md | 83 +++ .../dema/Dema.Quantower.cs | 2 +- lib/{averages => trends}/dema/Dema.Tests.cs | 0 .../dema/Dema.Validation.Tests.cs | 0 lib/{averages => trends}/dema/Dema.cs | 0 lib/{averages => trends}/dema/Dema.md | 0 lib/{averages => trends}/ema/Ema.Quantower.cs | 0 lib/{averages => trends}/ema/Ema.Tests.cs | 0 .../ema/Ema.Validation.Tests.cs | 0 lib/{averages => trends}/ema/Ema.cs | 0 lib/{averages => trends}/ema/Ema.md | 0 lib/{averages => trends}/hma/Hma.Quantower.cs | 2 +- lib/{averages => trends}/hma/Hma.Tests.cs | 0 .../hma/Hma.Validation.Tests.cs | 0 lib/{averages => trends}/hma/Hma.cs | 0 lib/{averages => trends}/hma/Hma.md | 0 lib/trends/kama/Kama.Quantower.cs | 70 +++ lib/trends/kama/Kama.Tests.cs | 171 ++++++ lib/trends/kama/Kama.Validation.Tests.cs | 170 ++++++ lib/trends/kama/Kama.cs | 324 +++++++++++ lib/trends/kama/Kama.md | 85 +++ lib/{averages => trends}/sma/Sma.Quantower.cs | 2 +- lib/{averages => trends}/sma/Sma.Tests.cs | 0 .../sma/Sma.Validation.Tests.cs | 0 lib/{averages => trends}/sma/Sma.cs | 0 lib/{averages => trends}/sma/Sma.md | 0 lib/{averages => trends}/t3/T3.Quantower.cs | 0 lib/{averages => trends}/t3/T3.Tests.cs | 0 .../t3/T3.Validation.Tests.cs | 0 lib/{averages => trends}/t3/T3.cs | 0 lib/{averages => trends}/t3/T3.md | 0 .../tema/Tema.Quantower.cs | 2 +- lib/{averages => trends}/tema/Tema.Tests.cs | 0 .../tema/Tema.Validation.Tests.cs | 0 lib/{averages => trends}/tema/Tema.cs | 0 lib/{averages => trends}/tema/Tema.md | 0 .../trima/Trima.Quantower.cs | 2 +- lib/{averages => trends}/trima/Trima.Tests.cs | 0 .../trima/Trima.Validation.Tests.cs | 0 lib/{averages => trends}/trima/Trima.cs | 0 lib/{averages => trends}/trima/Trima.md | 0 lib/{averages => trends}/wma/Wma.Quantower.cs | 2 +- lib/{averages => trends}/wma/Wma.Tests.cs | 0 .../wma/Wma.Validation.Tests.cs | 0 lib/{averages => trends}/wma/Wma.cs | 12 +- lib/{averages => trends}/wma/Wma.md | 0 quantalib.ndproj | 529 ++++++++++++++++++ quantower/Quantower.Tests.csproj | 4 +- quantower/{Averages.csproj => Trends.csproj} | 6 +- quantower/trends/AlmaIndicator.Tests.cs | 170 ++++++ quantower/{ => trends}/DemaIndicator.Tests.cs | 0 quantower/{ => trends}/EmaIndicator.Tests.cs | 0 quantower/{ => trends}/HmaIndicator.Tests.cs | 0 quantower/trends/KamaIndicator.Tests.cs | 170 ++++++ quantower/{ => trends}/SmaIndicator.Tests.cs | 0 quantower/{ => trends}/T3Indicator.Tests.cs | 0 quantower/{ => trends}/TemaIndicator.Tests.cs | 0 .../{ => trends}/TrimaIndicator.Tests.cs | 0 quantower/{ => trends}/WmaIndicator.Tests.cs | 0 72 files changed, 2834 insertions(+), 92 deletions(-) create mode 100644 .clinerules/good-indicator.md create mode 100644 lib/_sidebar.md delete mode 100644 lib/averages/_index.md create mode 100644 lib/index.html create mode 100644 lib/trends/_index.md create mode 100644 lib/trends/alma/Alma.Quantower.cs create mode 100644 lib/trends/alma/Alma.Tests.cs create mode 100644 lib/trends/alma/Alma.Validation.Tests.cs create mode 100644 lib/trends/alma/Alma.cs create mode 100644 lib/trends/alma/Alma.md rename lib/{averages => trends}/dema/Dema.Quantower.cs (96%) rename lib/{averages => trends}/dema/Dema.Tests.cs (100%) rename lib/{averages => trends}/dema/Dema.Validation.Tests.cs (100%) rename lib/{averages => trends}/dema/Dema.cs (100%) rename lib/{averages => trends}/dema/Dema.md (100%) rename lib/{averages => trends}/ema/Ema.Quantower.cs (100%) rename lib/{averages => trends}/ema/Ema.Tests.cs (100%) rename lib/{averages => trends}/ema/Ema.Validation.Tests.cs (100%) rename lib/{averages => trends}/ema/Ema.cs (100%) rename lib/{averages => trends}/ema/Ema.md (100%) rename lib/{averages => trends}/hma/Hma.Quantower.cs (97%) rename lib/{averages => trends}/hma/Hma.Tests.cs (100%) rename lib/{averages => trends}/hma/Hma.Validation.Tests.cs (100%) rename lib/{averages => trends}/hma/Hma.cs (100%) rename lib/{averages => trends}/hma/Hma.md (100%) create mode 100644 lib/trends/kama/Kama.Quantower.cs create mode 100644 lib/trends/kama/Kama.Tests.cs create mode 100644 lib/trends/kama/Kama.Validation.Tests.cs create mode 100644 lib/trends/kama/Kama.cs create mode 100644 lib/trends/kama/Kama.md rename lib/{averages => trends}/sma/Sma.Quantower.cs (97%) rename lib/{averages => trends}/sma/Sma.Tests.cs (100%) rename lib/{averages => trends}/sma/Sma.Validation.Tests.cs (100%) rename lib/{averages => trends}/sma/Sma.cs (100%) rename lib/{averages => trends}/sma/Sma.md (100%) rename lib/{averages => trends}/t3/T3.Quantower.cs (100%) rename lib/{averages => trends}/t3/T3.Tests.cs (100%) rename lib/{averages => trends}/t3/T3.Validation.Tests.cs (100%) rename lib/{averages => trends}/t3/T3.cs (100%) rename lib/{averages => trends}/t3/T3.md (100%) rename lib/{averages => trends}/tema/Tema.Quantower.cs (96%) rename lib/{averages => trends}/tema/Tema.Tests.cs (100%) rename lib/{averages => trends}/tema/Tema.Validation.Tests.cs (100%) rename lib/{averages => trends}/tema/Tema.cs (100%) rename lib/{averages => trends}/tema/Tema.md (100%) rename lib/{averages => trends}/trima/Trima.Quantower.cs (96%) rename lib/{averages => trends}/trima/Trima.Tests.cs (100%) rename lib/{averages => trends}/trima/Trima.Validation.Tests.cs (100%) rename lib/{averages => trends}/trima/Trima.cs (100%) rename lib/{averages => trends}/trima/Trima.md (100%) rename lib/{averages => trends}/wma/Wma.Quantower.cs (96%) rename lib/{averages => trends}/wma/Wma.Tests.cs (100%) rename lib/{averages => trends}/wma/Wma.Validation.Tests.cs (100%) rename lib/{averages => trends}/wma/Wma.cs (97%) rename lib/{averages => trends}/wma/Wma.md (100%) create mode 100644 quantalib.ndproj rename quantower/{Averages.csproj => Trends.csproj} (77%) create mode 100644 quantower/trends/AlmaIndicator.Tests.cs rename quantower/{ => trends}/DemaIndicator.Tests.cs (100%) rename quantower/{ => trends}/EmaIndicator.Tests.cs (100%) rename quantower/{ => trends}/HmaIndicator.Tests.cs (100%) create mode 100644 quantower/trends/KamaIndicator.Tests.cs rename quantower/{ => trends}/SmaIndicator.Tests.cs (100%) rename quantower/{ => trends}/T3Indicator.Tests.cs (100%) rename quantower/{ => trends}/TemaIndicator.Tests.cs (100%) rename quantower/{ => trends}/TrimaIndicator.Tests.cs (100%) rename quantower/{ => trends}/WmaIndicator.Tests.cs (100%) diff --git a/.clinerules/good-indicator.md b/.clinerules/good-indicator.md new file mode 100644 index 00000000..f036c888 --- /dev/null +++ b/.clinerules/good-indicator.md @@ -0,0 +1,156 @@ +# Good Indicator Guidelines + +This document defines the strict standards for creating high-quality technical indicators in the QuanTAlib library. All new indicators MUST adhere to these rules to ensure consistency, performance, and reliability. + +## 1. Architecture & Design Principles + +* **Zero Allocation:** The core calculation loop must not allocate memory on the heap. Use `stackalloc`, `Span`, and pinned memory where possible. +* **O(1) Complexity:** Streaming updates must be O(1) whenever mathematically possible. Use running sums/products or circular buffers to avoid re-iterating over history. +* **Dual API:** Provide both a stateful object-oriented API (`Update`) and a stateless static vector API (`Calculate`). +* **Bar Correction:** Support intra-bar updates via the `isNew` parameter. The indicator must be able to rollback the last update and apply a new value for the same timestamp. +* **Robustness:** Handle `NaN` and `Infinity` gracefully using last-valid-value substitution. Never propagate invalid values. +* **Reactive:** Implement `ITValuePublisher` to support event-driven architectures. + +## 2. File Structure + +Each indicator resides in its own directory such as `lib/trends/`, `lib/indicators/`, or `lib/oscillators/`. + +**Directory:** `lib/[category]/[name]/` + +| File | Purpose | Naming Convention | +|------|---------|-------------------| +| **Source** | Main implementation | `[Name].cs` (e.g., `Sma.cs`) | +| **Tests** | Unit tests | `[Name].Tests.cs` | +| **Validation** | Cross-library validation | `[Name].Validation.Tests.cs` | +| **Docs** | User documentation | `[Name].md` | +| **Quantower** | Quantower adapter | `[Name].Quantower.cs` | + +## 3. Implementation Rules (`[Name].cs`) + +### Class Definition + +* **Namespace:** `QuanTAlib` +* **Attributes:** `[SkipLocalsInit]` for performance. +* **Modifiers:** `public sealed class` +* **Interface:** Implements `ITValuePublisher` + +### State Management + +* Use `RingBuffer` for sliding window data. +* Maintain separate state variables for the *current* calculation (`_sum`, `_lastVal`) and the *previous* valid state (`_p_sum`, `_p_lastVal`) to support `isNew=false` updates. +* **Resync:** Implement a periodic full recalculation (e.g., every 1000 ticks) to prevent floating-point drift in running sums. + +### Constructor + +* Validate all parameters (throw `ArgumentException` for invalid values). +* Initialize `Name` property (e.g., `$"Sma({period})"`); +* Support chaining: `public [Name](ITValuePublisher source, ...)` + +### Update Method + +* **Signature:** `public TValue Update(TValue input, bool isNew = true)` +* **Attribute:** `[MethodImpl(MethodImplOptions.AggressiveInlining)]` +* **Logic:** + 1. **Input Validation:** Check `double.IsFinite`. If not, use `_lastValidValue`. + 2. **State Management:** + * If `isNew=true`: Save current state to `_p_*` variables, then update. + * If `isNew=false`: Restore state from `_p_*` variables, then update. + 3. **Calculation:** Perform the math. + 4. **Publish:** Update `Last` property, invoke `Pub` event, return `Last`. + +### Update Method (TSeries) + +* **Signature:** `public TSeries Update(TSeries source)` +* **Placement:** Must be adjacent to the `Update(TValue)` method. +* **Logic:** + 1. Create output series. + 2. Call static `Calculate(Span)` for performance. + 3. Restore internal state by replaying the last `Period` bars. + +### Static Calculate (TSeries) + +* Create a new instance of the indicator. +* Iterate through the source series. +* Return the resulting `TSeries`. + +### Static Calculate (Span) - **Critical for Performance** + +* **Signature:** `public static void Calculate(ReadOnlySpan source, Span output, ...)` +* **Attribute:** `[MethodImpl(MethodImplOptions.AggressiveInlining)]` +* **Optimization:** + +* Check for SIMD support (`Avx2.IsSupported`). +* Use `stackalloc` for small buffers (threshold ~256). +* Implement a scalar fallback path that handles `NaN` safely. +* Implement a SIMD path for large, clean datasets (optional but recommended for simple averages). + +## 4. Testing Standards + +### Unit Tests (`[Name].Tests.cs`) + +* **Framework:** xUnit +* **Coverage:** + +* Constructor validation (invalid params). +* Basic calculation correctness (compare against manual calc). +* `isNew=true` vs `isNew=false` behavior (bar correction). +* `Reset()` functionality. +* `IsHot` property behavior. +* `NaN` / `Infinity` handling (must not crash, must return finite values). +* Consistency between Object API, Static TSeries API, and Static Span API. +* Edge cases: Period=1, empty input, single input. + +### Validation Tests (`[Name].Validation.Tests.cs`) + +* **Purpose:** Verify accuracy against established libraries (Skender, TA-Lib, Tulip). +* **Data:** Use `GBM` (Geometric Brownian Motion) to generate realistic test data. +* **Scenarios:** + +* Batch processing. +* Streaming processing. +* Span/Vector processing. + +* **Tolerance:** Typically `1e-6` or `1e-9` depending on the algorithm. + +## 5. Documentation Standards (`[Name].md`) + +Follow the standard template and ensure strict adherence to Markdownlint rules, specifically: + +* **MD030:** Ensure exactly one space after list markers (e.g., `* Item`, not `*Item` or `* Item`). +* **MD032:** Ensure lists are surrounded by blank lines (one blank line before the first item and one after the last item). + +Template structure: + +1. **Title & Overview:** What is it? What does it do? +2. **Core Concepts:** Key features (e.g., equal weighting, noise reduction). +3. **Parameters:** Table of constructor parameters. +4. **Formula:** LaTeX formatted math ($$...$$). +5. **C# Implementation:** Code examples for: + * Standard usage. + * Span API (high performance). + * Bar correction (`isNew`). + * Eventing. +6. **Interpretation:** How to use it in trading. +7. **References:** Books or papers. + +## 6. Performance Guidelines + +* **Inlining:** Use `[MethodImpl(MethodImplOptions.AggressiveInlining)]` on all hot path methods (`Update`, `Calculate`). +* **Locals Init:** Use `[SkipLocalsInit]` on the class to skip zero-initialization of locals. +* **Loops:** Prefer `for` loops over `foreach` for arrays/spans. +* **Math:** Use `System.Math` or `System.Numerics`. Avoid LINQ in hot paths. +* **Memory:** **NEVER** use `new` inside the `Update` method. Pre-allocate everything in the constructor. + +## 7. Checklist for New Indicators + +* [ ] **File Structure:** Created all 4 required files? +* [ ] **Constructor:** Validates inputs? Sets `Name`? +* [ ] **Update:** Handles `isNew` correctly? Handles `NaN`? O(1)? +* [ ] **Static API:** Implemented `Calculate(Span)`? +* [ ] **Tests:** Unit tests pass? `NaN` tests included? +* [ ] **Validation:** Matches external libraries (Skender/TA-Lib)? +* [ ] **Docs:** Markdown file created with formula and examples? +* [ ] **Quantower:** Adapter created in `[Name].Quantower.cs`? +* [ ] **Quantower Tests:** Adapter tests created in `quantower/[category]/[Name]Indicator.Tests.cs`? +* [ ] **Index:** Added to category `_index.md` with link and description? +* [ ] **Performance:** No allocations in `Update`? `[SkipLocalsInit]` used? diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index acf70bec..1b48fbff 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -40,7 +40,7 @@ protected override void ManageState(bool isNew) } } ``` -**Pattern**: Use `_p_` prefix for backup variables (e.g., `_p_lastEma`, `_p_isInit`, `_p_e`). When `isNew=false`, restore ALL stateful variables before recalculating. See `lib/averages/Ema.cs` for reference implementation. +**Pattern**: Use `_p_` prefix for backup variables (e.g., `_p_lastEma`, `_p_isInit`, `_p_e`). When `isNew=false`, restore ALL stateful variables before recalculating. See `lib/trends/Ema.cs` for reference implementation. ## Development Workflow @@ -79,7 +79,7 @@ dotnet clean QuanTAlib.sln 1. **Research**: Get formula/specification. For non-trivial indicators, use Context7 to retrieve authoritative references. 2. **Location**: Place in appropriate `lib/` subdirectory: - - `averages/` - Moving averages (SMA, EMA, JMA, etc.) + - `trends/` - Trend indicators (SMA, EMA, JMA, etc.) - `oscillators/` - RSI, Stochastic, CCI, etc. - `momentum/` - MACD, ADX, ROC, etc. - `volatility/` - ATR, Bollinger Bands, volatility measures @@ -229,7 +229,7 @@ public class MyIndicator : Indicator, IWatchlistIndicator ``` lib/ ├── core/ # AbstractBase, CircularBuffer, TSeries, TBar, TValue, ITValue -├── averages/ # Moving averages: SMA, EMA, DEMA, TEMA, JMA, KAMA, etc. (25+ indicators) +├── trends/ # Trend indicators: SMA, EMA, DEMA, TEMA, JMA, KAMA, etc. (25+ indicators) ├── oscillators/ # RSI, Stochastic, Williams %R, CCI, Fisher, CTI, etc. ├── momentum/ # MACD, ADX, DMI, ROC, TRIX, Vortex, PMO, etc. ├── volatility/ # ATR, Bollinger Bands, Keltner Channels, volatility measures diff --git a/.gitignore b/.gitignore index b79d48d3..1512aa18 100644 --- a/.gitignore +++ b/.gitignore @@ -397,6 +397,7 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml .idea/ +NDependOut/ # SonarQube/SonarCloud .sonarqube/ @@ -413,7 +414,7 @@ ilspy/ #Ignore insiders AI rules .github/instructions/codacy.instructions.md - - -#Ignore vscode AI rules -.github\instructions\codacy.instructions.md + + +#Ignore vscode AI rules +.github\instructions\codacy.instructions.md diff --git a/QuanTAlib.sln b/QuanTAlib.sln index 969b164d..ddf1d548 100644 --- a/QuanTAlib.sln +++ b/QuanTAlib.sln @@ -11,7 +11,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuanTAlib.Tests", "lib\Quan EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "quantower", "quantower", "{6CF592EE-4302-E72F-3CB4-AB1D314DD5A8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Averages", "quantower\Averages.csproj", "{D8F03B19-F99F-475F-8951-85C9D2258B73}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Trends", "quantower\Trends.csproj", "{D8F03B19-F99F-475F-8951-85C9D2258B73}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quantower.Tests", "quantower\Quantower.Tests.csproj", "{576835AB-6453-4413-A2E7-54B6725CDF9D}" EndProject diff --git a/README.md b/README.md index 2cc2eb0d..0adc4512 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ TBar bar = csv.Next(isNew: true); Copy DLL files to Quantower installation: ``` -\Settings\Scripts\Indicators\QuanTAlib\Averages\Averages.dll +\Settings\Scripts\Indicators\QuanTAlib\Trends\Trends.dll ``` Where `` is the directory containing `Start.lnk`. @@ -138,7 +138,7 @@ QuanTAlib/ │ │ ├── tbar/ # TBar struct │ │ ├── tbarseries/ # TBarSeries class │ │ └── simd/ # SIMD extensions -│ ├── averages/ +│ ├── trends/ │ │ └── ema/ # EMA indicator + tests + docs │ └── feeds/ │ ├── csv/ # CSV file feed diff --git a/lib/_sidebar.md b/lib/_sidebar.md new file mode 100644 index 00000000..dc4d96a9 --- /dev/null +++ b/lib/_sidebar.md @@ -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) diff --git a/lib/averages/_index.md b/lib/averages/_index.md deleted file mode 100644 index cb75879c..00000000 --- a/lib/averages/_index.md +++ /dev/null @@ -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 | diff --git a/lib/index.html b/lib/index.html new file mode 100644 index 00000000..d82f671b --- /dev/null +++ b/lib/index.html @@ -0,0 +1,30 @@ + + + + + QuanTAlib Documentation + + + + + + +
+ + + + + + + + + + diff --git a/lib/trends/_index.md b/lib/trends/_index.md new file mode 100644 index 00000000..5dee5112 --- /dev/null +++ b/lib/trends/_index.md @@ -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 | | diff --git a/lib/trends/alma/Alma.Quantower.cs b/lib/trends/alma/Alma.Quantower.cs new file mode 100644 index 00000000..8a813bf2 --- /dev/null +++ b/lib/trends/alma/Alma.Quantower.cs @@ -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); + } +} diff --git a/lib/trends/alma/Alma.Tests.cs b/lib/trends/alma/Alma.Tests.cs new file mode 100644 index 00000000..e4252718 --- /dev/null +++ b/lib/trends/alma/Alma.Tests.cs @@ -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(() => new Alma(0)); + Assert.Throws(() => 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); + } +} diff --git a/lib/trends/alma/Alma.Validation.Tests.cs b/lib/trends/alma/Alma.Validation.Tests.cs new file mode 100644 index 00000000..e0a5e667 --- /dev/null +++ b/lib/trends/alma/Alma.Validation.Tests.cs @@ -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 _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) + _skenderQuotes = new List(); + 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(); + 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 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 qResults, List 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 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); + } + } +} diff --git a/lib/trends/alma/Alma.cs b/lib/trends/alma/Alma.cs new file mode 100644 index 00000000..4f87bccf --- /dev/null +++ b/lib/trends/alma/Alma.cs @@ -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; + +/// +/// ALMA: Arnaud Legoux Moving Average +/// +/// +/// 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. +/// +[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; + + /// + /// Display name for the indicator. + /// + public string Name { get; } + + public event Action? Pub; + + /// + /// Current ALMA value. + /// + public TValue Last { get; private set; } + + /// + /// True if the ALMA has enough data to produce valid results (buffer is full). + /// + public bool IsHot => _buffer.IsFull; + + /// + /// Creates ALMA with specified parameters. + /// + /// Window size (must be > 0) + /// Gaussian offset (0-1, default 0.85). Closer to 1 makes it more responsive. + /// Standard deviation (default 6). Higher values make it sharper. + 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(), new List()); + + int len = source.Count; + var t = new List(len); + var v = new List(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 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 buffer) + { + double sum = 0; + int i = 0; + int len = _period; + + if (Avx2.IsSupported && len >= Vector256.Count) + { + var vSum = Vector256.Zero; + ref double bufRef = ref MemoryMarshal.GetReference(buffer); + ref double wRef = ref MemoryMarshal.GetReference(_weights.AsSpan()); + + for (; i <= len - Vector256.Count; i += Vector256.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 source, Span 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 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; + } +} diff --git a/lib/trends/alma/Alma.md b/lib/trends/alma/Alma.md new file mode 100644 index 00000000..773037d4 --- /dev/null +++ b/lib/trends/alma/Alma.md @@ -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). diff --git a/lib/averages/dema/Dema.Quantower.cs b/lib/trends/dema/Dema.Quantower.cs similarity index 96% rename from lib/averages/dema/Dema.Quantower.cs rename to lib/trends/dema/Dema.Quantower.cs index 91447308..f2c325c3 100644 --- a/lib/averages/dema/Dema.Quantower.cs +++ b/lib/trends/dema/Dema.Quantower.cs @@ -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() { diff --git a/lib/averages/dema/Dema.Tests.cs b/lib/trends/dema/Dema.Tests.cs similarity index 100% rename from lib/averages/dema/Dema.Tests.cs rename to lib/trends/dema/Dema.Tests.cs diff --git a/lib/averages/dema/Dema.Validation.Tests.cs b/lib/trends/dema/Dema.Validation.Tests.cs similarity index 100% rename from lib/averages/dema/Dema.Validation.Tests.cs rename to lib/trends/dema/Dema.Validation.Tests.cs diff --git a/lib/averages/dema/Dema.cs b/lib/trends/dema/Dema.cs similarity index 100% rename from lib/averages/dema/Dema.cs rename to lib/trends/dema/Dema.cs diff --git a/lib/averages/dema/Dema.md b/lib/trends/dema/Dema.md similarity index 100% rename from lib/averages/dema/Dema.md rename to lib/trends/dema/Dema.md diff --git a/lib/averages/ema/Ema.Quantower.cs b/lib/trends/ema/Ema.Quantower.cs similarity index 100% rename from lib/averages/ema/Ema.Quantower.cs rename to lib/trends/ema/Ema.Quantower.cs diff --git a/lib/averages/ema/Ema.Tests.cs b/lib/trends/ema/Ema.Tests.cs similarity index 100% rename from lib/averages/ema/Ema.Tests.cs rename to lib/trends/ema/Ema.Tests.cs diff --git a/lib/averages/ema/Ema.Validation.Tests.cs b/lib/trends/ema/Ema.Validation.Tests.cs similarity index 100% rename from lib/averages/ema/Ema.Validation.Tests.cs rename to lib/trends/ema/Ema.Validation.Tests.cs diff --git a/lib/averages/ema/Ema.cs b/lib/trends/ema/Ema.cs similarity index 100% rename from lib/averages/ema/Ema.cs rename to lib/trends/ema/Ema.cs diff --git a/lib/averages/ema/Ema.md b/lib/trends/ema/Ema.md similarity index 100% rename from lib/averages/ema/Ema.md rename to lib/trends/ema/Ema.md diff --git a/lib/averages/hma/Hma.Quantower.cs b/lib/trends/hma/Hma.Quantower.cs similarity index 97% rename from lib/averages/hma/Hma.Quantower.cs rename to lib/trends/hma/Hma.Quantower.cs index 2feed6d8..bcc94bc5 100644 --- a/lib/averages/hma/Hma.Quantower.cs +++ b/lib/trends/hma/Hma.Quantower.cs @@ -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() { diff --git a/lib/averages/hma/Hma.Tests.cs b/lib/trends/hma/Hma.Tests.cs similarity index 100% rename from lib/averages/hma/Hma.Tests.cs rename to lib/trends/hma/Hma.Tests.cs diff --git a/lib/averages/hma/Hma.Validation.Tests.cs b/lib/trends/hma/Hma.Validation.Tests.cs similarity index 100% rename from lib/averages/hma/Hma.Validation.Tests.cs rename to lib/trends/hma/Hma.Validation.Tests.cs diff --git a/lib/averages/hma/Hma.cs b/lib/trends/hma/Hma.cs similarity index 100% rename from lib/averages/hma/Hma.cs rename to lib/trends/hma/Hma.cs diff --git a/lib/averages/hma/Hma.md b/lib/trends/hma/Hma.md similarity index 100% rename from lib/averages/hma/Hma.md rename to lib/trends/hma/Hma.md diff --git a/lib/trends/kama/Kama.Quantower.cs b/lib/trends/kama/Kama.Quantower.cs new file mode 100644 index 00000000..c963c745 --- /dev/null +++ b/lib/trends/kama/Kama.Quantower.cs @@ -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); + } +} diff --git a/lib/trends/kama/Kama.Tests.cs b/lib/trends/kama/Kama.Tests.cs new file mode 100644 index 00000000..db16ac79 --- /dev/null +++ b/lib/trends/kama/Kama.Tests.cs @@ -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(() => new Kama(0)); + Assert.Throws(() => new Kama(10, fastPeriod: 0)); + Assert.Throws(() => new Kama(10, slowPeriod: 0)); + Assert.Throws(() => 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); + } +} diff --git a/lib/trends/kama/Kama.Validation.Tests.cs b/lib/trends/kama/Kama.Validation.Tests.cs new file mode 100644 index 00000000..2ba3a468 --- /dev/null +++ b/lib/trends/kama/Kama.Validation.Tests.cs @@ -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 _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) + _skenderQuotes = new List(); + 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(); + 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 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 qResults, List 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 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); + } + } +} diff --git a/lib/trends/kama/Kama.cs b/lib/trends/kama/Kama.cs new file mode 100644 index 00000000..764d43b1 --- /dev/null +++ b/lib/trends/kama/Kama.cs @@ -0,0 +1,324 @@ +using System; +using System.Runtime.CompilerServices; + +namespace QuanTAlib; + +/// +/// KAMA: Kaufman's Adaptive Moving Average +/// +/// +/// 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]) +/// +[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; + + /// + /// Display name for the indicator. + /// + public string Name { get; } + + public event Action? Pub; + + /// + /// Current KAMA value. + /// + public TValue Last { get; private set; } + + /// + /// True if the KAMA has enough data to produce valid results. + /// + public bool IsHot => _buffer.IsFull; + + /// + /// Creates KAMA with specified parameters. + /// + /// Lookback period for Efficiency Ratio (default 10). + /// Fast EMA period for SC calculation (default 2). + /// Slow EMA period for SC calculation (default 30). + 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(), new List()); + + int len = source.Count; + var t = new List(len); + var v = new List(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 source, Span 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 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; + } +} diff --git a/lib/trends/kama/Kama.md b/lib/trends/kama/Kama.md new file mode 100644 index 00000000..8ef86312 --- /dev/null +++ b/lib/trends/kama/Kama.md @@ -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. diff --git a/lib/averages/sma/Sma.Quantower.cs b/lib/trends/sma/Sma.Quantower.cs similarity index 97% rename from lib/averages/sma/Sma.Quantower.cs rename to lib/trends/sma/Sma.Quantower.cs index 5e26082a..87413d99 100644 --- a/lib/averages/sma/Sma.Quantower.cs +++ b/lib/trends/sma/Sma.Quantower.cs @@ -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() { diff --git a/lib/averages/sma/Sma.Tests.cs b/lib/trends/sma/Sma.Tests.cs similarity index 100% rename from lib/averages/sma/Sma.Tests.cs rename to lib/trends/sma/Sma.Tests.cs diff --git a/lib/averages/sma/Sma.Validation.Tests.cs b/lib/trends/sma/Sma.Validation.Tests.cs similarity index 100% rename from lib/averages/sma/Sma.Validation.Tests.cs rename to lib/trends/sma/Sma.Validation.Tests.cs diff --git a/lib/averages/sma/Sma.cs b/lib/trends/sma/Sma.cs similarity index 100% rename from lib/averages/sma/Sma.cs rename to lib/trends/sma/Sma.cs diff --git a/lib/averages/sma/Sma.md b/lib/trends/sma/Sma.md similarity index 100% rename from lib/averages/sma/Sma.md rename to lib/trends/sma/Sma.md diff --git a/lib/averages/t3/T3.Quantower.cs b/lib/trends/t3/T3.Quantower.cs similarity index 100% rename from lib/averages/t3/T3.Quantower.cs rename to lib/trends/t3/T3.Quantower.cs diff --git a/lib/averages/t3/T3.Tests.cs b/lib/trends/t3/T3.Tests.cs similarity index 100% rename from lib/averages/t3/T3.Tests.cs rename to lib/trends/t3/T3.Tests.cs diff --git a/lib/averages/t3/T3.Validation.Tests.cs b/lib/trends/t3/T3.Validation.Tests.cs similarity index 100% rename from lib/averages/t3/T3.Validation.Tests.cs rename to lib/trends/t3/T3.Validation.Tests.cs diff --git a/lib/averages/t3/T3.cs b/lib/trends/t3/T3.cs similarity index 100% rename from lib/averages/t3/T3.cs rename to lib/trends/t3/T3.cs diff --git a/lib/averages/t3/T3.md b/lib/trends/t3/T3.md similarity index 100% rename from lib/averages/t3/T3.md rename to lib/trends/t3/T3.md diff --git a/lib/averages/tema/Tema.Quantower.cs b/lib/trends/tema/Tema.Quantower.cs similarity index 96% rename from lib/averages/tema/Tema.Quantower.cs rename to lib/trends/tema/Tema.Quantower.cs index eaa3aaff..a71378a4 100644 --- a/lib/averages/tema/Tema.Quantower.cs +++ b/lib/trends/tema/Tema.Quantower.cs @@ -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() { diff --git a/lib/averages/tema/Tema.Tests.cs b/lib/trends/tema/Tema.Tests.cs similarity index 100% rename from lib/averages/tema/Tema.Tests.cs rename to lib/trends/tema/Tema.Tests.cs diff --git a/lib/averages/tema/Tema.Validation.Tests.cs b/lib/trends/tema/Tema.Validation.Tests.cs similarity index 100% rename from lib/averages/tema/Tema.Validation.Tests.cs rename to lib/trends/tema/Tema.Validation.Tests.cs diff --git a/lib/averages/tema/Tema.cs b/lib/trends/tema/Tema.cs similarity index 100% rename from lib/averages/tema/Tema.cs rename to lib/trends/tema/Tema.cs diff --git a/lib/averages/tema/Tema.md b/lib/trends/tema/Tema.md similarity index 100% rename from lib/averages/tema/Tema.md rename to lib/trends/tema/Tema.md diff --git a/lib/averages/trima/Trima.Quantower.cs b/lib/trends/trima/Trima.Quantower.cs similarity index 96% rename from lib/averages/trima/Trima.Quantower.cs rename to lib/trends/trima/Trima.Quantower.cs index bff7c06f..3c032fce 100644 --- a/lib/averages/trima/Trima.Quantower.cs +++ b/lib/trends/trima/Trima.Quantower.cs @@ -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() { diff --git a/lib/averages/trima/Trima.Tests.cs b/lib/trends/trima/Trima.Tests.cs similarity index 100% rename from lib/averages/trima/Trima.Tests.cs rename to lib/trends/trima/Trima.Tests.cs diff --git a/lib/averages/trima/Trima.Validation.Tests.cs b/lib/trends/trima/Trima.Validation.Tests.cs similarity index 100% rename from lib/averages/trima/Trima.Validation.Tests.cs rename to lib/trends/trima/Trima.Validation.Tests.cs diff --git a/lib/averages/trima/Trima.cs b/lib/trends/trima/Trima.cs similarity index 100% rename from lib/averages/trima/Trima.cs rename to lib/trends/trima/Trima.cs diff --git a/lib/averages/trima/Trima.md b/lib/trends/trima/Trima.md similarity index 100% rename from lib/averages/trima/Trima.md rename to lib/trends/trima/Trima.md diff --git a/lib/averages/wma/Wma.Quantower.cs b/lib/trends/wma/Wma.Quantower.cs similarity index 96% rename from lib/averages/wma/Wma.Quantower.cs rename to lib/trends/wma/Wma.Quantower.cs index 69402c40..5ff255e6 100644 --- a/lib/averages/wma/Wma.Quantower.cs +++ b/lib/trends/wma/Wma.Quantower.cs @@ -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() { diff --git a/lib/averages/wma/Wma.Tests.cs b/lib/trends/wma/Wma.Tests.cs similarity index 100% rename from lib/averages/wma/Wma.Tests.cs rename to lib/trends/wma/Wma.Tests.cs diff --git a/lib/averages/wma/Wma.Validation.Tests.cs b/lib/trends/wma/Wma.Validation.Tests.cs similarity index 100% rename from lib/averages/wma/Wma.Validation.Tests.cs rename to lib/trends/wma/Wma.Validation.Tests.cs diff --git a/lib/averages/wma/Wma.cs b/lib/trends/wma/Wma.cs similarity index 97% rename from lib/averages/wma/Wma.cs rename to lib/trends/wma/Wma.cs index 26c4812b..cfae3607 100644 --- a/lib/averages/wma/Wma.cs +++ b/lib/trends/wma/Wma.cs @@ -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 source, Span 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; } diff --git a/lib/averages/wma/Wma.md b/lib/trends/wma/Wma.md similarity index 100% rename from lib/averages/wma/Wma.md rename to lib/trends/wma/Wma.md diff --git a/quantalib.ndproj b/quantalib.ndproj new file mode 100644 index 00000000..3844323d --- /dev/null +++ b/quantalib.ndproj @@ -0,0 +1,529 @@ + + + ./NDependOut + + + + . + + + + .NET 10.0 + + + True + True + True + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 1 + 0 + 0 + $ManDay$ + 50 + USD + After + 18 + 240 + 8 + 5 + 10 + 20 + 50 + 1200000000 + 12000000000 + 72000000000 + 360000000000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quantower/Quantower.Tests.csproj b/quantower/Quantower.Tests.csproj index 2e1ee149..6689f046 100644 --- a/quantower/Quantower.Tests.csproj +++ b/quantower/Quantower.Tests.csproj @@ -27,8 +27,8 @@ - - + + diff --git a/quantower/Averages.csproj b/quantower/Trends.csproj similarity index 77% rename from quantower/Averages.csproj rename to quantower/Trends.csproj index 27bbeb06..bf79456b 100644 --- a/quantower/Averages.csproj +++ b/quantower/Trends.csproj @@ -2,7 +2,7 @@ net8.0 - Averages + Trends Indicator bin\$(Configuration)\ false @@ -16,7 +16,7 @@ - + ..\.github\TradingPlatform.BusinessLayer.dll @@ -26,7 +26,7 @@ - + diff --git a/quantower/trends/AlmaIndicator.Tests.cs b/quantower/trends/AlmaIndicator.Tests.cs new file mode 100644 index 00000000..4741c8b6 --- /dev/null +++ b/quantower/trends/AlmaIndicator.Tests.cs @@ -0,0 +1,170 @@ +using Xunit; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib.Tests; + +public class AlmaIndicatorTests +{ + [Fact] + public void AlmaIndicator_Constructor_SetsDefaults() + { + var indicator = new AlmaIndicator(); + + Assert.Equal(9, indicator.Period); + Assert.Equal(0.85, indicator.Offset); + Assert.Equal(6.0, indicator.Sigma); + Assert.Equal(SourceType.Close, indicator.Source); + Assert.True(indicator.ShowColdValues); + Assert.Equal("ALMA - Arnaud Legoux Moving Average", indicator.Name); + Assert.False(indicator.SeparateWindow); + Assert.True(indicator.OnBackGround); + } + + [Fact] + public void AlmaIndicator_MinHistoryDepths_EqualsPeriod() + { + var indicator = new AlmaIndicator { Period = 20 }; + + Assert.Equal(20, indicator.MinHistoryDepths); + Assert.Equal(20, ((IWatchlistIndicator)indicator).MinHistoryDepths); + } + + [Fact] + public void AlmaIndicator_ShortName_IncludesPeriodAndSource() + { + var indicator = new AlmaIndicator { Period = 15 }; + + Assert.Contains("ALMA", indicator.ShortName); + Assert.Contains("15", indicator.ShortName); + } + + [Fact] + public void AlmaIndicator_Initialize_CreatesInternalAlma() + { + var indicator = new AlmaIndicator { Period = 10 }; + + // Initialize should not throw + indicator.Initialize(); + + // After init, line series should exist + Assert.Single(indicator.LinesSeries); + } + + [Fact] + public void AlmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue() + { + var indicator = new AlmaIndicator { Period = 3 }; + indicator.Initialize(); + + // Add historical data + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 102); + + // Process update + var args = new UpdateArgs(UpdateReason.HistoricalBar); + indicator.ProcessUpdate(args); + + // Line series should have a value + Assert.Equal(1, indicator.LinesSeries[0].Count); + Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0))); + } + + [Fact] + public void AlmaIndicator_ProcessUpdate_NewBar_ComputesValue() + { + var indicator = new AlmaIndicator { Period = 3 }; + indicator.Initialize(); + + // Add historical data + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 102); + indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106); + + // Process first update + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar)); + + // Line series should have values + Assert.Equal(2, indicator.LinesSeries[0].Count); + } + + [Fact] + public void AlmaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError() + { + var indicator = new AlmaIndicator { Period = 3 }; + indicator.Initialize(); + + // Add historical data + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 102); + + // Process historical bar first + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + double firstValue = indicator.LinesSeries[0].GetValue(0); + + // Update with new tick (same bar data - simulates intrabar update) + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick)); + double secondValue = indicator.LinesSeries[0].GetValue(0); + + // Both values should be finite + Assert.True(double.IsFinite(firstValue)); + Assert.True(double.IsFinite(secondValue)); + } + + [Fact] + public void AlmaIndicator_MultipleUpdates_ProducesCorrectAlmaSequence() + { + var indicator = new AlmaIndicator { Period = 3 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + double[] closes = { 100, 102, 104, 103, 105, 107, 106 }; + + foreach (var close in closes) + { + indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + now = now.AddMinutes(1); + } + + // All values should be finite + for (int i = 0; i < closes.Length; i++) + { + Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i))); + } + + // ALMA should be smoothing the values + double lastAlma = indicator.LinesSeries[0].GetValue(0); + Assert.True(lastAlma >= 100 && lastAlma <= 110); + } + + [Fact] + public void AlmaIndicator_DifferentSourceTypes_Work() + { + var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 }; + + foreach (var source in sources) + { + var indicator = new AlmaIndicator { Period = 3, Source = source }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 110, 90, 105); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + + Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)), + $"Source {source} should produce finite value"); + } + } + + [Fact] + public void AlmaIndicator_Period_CanBeChanged() + { + var indicator = new AlmaIndicator { Period = 5 }; + Assert.Equal(5, indicator.Period); + + indicator.Period = 20; + Assert.Equal(20, indicator.Period); + Assert.Equal(20, indicator.MinHistoryDepths); + } +} diff --git a/quantower/DemaIndicator.Tests.cs b/quantower/trends/DemaIndicator.Tests.cs similarity index 100% rename from quantower/DemaIndicator.Tests.cs rename to quantower/trends/DemaIndicator.Tests.cs diff --git a/quantower/EmaIndicator.Tests.cs b/quantower/trends/EmaIndicator.Tests.cs similarity index 100% rename from quantower/EmaIndicator.Tests.cs rename to quantower/trends/EmaIndicator.Tests.cs diff --git a/quantower/HmaIndicator.Tests.cs b/quantower/trends/HmaIndicator.Tests.cs similarity index 100% rename from quantower/HmaIndicator.Tests.cs rename to quantower/trends/HmaIndicator.Tests.cs diff --git a/quantower/trends/KamaIndicator.Tests.cs b/quantower/trends/KamaIndicator.Tests.cs new file mode 100644 index 00000000..bc979225 --- /dev/null +++ b/quantower/trends/KamaIndicator.Tests.cs @@ -0,0 +1,170 @@ +using Xunit; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib.Tests; + +public class KamaIndicatorTests +{ + [Fact] + public void KamaIndicator_Constructor_SetsDefaults() + { + var indicator = new KamaIndicator(); + + Assert.Equal(10, indicator.Period); + Assert.Equal(2, indicator.FastPeriod); + Assert.Equal(30, indicator.SlowPeriod); + Assert.Equal(SourceType.Close, indicator.Source); + Assert.True(indicator.ShowColdValues); + Assert.Equal("KAMA - Kaufman Adaptive Moving Average", indicator.Name); + Assert.False(indicator.SeparateWindow); + Assert.True(indicator.OnBackGround); + } + + [Fact] + public void KamaIndicator_MinHistoryDepths_EqualsPeriod() + { + var indicator = new KamaIndicator { Period = 20 }; + + Assert.Equal(20, indicator.MinHistoryDepths); + Assert.Equal(20, ((IWatchlistIndicator)indicator).MinHistoryDepths); + } + + [Fact] + public void KamaIndicator_ShortName_IncludesPeriodAndSource() + { + var indicator = new KamaIndicator { Period = 15 }; + + Assert.Contains("KAMA", indicator.ShortName); + Assert.Contains("15", indicator.ShortName); + } + + [Fact] + public void KamaIndicator_Initialize_CreatesInternalKama() + { + var indicator = new KamaIndicator { Period = 10 }; + + // Initialize should not throw + indicator.Initialize(); + + // After init, line series should exist + Assert.Single(indicator.LinesSeries); + } + + [Fact] + public void KamaIndicator_ProcessUpdate_HistoricalBar_ComputesValue() + { + var indicator = new KamaIndicator { Period = 3 }; + indicator.Initialize(); + + // Add historical data + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 102); + + // Process update + var args = new UpdateArgs(UpdateReason.HistoricalBar); + indicator.ProcessUpdate(args); + + // Line series should have a value + Assert.Equal(1, indicator.LinesSeries[0].Count); + Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0))); + } + + [Fact] + public void KamaIndicator_ProcessUpdate_NewBar_ComputesValue() + { + var indicator = new KamaIndicator { Period = 3 }; + indicator.Initialize(); + + // Add historical data + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 102); + indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106); + + // Process first update + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar)); + + // Line series should have values + Assert.Equal(2, indicator.LinesSeries[0].Count); + } + + [Fact] + public void KamaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError() + { + var indicator = new KamaIndicator { Period = 3 }; + indicator.Initialize(); + + // Add historical data + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 102); + + // Process historical bar first + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + double firstValue = indicator.LinesSeries[0].GetValue(0); + + // Update with new tick (same bar data - simulates intrabar update) + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick)); + double secondValue = indicator.LinesSeries[0].GetValue(0); + + // Both values should be finite + Assert.True(double.IsFinite(firstValue)); + Assert.True(double.IsFinite(secondValue)); + } + + [Fact] + public void KamaIndicator_MultipleUpdates_ProducesCorrectKamaSequence() + { + var indicator = new KamaIndicator { Period = 3 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + double[] closes = { 100, 102, 104, 103, 105, 107, 106 }; + + foreach (var close in closes) + { + indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + now = now.AddMinutes(1); + } + + // All values should be finite + for (int i = 0; i < closes.Length; i++) + { + Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i))); + } + + // KAMA should be smoothing the values + double lastKama = indicator.LinesSeries[0].GetValue(0); + Assert.True(lastKama >= 100 && lastKama <= 110); + } + + [Fact] + public void KamaIndicator_DifferentSourceTypes_Work() + { + var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 }; + + foreach (var source in sources) + { + var indicator = new KamaIndicator { Period = 3, Source = source }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 110, 90, 105); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + + Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)), + $"Source {source} should produce finite value"); + } + } + + [Fact] + public void KamaIndicator_Period_CanBeChanged() + { + var indicator = new KamaIndicator { Period = 5 }; + Assert.Equal(5, indicator.Period); + + indicator.Period = 20; + Assert.Equal(20, indicator.Period); + Assert.Equal(20, indicator.MinHistoryDepths); + } +} diff --git a/quantower/SmaIndicator.Tests.cs b/quantower/trends/SmaIndicator.Tests.cs similarity index 100% rename from quantower/SmaIndicator.Tests.cs rename to quantower/trends/SmaIndicator.Tests.cs diff --git a/quantower/T3Indicator.Tests.cs b/quantower/trends/T3Indicator.Tests.cs similarity index 100% rename from quantower/T3Indicator.Tests.cs rename to quantower/trends/T3Indicator.Tests.cs diff --git a/quantower/TemaIndicator.Tests.cs b/quantower/trends/TemaIndicator.Tests.cs similarity index 100% rename from quantower/TemaIndicator.Tests.cs rename to quantower/trends/TemaIndicator.Tests.cs diff --git a/quantower/TrimaIndicator.Tests.cs b/quantower/trends/TrimaIndicator.Tests.cs similarity index 100% rename from quantower/TrimaIndicator.Tests.cs rename to quantower/trends/TrimaIndicator.Tests.cs diff --git a/quantower/WmaIndicator.Tests.cs b/quantower/trends/WmaIndicator.Tests.cs similarity index 100% rename from quantower/WmaIndicator.Tests.cs rename to quantower/trends/WmaIndicator.Tests.cs