feat: Enhance volume indicators with ADOSC and SSF implementation and validation

This commit is contained in:
Miha Kralj
2025-12-20 15:08:07 -08:00
parent 5549c7329a
commit d21fea3c18
85 changed files with 5144 additions and 3954 deletions
+34 -108
View File
@@ -1,136 +1,62 @@
# SMA: Simple Moving Average
## What It Does
> "The vanilla ice cream of technical analysis. Boring, ubiquitous, and the only thing your grandfather and your high-frequency trading bot agree on."
The Simple Moving Average (SMA) is the most fundamental indicator in technical analysis. It calculates the unweighted mean of the previous $N$ data points. By smoothing out price fluctuations, it helps traders identify the direction of the trend and potential support/resistance levels.
The Simple Moving Average (SMA) is the unweighted arithmetic mean of the last $N$ data points. It acts as a low-pass filter, smoothing out high-frequency noise to reveal the underlying trend. While conceptually simple, efficient implementation on modern hardware requires careful attention to memory access patterns and vectorization.
## Historical Context
The concept of a moving average dates back to the early 20th century, used by statisticians to smooth time series data. In financial markets, it became a cornerstone of technical analysis with the advent of computing, allowing traders to filter out "noise" and focus on the underlying trend.
The concept of a moving average dates back to 1901 (R.H. Hooker) for smoothing weather data, but it became a staple of financial analysis in the mid-20th century. It is the baseline against which all other averages are compared.
## How It Works
## Architecture & Physics
### The Core Idea
The naive implementation of SMA sums $N$ numbers at every step, resulting in $O(N)$ complexity. QuanTAlib uses an optimized $O(1)$ approach.
The SMA treats every price in the lookback window equally. A price from 10 days ago has the same influence on the average as the price from today. This "democracy" of data points makes it stable but slow to react to recent changes compared to weighted averages like EMA or WMA.
### O(1) Running Sum
### Mathematical Foundation
We maintain a running `Sum` and a `RingBuffer` of history.
$$ Sum_{new} = Sum_{old} - Value_{oldest} + Value_{new} $$
$$ SMA = \frac{Sum_{new}}{N} $$
$$ SMA_t = \frac{P_t + P_{t-1} + \dots + P_{t-n+1}}{n} $$
This ensures that calculating an SMA(200) takes the exact same time as an SMA(10).
Where:
### Drift Correction
- $P$ = Price
- $n$ = Period length
Floating-point addition is not associative. Repeatedly adding and subtracting values from a running sum introduces cumulative error (drift) over millions of ticks. QuanTAlib implements a periodic **Resync** mechanism (every 1000 ticks) that recalculates the sum from scratch to ensure precision remains within `1e-9` of the true mean.
### Implementation Details: O(1) Streaming
### SIMD Optimization
A naive implementation sums all $N$ prices every bar, resulting in $O(N)$ complexity. We optimize this to **O(1)** using a sliding window algorithm:
For batch processing of large datasets, `Sma.Batch` utilizes `System.Runtime.Intrinsics` (AVX2/AVX-512) to process multiple data points in parallel, significantly outperforming scalar loops.
$$ Sum_{new} = Sum_{old} - P_{leaving} + P_{entering} $$
$$ SMA_{new} = \frac{Sum_{new}}{n} $$
## Mathematical Foundation
This ensures that calculating an SMA(200) takes the exact same amount of CPU time as an SMA(10).
### 1. The Mean
## Configuration
| Parameter | Default | Purpose | Adjustment Guidelines |
|-----------|---------|---------|----------------------|
| Period | 10 | Lookback window | Short (10-20) for short-term trends; Medium (50) for intermediate; Long (200) for major trends. |
$$ SMA_t = \frac{1}{N} \sum_{i=0}^{N-1} P_{t-i} $$
## Performance Profile
| Operation | Complexity | Description |
|-----------|------------|-------------------|
| Streaming update | O(1) | Sliding window sum |
| Bar correction | O(1) | Efficient state rollback |
| Batch processing | O(N) | Single pass through data |
| Memory footprint | O(period) | RingBuffer for lookback window |
The implementation is optimized for both streaming (latency) and batch (throughput) scenarios.
## Interpretation
### Zero-Allocation Design
### Trading Signals
The `RingBuffer` is pre-allocated at initialization. All updates are performed in-place using scalar operations or SIMD intrinsics, ensuring no heap allocations occur during the hot path.
#### Trend Direction
| Metric | Score | Notes |
| :--- | :--- | :--- |
| **Throughput** | High | Optimized running sum |
| **Complexity** | O(1) | Constant time update |
| **Accuracy** | 5/10 | Baseline accuracy, unweighted |
| **Timeliness** | 4/10 | Significant lag (N/2) |
| **Overshoot** | 8/10 | Generally stable, no projection |
| **Smoothness** | 6/10 | Susceptible to "drop-off" effect |
- **Uptrend:** Price > SMA and SMA slope is positive.
- **Downtrend:** Price < SMA and SMA slope is negative.
## Validation
#### Crossovers
Validated against TA-Lib (`TA_SMA`) and Skender.Stock.Indicators.
- **Golden Cross:** Short-term SMA (e.g., 50) crosses above Long-term SMA (e.g., 200). Bullish.
- **Death Cross:** Short-term SMA crosses below Long-term SMA. Bearish.
### Common Pitfalls
#### Support/Resistance
- The 50-day and 200-day SMAs are widely watched by institutions and often act as self-fulfilling support or resistance levels.
### When It Works Best
- **Strong Trends:** In clearly trending markets, SMA keeps you on the right side of the move.
### When It Struggles
- **Sideways Markets:** In ranging markets, price will constantly cross the SMA, generating false signals (whipsaws).
## Architecture Notes
This implementation makes specific trade-offs:
### Choice: RingBuffer for History
- **Implementation:** Uses a circular buffer to store the last $N$ prices.
- **Rationale:** Necessary to know which value is leaving the window ($P_{leaving}$) for the O(1) update.
### Choice: Periodic Resync
- **Implementation:** Recalculates the full sum every few thousand ticks.
- **Rationale:** Prevents floating-point errors from accumulating in the running sum over very long data streams.
## References
- Murphy, John J. "Technical Analysis of the Financial Markets." New York Institute of Finance, 1999.
## C# Usage
### Streaming Updates (Single Instance)
```csharp
using QuanTAlib;
var sma = new Sma(period: 20);
// Process each new bar
TValue result = sma.Update(new TValue(timestamp, closePrice));
Console.WriteLine($"SMA: {result.Value:F2}");
// Check if buffer is full
if (sma.IsHot)
{
// Indicator is fully initialized
}
```
### Batch Processing (Historical Data)
```csharp
// TSeries API
TSeries prices = ...;
TSeries smaValues = Sma.Batch(prices, period: 20);
// Span API (High Performance)
double[] prices = new double[1000];
double[] output = new double[1000];
Sma.Calculate(prices.AsSpan(), output.AsSpan(), period: 20);
```
### Bar Correction (isNew Parameter)
```csharp
var sma = new Sma(20);
// New bar
sma.Update(new TValue(time, 100), isNew: true);
// Intra-bar update
sma.Update(new TValue(time, 101), isNew: false); // Replaces 100 with 101
1. **Lag**: SMA has the most lag of all moving averages (Lag $\approx N/2$).
2. **Drop-off Effect**: An old, large outlier dropping out of the window causes the SMA to jump, even if the current price is flat. This "Barker effect" is why EMAs are often preferred.
3. **NaN Handling**: A single `NaN` in the history window corrupts the entire SMA. QuanTAlib handles this by substituting the last valid value.