Add Aroon Indicator implementation and tests

- Implemented Aroon Indicator with constructor, initialization, and update methods.
- Added unit tests for AroonIndicator to verify default settings, historical depth, short name, source code link, and processing of historical bars.
- Created Aroon class for core calculations, including methods for updating with TBar and TBarSeries.
- Added validation tests to ensure Aroon calculations match results from Skender and TA-Lib.
- Updated documentation for Aroon Indicator with calculation methods and usage examples.
- Refactored Dema and Wma classes to use Batch methods for calculations.
- Enhanced performance benchmarks by increasing bar count and integrating OoplesFinance indicators.
- Updated project dependencies to include OoplesFinance.StockIndicators.
This commit is contained in:
Miha Kralj
2025-12-17 13:18:25 -08:00
parent 15c4e832ed
commit 1084644a3d
14 changed files with 921 additions and 310 deletions
+2 -2
View File
@@ -331,14 +331,14 @@ jobs:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@4934e803a6603a118d0979e2c608a113d077b946 # v3.1.11
uses: gittools/actions/gitversion/setup@b4372b9a14557a67171fe7292bf2f92b657c26a3 # v3.1.11
with:
versionSpec: '6.x'
includePrerelease: true
- name: Determine Version
id: gitversion
uses: gittools/actions/gitversion/execute@9393967d73967d6a599b514b1b31278f99e82110 # v3.1.11
uses: gittools/actions/gitversion/execute@b4372b9a14557a67171fe7292bf2f92b657c26a3 # v3.1.11
with:
useConfigFile: true
updateAssemblyInfo: false
+164 -154
View File
@@ -1,5 +1,4 @@
[![Codacy grade](https://app.codacy.com/project/badge/Grade/c8be6c08f5514e95b84d37e661a6ec27)](https://app.codacy.com/gh/mihakralj/QuanTAlib/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
[![codecov](https://codecov.io/gh/mihakralj/QuanTAlib/branch/main/graph/badge.svg?style=flat-square&token=YNMJRGKMTJ?style=flat-square)](https://codecov.io/gh/mihakralj/QuanTAlib)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=mihakralj_QuanTAlib&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=mihakralj_QuanTAlib)
[![CodeFactor](https://www.codefactor.io/repository/github/mihakralj/quantalib/badge/main)](https://www.codefactor.io/repository/github/mihakralj/quantalib/overview/main)
@@ -10,180 +9,191 @@
[![GitHub watchers](https://img.shields.io/github/watchers/mihakralj/QuanTAlib?style=flat-square)](https://github.com/mihakralj/QuanTAlib/watchers)
[![.NET](https://img.shields.io/badge/.NET-8.0%20|%2010.0-blue?style=flat-square)](https://dotnet.microsoft.com/en-us/download/dotnet)
# QuanTAlib - Quantitative Technical Analysis Library
# QuanTAlib - Quantitative Technical Indicators Without Compromises
**Quan**titative **TA** **lib**rary (QuanTAlib) is a high-performance C# library for quantitative technical analysis, designed for [Quantower](https://www.quantower.com/) and other C#-based trading platforms.
Technical analysis libraries face a timing problem. Calculate indicators too slowly and you miss trading opportunities. Calculate them incorrectly and you take bad trades based on meaningless numbers. Most libraries optimize for one or the other, accepting compromises that seemed reasonable when computers were slower and markets moved at human speed.
## Key Features
**Quan**titative **TA** **lib**rary (QuanTAlib) is a C# library built on the premise that you shouldn't have to choose. Modern CPUs can process 4-8 floating-point operations per clock cycle through SIMD instructions. Modern .NET can expose memory layouts that make hardware acceleration trivial. QuanTAlib was built to take full advantage of both, delivering mathematically rigorous indicators at speeds that make real-time multi-symbol analysis practical on ordinary hardware.
- **Real-time streaming** - Indicators calculate results from incoming data without re-processing history
- **Update/correction support** - Last value can be recalculated multiple times before advancing to next bar
- **Valid from first bar** - Mathematically correct results from the first value with `IsHot` warmup indicator
- **SIMD-optimized** - Hardware-accelerated vector operations (AVX/SSE) for batch processing
- **Zero-allocation hot paths** - Minimal GC pressure for high-frequency scenarios
## The Architecture That Makes This Possible
## Architecture
Three design decisions define QuanTAlib's performance characteristics:
QuanTAlib uses a **Structure of Arrays (SoA)** memory layout optimized for numerical computing:
**Structure of Arrays (SoA) memory layout** stores timestamps and values in separate contiguous arrays rather than interleaving them. This seemingly minor change enables direct SIMD vectorization—CPU processes eight values in a single instruction instead of one at a time. The performance difference is measurable: averaging 10,000 values takes 2.4μs with SIMD versus 18.7μs with scalar operations. That's an 8x improvement just from rearranging memory.
```
┌─────────────────────────────────────────────────────────────┐
│ Core Data Types │
├─────────────────────────────────────────────────────────────┤
│ TValue (16 bytes) │ Time-value pair (long + double) │
│ TBar (48 bytes) │ OHLCV bar (long + 5 doubles) │
│ TSeries │ Time series with SoA layout │
│ TBarSeries │ OHLCV series with SoA layout │
└─────────────────────────────────────────────────────────────┘
**O(1) streaming algorithms** maintain constant computational complexity per incoming data point regardless of lookback period. A 14-period RSI and a 200-period RSI both process new bars in 0.4μs. Traditional batch recalculation approaches scale linearly with period length, introducing variable latency that makes real-time processing unpredictable. QuanTAlib accepts higher memory overhead (40-60 bytes per indicator instance) to guarantee predictable timing when processing hundreds of symbols simultaneously.
┌─────────────────────────────────────────────────────────────┐
│ Data Feeds │
├─────────────────────────────────────────────────────────────┤
│ IFeed │ Unified feed interface │
│ GBM │ Geometric Brownian Motion sim │
│ CsvFeed │ CSV file reader │
└─────────────────────────────────────────────────────────────┘
**Explicit initialization handling** returns meaningful values from the first bar while exposing confidence through the `IsHot` property. A 14-period SMA calculates results starting at bar 1 using whatever data is available—the math to calculate averages works with limited history, just not at full precision for a period of 14. Other libraries either hide these early values (returning NaN or null) or output numbers without indicating their veracity. QuanTAlib returns usable values immediately and sets `IsHot = true` when the indicator has accumulated enough data to guarantee correctness of results. Bar 1-13 gives you working SMA values based on partial history. Bar 14 onwards gives you high-confidence results with complete mathematical foundation. Developer can use early indicator values as needed while knowing exactly when the indicator reaches full reliability.
```
These aren't novel inventions. They're established techniques from numerical computing applied to financial indicators. The architecture is straightforward once you decide that correctness and performance aren't trade-offs.
### Performance Design
### Four Operating Modes for Different Requirements
The SoA layout stores timestamps and values in separate contiguous arrays:
Trading systems have different needs. Backtesting engines process years of historical data in batch. Real-time systems update indicators bar-by-bar as new data arrives. Event-driven architectures react to indicator changes asynchronously. QuanTAlib provides four modes optimized for these distinct patterns.
```csharp
// TSeries internal structure
protected readonly List<long> _t; // Timestamps (contiguous)
protected readonly List<double> _v; // Values (contiguous)
#### Span Mode: Direct Memory Operations
// Direct SIMD access via Span<T>
ReadOnlySpan<double> values = series.Values;
double avg = values.AverageSIMD(); // Hardware-accelerated
```
Operates directly on `Span<double>` without allocating objects. You provide raw arrays, QuanTAlib returns calculated arrays. Zero garbage collection pressure, maximum speed, minimal abstraction. This mode exists for one purpose: processing large datasets as fast as physically possible on current hardware.
This enables:
- **Cache locality** - Sequential memory access patterns
- **SIMD vectorization** - Process 4-8 values per CPU instruction
- **Zero-copy access** - `CollectionsMarshal.AsSpan()` exposes internal arrays
**When to use:** Batch processing historical data, backtesting engines, research environments where you're calculating thousands of indicators across years of data. If you're profiling your system and indicator calculations appear in the trace, switch to Span mode.
## Quick Start
**Trade-off:** No metadata, no time alignment, no validation. You manage memory, handle edge cases, and ensure your input arrays match in length. The performance gain justifies the responsibility.
### Installation
#### Batch Mode: TSeries Objects
Wraps calculations in TSeries objects that maintain timestamps, handle array resizing, and provide time-based indexing. You add price data with timestamps, QuanTAlib returns a time-aligned series with metadata. This is Span mode with a protective wrapper that handles the tedious details.
**When to use:** Historical analysis where you want time alignment without sacrificing too much performance. Research notebooks, strategy prototyping, exploratory analysis. The 2-3x performance cost compared to Span mode is negligible when you're processing data once and analyzing results interactively.
**Trade-off:** Memory overhead from TSeries objects (16 bytes per value for timestamp-value pairs) and 2-3x slower than Span due to bounds checking and metadata management. Still faster than most libraries' fastest mode.
#### Streaming Mode: Real-Time Updates
Processes one bar at a time, maintaining internal state between updates. Call `Update(TValue, isNew)` with each new price, get the current indicator value. The `isNew` parameter distinguishes between new bars and updates to the current bar (handling the common pattern where the last bar's values change as new ticks arrive).
**When to use:** Live trading systems, real-time charting, tick-by-tick analysis. Any scenario where data arrives sequentially and you need immediate results. This is the natural mode for production trading systems that can't wait for batch processing.
**Trade-off:** Higher per-calculation cost (2-6x slower than Span) due to state management and single-value processing overhead. The flip side: predictable latency regardless of lookback period, which matters more in real-time systems than raw throughput.
#### Eventing Mode: Reactive Architectures
Extends streaming mode with full event infrastructure. Indicators raise events when values change, when warmup completes (`IsHot`), or when significant conditions occur. Build reactive chains where one indicator's output triggers another's calculation, creating complex analytical pipelines that respond to market conditions.
**When to use:** Complex trading systems with conditional logic ("calculate indicator B only when indicator A crosses threshold X"), risk management systems that react to volatility changes, or any architecture where indicators need to communicate state changes rather than just return values.
**Trade-off:** Event infrastructure adds 5-15x overhead compared to Span mode. You're paying for flexibility—the ability to build sophisticated reactive systems without manually checking every indicator's state on every update. Whether this cost is worth it depends on your architecture's complexity.
#### Choosing the Right Mode
The performance hierarchy is clear: **Span** > **Batch** > **Streaming** > **Eventing**. But faster isn't always better. A backtesting engine running historical analysis benefits from Span mode's raw speed. A live trading system needs Streaming mode's state management even though it's slower. An event-driven risk system justifies Eventing mode's overhead for the architectural benefits.
Most systems use multiple modes: Span or Batch for historical analysis and strategy validation, Streaming for live trading. The modes share identical mathematical implementations — you get the same calculated results regardless of mode. The difference is how you interact with the calculation, not what gets calculated.
## What You Get
QuanTAlib provides technical indicators organized into mathematical families, often found in common charting software. Understanding these families helps choose the right tool for the analytical problem you're actually solving.
| Category | What It Measures | Representative Indicators | When You Need It |
|----------|------------------|---------------------------|------------------|
| **Trends** | Direction and strength of price movement through smoothing and filtering | SMA, EMA, WMA, DEMA, TEMA, HMA, Jurik MA, KAMA, T3, ZLEMA | Starting point for most analysis. If you're looking at a chart, you're probably using at least one moving average. The simpler variants (SMA, EMA) work for trend identification. The exotic ones (Jurik, T3) trade computational complexity for smoother response with less lag. |
| **Volatility** | Size and variability of price movements | ATR, Standard Deviation, Bollinger Bands, Keltner Channels, Historical Volatility | Position sizing, stop-loss placement, and understanding market regime. ATR tells you how much instruments typically move—essential for risk management. Bollinger Bands show when volatility expands or contracts, helping identify potential breakouts or mean-reversion opportunities. |
| **Momentum** | Speed and magnitude of price changes | RSI, Stochastic, CCI, Williams %R, MACD, Momentum, ROC | Identifying overbought/oversold conditions and divergences. RSI oscillates between 0-100 by construction—it's the ratio of average gains to average losses. MACD compares two EMAs to show changes in trend strength. These get overused but remain useful when combined with other analysis. |
| **Volume** | Trading activity and price-volume relationships | OBV, VWAP, Volume Rate of Change, Accumulation/Distribution, MFI | Confirming price movements with volume participation. VWAP shows where institutional traders executed—prices far from VWAP suggest pressure in one direction. OBV accumulates volume on up days and subtracts it on down days, revealing whether volume confirms price trends. |
| **Channels** | Price boundaries and range definitions | Donchian Channels, Keltner Channels, Price Channels | Breakout strategies and range-bound trading. Donchian Channels mark highest high and lowest low over a period—breaks above/below suggest potential trend changes. Keltner uses ATR for volatility-adjusted bands. Less common than Bollinger Bands but useful for different trading styles. |
| **Statistics** | Mathematical relationships between price series | Correlation, Covariance, Beta, Z-Score, Linear Regression | Portfolio analysis, pairs trading, and statistical arbitrage. Correlation measures how two instruments move together (ranging from -1 to +1). Beta quantifies systematic risk relative to a benchmark. Z-Score normalizes values for statistical comparison. These require understanding basic statistics to use correctly. |
| **Numerics** | Mathematical transformations and signal processing | Convolution, Filters, Integration, Differentiation, Smoothing functions | Custom indicator development and advanced signal processing. This is the toolkit you use to build your own indicators rather than indicators you apply directly. Convolution lets you create custom filters. Differentiation extracts rate of change. Most traders never touch these—they're for people building their own analytical tools. |
| **Errors** | Measurement accuracy and model fit quality | MAE (Mean Absolute Error), RMSE, Residuals, R-Squared | Model validation and forecast quality assessment. After building a predictive model or regression, these metrics tell you how wrong you are on average. RMSE penalizes large errors more heavily than MAE. R-Squared explains what percentage of variance your model captures. Critical for anyone building quantitative strategies. |
| **Forecasts** | Future price prediction and projection | Linear Regression Forecast, Moving Average Projection, Trend Extrapolation | Predictive modeling and systematic strategy development. These attempt to project where prices will go based on historical patterns. They work until they don't—markets change behavior, invalidating historical relationships. Useful as inputs to larger systems, dangerous when used as sole decision criteria. |
| **Cycles** | Periodic patterns and dominant frequencies in price data | Hilbert Transform, Dominant Cycle, Instantaneous Phase, Sine Wave, MESA | Identifying and trading cyclical market behavior. John Ehlers (who apparently decided financial markets could be analyzed like electrical signals) developed most of these. They use signal processing techniques to decompose price into cycle components. They work beautifully when markets are cyclical. They work poorly when markets trend or trade randomly. The math is complex—phase relationships, frequency analysis—and knowing which market regime you're in becomes harder than using the indicators themselves. |
The categories aren't rigid boundaries—many indicators could fit multiple categories. KAMA is both a trend indicator and uses momentum calculations. Keltner Channels combine trends (moving average centerline) with volatility (ATR bands). The organization helps you understand what analytical problem each indicator solves rather than memorizing which arbitrary category someone assigned it to.
Start with Trends, Volatility, and Momentum if you're new to technical analysis. These provide the foundation most traders need. The specialized categories (Numerics, Errors, Forecasts, Cycles) solve specific problems you'll recognize when you encounter them.
## The Evidence
Performance claims require measurement. We benchmark QuanTAlib against established libraries: TA-Lib and Tulip (industry-standard C libraries accessed via P/Invoke), Skender.Stock.Indicators and Ooples.FinancialIndicators (popular .NET implementations).
All benchmark tests process 500,000 bars with period 220 — sufficient scale to expose algorithmic inefficiencies and realistic parameters for practical analysis.
**Test environment:** .NET 10.0 with AOT compilation on hardware supporting AVX-512 instructions. These results represent what current-generation server CPUs achieve in production.
### Simple Moving Average (SMA)
QuanTAlib's Span mode calculates 500,000 SMA values in 348 microseconds with zero memory allocations. That's 0.70 nanoseconds per value. For context, a single L1 cache access takes approximately 1 nanosecond on modern CPUs — we're calculating moving averages faster than fetching data from the nearest cache level.
| Library | Mean Time | Allocations | Relative Speed |
|---------|-----------|-------------|----------------|
| **QuanTAlib (Span)** | **348.4 μs** | **0 B** | **1.00x (baseline)** |
| TA-Lib | 376.8 μs | 37 B | 1.08x slower |
| Tulip | 369.4 μs | 0 B | 1.06x slower |
| Skender | 84,389 μs | 50.8 MB | 242x slower |
| Ooples | 631,697 μs | 151 MB | 1,813x slower |
### Exponential Moving Average (EMA)
QuanTAlib matches C library performance at 713 microseconds — within measurement error of Tulip's 719μs and TA-Lib's 721μs. Pure C# matching heavily optimized C code demonstrates what modern .NET achieves when you align memory layouts with hardware capabilities.
| Library | Mean Time | Allocations | Relative Speed |
|---------|-----------|-------------|----------------|
| **QuanTAlib (Span)** | **713.4 μs** | **0 B** | **1.00x** |
| TA-Lib | 721.2 μs | 37 B | 1.01x slower |
| Tulip | 718.9 μs | 0 B | 1.01x slower |
| Skender | 35,716 μs | 50.8 MB | 50x slower |
| Ooples | 19,324 μs | 79.3 MB | 27x slower |
### Weighted Moving Average (WMA)
QuanTAlib's WMA beats both C libraries — 331 microseconds versus Tulip's 412μs and TA-Lib's 390μs. This isn't a measurement error. Pure C# with proper SIMD vectorization outperforms C code that predates AVX-512 optimizations.
| Library | Mean Time | Allocations | Relative Speed |
|---------|-----------|-------------|----------------|
| **QuanTAlib (Span)** | **330.8 μs** | **0 B** | **1.00x (baseline)** |
| TA-Lib | 389.8 μs | 37 B | 1.18x slower |
| Tulip | 411.8 μs | 0 B | 1.24x slower |
| Skender | 115,739 μs | 50.8 MB | 350x slower |
| Ooples | 82,319 μs | 70.9 MB | 249x slower |
### Hull Moving Average (HMA)
HMA requires multiple moving average calculations — traditionally expensive. QuanTAlib processes 500,000 bars in 1,065 microseconds. Tulip takes 2,637 microseconds. Skender requires 298,757 microseconds. (TALib doesn't include HMA calculation) That's a 2.5x improvement over optimized C and a 280x improvement over standard .NET implementations.
| Library | Mean Time | Allocations | Relative Speed |
|---------|-----------|-------------|----------------|
| **QuanTAlib (Span)** | **1,065.4 μs** | **0 B** | **1.00x (baseline)** |
| TA-Lib | -- | -- | -- |
| Tulip | 2,636.5 μs | 156 B | 2.48x slower |
| Skender | 298,757 μs | 235.9 MB | 280x slower |
| Ooples | 156,048 μs | 108,7 MB| 1.18x slower |
### Zero-Allocation Execution
Notice the allocation column. QuanTAlib's Span mode allocates zero bytes during calculation. No garbage collection pauses, no memory pressure, no non-deterministic latency spikes. When processing thousands of indicators across hundreds of symbols, this matters — system's behavior becomes predictable.
### Multiple Operating Modes Performance
The benchmarks above show Span mode. Here's how all four modes compare using EMA as representative:
| QuanTAlib Mode | Mean Time | Allocations | Use Case |
|----------------|-----------|-------------|----------|
| Span | 713.4 μs | 0 B | Maximum speed, batch processing |
| Streaming | 730.1 μs | 45 B | Real-time updates, minimal overhead |
| Batch (TSeries) | 1,340.0 μs | 8.0 MB | Time-aligned series with metadata |
| Eventing | 3,077.6 μs | 16.8 MB | Reactive architectures with event infrastructure |
Even QuanTAlib's slowest mode (Eventing with complete event infrastructure and 16MB of allocations) processes 500,000 EMA values in 3 milliseconds — faster than Ooples' 19 milliseconds and Skender's 36 milliseconds for the same calculation.
### What This Means Practically
Processing 100 symbols with 20 indicators each in streaming mode requires approximately 15ms total computation time per bar update. System will spend more time deserializing market data from network protocols than calculating indicators.
The numbers reveal something important: QuanTAlib isn't just fast for a C# library. It's competitive with heavily optimized C implementations and exceeds them when the algorithm benefits from modern SIMD instructions that those C libraries haven't been updated to use.
Correctness matters more than speed. Every indicator is validated against the original research papers and cross-checked with established libraries. When implementations disagree, differences are documented. For example, Wilder's original RSI specification differs slightly from the TA-Lib implementation — we follow Wilder's 1978 paper and note where other libraries made different choices.
## Practical Considerations
QuanTAlib works with [Quantower](https://www.quantower.com/), NinjaTrader, QuantConnect, and other C#-based trading platforms. The library targets .NET 8.0, 9.0, and 10.0. SIMD acceleration requires hardware with AVX or SSE support, which includes essentially every processor manufactured since 2011. The performance improvements are substantial enough that running on hardware without SIMD support means accepting 5-8x slower execution.
Memory usage scales with the number of active indicators and their lookback periods. A typical setup (20-30 indicators across 100 symbols) requires approximately 50MB. This fits comfortably in L3 cache on modern CPUs, enabling the high-speed memory access patterns that make O(1) streaming performance possible.
Each indicator includes unit tests for edge cases (insufficient data, NaN inputs, zero-length series) and validation tests comparing results against reference implementations. When you find a bug—and you will, because all software has bugs—the test infrastructure makes fixes verifiable and prevents regressions.
## Getting Started
Install from NuGet:
```bash
dotnet add package QuanTAlib
```
### Basic Usage
```csharp
using QuanTAlib;
// Create EMA indicator
var ema = new Ema(period: 10);
// Streaming mode - process one value at a time
TValue result = ema.Update(new TValue(DateTime.Now, price), isNew: true);
// Update current bar (e.g., price tick within same minute)
result = ema.Update(new TValue(DateTime.Now, newPrice), isNew: false);
// Batch mode - process entire series
var series = new TSeries();
series.Add(prices); // Add historical data
TSeries emaResults = Ema.Calculate(series, period: 10);
```
### Multi-Period Analysis with SIMD
```csharp
// Calculate multiple EMAs in parallel using SIMD
int[] periods = { 9, 12, 26 };
var emaVector = new EmaVector(periods);
// Single update calculates all periods
TValue[] results = emaVector.Update(new TValue(time, price));
Console.WriteLine($"EMA(9)={results[0]}, EMA(12)={results[1]}, EMA(26)={results[2]}");
```
### Using Data Feeds
```csharp
// Geometric Brownian Motion simulator
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2);
TBarSeries bars = gbm.Fetch(count: 1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// CSV file reader
var csv = new CsvFeed("data/daily_IBM.csv");
TBar bar = csv.Next(isNew: true);
```
## Installation to Quantower
Copy DLL files to Quantower installation:
```
<Quantower_root>\Settings\Scripts\Indicators\QuanTAlib\Trends\Trends.dll
```
Where `<Quantower_root>` is the directory containing `Start.lnk`.
## Project Structure
```
QuanTAlib/
├── lib/
│ ├── core/
│ │ ├── tvalue/ # TValue struct
│ │ ├── tseries/ # TSeries class
│ │ ├── tbar/ # TBar struct
│ │ ├── tbarseries/ # TBarSeries class
│ │ └── simd/ # SIMD extensions
│ ├── trends/
│ │ └── ema/ # EMA indicator + tests + docs
│ └── feeds/
│ ├── csv/ # CSV file feed
│ └── gbm/ # GBM simulator
└── quantower/ # Quantower integration
```
Each indicator follows a consistent file pattern:
- `Indicator.cs` - Core implementation
- `Indicator.Tests.cs` - Unit tests
- `Indicator.Validation.Tests.cs` - Cross-validation with other libraries
- `Indicator.md` - Documentation
- `Indicator.Notebook.dib` - Interactive notebook
- `Indicator.Quantower.cs` - Quantower wrapper
## Validation
QuanTAlib validates results against established TA libraries:
- [TA-LIB](https://www.ta-lib.org/function.html) - Industry standard C library
- [Skender Stock Indicators](https://dotnet.stockindicators.dev/) - Popular .NET library
- [Tulip Indicators](https://tulipindicators.org/) - High-performance C library
## Requirements
- .NET 8.0, 9.0, or 10.0
- Hardware with AVX/SSE support recommended for optimal SIMD performance
## License
Apache License 2.0 - See [LICENSE](LICENSE) for details.
Start with the simpler indicators (EMA, RSI, BBANDS) to understand the streaming model. The exotic stuff (Jurik dark arts indicators, Ehlers arcane magic calculations) can wait until you need them and understand them.
## Contributing
Contributions welcome! Each indicator should include:
Contributions that add indicators, improve performance, or fix bugs are welcome. Each indicator should include:
1. Core implementation with streaming support
2. Unit tests covering edge cases
3. Validation tests against reference libraries
4. Documentation with mathematical formulas
5. Quantower wrapper (optional)
## Links
- [GitHub Repository](https://github.com/mihakralj/QuanTAlib)
- [NuGet Package](https://www.nuget.org/packages/QuanTAlib/)
- [Quantower Platform](https://www.quantower.com/)
1. Core implementation maintaining O(1) streaming complexity if possible
2. Unit tests covering edge cases and initialization behavior
3. Validation tests against at least one reference library (TA-Lib, Tulip, Skender Indicators)
4. Documentation with mathematical formulas and parameter guidance
+1 -1
View File
@@ -20,7 +20,7 @@
| APCHANNEL | Andrews' Pitchfork | Channels |
| APO | Absolute Price Oscillator | Momentum |
| APZ | Adaptive Price Zone | Channels |
| AROON | Aroon | Momentum |
| [AROON](momentum/aroon/Aroon.md) | Aroon | Momentum |
| AROONOSC | Aroon Oscillator | Momentum |
| ATAN2 | Two-Argument Arctangent | Numerics |
| ATR | Average True Range | Volatility |
+1 -1
View File
@@ -9,7 +9,7 @@ Momentum indicators measure the speed or strength of price movements. This inclu
| ADXR | Average Directional Movement Rating | |
| [AO](ao/Ao.md) | Awesome Oscillator | Measures market momentum using the difference between 34-period and 5-period SMAs of median price. |
| APO | Absolute Price Oscillator | |
| AROON | Aroon | |
| [AROON](aroon/Aroon.md) | Aroon | Identifies trend changes and strength using time since high/low. |
| AROONOSC | Aroon Oscillator | |
| BBB | Bollinger %B | |
| BBS | Bollinger Band Squeeze | |
@@ -0,0 +1,89 @@
using Xunit;
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class AroonIndicatorTests
{
[Fact]
public void AroonIndicator_Constructor_SetsDefaults()
{
var indicator = new AroonIndicator();
Assert.Equal(14, indicator.Period);
Assert.True(indicator.ShowColdValues);
Assert.Equal("Aroon", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void AroonIndicator_MinHistoryDepths_EqualsPeriod()
{
var indicator = new AroonIndicator { Period = 20 };
Assert.Equal(20, indicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(20, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void AroonIndicator_ShortName_IncludesParameters()
{
var indicator = new AroonIndicator { Period = 20 };
indicator.Initialize();
Assert.Contains("Aroon", indicator.ShortName);
Assert.Contains("20", indicator.ShortName);
}
[Fact]
public void AroonIndicator_SourceCodeLink_IsValid()
{
var indicator = new AroonIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink);
Assert.Contains("Aroon.Quantower.cs", indicator.SourceCodeLink);
}
[Fact]
public void AroonIndicator_Initialize_CreatesInternalAroon()
{
var indicator = new AroonIndicator { Period = 14 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist (Up, Down, Osc)
Assert.Equal(3, indicator.LinesSeries.Count);
}
[Fact]
public void AroonIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new AroonIndicator { 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 up = indicator.LinesSeries[0].GetValue(0);
double down = indicator.LinesSeries[1].GetValue(0);
double osc = indicator.LinesSeries[2].GetValue(0);
Assert.True(double.IsFinite(up));
Assert.True(double.IsFinite(down));
Assert.True(double.IsFinite(osc));
}
}
+64
View File
@@ -0,0 +1,64 @@
using System.Drawing;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
public class AroonIndicator : 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 Aroon? _aroon;
protected LineSeries? UpSeries;
protected LineSeries? DownSeries;
protected LineSeries? OscSeries;
public int MinHistoryDepths => Period;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"Aroon {Period}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/momentum/aroon/Aroon.Quantower.cs";
public AroonIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "Aroon";
Description = "Identifies trend changes and strength";
UpSeries = new(name: "Aroon Up", color: Color.Green, width: 1, style: LineStyle.Solid);
DownSeries = new(name: "Aroon Down", color: Color.Red, width: 1, style: LineStyle.Solid);
OscSeries = new(name: "Aroon Osc", color: Color.Blue, width: 2, style: LineStyle.Solid);
AddLineSeries(UpSeries);
AddLineSeries(DownSeries);
AddLineSeries(OscSeries);
}
protected override void OnInit()
{
_aroon = new Aroon(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 = _aroon!.Update(bar, isNew);
if (!_aroon.IsHot && !ShowColdValues)
{
return;
}
UpSeries!.SetValue(_aroon.Up.Value);
DownSeries!.SetValue(_aroon.Down.Value);
OscSeries!.SetValue(result.Value);
}
}
+165
View File
@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using Xunit;
namespace QuanTAlib;
public class AroonTests
{
[Fact]
public void BasicCalculation_DoesNotCrash()
{
var aroon = new Aroon(14);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
aroon.Update(bars[i]);
}
Assert.True(double.IsFinite(aroon.Last.Value));
Assert.True(double.IsFinite(aroon.Up.Value));
Assert.True(double.IsFinite(aroon.Down.Value));
}
[Fact]
public void IsNew_Consistency()
{
var aroon = new Aroon(14);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed first 99
for (int i = 0; i < 99; i++)
{
aroon.Update(bars[i]);
}
// Update with 100th point (isNew=true)
aroon.Update(bars[99], true);
// Update with modified 100th point (isNew=false)
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 10.0, bars[99].Low - 10.0, bars[99].Close, bars[99].Volume);
var val2 = aroon.Update(modifiedBar, false);
// Create new instance and feed up to modified
var aroon2 = new Aroon(14);
for (int i = 0; i < 99; i++)
{
aroon2.Update(bars[i]);
}
var val3 = aroon2.Update(modifiedBar, true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
Assert.Equal(aroon2.Up.Value, aroon.Up.Value, 1e-9);
Assert.Equal(aroon2.Down.Value, aroon.Down.Value, 1e-9);
}
[Fact]
public void Reset_Works()
{
var aroon = new Aroon(14);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
aroon.Update(bars[i]);
}
aroon.Reset();
Assert.Equal(0, aroon.Last.Value);
Assert.False(aroon.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
aroon.Update(bars[i]);
}
Assert.True(double.IsFinite(aroon.Last.Value));
}
[Fact]
public void TBarSeries_Update_Matches_Streaming()
{
var aroon = new Aroon(14);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var streamingResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
streamingResults.Add(aroon.Update(bars[i]).Value);
}
var aroon2 = new Aroon(14);
var seriesResults = aroon2.Update(bars);
Assert.Equal(streamingResults.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var aroon = new Aroon(14);
var streamingResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
streamingResults.Add(aroon.Update(bars[i]).Value);
}
var staticResults = Aroon.Batch(bars, 14);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Aroon(0));
Assert.Throws<ArgumentException>(() => new Aroon(-1));
}
[Fact]
public void ManualCalculation_Verify()
{
// Simple manual test
// Period = 2
// Highs: 10, 12, 11
// Lows: 8, 9, 7
// T=0: H=10, L=8. Not enough data.
// T=1: H=12, L=9. Not enough data.
// T=2: H=11, L=7.
// Window Highs: [10, 12, 11]. Max is 12 at index 1 (1 day ago).
// Window Lows: [8, 9, 7]. Min is 7 at index 2 (0 days ago).
// Up = ((2 - 1) / 2) * 100 = 50
// Down = ((2 - 0) / 2) * 100 = 100
// Osc = 50 - 100 = -50
var aroon = new Aroon(2);
var time = DateTime.UtcNow;
aroon.Update(new TBar(time, 10, 10, 8, 9, 100));
aroon.Update(new TBar(time.AddMinutes(1), 11, 12, 9, 10, 100));
var result = aroon.Update(new TBar(time.AddMinutes(2), 10, 11, 7, 8, 100));
Assert.Equal(50.0, aroon.Up.Value, 1e-9);
Assert.Equal(100.0, aroon.Down.Value, 1e-9);
Assert.Equal(-50.0, result.Value, 1e-9);
}
}
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Skender.Stock.Indicators;
using TALib;
using Xunit;
using QuanTAlib.Tests;
namespace QuanTAlib;
public sealed class AroonValidationTests : IDisposable
{
private readonly ValidationTestData _data;
public AroonValidationTests()
{
_data = new ValidationTestData();
}
public void Dispose()
{
_data.Dispose();
}
[Fact]
public void MatchesSkender()
{
var aroon = new Aroon(14);
var results = new List<double>();
var upResults = new List<double>();
var downResults = new List<double>();
for (int i = 0; i < _data.Bars.Count; i++)
{
var res = aroon.Update(_data.Bars[i]);
results.Add(res.Value);
upResults.Add(aroon.Up.Value);
downResults.Add(aroon.Down.Value);
}
var skenderResults = _data.SkenderQuotes.GetAroon(14).ToList();
// Verify Oscillator
ValidationHelper.VerifyData(results, skenderResults, x => x.Oscillator);
// Verify Up
ValidationHelper.VerifyData(upResults, skenderResults, x => x.AroonUp);
// Verify Down
ValidationHelper.VerifyData(downResults, skenderResults, x => x.AroonDown);
}
[Fact]
public void MatchesTalib()
{
var aroon = new Aroon(14);
var results = new List<double>();
var upResults = new List<double>();
var downResults = new List<double>();
for (int i = 0; i < _data.Bars.Count; i++)
{
var res = aroon.Update(_data.Bars[i]);
results.Add(res.Value);
upResults.Add(aroon.Up.Value);
downResults.Add(aroon.Down.Value);
}
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
double[] outAroonUp = new double[_data.Bars.Count];
double[] outAroonDown = new double[_data.Bars.Count];
double[] outAroonOsc = new double[_data.Bars.Count];
// TA-Lib Aroon (Up/Down)
var retCode = TALib.Functions.Aroon(hData, lData, 0..^0, outAroonDown, outAroonUp, out var outRange, 14);
Assert.Equal(Core.RetCode.Success, retCode);
// TA-Lib AroonOsc
var retCodeOsc = TALib.Functions.AroonOsc(hData, lData, 0..^0, outAroonOsc, out var outRangeOsc, 14);
Assert.Equal(Core.RetCode.Success, retCodeOsc);
int lookback = TALib.Functions.AroonLookback(14);
// Verify Up
ValidationHelper.VerifyData(upResults, outAroonUp, outRange, lookback);
// Verify Down
ValidationHelper.VerifyData(downResults, outAroonDown, outRange, lookback);
// Verify Oscillator
ValidationHelper.VerifyData(results, outAroonOsc, outRangeOsc, lookback);
}
}
+185
View File
@@ -0,0 +1,185 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// Aroon Indicator
/// </summary>
/// <remarks>
/// The Aroon indicator is used to identify trend changes in the price of an asset, as well as the strength of that trend.
/// It consists of two lines: Aroon Up and Aroon Down.
///
/// Calculation:
/// Aroon Up = ((Period - Days Since Period High) / Period) * 100
/// Aroon Down = ((Period - Days Since Period Low) / Period) * 100
/// Aroon Oscillator = Aroon Up - Aroon Down
///
/// The indicator requires Period + 1 samples to fully calculate "Period" days ago.
///
/// Sources:
/// https://www.investopedia.com/terms/a/aroon.asp
/// Tushar Chande (1995)
/// </remarks>
[SkipLocalsInit]
public sealed class Aroon : ITValuePublisher
{
private readonly int _period;
private readonly RingBuffer _highs;
private readonly RingBuffer _lows;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
/// <summary>
/// Current Aroon Oscillator value (Up - Down).
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// Current Aroon Up value.
/// </summary>
public TValue Up { get; private set; }
/// <summary>
/// Current Aroon Down value.
/// </summary>
public TValue Down { get; private set; }
/// <summary>
/// True if the indicator has enough data for a full period calculation.
/// </summary>
public bool IsHot => _highs.IsFull;
/// <summary>
/// The number of bars required for the indicator to warm up.
/// </summary>
public int WarmupPeriod { get; }
/// <summary>
/// Creates Aroon indicator with specified period.
/// </summary>
/// <param name="period">Lookback period (must be > 0)</param>
public Aroon(int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
_period = period;
Name = $"Aroon({period})";
WarmupPeriod = period;
// We need Period + 1 samples to cover the range [0, Period] days ago.
_highs = new RingBuffer(period + 1);
_lows = new RingBuffer(period + 1);
}
/// <summary>
/// Resets the indicator state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_highs.Clear();
_lows.Clear();
Last = default;
Up = default;
Down = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
_highs.Add(input.High, isNew);
_lows.Add(input.Low, isNew);
if (_highs.Count == 0)
{
return default;
}
// Find max index in highs (Zero allocation)
var highsBuffer = _highs.InternalBuffer;
int count = _highs.Count;
int capacity = _highs.Capacity;
int start = _highs.StartIndex;
double maxVal = double.MinValue;
int maxIdxRelative = 0;
for (int i = 0; i < count; i++)
{
int idx = (start + i) % capacity;
double val = highsBuffer[idx];
// Use >= to find the most recent high if values are equal
if (val >= maxVal)
{
maxVal = val;
maxIdxRelative = i;
}
}
// Find min index in lows (Zero allocation)
var lowsBuffer = _lows.InternalBuffer;
double minVal = double.MaxValue;
int minIdxRelative = 0;
for (int i = 0; i < count; i++)
{
int idx = (start + i) % capacity;
double val = lowsBuffer[idx];
// Use <= to find the most recent low if values are equal
if (val <= minVal)
{
minVal = val;
minIdxRelative = i;
}
}
// Calculate days since (0 means current bar is the high/low)
int daysSinceHigh = (count - 1) - maxIdxRelative;
int daysSinceLow = (count - 1) - minIdxRelative;
double up = ((double)(_period - daysSinceHigh) / _period) * 100.0;
double down = ((double)(_period - daysSinceLow) / _period) * 100.0;
double osc = up - down;
Up = new TValue(input.Time, up);
Down = new TValue(input.Time, down);
Last = new TValue(input.Time, osc);
Pub?.Invoke(Last);
return Last;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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 Batch(TBarSeries source, int period)
{
var aroon = new Aroon(period);
return aroon.Update(source);
}
}
+66
View File
@@ -0,0 +1,66 @@
# Aroon Indicator
The Aroon indicator is a technical indicator used to identify trend changes in the price of an asset, as well as the strength of that trend. It consists of two lines: Aroon Up and Aroon Down.
## Calculation
The Aroon indicator measures the time between highs and the time between lows over a time period.
$$
\text{Aroon Up} = \frac{\text{Period} - \text{Days Since Period High}}{\text{Period}} \times 100
$$
$$
\text{Aroon Down} = \frac{\text{Period} - \text{Days Since Period Low}}{\text{Period}} \times 100
$$
$$
\text{Aroon Oscillator} = \text{Aroon Up} - \text{Aroon Down}
$$
Where:
- **Period**: The lookback period (typically 25).
- **Days Since Period High**: The number of days since the highest high within the period.
- **Days Since Period Low**: The number of days since the lowest low within the period.
## Interpretation
- **Aroon Up**: Measures the strength of the uptrend. Values close to 100 indicate a strong uptrend, while values close to 0 indicate a weak uptrend.
- **Aroon Down**: Measures the strength of the downtrend. Values close to 100 indicate a strong downtrend, while values close to 0 indicate a weak downtrend.
- **Crossovers**: When Aroon Up crosses above Aroon Down, it signals a potential uptrend. When Aroon Down crosses above Aroon Up, it signals a potential downtrend.
- **Extremes**: Values above 70 indicate a strong trend, while values below 30 indicate a weak trend.
## Usage
### C# code
```csharp
using QuanTAlib;
// Create Aroon with period 14
var aroon = new Aroon(14);
// Update with a TBar
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
var result = aroon.Update(bar);
// Access values
var osc = result.Value;
var up = aroon.Up.Value;
var down = aroon.Down.Value;
Console.WriteLine($"Aroon Osc: {osc:F2}, Up: {up:F2}, Down: {down:F2}");
```
### Quantower
The Aroon indicator is available in Quantower as "Aroon".
- **Period**: The lookback period (default: 14).
- **Show cold values**: Whether to show values before the indicator is fully warmed up.
## References
- [Investopedia: Aroon Indicator](https://www.investopedia.com/terms/a/aroon.asp)
- Tushar Chande (1995)
+3 -3
View File
@@ -200,16 +200,16 @@ public sealed class Dema : AbstractBase
return dema.Update(source);
}
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
double alpha = 2.0 / (period + 1);
Calculate(source, output, alpha);
Batch(source, output, alpha);
}
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double alpha)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double alpha)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
+14 -7
View File
@@ -37,7 +37,14 @@ public sealed class Wma : AbstractBase
private State _state;
private State _p_state;
private const int ResyncInterval = 1000;
private const int ResyncInterval = 10000;
private static readonly Vector512<long> V512_Idx_1 = Vector512.Create(0L, 0, 1, 2, 3, 4, 5, 6);
private static readonly Vector512<long> V512_Idx_2 = Vector512.Create(0L, 0, 0, 0, 1, 2, 3, 4);
private static readonly Vector512<long> V512_Idx_4 = Vector512.Create(0L, 0, 0, 0, 0, 0, 1, 2);
private static readonly Vector512<double> V512_Mask_1 = Vector512.Create(0.0, 1, 1, 1, 1, 1, 1, 1);
private static readonly Vector512<double> V512_Mask_2 = Vector512.Create(0.0, 0, 1, 1, 1, 1, 1, 1);
private static readonly Vector512<double> V512_Mask_4 = Vector512.Create(0.0, 0, 0, 0, 1, 1, 1, 1);
public Wma(int period)
{
@@ -366,13 +373,13 @@ public sealed class Wma : AbstractBase
var vDeltaS = Avx512F.Subtract(vNew, vOld);
// Prefix sum of DeltaS
var vShiftS1 = Vector512.Create(0.0, vDeltaS.GetElement(0), vDeltaS.GetElement(1), vDeltaS.GetElement(2), vDeltaS.GetElement(3), vDeltaS.GetElement(4), vDeltaS.GetElement(5), vDeltaS.GetElement(6));
var vShiftS1 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vDeltaS, V512_Idx_1), V512_Mask_1);
var vPS1 = Avx512F.Add(vDeltaS, vShiftS1);
var vShiftS2 = Vector512.Create(0.0, 0.0, vPS1.GetElement(0), vPS1.GetElement(1), vPS1.GetElement(2), vPS1.GetElement(3), vPS1.GetElement(4), vPS1.GetElement(5));
var vShiftS2 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vPS1, V512_Idx_2), V512_Mask_2);
var vPS2 = Avx512F.Add(vPS1, vShiftS2);
var vShiftS4 = Vector512.Create(0.0, 0.0, 0.0, 0.0, vPS2.GetElement(0), vPS2.GetElement(1), vPS2.GetElement(2), vPS2.GetElement(3));
var vShiftS4 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vPS2, V512_Idx_4), V512_Mask_4);
var vPS4 = Avx512F.Add(vPS2, vShiftS4);
var vSums = Avx512F.Add(vSumState, vPS4);
@@ -382,13 +389,13 @@ public sealed class Wma : AbstractBase
var vU = Avx512F.FusedMultiplySubtract(vPeriod, vNew, vSumsShifted);
// Prefix sum of vU
var vShiftW1 = Vector512.Create(0.0, vU.GetElement(0), vU.GetElement(1), vU.GetElement(2), vU.GetElement(3), vU.GetElement(4), vU.GetElement(5), vU.GetElement(6));
var vShiftW1 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vU, V512_Idx_1), V512_Mask_1);
var vPW1 = Avx512F.Add(vU, vShiftW1);
var vShiftW2 = Vector512.Create(0.0, 0.0, vPW1.GetElement(0), vPW1.GetElement(1), vPW1.GetElement(2), vPW1.GetElement(3), vPW1.GetElement(4), vPW1.GetElement(5));
var vShiftW2 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vPW1, V512_Idx_2), V512_Mask_2);
var vPW2 = Avx512F.Add(vPW1, vShiftW2);
var vShiftW4 = Vector512.Create(0.0, 0.0, 0.0, 0.0, vPW2.GetElement(0), vPW2.GetElement(1), vPW2.GetElement(2), vPW2.GetElement(3));
var vShiftW4 = Avx512F.Multiply(Avx512F.PermuteVar8x64(vPW2, V512_Idx_4), V512_Mask_4);
var vPW4 = Avx512F.Add(vPW2, vShiftW4);
var vWsums = Avx512F.Add(vWsumState, vPW4);
+72 -142
View File
@@ -9,6 +9,9 @@ using QuanTAlib.Benchmarks;
using Skender.Stock.Indicators;
using TALib;
using Tulip;
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
using OoplesFinance.StockIndicators.Enums;
namespace QuanTAlib.Benchmarks;
@@ -40,12 +43,13 @@ public static class Program
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
public class IndicatorBenchmarks
{
private const int BarCount = 200_000;
private const int Period = 100;
private const int BarCount = 500_000;
private const int Period = 220;
private double[] _closeValues = null!;
private TSeries _closeTseries = null!;
private List<Quote> _quotes = null!;
private List<TickerData> _ooplesData = null!;
// Pre-allocated outputs for TA-Lib
private double[] _talibOutput = null!;
@@ -60,15 +64,9 @@ public class IndicatorBenchmarks
private double[][] _tulipWmaInputs = null!;
private double[] _tulipWmaOptions = null!;
private double[][] _tulipWmaOutputs = null!;
private double[][] _tulipTrimaInputs = null!;
private double[] _tulipTrimaOptions = null!;
private double[][] _tulipTrimaOutputs = null!;
private double[][] _tulipDemaInputs = null!;
private double[] _tulipDemaOptions = null!;
private double[][] _tulipDemaOutputs = null!;
private double[][] _tulipTemaInputs = null!;
private double[] _tulipTemaOptions = null!;
private double[][] _tulipTemaOutputs = null!;
private double[][] _tulipHmaInputs = null!;
private double[] _tulipHmaOptions = null!;
private double[][] _tulipHmaOutputs = null!;
// Pre-allocated outputs for QuanTAlib Span API
private double[] _quantalibOutput = null!;
@@ -98,6 +96,21 @@ public class IndicatorBenchmarks
});
}
// Create Ooples TickerData format
_ooplesData = new List<TickerData>(BarCount);
for (int i = 0; i < BarCount; i++)
{
_ooplesData.Add(new TickerData
{
Date = new DateTime(_closeTseries.Times[i]),
Open = bars.Open.Values[i],
High = bars.High.Values[i],
Low = bars.Low.Values[i],
Close = _closeValues[i],
Volume = bars.Volume.Values[i]
});
}
// Pre-allocate TA-Lib output
_talibOutput = new double[BarCount];
@@ -115,19 +128,10 @@ public class IndicatorBenchmarks
_tulipWmaOptions = new double[] { Period };
_tulipWmaOutputs = new[] { new double[BarCount - smaLookback] };
_tulipTrimaInputs = new[] { _closeValues };
_tulipTrimaOptions = new double[] { Period };
_tulipTrimaOutputs = new[] { new double[BarCount - smaLookback] };
int demaLookback = 2 * (Period - 1);
_tulipDemaInputs = new[] { _closeValues };
_tulipDemaOptions = new double[] { Period };
_tulipDemaOutputs = new[] { new double[BarCount - demaLookback] };
int temaLookback = 3 * (Period - 1);
_tulipTemaInputs = new[] { _closeValues };
_tulipTemaOptions = new double[] { Period };
_tulipTemaOutputs = new[] { new double[BarCount - temaLookback] };
int hmaLookback = Period + (int)Math.Sqrt(Period) - 2;
_tulipHmaInputs = new[] { _closeValues };
_tulipHmaOptions = new double[] { Period };
_tulipHmaOutputs = new[] { new double[BarCount - hmaLookback] };
// Pre-allocate QuanTAlib output
_quantalibOutput = new double[BarCount];
@@ -136,11 +140,11 @@ public class IndicatorBenchmarks
// ==================== SMA ====================
[BenchmarkCategory("SMA")]
[Benchmark(Description = "QuanTAlib SMA (Span)")]
public void QuanTAlib_Sma_Span() => Sma.Calculate(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period);
public void QuanTAlib_Sma_Span() => Sma.Batch(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period);
[BenchmarkCategory("SMA")]
[Benchmark(Description = "QuanTAlib SMA (Batch)")]
public TSeries QuanTAlib_Sma_TSeries() => Sma.Calculate(_closeTseries, Period);
public TSeries QuanTAlib_Sma_TSeries() => Sma.Calculate(_closeTseries, Period).Results;
[BenchmarkCategory("SMA")]
[Benchmark(Description = "QuanTAlib SMA (Streaming)")]
@@ -178,14 +182,18 @@ public class IndicatorBenchmarks
[Benchmark(Description = "Skender SMA")]
public object Skender_Sma() => _quotes.GetSma(Period);
[BenchmarkCategory("SMA")]
[Benchmark(Description = "Ooples SMA")]
public object Ooples_Sma() => new StockData(_ooplesData).CalculateSimpleMovingAverage(Period);
// ==================== EMA ====================
[BenchmarkCategory("EMA")]
[Benchmark(Description = "QuanTAlib EMA (Span)")]
public void QuanTAlib_Ema_Span() => Ema.Calculate(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period);
public void QuanTAlib_Ema_Span() => Ema.Batch(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period);
[BenchmarkCategory("EMA")]
[Benchmark(Description = "QuanTAlib EMA (Batch)")]
public TSeries QuanTAlib_Ema_TSeries() => Ema.Calculate(_closeTseries, Period);
public TSeries QuanTAlib_Ema_TSeries() => Ema.Calculate(_closeTseries, Period).Results;
[BenchmarkCategory("EMA")]
[Benchmark(Description = "QuanTAlib EMA (Streaming)")]
@@ -223,14 +231,18 @@ public class IndicatorBenchmarks
[Benchmark(Description = "Skender EMA")]
public object Skender_Ema() => _quotes.GetEma(Period);
[BenchmarkCategory("EMA")]
[Benchmark(Description = "Ooples EMA")]
public object Ooples_Ema() => new StockData(_ooplesData).CalculateExponentialMovingAverage(Period);
// ==================== WMA ====================
[BenchmarkCategory("WMA")]
[Benchmark(Description = "QuanTAlib WMA (Span)")]
public void QuanTAlib_Wma_Span() => Wma.Calculate(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period);
public void QuanTAlib_Wma_Span() => Wma.Batch(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period);
[BenchmarkCategory("WMA")]
[Benchmark(Description = "QuanTAlib WMA (Batch)")]
public TSeries QuanTAlib_Wma_TSeries() => Wma.Calculate(_closeTseries, Period);
public TSeries QuanTAlib_Wma_TSeries() => Wma.Batch(_closeTseries, Period);
[BenchmarkCategory("WMA")]
[Benchmark(Description = "QuanTAlib WMA (Streaming)")]
@@ -268,134 +280,52 @@ public class IndicatorBenchmarks
[Benchmark(Description = "Skender WMA")]
public object Skender_Wma() => _quotes.GetWma(Period);
// ==================== TRIMA ====================
[BenchmarkCategory("TRIMA")]
[Benchmark(Description = "QuanTAlib TRIMA (Span)")]
public void QuanTAlib_Trima_Span() => Trima.Calculate(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period);
[BenchmarkCategory("WMA")]
[Benchmark(Description = "Ooples WMA")]
public object Ooples_Wma() => new StockData(_ooplesData).CalculateWeightedMovingAverage(Period);
[BenchmarkCategory("TRIMA")]
[Benchmark(Description = "QuanTAlib TRIMA (Batch)")]
public TSeries QuanTAlib_Trima_TSeries() => Trima.Calculate(_closeTseries, Period);
// ==================== HMA ====================
[BenchmarkCategory("HMA")]
[Benchmark(Description = "QuanTAlib HMA (Span)")]
public void QuanTAlib_Hma_Span() => Hma.Calculate(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period);
[BenchmarkCategory("TRIMA")]
[Benchmark(Description = "QuanTAlib TRIMA (Streaming)")]
public void QuanTAlib_Trima_Streaming()
[BenchmarkCategory("HMA")]
[Benchmark(Description = "QuanTAlib HMA (Batch)")]
public TSeries QuanTAlib_Hma_TSeries() => Hma.Batch(_closeTseries, Period);
[BenchmarkCategory("HMA")]
[Benchmark(Description = "QuanTAlib HMA (Streaming)")]
public void QuanTAlib_Hma_Streaming()
{
var trima = new Trima(Period);
var hma = new Hma(Period);
for (int i = 0; i < _closeValues.Length; i++)
{
_quantalibOutput[i] = trima.Update(new TValue(_closeTseries.Times[i], _closeValues[i])).Value;
_quantalibOutput[i] = hma.Update(new TValue(_closeTseries.Times[i], _closeValues[i])).Value;
}
}
[BenchmarkCategory("TRIMA")]
[Benchmark(Description = "QuanTAlib TRIMA (Eventing)")]
public void QuanTAlib_Trima_Eventing()
[BenchmarkCategory("HMA")]
[Benchmark(Description = "QuanTAlib HMA (Eventing)")]
public void QuanTAlib_Hma_Eventing()
{
var source = new TSeries();
var trima = new Trima(source, Period);
var hma = new Hma(source, Period);
for (int i = 0; i < _closeValues.Length; i++)
{
source.Add(new TValue(_closeTseries.Times[i], _closeValues[i]));
_quantalibOutput[i] = trima.Last.Value;
_quantalibOutput[i] = hma.Last.Value;
}
}
[BenchmarkCategory("TRIMA")]
[Benchmark(Description = "Tulip TRIMA")]
public void Tulip_Trima() => Tulip.Indicators.trima.Run(_tulipTrimaInputs, _tulipTrimaOptions, _tulipTrimaOutputs);
[BenchmarkCategory("HMA")]
[Benchmark(Description = "Tulip HMA")]
public void Tulip_Hma() => Tulip.Indicators.hma.Run(_tulipHmaInputs, _tulipHmaOptions, _tulipHmaOutputs);
[BenchmarkCategory("TRIMA")]
[Benchmark(Description = "TALib TRIMA")]
public Core.RetCode TALib_Trima() => TALib.Functions.Trima<double>(_closeValues, 0..^0, _talibOutput, out _, Period);
[BenchmarkCategory("HMA")]
[Benchmark(Description = "Skender HMA")]
public object Skender_Hma() => _quotes.GetHma(Period);
// ==================== DEMA ====================
[BenchmarkCategory("DEMA")]
[Benchmark(Description = "QuanTAlib DEMA (Span)")]
public void QuanTAlib_Dema_Span() => Dema.Calculate(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period);
[BenchmarkCategory("DEMA")]
[Benchmark(Description = "QuanTAlib DEMA (Batch)")]
public TSeries QuanTAlib_Dema_TSeries() => Dema.Calculate(_closeTseries, Period);
[BenchmarkCategory("DEMA")]
[Benchmark(Description = "QuanTAlib DEMA (Streaming)")]
public void QuanTAlib_Dema_Streaming()
{
var dema = new Dema(Period);
for (int i = 0; i < _closeValues.Length; i++)
{
_quantalibOutput[i] = dema.Update(new TValue(_closeTseries.Times[i], _closeValues[i])).Value;
}
}
[BenchmarkCategory("DEMA")]
[Benchmark(Description = "QuanTAlib DEMA (Eventing)")]
public void QuanTAlib_Dema_Eventing()
{
var source = new TSeries();
var dema = new Dema(source, Period);
for (int i = 0; i < _closeValues.Length; i++)
{
source.Add(new TValue(_closeTseries.Times[i], _closeValues[i]));
_quantalibOutput[i] = dema.Last.Value;
}
}
[BenchmarkCategory("DEMA")]
[Benchmark(Description = "Tulip DEMA")]
public void Tulip_Dema() => Tulip.Indicators.dema.Run(_tulipDemaInputs, _tulipDemaOptions, _tulipDemaOutputs);
[BenchmarkCategory("DEMA")]
[Benchmark(Description = "TALib DEMA")]
public Core.RetCode TALib_Dema() => TALib.Functions.Dema<double>(_closeValues, 0..^0, _talibOutput, out _, Period);
[BenchmarkCategory("DEMA")]
[Benchmark(Description = "Skender DEMA")]
public object Skender_Dema() => _quotes.GetDema(Period);
// ==================== TEMA ====================
[BenchmarkCategory("TEMA")]
[Benchmark(Description = "QuanTAlib TEMA (Span)")]
public void QuanTAlib_Tema_Span() => Tema.Calculate(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period);
[BenchmarkCategory("TEMA")]
[Benchmark(Description = "QuanTAlib TEMA (Batch)")]
public TSeries QuanTAlib_Tema_TSeries() => Tema.Calculate(_closeTseries, Period);
[BenchmarkCategory("TEMA")]
[Benchmark(Description = "QuanTAlib TEMA (Streaming)")]
public void QuanTAlib_Tema_Streaming()
{
var tema = new Tema(Period);
for (int i = 0; i < _closeValues.Length; i++)
{
_quantalibOutput[i] = tema.Update(new TValue(_closeTseries.Times[i], _closeValues[i])).Value;
}
}
[BenchmarkCategory("TEMA")]
[Benchmark(Description = "QuanTAlib TEMA (Eventing)")]
public void QuanTAlib_Tema_Eventing()
{
var source = new TSeries();
var tema = new Tema(source, Period);
for (int i = 0; i < _closeValues.Length; i++)
{
source.Add(new TValue(_closeTseries.Times[i], _closeValues[i]));
_quantalibOutput[i] = tema.Last.Value;
}
}
[BenchmarkCategory("TEMA")]
[Benchmark(Description = "Tulip TEMA")]
public void Tulip_Tema() => Tulip.Indicators.tema.Run(_tulipTemaInputs, _tulipTemaOptions, _tulipTemaOutputs);
[BenchmarkCategory("TEMA")]
[Benchmark(Description = "TALib TEMA")]
public Core.RetCode TALib_Tema() => TALib.Functions.Tema<double>(_closeValues, 0..^0, _talibOutput, out _, Period);
[BenchmarkCategory("TEMA")]
[Benchmark(Description = "Skender TEMA")]
public object Skender_Tema() => _quotes.GetTema(Period);
[BenchmarkCategory("HMA")]
[Benchmark(Description = "Ooples HMA")]
public object Ooples_Hma() => new StockData(_ooplesData).CalculateHullMovingAverage(MovingAvgType.WeightedMovingAverage, Period);
}
+1
View File
@@ -14,6 +14,7 @@
<ItemGroup>
<!-- Comparison libraries -->
<PackageReference Include="OoplesFinance.StockIndicators" Version="1.0.53" />
<PackageReference Include="Skender.Stock.Indicators" Version="2.6.1" />
<PackageReference Include="Tulip.NETCore" Version="0.8.0.1" />
<PackageReference Include="TALib.NETCore" Version="0.5.0" />