mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-29 02:07:42 +00:00
feat: Implement ADX Indicator with Quantower integration
This commit is contained in:
@@ -1,183 +0,0 @@
|
||||
# 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
|
||||
|
||||
* **Source Material:** The algorithm and markdown documentation foundation should be sourced from [https://github.com/mihakralj/pinescript/blob/main/indicators/](PineScript).
|
||||
* **Zero Allocation:** The core calculation loop must not allocate memory on the heap. Use `stackalloc`, `Span<T>`, 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.
|
||||
* **Time Handling:** Always use `DateTime.UtcNow` instead of `DateTime.Now` to ensure consistent time handling across timezones.
|
||||
|
||||
## 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` |
|
||||
| **Quantower Tests** | Quantower adapter tests | `[Name].Quantower.Tests.cs` |
|
||||
|
||||
## 3. Implementation Rules (`[Name].cs`)
|
||||
|
||||
### Class Definition
|
||||
|
||||
* **Namespace:** `QuanTAlib`
|
||||
* **Attributes:** `[SkipLocalsInit]` for performance.
|
||||
* **Modifiers:** `public sealed class`
|
||||
* **Interface:** Implements `ITValuePublisher`
|
||||
|
||||
### State Management
|
||||
|
||||
* **Scalar State:** Use a `private record struct State` to group all scalar state variables. This ensures value semantics, automatic `IEquatable` implementation, and cleaner rollback logic.
|
||||
* **State Variables:** Maintain `private State _state;` (current) and `private State _p_state;` (previous valid state).
|
||||
* **Buffers:** Use `RingBuffer` for sliding window data.
|
||||
* **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. **State Rollback:**
|
||||
|
||||
```csharp
|
||||
if (isNew) {
|
||||
_p_state = _state;
|
||||
// ... update state (e.g. counters) ...
|
||||
} else {
|
||||
_state = _p_state;
|
||||
// ... update state ...
|
||||
}
|
||||
```
|
||||
|
||||
2. **Input Validation:** Check `double.IsFinite`. If not, use `_lastValidValue` (stored in `State`).
|
||||
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 (or full series if recursive).
|
||||
|
||||
### 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<double> source, Span<double> output, ...)`
|
||||
* **Attribute:** `[MethodImpl(MethodImplOptions.AggressiveInlining)]`
|
||||
* **Optimization:**
|
||||
|
||||
* Check for SIMD support (`Avx2.IsSupported`).
|
||||
* Use `stackalloc` for small buffers (threshold ~256) and for internal state buffers in recursive algorithms where SIMD is not applicable.
|
||||
* 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
|
||||
* **Data Generation:** Use `GBM` (Geometric Brownian Motion) for generating realistic test data. Avoid using `System.Random` directly.
|
||||
* **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 **ALL** available external libraries (Skender, TA-Lib, Tulip, OoplesFinance, Python libraries, etc.) where the indicator is implemented. You must actively search for existing implementations to validate against.
|
||||
* **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).
|
||||
* **No Issues:** Ensure that markdownlint shows no issues for the file.
|
||||
|
||||
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. Quantower Adapter
|
||||
|
||||
* **Implementation:** Create a wrapper class in `[Name].Quantower.cs` that adapts the QuanTAlib indicator for the Quantower platform.
|
||||
* **Tests:** Create unit tests in `[Name].Quantower.Tests.cs` to verify the adapter's functionality using mocks where necessary.
|
||||
|
||||
## 7. Code Review
|
||||
|
||||
* **Tool:** Run CodeRabbit on the changes.
|
||||
* **Requirement:** Address and fix **ALL** issues identified by the CodeRabbit review before considering the task complete.
|
||||
|
||||
## 8. 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.
|
||||
|
||||
## 9. Checklist for New Indicators
|
||||
|
||||
* [ ] **Source Material:** Sourced algorithm and docs from `mihakralj/pinescript` or `mihakralj/quantalib`?
|
||||
* [ ] **File Structure:** Created all 6 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 **ALL** available external libraries?
|
||||
* [ ] **Docs:** Markdown file created with formula and examples? Linted (MD030, MD032)?
|
||||
* [ ] **Quantower:** Adapter created in `[Name].Quantower.cs`?
|
||||
* [ ] **Quantower Tests:** Adapter tests created in `[Name].Quantower.Tests.cs`?
|
||||
* [ ] **Code Review:** Ran CodeRabbit and fixed all issues?
|
||||
* [ ] **Index:** Added to category `_index.md` with link and description?
|
||||
* [ ] **Performance:** No allocations in `Update`? `[SkipLocalsInit]` used?
|
||||
@@ -26,6 +26,17 @@ We do not store objects in lists. We store primitive arrays.
|
||||
* `TSeries`: The primary data structure for time series.
|
||||
* `ITValuePublisher`: The interface for reactive data flow.
|
||||
|
||||
### Design Principles
|
||||
|
||||
* **Source Material:** The algorithm and markdown documentation foundation should be sourced from [https://github.com/mihakralj/pinescript/blob/main/indicators/](PineScript).
|
||||
* **Zero Allocation:** The core calculation loop must not allocate memory on the heap. Use `stackalloc`, `Span<T>`, 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.
|
||||
* **Time Handling:** Always use `DateTime.UtcNow` instead of `DateTime.Now` to ensure consistent time handling across timezones.
|
||||
|
||||
### Performance Rules
|
||||
|
||||
1. **Zero Allocation**: The `Update` method MUST NOT allocate memory on the heap. Use `stackalloc` or pre-allocated buffers.
|
||||
@@ -44,13 +55,33 @@ Directory: `lib/[category]/[name]/` (e.g., `lib/trends/sma/`)
|
||||
|
||||
| File | Naming | Purpose |
|
||||
|------|--------|---------|
|
||||
| **Source** | `[Name].cs` | Main logic. `public sealed class`. |
|
||||
| **Source** | `[Name].cs` | Main implementation. `public sealed class`. |
|
||||
| **Tests** | `[Name].Tests.cs` | xUnit tests (correctness, edge cases). |
|
||||
| **Validation** | `[Name].Validation.Tests.cs` | Compare against TA-Lib, Skender, etc. |
|
||||
| **Docs** | `[Name].md` | User documentation with formulas. |
|
||||
| **Adapter** | `[Name].Quantower.cs` | Quantower platform integration. |
|
||||
| **Adapter Tests** | `[Name].Quantower.Tests.cs` | Tests for the adapter. |
|
||||
|
||||
### Class Definition
|
||||
|
||||
* **Namespace:** `QuanTAlib`
|
||||
* **Attributes:** `[SkipLocalsInit]` for performance.
|
||||
* **Modifiers:** `public sealed class`
|
||||
* **Interface:** Implements `ITValuePublisher`
|
||||
|
||||
### State Management
|
||||
|
||||
* **Scalar State:** Use a `private record struct State` to group all scalar state variables. This ensures value semantics, automatic `IEquatable` implementation, and cleaner rollback logic.
|
||||
* **State Variables:** Maintain `private State _state;` (current) and `private State _p_state;` (previous valid state).
|
||||
* **Buffers:** Use `RingBuffer` for sliding window data.
|
||||
* **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, ...)`
|
||||
|
||||
### The `Update` Method Contract
|
||||
|
||||
The `Update` method is the heart of the indicator.
|
||||
@@ -59,35 +90,94 @@ The `Update` method is the heart of the indicator.
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
```
|
||||
|
||||
* **`isNew = true`**: A new bar has arrived. Save current state to history (or `_p_` variables), then calculate.
|
||||
* **`isNew = false`**: The current bar is updating (tick data). Restore state from history (or `_p_` variables), then recalculate.
|
||||
* **NaN Handling**: If input is `NaN` or `Infinity`, use the last valid value. Never propagate `NaN`.
|
||||
* **Attribute:** `[MethodImpl(MethodImplOptions.AggressiveInlining)]`
|
||||
* **Logic:**
|
||||
1. **State Rollback:**
|
||||
|
||||
### State Management
|
||||
```csharp
|
||||
if (isNew) {
|
||||
_p_state = _state;
|
||||
// ... update state (e.g. counters) ...
|
||||
} else {
|
||||
_state = _p_state;
|
||||
// ... update state ...
|
||||
}
|
||||
```
|
||||
|
||||
* **Scalar State:** Use a `private record struct State` to group all scalar state variables. This ensures value semantics, automatic `IEquatable` implementation, and cleaner rollback logic.
|
||||
* **State Variables:** Maintain `private State _state;` (current) and `private State _p_state;` (previous valid state).
|
||||
* **Buffers:** Use `RingBuffer` for sliding windows.
|
||||
* **Resync:** Periodically recalculate running sums to prevent floating-point drift.
|
||||
2. **Input Validation:** Check `double.IsFinite`. If not, use `_lastValidValue` (stored in `State`).
|
||||
3. **Calculation:** Perform the math.
|
||||
4. **Publish:** Update `Last` property, invoke `Pub` event, return `Last`.
|
||||
|
||||
### Dual API Requirement
|
||||
### Update Method (TSeries)
|
||||
|
||||
1. **Stateful (Streaming)**: `Update(TValue)` for live data.
|
||||
2. **Stateless (Vector)**: `static void Calculate(ReadOnlySpan<double> src, Span<double> dst)` for batch history.
|
||||
* **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 (or full series if recursive).
|
||||
|
||||
### 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<double> source, Span<double> output, ...)`
|
||||
* **Attribute:** `[MethodImpl(MethodImplOptions.AggressiveInlining)]`
|
||||
* **Optimization:**
|
||||
* Check for SIMD support (`Avx2.IsSupported`).
|
||||
* Use `stackalloc` for small buffers (threshold ~256) and for internal state buffers in recursive algorithms where SIMD is not applicable.
|
||||
* 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 Protocol
|
||||
|
||||
### Unit Tests (`[Name].Tests.cs`)
|
||||
|
||||
* Use `GBM` (Geometric Brownian Motion) for data generation.
|
||||
* Test `isNew=true` vs `isNew=false` consistency.
|
||||
* Test `Reset()` and `IsHot` (warmup).
|
||||
* Test edge cases: `NaN` inputs, empty series, period=1.
|
||||
* **Framework:** xUnit
|
||||
* **Data Generation:** Use `GBM` (Geometric Brownian Motion) for generating realistic test data. Avoid using `System.Random` directly.
|
||||
* **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`)
|
||||
|
||||
* **Mandatory**: You MUST validate against at least one external authority (TA-Lib, Skender, Tulip, OoplesFinance, Python libs).
|
||||
* **Tolerance**: Typically `1e-6` to `1e-9`.
|
||||
* **Data**: Use `ValidationTestData` class which wraps `GBM` (Geometric Brownian Motion) to generate realistic test data and provides pre-calculated Skender quotes.
|
||||
|
||||
#### External Library Usage Guide
|
||||
|
||||
* **Skender.Stock.Indicators:**
|
||||
* Use `_data.SkenderQuotes.Get[Indicator](...)`.
|
||||
* Compare using `ValidationHelper.VerifyData`.
|
||||
|
||||
* **TA-Lib (TALib.NETCore):**
|
||||
* Namespace: `using TALib;`
|
||||
* Method: `TALib.Functions.[Indicator]<double>(...)`.
|
||||
* Check `Assert.Equal(Core.RetCode.Success, retCode)`.
|
||||
* Use `ValidationHelper.VerifyData` with `outRange` and `lookback`.
|
||||
|
||||
* **Tulip (Tulip.NETCore):**
|
||||
* Namespace: `using Tulip;`
|
||||
* Method: `Tulip.Indicators.[indicator].Run(...)`.
|
||||
* Handle lookback/offset manually (Tulip output is shorter than input).
|
||||
* Use `ValidationHelper.VerifyData` with `lookback`.
|
||||
|
||||
* **OoplesFinance.StockIndicators:**
|
||||
* Namespace: `using OoplesFinance.StockIndicators;`
|
||||
* Convert data: `_data.SkenderQuotes.Select(q => new TickerData { ... }).ToList()`.
|
||||
* Use `new StockData(ooplesData).Calculate[Indicator](...)`.
|
||||
* Compare using `ValidationHelper.VerifyData`.
|
||||
|
||||
## 5. Documentation Standards
|
||||
|
||||
@@ -95,8 +185,20 @@ public TValue Update(TValue input, bool isNew = true)
|
||||
* **Content**: Title, Description, Parameters, Formula (LaTeX), C# Usage Examples.
|
||||
* **Index**: Add the new indicator to the category index (e.g., `lib/trends/_index.md`).
|
||||
* **Linting**: Ensure that markdownlint shows no issues for the file.
|
||||
* **MD030:** Ensure exactly one space after list markers.
|
||||
* **MD032:** Ensure lists are surrounded by blank lines.
|
||||
|
||||
## 6. Development Checklist
|
||||
## 6. Quantower Adapter
|
||||
|
||||
* **Implementation:** Create a wrapper class in `[Name].Quantower.cs` that adapts the QuanTAlib indicator for the Quantower platform.
|
||||
* **Tests:** Create unit tests in `[Name].Quantower.Tests.cs` to verify the adapter's functionality using mocks where necessary.
|
||||
|
||||
## 7. Code Review
|
||||
|
||||
* **Tool:** Run CodeRabbit on the changes.
|
||||
* **Requirement:** Address and fix **ALL** issues identified by the CodeRabbit review before considering the task complete.
|
||||
|
||||
## 8. Development Checklist
|
||||
|
||||
When creating a new indicator, you are **DONE** only when:
|
||||
|
||||
@@ -108,9 +210,10 @@ When creating a new indicator, you are **DONE** only when:
|
||||
* [ ] Unit tests pass (including edge cases).
|
||||
* [ ] Validation tests pass against external libs.
|
||||
* [ ] Documentation is complete and linked in `_index.md`.
|
||||
* [ ] Quantower adapter and tests are implemented.
|
||||
* [ ] CodeRabbit review issues are resolved.
|
||||
|
||||
## 7. Forbidden Actions
|
||||
## 9. Forbidden Actions
|
||||
|
||||
* **DO NOT** use LINQ in hot paths (`Update` or `Calculate`).
|
||||
* **DO NOT** use `new` inside `Update`.
|
||||
@@ -118,7 +221,7 @@ When creating a new indicator, you are **DONE** only when:
|
||||
* **DO NOT** remove `[SkipLocalsInit]` or `[MethodImpl]` attributes.
|
||||
* **DO NOT** ignore `NaN` inputs; handle them safely.
|
||||
|
||||
## 8. Context & Resources
|
||||
## 10. Context & Resources
|
||||
|
||||
* **Time**: Use `DateTime.UtcNow`.
|
||||
* **Math**: Use `System.Math` or `System.Numerics`.
|
||||
|
||||
@@ -4,6 +4,7 @@ Trend indicators help identify the direction and strength of a market trend. Mov
|
||||
|
||||
| Indicator | Full Name | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| [ADX](adx/Adx.md) | Average Directional Index | Measures the strength of a trend, regardless of its direction. |
|
||||
| ALLIGATOR | Williams Alligator | |
|
||||
| [ALMA](alma/Alma.md) | Arnaud Legoux MA | Uses Gaussian distribution weights to balance smoothness and responsiveness. |
|
||||
| AMAT | Archer Moving Averages Trends | |
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using Xunit;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AdxIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void AdxIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new AdxIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("ADX - Average Directional Index", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdxIndicator_MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var indicator = new AdxIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(20, indicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(20, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdxIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new AdxIndicator { Period = 20 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("ADX", indicator.ShortName);
|
||||
Assert.Contains("20", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdxIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new AdxIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink);
|
||||
Assert.Contains("Adx.Quantower.cs", indicator.SourceCodeLink);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdxIndicator_Initialize_CreatesInternalAdx()
|
||||
{
|
||||
var indicator = new AdxIndicator { Period = 14 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (ADX, +DI, -DI)
|
||||
Assert.Equal(3, indicator.LinesSeries.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdxIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AdxIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for Period
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double adx = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(adx));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class AdxIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Adx? _adx;
|
||||
protected LineSeries? AdxSeries;
|
||||
protected LineSeries? DiPlusSeries;
|
||||
protected LineSeries? DiMinusSeries;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"ADX {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/adx/Adx.Quantower.cs";
|
||||
|
||||
public AdxIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "ADX - Average Directional Index";
|
||||
Description = "Measures the strength of a trend";
|
||||
|
||||
AdxSeries = new(name: "ADX", color: Color.Blue, width: 2, style: LineStyle.Solid);
|
||||
DiPlusSeries = new(name: "+DI", color: Color.Green, width: 1, style: LineStyle.Solid);
|
||||
DiMinusSeries = new(name: "-DI", color: Color.Red, width: 1, style: LineStyle.Solid);
|
||||
|
||||
AddLineSeries(AdxSeries);
|
||||
AddLineSeries(DiPlusSeries);
|
||||
AddLineSeries(DiMinusSeries);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_adx = new Adx(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
|
||||
|
||||
TBar bar = this.GetInputBar(args);
|
||||
|
||||
TValue result = _adx!.Update(bar, isNew);
|
||||
|
||||
if (!_adx.IsHot && !ShowColdValues)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AdxSeries!.SetValue(result.Value);
|
||||
DiPlusSeries!.SetValue(_adx.DiPlus.Value);
|
||||
DiMinusSeries!.SetValue(_adx.DiMinus.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AdxTests
|
||||
{
|
||||
private readonly GBM _gbm = new();
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ThrowsArgumentException_WhenPeriodIsInvalid()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Adx(0));
|
||||
Assert.Throws<ArgumentException>(() => new Adx(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsValidValues_WhenInputIsValid()
|
||||
{
|
||||
var adx = new Adx(14);
|
||||
var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = adx.Update(bar);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_HandlesIsNewCorrectly()
|
||||
{
|
||||
var adx = new Adx(14);
|
||||
// We need enough bars to warm up ADX (2 * Period)
|
||||
int count = 2 * 14 + 5;
|
||||
var bars = _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed all but last bar
|
||||
for (int i = 0; i < count - 1; i++)
|
||||
{
|
||||
adx.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Update with last bar (isNew=true)
|
||||
var result1 = adx.Update(bars[count - 1], true);
|
||||
|
||||
// Update with modified last bar (isNew=false)
|
||||
var modifiedBar = new TBar(bars[count - 1].Time, bars[count - 1].Open, bars[count - 1].High + 1, bars[count - 1].Low - 1, bars[count - 1].Close, bars[count - 1].Volume);
|
||||
var result2 = adx.Update(modifiedBar, false);
|
||||
|
||||
// The result should change because High/Low changed, affecting TR and DM
|
||||
Assert.NotEqual(result1.Value, result2.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ResetsState()
|
||||
{
|
||||
var adx = new Adx(14);
|
||||
var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); // Increased to 100
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
adx.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(adx.IsHot);
|
||||
adx.Reset();
|
||||
Assert.False(adx.IsHot);
|
||||
Assert.Equal(0, adx.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrue_AfterWarmup()
|
||||
{
|
||||
var adx = new Adx(14);
|
||||
var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
int i = 0;
|
||||
for (; i < bars.Count; i++)
|
||||
{
|
||||
adx.Update(bars[i]);
|
||||
if (adx.IsHot) break;
|
||||
}
|
||||
|
||||
Assert.True(i < bars.Count);
|
||||
Assert.True(adx.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_HandlesNaN_Gracefully()
|
||||
{
|
||||
var adx = new Adx(14);
|
||||
var bar = new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 0);
|
||||
|
||||
var result = adx.Update(bar);
|
||||
|
||||
// Should not throw and return finite value (likely 0 or last valid)
|
||||
// Since it's the first value, it might be 0.
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_ReturnsValidResult()
|
||||
{
|
||||
var adx = new Adx(14);
|
||||
var val = new TValue(DateTime.UtcNow, 100);
|
||||
|
||||
var result = adx.Update(val);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using Xunit;
|
||||
using QuanTAlib.Tests;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class AdxValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
|
||||
public AdxValidationTests()
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_data.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchesSkender()
|
||||
{
|
||||
var adx = new Adx(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
var res = adx.Update(_data.Bars[i]);
|
||||
results.Add(res.Value);
|
||||
}
|
||||
|
||||
var skenderResults = _data.SkenderQuotes.GetAdx(14).ToList();
|
||||
|
||||
ValidationHelper.VerifyData(results, skenderResults, x => x.Adx);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchesTalib()
|
||||
{
|
||||
var adx = new Adx(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
var res = adx.Update(_data.Bars[i]);
|
||||
results.Add(res.Value);
|
||||
}
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] cData = _data.Bars.Close.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = TALib.Functions.Adx(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.AdxLookback(14);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// ADX: Average Directional Index
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ADX measures the strength of a trend, regardless of its direction.
|
||||
/// It is derived from the Smoothed Directional Movement Index (DX).
|
||||
///
|
||||
/// Calculation:
|
||||
/// 1. Calculate True Range (TR), +DM, and -DM
|
||||
/// 2. Smooth TR, +DM, -DM using RMA (Wilder's Moving Average)
|
||||
/// - First value is SMA of first Period values
|
||||
/// - Subsequent values: Previous + (Input - Previous) / Period
|
||||
/// 3. Calculate +DI = (+DM_smooth / TR_smooth) * 100
|
||||
/// 4. Calculate -DI = (-DM_smooth / TR_smooth) * 100
|
||||
/// 5. Calculate DX = |(+DI - -DI) / (+DI + -DI)| * 100
|
||||
/// 6. ADX = RMA(DX)
|
||||
/// - First value is SMA of first Period DX values
|
||||
/// - Subsequent values: Previous + (Input - Previous) / Period
|
||||
///
|
||||
/// Sources:
|
||||
/// https://www.investopedia.com/terms/a/adx.asp
|
||||
/// "New Concepts in Technical Trading Systems" by J. Welles Wilder
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Adx : ITValuePublisher
|
||||
{
|
||||
private readonly int _period;
|
||||
private TBar _prevBar;
|
||||
private TBar _p_prevBar;
|
||||
private bool _isInitialized;
|
||||
|
||||
// State for TR, +DM, -DM smoothing
|
||||
private double _trSum, _dmPlusSum, _dmMinusSum;
|
||||
private double _p_trSum, _p_dmPlusSum, _p_dmMinusSum;
|
||||
private int _samples;
|
||||
private int _p_samples;
|
||||
|
||||
private double _trSmooth, _dmPlusSmooth, _dmMinusSmooth;
|
||||
private double _p_trSmooth, _p_dmPlusSmooth, _p_dmMinusSmooth;
|
||||
|
||||
// State for ADX smoothing
|
||||
private double _dxSum;
|
||||
private double _p_dxSum;
|
||||
private int _dxSamples;
|
||||
private int _p_dxSamples;
|
||||
|
||||
private double _adx;
|
||||
private double _p_adx;
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
public event Action<TValue>? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Current ADX value.
|
||||
/// </summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current +DI value.
|
||||
/// </summary>
|
||||
public TValue DiPlus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current -DI value.
|
||||
/// </summary>
|
||||
public TValue DiMinus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the ADX has warmed up and is providing valid results.
|
||||
/// </summary>
|
||||
public bool IsHot => _dxSamples >= _period;
|
||||
|
||||
/// <summary>
|
||||
/// Creates ADX with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Period for ADX calculation (must be > 0)</param>
|
||||
public Adx(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_period = period;
|
||||
Name = $"Adx({period})";
|
||||
_isInitialized = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the ADX state.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_prevBar = default;
|
||||
_p_prevBar = default;
|
||||
_isInitialized = false;
|
||||
|
||||
_trSum = _dmPlusSum = _dmMinusSum = 0;
|
||||
_p_trSum = _p_dmPlusSum = _p_dmMinusSum = 0;
|
||||
_samples = _p_samples = 0;
|
||||
|
||||
_trSmooth = _dmPlusSmooth = _dmMinusSmooth = 0;
|
||||
_p_trSmooth = _p_dmPlusSmooth = _p_dmMinusSmooth = 0;
|
||||
|
||||
_dxSum = _p_dxSum = 0;
|
||||
_dxSamples = _p_dxSamples = 0;
|
||||
|
||||
_adx = _p_adx = 0;
|
||||
|
||||
Last = default;
|
||||
DiPlus = default;
|
||||
DiMinus = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_prevBar = _prevBar;
|
||||
_p_trSum = _trSum;
|
||||
_p_dmPlusSum = _dmPlusSum;
|
||||
_p_dmMinusSum = _dmMinusSum;
|
||||
_p_samples = _samples;
|
||||
_p_trSmooth = _trSmooth;
|
||||
_p_dmPlusSmooth = _dmPlusSmooth;
|
||||
_p_dmMinusSmooth = _dmMinusSmooth;
|
||||
_p_dxSum = _dxSum;
|
||||
_p_dxSamples = _dxSamples;
|
||||
_p_adx = _adx;
|
||||
}
|
||||
else
|
||||
{
|
||||
_prevBar = _p_prevBar;
|
||||
_trSum = _p_trSum;
|
||||
_dmPlusSum = _p_dmPlusSum;
|
||||
_dmMinusSum = _p_dmMinusSum;
|
||||
_samples = _p_samples;
|
||||
_trSmooth = _p_trSmooth;
|
||||
_dmPlusSmooth = _p_dmPlusSmooth;
|
||||
_dmMinusSmooth = _p_dmMinusSmooth;
|
||||
_dxSum = _p_dxSum;
|
||||
_dxSamples = _p_dxSamples;
|
||||
_adx = _p_adx;
|
||||
}
|
||||
|
||||
if (!_isInitialized)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_prevBar = input;
|
||||
_isInitialized = true;
|
||||
}
|
||||
return new TValue(input.Time, 0);
|
||||
}
|
||||
|
||||
// Calculate TR
|
||||
double hl = input.High - input.Low;
|
||||
double hpc = Math.Abs(input.High - _prevBar.Close);
|
||||
double lpc = Math.Abs(input.Low - _prevBar.Close);
|
||||
double tr = Math.Max(hl, Math.Max(hpc, lpc));
|
||||
|
||||
// Calculate DM
|
||||
double dmPlus = 0;
|
||||
double dmMinus = 0;
|
||||
double upMove = input.High - _prevBar.High;
|
||||
double downMove = _prevBar.Low - input.Low;
|
||||
|
||||
if (upMove > downMove && upMove > 0)
|
||||
dmPlus = upMove;
|
||||
|
||||
if (downMove > upMove && downMove > 0)
|
||||
dmMinus = downMove;
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_prevBar = input;
|
||||
}
|
||||
|
||||
// Smooth TR, +DM, -DM
|
||||
if (_samples < _period)
|
||||
{
|
||||
_trSum += tr;
|
||||
_dmPlusSum += dmPlus;
|
||||
_dmMinusSum += dmMinus;
|
||||
_samples++;
|
||||
|
||||
if (_samples == _period)
|
||||
{
|
||||
// Wilder's initialization for TR, +DM, and -DM uses the un-averaged sum (scaled sum).
|
||||
// Since +DI and -DI are ratios (+DM/TR and -DM/TR), the scaling factor (1/Period)
|
||||
// cancels out mathematically. This differs from the ADX smoothing later, which
|
||||
// explicitly uses a true SMA (sum / Period) for its initialization.
|
||||
_trSmooth = _trSum;
|
||||
_dmPlusSmooth = _dmPlusSum;
|
||||
_dmMinusSmooth = _dmMinusSum;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// RMA: Previous + (Input - Previous) / Period
|
||||
// Or: Previous * (1 - 1/Period) + Input * (1/Period)
|
||||
// Or: (Previous * (Period - 1) + Input) / Period
|
||||
// Wilder uses sums, but effectively it's RMA.
|
||||
// Standard formula:
|
||||
// Smooth = Smooth - (Smooth / Period) + Input
|
||||
|
||||
_trSmooth = _trSmooth - (_trSmooth / _period) + tr;
|
||||
_dmPlusSmooth = _dmPlusSmooth - (_dmPlusSmooth / _period) + dmPlus;
|
||||
_dmMinusSmooth = _dmMinusSmooth - (_dmMinusSmooth / _period) + dmMinus;
|
||||
}
|
||||
|
||||
// Calculate DI and DX
|
||||
double diPlus = 0;
|
||||
double diMinus = 0;
|
||||
double dx = 0;
|
||||
|
||||
if (_samples >= _period)
|
||||
{
|
||||
if (_trSmooth > 1e-10)
|
||||
{
|
||||
diPlus = (_dmPlusSmooth / _trSmooth) * 100.0;
|
||||
diMinus = (_dmMinusSmooth / _trSmooth) * 100.0;
|
||||
}
|
||||
|
||||
double diSum = diPlus + diMinus;
|
||||
if (diSum > 1e-10)
|
||||
{
|
||||
dx = (Math.Abs(diPlus - diMinus) / diSum) * 100.0;
|
||||
}
|
||||
|
||||
// Smooth DX to get ADX
|
||||
if (_dxSamples < _period)
|
||||
{
|
||||
_dxSum += dx;
|
||||
_dxSamples++;
|
||||
|
||||
if (_dxSamples == _period)
|
||||
{
|
||||
_adx = _dxSum / _period; // First ADX is SMA of DX
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ADX = (Prior ADX * (Period - 1) + Current DX) / Period
|
||||
_adx = ((_adx * (_period - 1)) + dx) / _period;
|
||||
}
|
||||
}
|
||||
|
||||
DiPlus = new TValue(input.Time, diPlus);
|
||||
DiMinus = new TValue(input.Time, diMinus);
|
||||
Last = new TValue(input.Time, _adx);
|
||||
|
||||
Pub?.Invoke(Last);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
return Update(new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0), isNew);
|
||||
}
|
||||
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
var t = new List<long>(source.Count);
|
||||
var v = new List<double>(source.Count);
|
||||
|
||||
Reset();
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var val = Update(source[i], true);
|
||||
t.Add(val.Time);
|
||||
v.Add(val.Value);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public static TSeries Calculate(TBarSeries source, int period)
|
||||
{
|
||||
var adx = new Adx(period);
|
||||
return adx.Update(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# ADX - Average Directional Index
|
||||
|
||||
The Average Directional Index (ADX) is a technical analysis indicator used to determine the strength of a trend. The trend can be either up or down, and this is shown by two accompanying indicators, the Negative Directional Indicator (-DI) and the Positive Directional Indicator (+DI). Therefore, ADX consists of three separate lines.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- **Trend Strength:** ADX measures the strength of the trend, not the direction.
|
||||
- **Directional Movement:** +DI and -DI show the direction of the trend.
|
||||
- **Range:** ADX values range from 0 to 100. Values above 25 usually indicate a strong trend.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| Period | int | 14 | The number of periods used for the calculation. |
|
||||
|
||||
## Formula
|
||||
|
||||
1. **Calculate True Range (TR), +DM, and -DM:**
|
||||
$$TR = \max(High - Low, |High - PreviousClose|, |Low - PreviousClose|)$$
|
||||
$$+DM = \text{if } (High - PreviousHigh) > (PreviousLow - Low) \text{ and } (High - PreviousHigh) > 0 \text{ then } (High - PreviousHigh) \text{ else } 0$$
|
||||
$$-DM = \text{if } (PreviousLow - Low) > (High - PreviousHigh) \text{ and } (PreviousLow - Low) > 0 \text{ then } (PreviousLow - Low) \text{ else } 0$$
|
||||
|
||||
2. **Smooth TR, +DM, -DM:**
|
||||
Using Wilder's Moving Average (RMA) over `Period`.
|
||||
$$TR_{smooth} = RMA(TR, Period)$$
|
||||
$$+DM_{smooth} = RMA(+DM, Period)$$
|
||||
$$-DM_{smooth} = RMA(-DM, Period)$$
|
||||
|
||||
3. **Calculate +DI and -DI:**
|
||||
$$+DI = \frac{+DM_{smooth}}{TR_{smooth}} \times 100$$
|
||||
$$-DI = \frac{-DM_{smooth}}{TR_{smooth}} \times 100$$
|
||||
|
||||
4. **Calculate DX:**
|
||||
$$DX = \frac{|+DI - -DI|}{+DI + -DI} \times 100$$
|
||||
|
||||
5. **Calculate ADX:**
|
||||
$$ADX = RMA(DX, Period)$$
|
||||
|
||||
## C# Implementation
|
||||
|
||||
### Standard Usage
|
||||
|
||||
```csharp
|
||||
// Create ADX with period 14
|
||||
var adx = new Adx(14);
|
||||
|
||||
// Update with TBar
|
||||
var result = adx.Update(new TBar(time, open, high, low, close, volume));
|
||||
Console.WriteLine($"ADX: {result.Value}");
|
||||
```
|
||||
|
||||
### Streaming with TBarSeries
|
||||
|
||||
```csharp
|
||||
var adx = new Adx(14);
|
||||
var series = new TBarSeries();
|
||||
// ... populate series ...
|
||||
var results = adx.Update(series);
|
||||
```
|
||||
|
||||
### Static Calculation
|
||||
|
||||
```csharp
|
||||
var results = Adx.Calculate(series, 14);
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **ADX < 20:** Weak trend or non-trending market.
|
||||
- **ADX > 25:** Strong trend.
|
||||
- **ADX > 40:** Very strong trend.
|
||||
- **ADX > 50:** Extremely strong trend.
|
||||
|
||||
Traders typically use ADX to determine whether to use a trend-following system or a range-trading system. When ADX is high, trend-following strategies are preferred. When ADX is low, range-trading strategies are preferred.
|
||||
|
||||
## References
|
||||
|
||||
- [Investopedia - Average Directional Index (ADX)](https://www.investopedia.com/terms/a/adx.asp)
|
||||
- Wilder, J. Welles. *New Concepts in Technical Trading Systems*. Trend Research, 1978.
|
||||
Reference in New Issue
Block a user