mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-27 17:27:43 +00:00
feat(dynamics): add PlusDI, MinusDI, PlusDM, MinusDM indicators
Complete thin Dx-composition wrapper indicators with full test coverage: - PlusDi/MinusDi: Directional Indicator wrappers (DiPlus/DiMinus from Dx) - PlusDm/MinusDm: Directional Movement wrappers (DmPlus/DmMinus from Dx) - Individual validation tests per indicator directory (TALib, Skender, bounds) - Combined unit tests (DiDm.Tests.cs) and validation tests (DiDm.Validation.Tests.cs) - Quantower wrappers + tests for all 4 indicators - PineScript v6 implementations with compensated RMA - Normalized .md documentation for all indicators and categories - 182 tests passing, 0 failures
This commit is contained in:
+8
-2
@@ -99,7 +99,7 @@
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.markdown-section h2,
|
||||
.markdown-section h2,
|
||||
.markdown-section h3,
|
||||
.markdown-section h4,
|
||||
.markdown-section h5,
|
||||
@@ -113,6 +113,10 @@
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.markdown-section h3 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.markdown-section p,
|
||||
.markdown-section ul,
|
||||
.markdown-section ol,
|
||||
@@ -196,8 +200,10 @@
|
||||
}
|
||||
|
||||
/* Pine Script syntax highlighting — GitHub dark palette */
|
||||
.token.comment,
|
||||
.token.block-comment,
|
||||
.token.doctype,
|
||||
.token.annotation { color: #8b949e; font-style: italic; }
|
||||
.token.annotation { color: green; }
|
||||
.token.type-definition { color: #79c0ff; }
|
||||
.token.class-name { color: #d2a8ff; }
|
||||
.token.constant { color: #ffa657; }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Channels
|
||||
|
||||
> "In trending markets, ride the channel. In ranging markets, fade the edges." Unknown
|
||||
|
||||
Channels define dynamic support and resistance. Upper band shows where price tends to find resistance; lower band shows support. Width measures volatility; price position within channel measures momentum and mean-reversion potential.
|
||||
|
||||
## Indicators
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# ABERR: Aberration Bands
|
||||
|
||||
> *Aberration measures the distance between price and its smoothed self — when the gap grows extreme, reversion whispers.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -73,23 +75,6 @@ $$\text{MAD} = \sigma \sqrt{\frac{2}{\pi}} \approx 0.7979\,\sigma$$
|
||||
|
||||
Therefore ABERR with $k = 2.0$ captures approximately the same range as Bollinger Bands with $k \approx 1.596$.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function ABERR(source, ma_line, period, multiplier):
|
||||
// Deviation from center line
|
||||
deviation = |source - ma_line|
|
||||
|
||||
// Average absolute deviation (SMA of deviations)
|
||||
avg_dev = SMA(deviation, period)
|
||||
|
||||
// Band construction
|
||||
upper = ma_line + multiplier * avg_dev
|
||||
lower = ma_line - multiplier * avg_dev
|
||||
|
||||
return [upper, lower, avg_dev]
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Description |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# ACCBANDS: Acceleration Bands
|
||||
|
||||
> *Acceleration bands widen with high-low range, framing the expected reach of each bar's ambition.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -63,26 +65,6 @@ Three independent circular buffers maintain running sums for $O(1)$ streaming up
|
||||
| `period` | Lookback period for the three SMAs ($n$) | 20 | $> 0$ |
|
||||
| `factor` | Multiplier for normalized width ($F$) | 4.0 | $> 0$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function ACCBANDS(high, low, close, period, factor):
|
||||
// Per-bar normalized width
|
||||
denom = high + low
|
||||
w = denom ≠ 0 ? (high - low) / denom : 0
|
||||
|
||||
// Adjusted prices
|
||||
adj_high = high * (1 + factor * w)
|
||||
adj_low = low * (1 - factor * w)
|
||||
|
||||
// Three independent SMAs
|
||||
upper = SMA(adj_high, period)
|
||||
lower = SMA(adj_low, period)
|
||||
middle = SMA(close, period)
|
||||
|
||||
return [middle, upper, lower]
|
||||
```
|
||||
|
||||
### Breakout Rule (Headley)
|
||||
|
||||
A trend is confirmed when:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# APCHANNEL: Adaptive Price Channel
|
||||
|
||||
> *An adaptive channel reshapes its width in real time, tracking the market's own sense of normal.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -74,30 +76,6 @@ $$t_{1/2} = \frac{\ln 2}{\ln(1 / (1 - \alpha))}$$
|
||||
|
||||
For $\alpha = 0.2$: $t_{1/2} \approx 3.1$ bars. For $\alpha = 0.05$: $t_{1/2} \approx 13.5$ bars.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function APCHANNEL(high, low, alpha):
|
||||
validate: 0 < alpha ≤ 1
|
||||
decay = 1 - alpha
|
||||
|
||||
// EMA of highs
|
||||
if first_bar:
|
||||
upper = high
|
||||
else:
|
||||
upper = decay * upper + alpha * high
|
||||
|
||||
// EMA of lows
|
||||
if first_bar:
|
||||
lower = low
|
||||
else:
|
||||
lower = decay * lower + alpha * low
|
||||
|
||||
middle = (upper + lower) / 2
|
||||
|
||||
return [middle, upper, lower]
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Description |
|
||||
|
||||
+2
-33
@@ -1,5 +1,7 @@
|
||||
# APZ: Adaptive Price Zone
|
||||
|
||||
> *The adaptive price zone contracts in calm and expands in chaos, mapping volatility into a living boundary.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -88,39 +90,6 @@ $$P_{\text{effective}} = \sqrt{P} \approx \frac{2}{\alpha} - 1$$
|
||||
|
||||
For $P = 20$: $P_{\text{eff}} \approx 4.47$. For $P = 100$: $P_{\text{eff}} \approx 10$.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function APZ(source, high, low, period, multiplier):
|
||||
validate: period > 0, multiplier > 0
|
||||
|
||||
alpha = 2 / (√period + 1)
|
||||
beta = 1 - alpha
|
||||
|
||||
// Double-smoothed EMA of price
|
||||
ema1_price = alpha * source + beta * ema1_price
|
||||
center = alpha * ema1_price + beta * center
|
||||
|
||||
// Double-smoothed EMA of range
|
||||
range = high - low
|
||||
ema1_range = alpha * range + beta * ema1_range
|
||||
smooth_range = alpha * ema1_range + beta * smooth_range
|
||||
|
||||
// Warmup compensator
|
||||
e *= beta²
|
||||
if e > 1e-10:
|
||||
compensator = 1 / (1 - e)
|
||||
center *= compensator
|
||||
smooth_range *= compensator
|
||||
|
||||
// Bands
|
||||
width = multiplier * smooth_range
|
||||
upper = center + width
|
||||
lower = center - width
|
||||
|
||||
return [center, upper, lower]
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Description |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# ATRBANDS: Average True Range Bands
|
||||
|
||||
> *True range bands let volatility itself draw the envelope — wider when uncertain, tighter when resolved.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -72,33 +74,6 @@ The SMA uses a circular buffer for $O(1)$ running sums. The ATR uses recursive I
|
||||
| Gap-up | $\|H_t - C_{t-1}\|$ | Upward gap distance |
|
||||
| Gap-down | $\|L_t - C_{t-1}\|$ | Downward gap distance |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function ATRBANDS(source, high, low, close, period, multiplier):
|
||||
validate: period > 0, multiplier > 0
|
||||
|
||||
// True Range
|
||||
tr = max(high - low, |high - prev_close|, |low - prev_close|)
|
||||
prev_close = close
|
||||
|
||||
// ATR via Wilder's smoothing (RMA)
|
||||
alpha = 1 / period
|
||||
raw_rma = (raw_rma * (period - 1) + tr) / period
|
||||
e *= (1 - alpha)
|
||||
atr = e > ε ? raw_rma / (1 - e) : raw_rma
|
||||
|
||||
// Center line (SMA via circular buffer)
|
||||
middle = SMA(source, period)
|
||||
|
||||
// Bands
|
||||
width = atr * multiplier
|
||||
upper = middle + width
|
||||
lower = middle - width
|
||||
|
||||
return [middle, upper, lower]
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Description |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# BBANDS: Bollinger Bands
|
||||
|
||||
> *Standard deviation channels adapt to the market's own volatility rhythm, expanding and contracting like breathing.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -78,36 +80,6 @@ The circular buffer maintains running sums of $x$ and $x^2$, enabling $O(1)$ com
|
||||
| 2.0 | 95.4% | 75.0% |
|
||||
| 3.0 | 99.7% | 88.9% |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function BBANDS(source, period, multiplier):
|
||||
validate: period > 0, multiplier > 0
|
||||
|
||||
// Circular buffer maintains running sums
|
||||
sum += source; sumSq += source²
|
||||
oldest = buffer[head]
|
||||
if oldest exists: sum -= oldest; sumSq -= oldest²
|
||||
|
||||
// SMA (middle band)
|
||||
middle = sum / count
|
||||
|
||||
// Population standard deviation
|
||||
variance = max(0, sumSq/count - middle²)
|
||||
sigma = √variance
|
||||
dev = multiplier * sigma
|
||||
|
||||
// Bands
|
||||
upper = middle + dev
|
||||
lower = middle - dev
|
||||
|
||||
// Derived metrics
|
||||
bandwidth = (upper - lower) / middle
|
||||
percentB = (source - lower) / (upper - lower)
|
||||
|
||||
return [middle, upper, lower, bandwidth, percentB]
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Range | Meaning |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# DCHANNEL: Donchian Channels
|
||||
|
||||
> *The highest high and lowest low over a window — Donchian's simplicity captures breakout potential in two lines.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -63,29 +65,6 @@ The bands stay flat until either a new extreme occurs or the old extreme exits t
|
||||
|-----------|-------------|---------|------------|
|
||||
| `period` | Lookback window for high/low extremes ($n$) | 20 | $> 0$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function DCHANNEL(high, low, period):
|
||||
validate: period > 0
|
||||
|
||||
// Monotonic deque for max (upper band)
|
||||
while max_deque.front is outside window: pop front
|
||||
while max_deque.back value ≤ high: pop back
|
||||
push high to max_deque back
|
||||
upper = max_deque.front value
|
||||
|
||||
// Monotonic deque for min (lower band)
|
||||
while min_deque.front is outside window: pop front
|
||||
while min_deque.back value ≥ low: pop back
|
||||
push low to min_deque back
|
||||
lower = min_deque.front value
|
||||
|
||||
middle = (upper + lower) / 2
|
||||
|
||||
return [middle, upper, lower]
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Description |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# DECAYCHANNEL: Decay Min-Max Channel
|
||||
|
||||
> *Extremes that decay over time give recent boundaries more weight, fading yesterday's peaks gradually.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -79,43 +81,6 @@ $$\text{decayRate}(T) = 1 - e^{-\lambda T} = 1 - e^{-\ln 2} = 0.5$$
|
||||
|
||||
After $2T$ bars: 75% decay. After $3T$ bars: 87.5% decay.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function DECAYCHANNEL(high, low, period):
|
||||
validate: period > 0
|
||||
lambda = ln(2) / period
|
||||
|
||||
// Scan buffer for Donchian bounds
|
||||
periodMax = max(high_buffer over period)
|
||||
periodMin = min(low_buffer over period)
|
||||
periodAvg = avg(midpoints over period)
|
||||
|
||||
// Snap or age
|
||||
if high ≥ currentMax:
|
||||
currentMax = high; ageMax = 0
|
||||
else:
|
||||
ageMax += 1
|
||||
|
||||
if low ≤ currentMin:
|
||||
currentMin = low; ageMin = 0
|
||||
else:
|
||||
ageMin += 1
|
||||
|
||||
// Decay toward midpoint
|
||||
midpoint = (currentMax + currentMin) / 2
|
||||
maxDecay = 1 - exp(-lambda * ageMax)
|
||||
minDecay = 1 - exp(-lambda * ageMin)
|
||||
currentMax -= maxDecay * (currentMax - midpoint)
|
||||
currentMin -= minDecay * (currentMin - midpoint)
|
||||
|
||||
// Clamp to Donchian bounds
|
||||
currentMax = min(currentMax, periodMax)
|
||||
currentMin = max(currentMin, periodMin)
|
||||
|
||||
return [currentMax, currentMin]
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Description |
|
||||
|
||||
+2
-21
@@ -1,5 +1,7 @@
|
||||
# FCB: Fractal Chaos Bands
|
||||
|
||||
> *Fractal chaos bands connect swing pivots into a channel, letting the market's own geometry define containment.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -74,27 +76,6 @@ Fractal detection is $O(1)$ (3 comparisons). Deque maintenance is $O(1)$ amortiz
|
||||
|-----------|-------------|---------|------------|
|
||||
| `period` | Lookback window for highest/lowest fractal values | 20 | $> 0$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function FCB(high, low, period):
|
||||
validate: period > 0
|
||||
|
||||
// 3-bar fractal detection (confirmed at current bar)
|
||||
is_fractal_high = high[1] > high[2] AND high[1] > high[0]
|
||||
is_fractal_low = low[1] < low[2] AND low[1] < low[0]
|
||||
|
||||
// Update persistent fractal values
|
||||
if is_fractal_high: hi_fractal = high[1]
|
||||
if is_fractal_low: lo_fractal = low[1]
|
||||
|
||||
// Sliding window max/min via monotonic deques
|
||||
upper = max(hi_fractal over period) // deque-based
|
||||
lower = min(lo_fractal over period) // deque-based
|
||||
|
||||
return [upper, lower]
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Description |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# JBANDS: Jurik Adaptive Envelope Bands
|
||||
|
||||
> *Jurik's adaptive envelope adjusts its width with price dynamics, hugging trends and releasing during consolidation.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -100,37 +102,6 @@ Dominated by the trimmed mean's partial sort: $O(n \log n)$ for the 128-element
|
||||
| $\text{sqrtDiv}$ | $\text{\_SQRT\_PARAM} / (\text{\_SQRT\_PARAM} + 1)$ |
|
||||
| $P_{\text{exp}}$ | $\max(\text{\_LOG\_PARAM} - 2,\; 0.5)$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function JBANDS(source, period, phase):
|
||||
precompute constants from period and phase
|
||||
|
||||
// 1. Local deviation
|
||||
dLocal = max(|source - upper|, |source - lower|) + ε
|
||||
|
||||
// 2. Volatility: 10-bar SMA → 128-bar trimmed mean
|
||||
highD = SMA(dLocal, 10)
|
||||
dRef = TrimmedMean(highD_history, 128)
|
||||
|
||||
// 3. Dynamic exponent
|
||||
ratio = |source - band| / dRef
|
||||
d = clamp(ratio^P_exp, 1, LOG_PARAM)
|
||||
|
||||
// 4. Snap-and-decay bands
|
||||
adapt = sqrtDiv^√d
|
||||
if source > upper: upper = source
|
||||
else: upper = source - (source - upper) * adapt
|
||||
(symmetric for lower)
|
||||
|
||||
// 5. JMA center line (2-pole IIR)
|
||||
alpha = lenDiv^d
|
||||
... (c0, c8, a8 recursion) ...
|
||||
jma = prev_jma + a8
|
||||
|
||||
return [jma, upper, lower]
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Description |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# KCHANNEL: Keltner Channel
|
||||
|
||||
> *Keltner wraps an EMA in ATR-scaled bands — a volatility envelope that responds to both trend and range.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -81,33 +83,6 @@ $O(1)$ per bar: one EMA update, one True Range computation, one RMA update, and
|
||||
| Gap sensitivity | Yes (via TR) | Yes (via TR) | No |
|
||||
| Distribution assumption | None | None | Gaussian |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function KCHANNEL(source, high, low, close, period, multiplier):
|
||||
validate: period > 0, multiplier > 0
|
||||
|
||||
// EMA center line (with warmup compensation)
|
||||
alpha = 2 / (period + 1)
|
||||
raw_ema = alpha * source + (1-alpha) * raw_ema
|
||||
weight = alpha + (1-alpha) * weight
|
||||
ema = raw_ema / weight
|
||||
|
||||
// ATR (Wilder's RMA with warmup)
|
||||
tr = max(high - low, |high - prev_close|, |low - prev_close|)
|
||||
prev_close = close
|
||||
raw_rma = (raw_rma * (period-1) + tr) / period
|
||||
e *= (1 - 1/period)
|
||||
atr = e > ε ? raw_rma / (1-e) : raw_rma
|
||||
|
||||
// Bands
|
||||
width = multiplier * atr
|
||||
upper = ema + width
|
||||
lower = ema - width
|
||||
|
||||
return [ema, upper, lower]
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Description |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# MAENV: Moving Average Envelope
|
||||
|
||||
> *A fixed percentage above and below a moving average — the simplest envelope assumes symmetry in price behavior.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -71,25 +73,6 @@ $O(1)$ for SMA and EMA modes. $O(n)$ for WMA mode due to the weighted sum.
|
||||
| `ma_type` | Moving average type: 0=SMA, 1=EMA, 2=WMA | 1 (EMA) | $\{0, 1, 2\}$ |
|
||||
| `source` | Input price series | close | |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function MAENV(source, period, percentage, ma_type):
|
||||
validate: period > 0, percentage > 0
|
||||
|
||||
// Compute center line based on MA type
|
||||
if ma_type == 0: middle = SMA(source, period)
|
||||
if ma_type == 1: middle = EMA(source, period) // with warmup
|
||||
if ma_type == 2: middle = WMA(source, period)
|
||||
|
||||
// Fixed percentage offset
|
||||
dist = middle * percentage / 100
|
||||
upper = middle + dist
|
||||
lower = middle - dist
|
||||
|
||||
return [middle, upper, lower]
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Description |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# MMCHANNEL: Min-Max Channel
|
||||
|
||||
> *The raw min-max channel captures absolute extremes — no smoothing, no forgiveness, just the bounds of recent history.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# PCHANNEL: Price Channel
|
||||
|
||||
> *Price channels frame the trading range by its own high-low extremes, defining the field of play.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -79,41 +81,6 @@ Streaming: $O(1)$ amortized per bar. Each element enters and exits each deque at
|
||||
|--------|------|------------|-------------|
|
||||
| $n$ | period | $> 0$ | Lookback window size |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function pchannel(high[], low[], period):
|
||||
max_deque = empty // decreasing monotonic deque of indices
|
||||
min_deque = empty // increasing monotonic deque of indices
|
||||
hbuf = circular_buffer(period)
|
||||
lbuf = circular_buffer(period)
|
||||
|
||||
for each bar t:
|
||||
hbuf[t mod period] = high[t]
|
||||
lbuf[t mod period] = low[t]
|
||||
|
||||
// expire stale front entries
|
||||
while max_deque not empty AND max_deque.front <= t - period:
|
||||
max_deque.pop_front()
|
||||
while min_deque not empty AND min_deque.front <= t - period:
|
||||
min_deque.pop_front()
|
||||
|
||||
// remove dominated back entries
|
||||
while max_deque not empty AND hbuf[max_deque.back mod period] <= high[t]:
|
||||
max_deque.pop_back()
|
||||
while min_deque not empty AND lbuf[min_deque.back mod period] >= low[t]:
|
||||
min_deque.pop_back()
|
||||
|
||||
max_deque.push_back(t)
|
||||
min_deque.push_back(t)
|
||||
|
||||
upper = hbuf[max_deque.front mod period]
|
||||
lower = lbuf[min_deque.front mod period]
|
||||
middle = (upper + lower) / 2
|
||||
|
||||
emit (upper, middle, lower)
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Interpretation |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# REGCHANNEL: Linear Regression Channel
|
||||
|
||||
> *A regression line flanked by standard error bands — the channel where statistics meets price trajectory.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -101,45 +103,6 @@ $$
|
||||
|
||||
For $n \geq 2$, $D > 0$ always, ensuring numerical stability.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function regchannel(source[], period, multiplier):
|
||||
buf = ring_buffer(period)
|
||||
sum_x = period * (period - 1) / 2
|
||||
sum_x2 = period * (period - 1) * (2 * period - 1) / 6
|
||||
denom = period * sum_x2 - sum_x * sum_x
|
||||
|
||||
for each bar t:
|
||||
buf.add(source[t])
|
||||
n = buf.count
|
||||
|
||||
// pass 1: accumulate sums for regression
|
||||
sum_y = 0
|
||||
sum_xy = 0
|
||||
for i = 0 to n-1:
|
||||
y = buf[i]
|
||||
sum_y += y
|
||||
sum_xy += i * y
|
||||
|
||||
slope = (n * sum_xy - sum_x * sum_y) / denom
|
||||
intercept = (sum_y - slope * sum_x) / n
|
||||
middle = slope * (n - 1) + intercept
|
||||
|
||||
// pass 2: residual standard deviation
|
||||
ssr = 0
|
||||
for i = 0 to n-1:
|
||||
predicted = slope * i + intercept
|
||||
residual = buf[i] - predicted
|
||||
ssr += residual * residual
|
||||
|
||||
stddev = sqrt(ssr / n)
|
||||
upper = middle + multiplier * stddev
|
||||
lower = middle - multiplier * stddev
|
||||
|
||||
emit (upper, middle, lower)
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Interpretation |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# SDCHANNEL: Standard Deviation Channel
|
||||
|
||||
> *Standard deviation channels center on a moving average and let dispersion define the expected range.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -97,44 +99,6 @@ $$
|
||||
|
||||
For $n \geq 2$, $D > 0$ always holds, so the slope denominator is never zero.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function sdchannel(source[], period, multiplier):
|
||||
buf = ring_buffer(period)
|
||||
sum_x = period * (period - 1) / 2
|
||||
sum_x2 = period * (period - 1) * (2 * period - 1) / 6
|
||||
denom = period * sum_x2 - sum_x * sum_x
|
||||
|
||||
for each bar t:
|
||||
buf.add(source[t])
|
||||
n = buf.count
|
||||
|
||||
// pass 1: regression coefficients
|
||||
sum_y = 0, sum_xy = 0
|
||||
for i = 0 to n-1:
|
||||
y = buf[i]
|
||||
sum_y += y
|
||||
sum_xy += i * y
|
||||
|
||||
slope = (n * sum_xy - sum_x * sum_y) / denom
|
||||
intercept = (sum_y - slope * sum_x) / n
|
||||
middle = slope * (n - 1) + intercept
|
||||
|
||||
// pass 2: residual standard deviation
|
||||
ssr = 0
|
||||
for i = 0 to n-1:
|
||||
predicted = slope * i + intercept
|
||||
residual = buf[i] - predicted
|
||||
ssr += residual * residual
|
||||
|
||||
stddev = sqrt(ssr / n)
|
||||
upper = middle + multiplier * stddev
|
||||
lower = middle - multiplier * stddev
|
||||
|
||||
emit (upper, middle, lower)
|
||||
```
|
||||
|
||||
### Slope Interpretation
|
||||
|
||||
| Slope | Market State |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# STARCHANNEL: Stoller Average Range Channel
|
||||
|
||||
> *Stoller channels use ATR to build a corridor around the average — a volatility-aware boundary for range traders.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -90,51 +92,6 @@ Streaming: $O(1)$ per bar. The SMA uses a running sum with circular buffer (add
|
||||
| $k$ | multiplier | 2.0 | $> 0$ | ATR multiplier for band width |
|
||||
| $n_{\text{atr}}$ | atr_length | 0 | $\geq 0$ | Separate ATR period (0 = same as SMA period) |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function starchannel(source[], high[], low[], close[], period, multiplier, atr_length):
|
||||
effective_atr = atr_length > 0 ? atr_length : period
|
||||
alpha = 1.0 / effective_atr
|
||||
|
||||
buf = circular_buffer(period)
|
||||
sum = 0.0
|
||||
count = 0
|
||||
|
||||
raw_rma = 0.0
|
||||
e = 1.0 // warmup compensator
|
||||
prevClose = close[0]
|
||||
EPSILON = 1e-10
|
||||
|
||||
for each bar t:
|
||||
// SMA via running sum
|
||||
if buf.is_full:
|
||||
sum -= buf.oldest
|
||||
count -= 1
|
||||
buf.add(source[t])
|
||||
sum += source[t]
|
||||
count += 1
|
||||
middle = sum / count
|
||||
|
||||
// True Range
|
||||
tr = max(high[t] - low[t],
|
||||
abs(high[t] - prevClose),
|
||||
abs(low[t] - prevClose))
|
||||
prevClose = close[t]
|
||||
|
||||
// RMA with warmup compensator
|
||||
raw_rma = (raw_rma * (effective_atr - 1) + tr) / effective_atr
|
||||
e = (1 - alpha) * e
|
||||
atr = e > EPSILON ? raw_rma / (1 - e) : raw_rma
|
||||
|
||||
// Bands
|
||||
width = atr * multiplier
|
||||
upper = middle + width
|
||||
lower = middle - width
|
||||
|
||||
emit (middle, upper, lower)
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Interpretation |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# STBANDS: Super Trend Bands
|
||||
|
||||
> *Super Trend bands fuse trend direction with volatility width, flipping their bias at each breakout.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -111,62 +113,6 @@ Streaming: $O(1)$ per bar. The TR running sum uses a ring buffer; the ratchet lo
|
||||
| $n$ | period | 10 | $> 0$ | ATR lookback period |
|
||||
| $k$ | multiplier | 3.0 | $> 0$ | ATR multiplier for band distance from HL2 |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function stbands(high[], low[], close[], period, multiplier):
|
||||
tr_buf = ring_buffer(period)
|
||||
tr_sum = 0, count = 0
|
||||
prev_close = close[0]
|
||||
final_upper = NaN, final_lower = NaN
|
||||
trend = +1
|
||||
|
||||
for each bar t:
|
||||
h = high[t], l = low[t], c = close[t]
|
||||
|
||||
// True Range
|
||||
tr = max(h - l, abs(h - prev_close), abs(l - prev_close))
|
||||
|
||||
// ATR via running sum ring buffer
|
||||
if tr_buf.is_full:
|
||||
tr_sum -= tr_buf.oldest
|
||||
count -= 1
|
||||
tr_buf.add(tr)
|
||||
tr_sum += tr
|
||||
count += 1
|
||||
atr = tr_sum / count
|
||||
|
||||
// Basic bands centered on HL2
|
||||
hl2 = (h + l) / 2
|
||||
basic_upper = hl2 + multiplier * atr
|
||||
basic_lower = hl2 - multiplier * atr
|
||||
|
||||
if t == 0:
|
||||
final_upper = basic_upper
|
||||
final_lower = basic_lower
|
||||
trend = +1
|
||||
else:
|
||||
// Ratchet: upper only tightens or resets on breakout
|
||||
if basic_upper < final_upper OR prev_close > final_upper:
|
||||
final_upper = basic_upper
|
||||
// otherwise hold
|
||||
|
||||
// Ratchet: lower only tightens or resets on breakdown
|
||||
if basic_lower > final_lower OR prev_close < final_lower:
|
||||
final_lower = basic_lower
|
||||
// otherwise hold
|
||||
|
||||
// Trend flip
|
||||
if c <= final_lower:
|
||||
trend = +1
|
||||
else if c >= final_upper:
|
||||
trend = -1
|
||||
// otherwise hold previous trend
|
||||
|
||||
prev_close = c
|
||||
emit (final_upper, final_lower, trend)
|
||||
```
|
||||
|
||||
### Band State Transitions
|
||||
|
||||
| Condition | Upper Band | Lower Band |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# TTM_LRC: TTM Linear Regression Channel
|
||||
|
||||
> *Linear regression channels project the statistical trend and drape standard deviation curtains around it.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -90,50 +92,6 @@ Per bar: $O(n)$ due to two loops over the window. Memory: a ring buffer of $n$ d
|
||||
| $n$ | period | 100 | $> 1$ | Lookback window for regression |
|
||||
| $k$ | deviations | 2.0 | $> 0$ | Outer band stddev multiplier |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function ttm_lrc(source[], period, deviations):
|
||||
buf = ring_buffer(period)
|
||||
sum_x = period * (period - 1) / 2
|
||||
sum_x2 = period * (period - 1) * (2 * period - 1) / 6
|
||||
denom = period * sum_x2 - sum_x * sum_x
|
||||
|
||||
for each bar t:
|
||||
buf.add(source[t])
|
||||
n = buf.count
|
||||
|
||||
// pass 1: regression
|
||||
sum_y = 0, sum_xy = 0
|
||||
for i = 0 to n-1:
|
||||
y = buf[i]
|
||||
sum_y += y
|
||||
sum_xy += i * y
|
||||
|
||||
slope = (n * sum_xy - sum_x * sum_y) / denom
|
||||
intercept = (sum_y - slope * sum_x) / n
|
||||
midline = slope * (n - 1) + intercept
|
||||
|
||||
// pass 2: residuals
|
||||
ssr = 0, sst = 0
|
||||
mean_y = sum_y / n
|
||||
for i = 0 to n-1:
|
||||
predicted = slope * i + intercept
|
||||
residual = buf[i] - predicted
|
||||
ssr += residual * residual
|
||||
sst += (buf[i] - mean_y)^2
|
||||
|
||||
stddev = sqrt(ssr / n)
|
||||
r_squared = sst > 0 ? 1 - ssr / sst : 0
|
||||
|
||||
upper1 = midline + 1.0 * stddev
|
||||
lower1 = midline - 1.0 * stddev
|
||||
upper2 = midline + deviations * stddev
|
||||
lower2 = midline - deviations * stddev
|
||||
|
||||
emit (midline, upper1, lower1, upper2, lower2, slope, r_squared)
|
||||
```
|
||||
|
||||
### Statistical Zone Interpretation
|
||||
|
||||
| Zone | Probability | Interpretation |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# UBANDS: Ehlers Ultimate Bands
|
||||
|
||||
> *Ehlers' ultimate bands apply cycle-aware smoothing to define an envelope that resonates with dominant frequency.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -107,47 +109,6 @@ $$
|
||||
|
||||
Cutoff frequency: approximately $f_c \approx 1/(2\pi n)$ cycles per bar. Rolloff: 12 dB/octave.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function ubands(source[], period, multiplier):
|
||||
// precompute USF coefficients
|
||||
arg = sqrt(2) * pi / period
|
||||
c2 = 2 * exp(-arg) * cos(arg)
|
||||
c3 = -exp(-2 * arg)
|
||||
c1 = (1 + c2 - c3) / 4
|
||||
|
||||
usf_prev1 = NaN, usf_prev2 = NaN
|
||||
|
||||
for each bar t:
|
||||
s0 = source[t]
|
||||
s1 = source[t-1] // or s0 if unavailable
|
||||
s2 = source[t-2] // or s1 if unavailable
|
||||
|
||||
if usf not initialized:
|
||||
usf = s0
|
||||
else:
|
||||
usf = (1 - c1)*s0 + (2*c1 - c2)*s1
|
||||
- (c1 + c3)*s2 + c2*usf_prev1 + c3*usf_prev2
|
||||
|
||||
usf_prev2 = usf_prev1
|
||||
usf_prev1 = usf
|
||||
|
||||
// RMS of residuals over window
|
||||
sum_sq = 0, count = 0
|
||||
for i = 0 to period-1:
|
||||
r = source[t-i] - usf_at[t-i] // residual at bar t-i
|
||||
if r is valid:
|
||||
sum_sq += r * r
|
||||
count += 1
|
||||
|
||||
rms = count > 0 ? sqrt(sum_sq / count) : 0
|
||||
upper = usf + multiplier * rms
|
||||
lower = usf - multiplier * rms
|
||||
|
||||
emit (upper, usf, lower)
|
||||
```
|
||||
|
||||
### RMS vs Standard Deviation
|
||||
|
||||
Standard deviation measures dispersion around the mean: $\sigma = \sqrt{E[(X - \mu)^2]}$. RMS measures dispersion around zero: $\text{RMS} = \sqrt{E[X^2]}$. Since the residuals $r_t = P_t - \text{USF}_t$ are already deviations from the smooth centerline, RMS is the correct measure. When the mean of residuals is zero (as it approximately is for a well-fitted filter), RMS equals standard deviation.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# UCHANNEL: Ehlers Ultimate Channel
|
||||
|
||||
> *The ultimate channel uses Ehlers' signal processing to carve boundaries that track the market's hidden periodicity.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -108,55 +110,6 @@ $$
|
||||
|
||||
Frequency response: cutoff at approximately $f_c \approx 1/(2\pi n)$ cycles per bar; 12 dB/octave rolloff.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function uchannel(close[], high[], low[], strPeriod, centerPeriod, multiplier):
|
||||
// compute USF coefficients for STR
|
||||
arg_s = sqrt(2) * pi / strPeriod
|
||||
c2_s = 2 * exp(-arg_s) * cos(arg_s)
|
||||
c3_s = -exp(-2 * arg_s)
|
||||
c1_s = (1 + c2_s - c3_s) / 4
|
||||
|
||||
// compute USF coefficients for centerline
|
||||
arg_c = sqrt(2) * pi / centerPeriod
|
||||
c2_c = 2 * exp(-arg_c) * cos(arg_c)
|
||||
c3_c = -exp(-2 * arg_c)
|
||||
c1_c = (1 + c2_c - c3_c) / 4
|
||||
|
||||
usf_str = [NaN, NaN] // two-element state
|
||||
usf_cen = [NaN, NaN]
|
||||
|
||||
for each bar t:
|
||||
// True Range
|
||||
th = max(high[t], close[t-1])
|
||||
tl = min(low[t], close[t-1])
|
||||
tr = th - tl
|
||||
|
||||
// USF for True Range → STR
|
||||
if usf_str not initialized:
|
||||
str_val = tr
|
||||
else:
|
||||
str_val = (1-c1_s)*tr + (2*c1_s-c2_s)*tr[t-1]
|
||||
- (c1_s+c3_s)*tr[t-2]
|
||||
+ c2_s*usf_str[0] + c3_s*usf_str[1]
|
||||
usf_str = [str_val, usf_str[0]]
|
||||
|
||||
// USF for close → centerline
|
||||
if usf_cen not initialized:
|
||||
center = close[t]
|
||||
else:
|
||||
center = (1-c1_c)*close[t] + (2*c1_c-c2_c)*close[t-1]
|
||||
- (c1_c+c3_c)*close[t-2]
|
||||
+ c2_c*usf_cen[0] + c3_c*usf_cen[1]
|
||||
usf_cen = [center, usf_cen[0]]
|
||||
|
||||
upper = center + multiplier * str_val
|
||||
lower = center - multiplier * str_val
|
||||
|
||||
emit (upper, center, lower)
|
||||
```
|
||||
|
||||
### UCHANNEL vs UBANDS
|
||||
|
||||
| Aspect | UBANDS | UCHANNEL |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# VWAPBANDS: VWAP with Dual Standard Deviation Bands
|
||||
|
||||
> *VWAP anchored by dual deviation bands reveals where volume-weighted fair value ends and excess begins.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -86,48 +88,6 @@ Streaming: $O(1)$ per bar. Three additions to running sums, one division, one sq
|
||||
|--------|------|---------|------------|-------------|
|
||||
| $k$ | multiplier | 1.0 | $> 0$ | Scales the standard deviation for band width |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function vwapbands(source[], volume[], reset[], multiplier):
|
||||
sum_pv = 0, sum_vol = 0, sum_pv2 = 0, count = 0
|
||||
|
||||
for each bar t:
|
||||
price = source[t]
|
||||
vol = volume[t]
|
||||
|
||||
if reset[t]:
|
||||
// session boundary: restart accumulation
|
||||
if vol > 0:
|
||||
sum_pv = price * vol
|
||||
sum_vol = vol
|
||||
sum_pv2 = price * price * vol
|
||||
count = 1
|
||||
else:
|
||||
sum_pv = 0, sum_vol = 0, sum_pv2 = 0, count = 0
|
||||
else:
|
||||
if vol > 0:
|
||||
sum_pv += price * vol
|
||||
sum_vol += vol
|
||||
sum_pv2 += price * price * vol
|
||||
count += 1
|
||||
|
||||
vwap = sum_vol > 0 ? sum_pv / sum_vol : price
|
||||
|
||||
variance = 0
|
||||
if sum_vol > 0 and count > 1:
|
||||
variance = max(0, sum_pv2 / sum_vol - vwap * vwap)
|
||||
|
||||
stddev = sqrt(variance)
|
||||
|
||||
upper1 = vwap + multiplier * stddev
|
||||
lower1 = vwap - multiplier * stddev
|
||||
upper2 = vwap + 2 * multiplier * stddev
|
||||
lower2 = vwap - 2 * multiplier * stddev
|
||||
|
||||
emit (vwap, upper1, lower1, upper2, lower2, stddev)
|
||||
```
|
||||
|
||||
### Statistical Zone Interpretation
|
||||
|
||||
| Zone | Coverage | Interpretation |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# VWAPSD: VWAP with Standard Deviation Bands
|
||||
|
||||
> *Standard deviation bands around VWAP measure institutional consensus — proximity signals fair value, distance signals opportunity.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Channel |
|
||||
@@ -84,40 +86,6 @@ Streaming: $O(1)$ per bar. Three additions to running sums, one division, one sq
|
||||
|--------|------|---------|------------|-------------|
|
||||
| $k$ | numDevs | 2.0 | $0.1$ – $5.0$ | Number of standard deviations for bands |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function vwapsd(source[], volume[], reset[], numDevs):
|
||||
sum_pv = 0, sum_vol = 0, sum_pv2 = 0
|
||||
|
||||
for each bar t:
|
||||
price = source[t]
|
||||
vol = volume[t]
|
||||
|
||||
if reset[t]:
|
||||
if vol > 0:
|
||||
sum_pv = price * vol
|
||||
sum_vol = vol
|
||||
sum_pv2 = price * price * vol
|
||||
else:
|
||||
sum_pv = 0, sum_vol = 0, sum_pv2 = 0
|
||||
else:
|
||||
if vol > 0:
|
||||
sum_pv += price * vol
|
||||
sum_vol += vol
|
||||
sum_pv2 += price * price * vol
|
||||
|
||||
vwap = sum_vol > 0 ? sum_pv / sum_vol : price
|
||||
|
||||
variance = sum_vol > 0 ? sum_pv2 / sum_vol - vwap * vwap : 0
|
||||
stddev = sqrt(max(0, variance))
|
||||
|
||||
upper = vwap + numDevs * stddev
|
||||
lower = vwap - numDevs * stddev
|
||||
|
||||
emit (vwap, upper, lower)
|
||||
```
|
||||
|
||||
### VWAPSD vs VWAPBANDS
|
||||
|
||||
| Aspect | VWAPSD | VWAPBANDS |
|
||||
|
||||
@@ -34,4 +34,3 @@ All Core indicators share common traits:
|
||||
| TBar | AVGPRICE, MEDPRICE, MIDPRICE, MIDBODY, TYPPRICE, WCLPRICE | OHLCV bars |
|
||||
| TValue | MIDPOINT | Single value series |
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# AVGPRICE: Average Price
|
||||
|
||||
> *The four-point average distills an entire bar into a single representative price — open, high, low, and close in equal measure.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Core |
|
||||
@@ -62,22 +64,6 @@ $O(1)$ per bar. Two additions, one FMA. No memory allocation. Always hot after t
|
||||
| Typical Price | $(H+L+C) \times \frac{1}{3}$ | `HLC3` | `Typprice` |
|
||||
| Weighted Close | $(H+L+2C) \times 0.25$ | `HLCC4` | `Wclprice` |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function AVGPRICE(bar):
|
||||
o, h, l, c ← bar.Open, bar.High, bar.Low, bar.Close
|
||||
|
||||
// Substitute last-valid for non-finite inputs
|
||||
if !finite(o): o ← lastValidOpen
|
||||
if !finite(h): h ← lastValidHigh
|
||||
if !finite(l): l ← lastValidLow
|
||||
if !finite(c): c ← lastValidClose
|
||||
|
||||
result ← FMA(o + h, 0.25, (l + c) × 0.25)
|
||||
return result
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Context | Meaning |
|
||||
|
||||
+2
-27
@@ -1,5 +1,7 @@
|
||||
# HA: Heikin-Ashi
|
||||
|
||||
> *The trend is your friend — but only if the noise doesn't make you abandon it at the first bump.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Core |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires `1` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "The trend is your friend — but only if the noise doesn't make you abandon it at the first bump." — Every trader, eventually
|
||||
|
||||
HA transforms standard OHLC bars into smoothed Heikin-Ashi candles by averaging each component with its predecessor. The Close is the bar's four-price mean $(O+H+L+C)/4$, the Open is a recursive midpoint of the prior HA Open and HA Close, and High/Low are clamped extremes that guarantee the HA body always fits inside the HA wick. Unlike most indicators that reduce a bar to a single scalar, HA outputs a complete `TBar` — four smoothed prices per bar — making it a bar-to-bar transform rather than a bar-to-value reduction. The recursive Open gives HA an IIR character: each bar carries a decaying memory of the entire price history, which is what flattens trend noise but also why HA prices do not match any actual traded price.
|
||||
|
||||
## Historical Context
|
||||
@@ -100,31 +100,6 @@ $$\text{WarmupPeriod} = 1$$
|
||||
|
||||
HA is "hot" from bar 1. The seed bar uses $(O_0 + C_0)/2$ for HA_Open and produces valid output immediately. The recursive filter converges rapidly due to the $\beta = 0.5$ decay — after 7 bars, the contribution of the seed value is less than 0.4%.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function HA(bar, prevHaOpen, prevHaClose):
|
||||
o, h, l, c ← bar.Open, bar.High, bar.Low, bar.Close
|
||||
|
||||
// Substitute last-valid for non-finite inputs
|
||||
if !finite(o): o ← lastValidOpen
|
||||
if !finite(h): h ← lastValidHigh
|
||||
if !finite(l): l ← lastValidLow
|
||||
if !finite(c): c ← lastValidClose
|
||||
|
||||
haClose ← FMA(o + h, 0.25, (l + c) × 0.25)
|
||||
|
||||
if firstBar:
|
||||
haOpen ← (o + c) × 0.5
|
||||
else:
|
||||
haOpen ← (prevHaOpen + prevHaClose) × 0.5
|
||||
|
||||
haHigh ← max(h, haOpen, haClose)
|
||||
haLow ← min(l, haOpen, haClose)
|
||||
|
||||
return TBar(bar.Time, haOpen, haHigh, haLow, haClose, bar.Volume)
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Candle Pattern | Meaning |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# MEDPRICE: Median Price
|
||||
|
||||
> *The midpoint of high and low captures the bar's central tendency, ignoring where it opened or closed.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Core |
|
||||
@@ -62,20 +64,6 @@ $O(1)$ per bar. One addition, one multiply. No memory allocation. Always hot aft
|
||||
| AVGPRICE | O, H, L, C | Equal | Fully balanced |
|
||||
| WCLPRICE | H, L, C | C double-weighted | Close-biased |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function MEDPRICE(bar):
|
||||
h, l ← bar.High, bar.Low
|
||||
|
||||
// Substitute last-valid for non-finite inputs
|
||||
if !finite(h): h ← lastValidHigh
|
||||
if !finite(l): l ← lastValidLow
|
||||
|
||||
result ← (h + l) × 0.5
|
||||
return result
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Context | Meaning |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# MIDBODY: Open-Close Average
|
||||
|
||||
> *The average of open and close reveals the candle body's center — where intent met execution.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Core |
|
||||
@@ -70,13 +72,6 @@ None. OC2 is parameterless.
|
||||
| AVGPRICE | $(O+H+L+C) \times 0.25$ | O, H, L, C | Fully balanced |
|
||||
| WCLPRICE | $(H+L+2C) \times 0.25$ | H, L, C | Close double-weighted |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```text
|
||||
function Midbody(bar):
|
||||
return (bar.Open + bar.Close) * 0.5
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
- OC2 > Close: bar closed below its midpoint (bearish lean)
|
||||
@@ -119,9 +114,3 @@ The batch loop is a trivial element-wise `(a[i] + b[i]) * 0.5`. Auto-vectorizati
|
||||
|
||||
- **Skender.Stock.Indicators** `CandlePart.OC2` enum documentation.
|
||||
- **Murphy, J.J.** *Technical Analysis of the Financial Markets*. New York Institute of Finance, 1999.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# MIDPOINT: Rolling Range Midpoint
|
||||
|
||||
> *The center holds, but only for the window you're watching.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Core |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires `period` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "The center holds, but only for the window you're watching." — Statistical folk wisdom
|
||||
|
||||
Single-series rolling midpoint: `(Highest(V, N) + Lowest(V, N)) * 0.5`. Returns the center of the value range within a lookback window. TA-Lib compatible (`MIDPOINT` function). Unlike MIDPRICE which operates on separate High/Low bar channels, MIDPOINT operates on a single value series.
|
||||
|
||||
## Historical Context
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# MIDPRICE: Midpoint Price over Period
|
||||
|
||||
> *The midpoint of highest high and lowest low over a period anchors price to its range center.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Core |
|
||||
@@ -71,25 +73,6 @@ $O(N)$ per bar where $N$ is the period, due to linear scan for max/min. For typi
|
||||
| MEDPRICE | $(H + L) \times 0.5$ | TBar (single bar) | Stateless |
|
||||
| Donchian Mid | Same as MIDPRICE | TBar (H/L channels) | Rolling window |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function MIDPRICE(bar, period):
|
||||
validate: period ≥ 1
|
||||
|
||||
h, l ← bar.High, bar.Low
|
||||
|
||||
// Substitute last-valid for non-finite inputs
|
||||
if !finite(h): h ← lastValidHigh
|
||||
if !finite(l): l ← lastValidLow
|
||||
|
||||
highBuffer.Add(h)
|
||||
lowBuffer.Add(l)
|
||||
|
||||
result ← (highBuffer.Max() + lowBuffer.Min()) × 0.5
|
||||
return result
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Context | Meaning |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# TYPPRICE: Typical Price
|
||||
|
||||
> *Typical price weights high, low, and close equally — a three-point summary that drops the open and keeps the essential.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Core |
|
||||
@@ -59,23 +61,6 @@ $O(1)$ per bar. One addition, one FMA. No memory allocation. Always hot after th
|
||||
|
||||
Division by a non-power-of-two constant is 4-5x more expensive than multiplication on modern x86 CPUs (~15 cycles vs ~3 cycles). Precomputing $\frac{1}{3}$ as a `const double` and multiplying eliminates the division entirely. The compiler constant-folds `1.0 / 3.0` to the IEEE 754 double `0x3FD5555555555555` at compile time, so the hot path sees only multiply/FMA operations.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```text
|
||||
function TYPPRICE(bar):
|
||||
const OneThird ← 1.0 / 3.0 // compile-time constant
|
||||
|
||||
o, h, l ← bar.Open, bar.High, bar.Low
|
||||
|
||||
// Substitute last-valid for non-finite inputs
|
||||
if !finite(o): o ← lastValidOpen
|
||||
if !finite(h): h ← lastValidHigh
|
||||
if !finite(l): l ← lastValidLow
|
||||
|
||||
result ← FMA(o, OneThird, (h + l) × OneThird)
|
||||
return result
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Context | Meaning |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# WCLPRICE: Weighted Close Price
|
||||
|
||||
> *Weighted close doubles the closing price's vote, acknowledging that where a bar ends matters most.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Core |
|
||||
@@ -66,21 +68,6 @@ $O(1)$ per bar. One addition, one FMA. No memory allocation. Always hot after th
|
||||
| TYPPRICE | 0% | 33.3% | 33.3% | 33.3% |
|
||||
| **WCLPRICE** | **0%** | **25%** | **25%** | **50%** |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function WCLPRICE(bar):
|
||||
h, l, c ← bar.High, bar.Low, bar.Close
|
||||
|
||||
// Substitute last-valid for non-finite inputs
|
||||
if !finite(h): h ← lastValidHigh
|
||||
if !finite(l): l ← lastValidLow
|
||||
if !finite(c): c ← lastValidClose
|
||||
|
||||
result ← FMA(c, 0.5, (h + l) × 0.25)
|
||||
return result
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Context | Meaning |
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Cycles
|
||||
|
||||
> "The market is a discounting mechanism that anticipates cycles before they complete." Unknown
|
||||
|
||||
Cycle analysis identifies repeating patterns in price data. John Ehlers pioneered digital signal processing techniques for financial cycles, using Hilbert transforms and autocorrelation to detect dominant periods. Cycles exist but are non-stationary: period and amplitude shift over time.
|
||||
|
||||
## Indicators
|
||||
|
||||
+2
-42
@@ -1,5 +1,7 @@
|
||||
# CCOR: Ehlers Correlation Cycle
|
||||
|
||||
> *Correlation cycles reveal hidden periodicities by measuring how well price correlates with a rotating reference wave.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -94,48 +96,6 @@ Where:
|
||||
- Real: $y_k = \cos(2\pi k / N)$
|
||||
- Imaginary: $y_k = -\sin(2\pi k / N)$
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function CCOR(source, period, threshold):
|
||||
// Real correlation
|
||||
Sx_r = Sy_r = Sxx_r = Sxy_r = Syy_r = 0
|
||||
for k = 0 to period-1:
|
||||
x = source[k]
|
||||
y = cos(2π * k / period)
|
||||
Sx_r += x; Sy_r += y
|
||||
Sxx_r += x*x; Sxy_r += x*y; Syy_r += y*y
|
||||
denom_r = (N*Sxx_r - Sx_r²) * (N*Syy_r - Sy_r²)
|
||||
real = denom_r > 0 ? (N*Sxy_r - Sx_r*Sy_r) / √denom_r : 0
|
||||
|
||||
// Imaginary correlation (same accumulators for x, different y)
|
||||
Sx_i = Sy_i = Sxx_i = Sxy_i = Syy_i = 0
|
||||
for k = 0 to period-1:
|
||||
x = source[k]
|
||||
y = -sin(2π * k / period)
|
||||
Sx_i += x; Sy_i += y
|
||||
Sxx_i += x*x; Sxy_i += x*y; Syy_i += y*y
|
||||
denom_i = (N*Sxx_i - Sx_i²) * (N*Syy_i - Sy_i²)
|
||||
imag = denom_i > 0 ? (N*Sxy_i - Sx_i*Sy_i) / √denom_i : 0
|
||||
|
||||
// Phasor angle
|
||||
angle = 0
|
||||
if imag ≠ 0: angle = 90 + atan(real/imag) * (180/π)
|
||||
if imag > 0: angle -= 180
|
||||
|
||||
// Monotonic constraint
|
||||
angle = max(angle, prev_angle)
|
||||
prev_angle = angle
|
||||
|
||||
// State detection
|
||||
Δθ = |angle - saved_prev_angle|
|
||||
state = 0
|
||||
if Δθ < threshold and angle ≥ 0: state = +1
|
||||
if Δθ < threshold and angle ≤ 0: state = -1
|
||||
|
||||
return [real, imag, angle, state]
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Range | Meaning |
|
||||
|
||||
+2
-27
@@ -1,5 +1,7 @@
|
||||
# CCYC: Ehlers Cyber Cycle
|
||||
|
||||
> *The Cyber Cycle isolator extracts the dominant cycle component while suppressing trend — pure periodicity distilled.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -109,33 +111,6 @@ $$z^2 - 2(1-\alpha)z + (1-\alpha)^2 = 0$$
|
||||
|
||||
has a double pole at $z = 1 - \alpha$. For $\alpha = 0.07$, the pole is at $z = 0.93$, well inside the unit circle (stable), with a $-3$ dB cutoff period of approximately $\frac{2\pi}{\alpha} \approx 90$ bars.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function CCYC(source, alpha):
|
||||
validate: 0 < alpha < 1
|
||||
|
||||
// FIR smoother
|
||||
smooth = (source[0] + 2*source[1] + 2*source[2] + source[3]) / 6
|
||||
|
||||
// IIR coefficients
|
||||
c_hp = (1 - 0.5*alpha)²
|
||||
c_fb1 = 2*(1 - alpha)
|
||||
c_fb2 = -(1 - alpha)²
|
||||
|
||||
if bar_count < 7:
|
||||
// Bootstrap: second-difference of raw price
|
||||
cycle = (source[0] - 2*source[1] + source[2]) / 4
|
||||
else:
|
||||
// Steady-state: 2-pole high-pass on smoothed input
|
||||
cycle = c_hp * (smooth - 2*smooth[1] + smooth[2])
|
||||
+ c_fb1 * cycle[1] + c_fb2 * cycle[2]
|
||||
|
||||
trigger = cycle[1]
|
||||
|
||||
return [cycle, trigger]
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Description |
|
||||
|
||||
+2
-25
@@ -1,5 +1,7 @@
|
||||
# CG: Ehlers Center of Gravity
|
||||
|
||||
> *Center of Gravity locates the balance point of price over a window, anticipating turns before they arrive.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -54,31 +56,6 @@ Streaming uses running sums for both numerator and denominator: $O(1)$ per bar w
|
||||
|-----------|-------------|---------|------------|
|
||||
| `period` | Lookback window length | 10 | $> 0$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function CG(source, period):
|
||||
buffer ← RingBuffer(period)
|
||||
runNum ← 0 // weighted sum
|
||||
runDen ← 0 // simple sum
|
||||
|
||||
for each price in source:
|
||||
buffer.Add(price)
|
||||
if buffer.Count < period: continue
|
||||
|
||||
// Compute from buffer (or maintain running sums)
|
||||
num = 0
|
||||
den = 0
|
||||
for i = 0 to period-1:
|
||||
w = i + 1
|
||||
num += w * buffer[i]
|
||||
den += buffer[i]
|
||||
|
||||
cg = (den ≠ 0) ? (num / den) - (period + 1) / 2.0 : 0
|
||||
|
||||
emit cg
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Condition | Meaning |
|
||||
|
||||
+2
-28
@@ -1,5 +1,7 @@
|
||||
# DSP: Ehlers Detrended Synthetic Price
|
||||
|
||||
> *Detrended synthetic price removes the trend to expose the oscillation underneath — the signal beneath the drift.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -66,34 +68,6 @@ $O(1)$ per bar with $O(1)$ memory. Two EMA state variables plus two bias correct
|
||||
|-----------|-------------|---------|------------|
|
||||
| `period` | Dominant cycle period | 40 | $\geq 4$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function DSP(source, period):
|
||||
pFast ← max(2, round(period / 4))
|
||||
pSlow ← max(3, round(period / 2))
|
||||
αFast ← 2 / (pFast + 1)
|
||||
αSlow ← 2 / (pSlow + 1)
|
||||
|
||||
emaFastRaw ← 0
|
||||
emaSlowRaw ← 0
|
||||
decayFast ← 1.0 // (1 - αFast)^n
|
||||
decaySlow ← 1.0 // (1 - αSlow)^n
|
||||
|
||||
for each price in source:
|
||||
emaFastRaw ← FMA(αFast, price, (1 - αFast) * emaFastRaw)
|
||||
emaSlowRaw ← FMA(αSlow, price, (1 - αSlow) * emaSlowRaw)
|
||||
|
||||
decayFast *= (1 - αFast)
|
||||
decaySlow *= (1 - αSlow)
|
||||
|
||||
emaFast ← emaFastRaw / (1 - decayFast)
|
||||
emaSlow ← emaSlowRaw / (1 - decaySlow)
|
||||
|
||||
dsp ← emaFast - emaSlow
|
||||
emit dsp
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Condition | Meaning |
|
||||
|
||||
+2
-45
@@ -1,5 +1,7 @@
|
||||
# EACP: Ehlers Autocorrelation Periodogram
|
||||
|
||||
> *Autocorrelation periodogram scans every possible cycle length and ranks them by strength — a spectral fingerprint of the market.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -72,51 +74,6 @@ $O(N \times M)$ per bar where $N$ is the period range and $M$ is the averaging l
|
||||
| `maxPeriod` | Maximum period to evaluate | 48 | $> minPeriod$ |
|
||||
| `enhance` | Apply cubic emphasis to spectral peaks | true | |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function EACP(source, minPeriod, maxPeriod, enhance):
|
||||
N ← maxPeriod - minPeriod + 1
|
||||
M ← maxPeriod // averaging window
|
||||
hpBuf ← HighPassFilter(source)
|
||||
ssfBuf ← SuperSmoother(hpBuf)
|
||||
|
||||
power[N] ← {0}
|
||||
smoothPower[N] ← {0}
|
||||
|
||||
for each bar:
|
||||
// Autocorrelation for each lag
|
||||
corr[0..maxPeriod] ← PearsonAutocorrelation(ssfBuf, M)
|
||||
|
||||
// DFT: convert autocorrelation to power spectrum
|
||||
for p = minPeriod to maxPeriod:
|
||||
cosPower ← 0
|
||||
for k = 0 to M-1:
|
||||
cosPower += corr[k] * cos(2π * k / p)
|
||||
power[p] ← cosPower²
|
||||
|
||||
// Exponential smoothing of spectrum
|
||||
for p = minPeriod to maxPeriod:
|
||||
smoothPower[p] ← 0.2 * power[p] + 0.8 * smoothPower[p]
|
||||
|
||||
// Optional cubic enhancement
|
||||
if enhance:
|
||||
for p: smoothPower[p] ← smoothPower[p]³
|
||||
|
||||
// AGC normalization
|
||||
maxPow ← max(smoothPower)
|
||||
for p: smoothPower[p] /= maxPow // normalize to [0, 1]
|
||||
|
||||
// Center-of-gravity dominant cycle
|
||||
num ← 0; den ← 0
|
||||
for p = minPeriod to maxPeriod:
|
||||
num += smoothPower[p] * p
|
||||
den += smoothPower[p]
|
||||
dominantCycle ← (den > 0) ? num / den : (minPeriod + maxPeriod) / 2
|
||||
|
||||
emit dominantCycle
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Meaning |
|
||||
|
||||
+2
-39
@@ -1,5 +1,7 @@
|
||||
# EBSW: Ehlers Even Better Sinewave
|
||||
|
||||
> *Even Better Sinewave refines cycle detection by combining bandpass filtering with adaptive gain normalization.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -78,45 +80,6 @@ b = 2·a·cos(√2·π / ssfLength)
|
||||
c₁ = (1 - b + a²) / 2
|
||||
```
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function EBSW(source, hpLength, ssfLength):
|
||||
// Precompute HP coefficient
|
||||
α₁ ← (1 - sin(2π/hpLength)) / cos(2π/hpLength)
|
||||
|
||||
// Precompute SSF coefficients
|
||||
a ← exp(-√2·π / ssfLength)
|
||||
b ← 2·a·cos(√2·π / ssfLength)
|
||||
c₁ ← (1 - b + a²) / 2
|
||||
|
||||
hp_prev ← 0; p_prev ← 0
|
||||
filt_1 ← 0; filt_2 ← 0
|
||||
|
||||
for each price in source:
|
||||
// High-pass filter
|
||||
hp ← 0.5·(1 + α₁)·(price - p_prev) + α₁·hp_prev
|
||||
|
||||
// Super-smoother
|
||||
filt ← c₁·(hp + hp_prev) + b·filt_1 - a²·filt_2
|
||||
|
||||
// Wave (3-bar average of filtered signal)
|
||||
wave ← (filt + filt_1 + filt_2) / 3
|
||||
|
||||
// Power (3-bar RMS²)
|
||||
power ← (filt² + filt_1² + filt_2²) / 3
|
||||
|
||||
// AGC normalization
|
||||
ebsw ← (power > 0) ? wave / √power : 0
|
||||
ebsw ← clamp(ebsw, -1, +1)
|
||||
|
||||
// Shift state
|
||||
hp_prev ← hp; p_prev ← price
|
||||
filt_2 ← filt_1; filt_1 ← filt
|
||||
|
||||
emit ebsw
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Condition | Meaning |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# HOMOD: Ehlers Homodyne Discriminator
|
||||
|
||||
> *The homodyne discriminator locks onto a cycle's frequency by comparing successive analytic signal rotations.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -71,60 +73,6 @@ $O(1)$ per bar with $O(1)$ memory. The pipeline consists entirely of fixed-depth
|
||||
| `minPeriod` | Minimum detectable period | 6.0 | $> 0$ |
|
||||
| `maxPeriod` | Maximum detectable period | 50.0 | $> minPeriod$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function HOMOD(source, minPeriod, maxPeriod):
|
||||
A ← 0.0962; B ← 0.5769
|
||||
smoothBuf ← CircularBuffer(7)
|
||||
detBuf ← CircularBuffer(7)
|
||||
|
||||
I2_prev ← 0; Q2_prev ← 0
|
||||
Re_prev ← 0; Im_prev ← 0
|
||||
period_prev ← (minPeriod + maxPeriod) / 2
|
||||
|
||||
for each price in source:
|
||||
// 4-bar WMA
|
||||
smooth ← (4·price + 3·p[1] + 2·p[2] + p[3]) / 10
|
||||
smoothBuf.Add(smooth)
|
||||
|
||||
// Detrender (Hilbert FIR)
|
||||
det ← A·smooth[0] + B·smooth[2] - B·smooth[4] - A·smooth[6]
|
||||
detBuf.Add(det)
|
||||
|
||||
// I1 = det[3], Q1 = Hilbert(det)
|
||||
I1 ← det[3]
|
||||
Q1 ← A·det[0] + B·det[2] - B·det[4] - A·det[6]
|
||||
|
||||
// Hilbert of I1 and Q1
|
||||
jI ← HilbertFIR(I1_history)
|
||||
jQ ← HilbertFIR(Q1_history)
|
||||
|
||||
// Phasor components (smoothed)
|
||||
I2 ← 0.2·(I1 - jQ) + 0.8·I2_prev
|
||||
Q2 ← 0.2·(Q1 + jI) + 0.8·Q2_prev
|
||||
|
||||
// Homodyne mixing
|
||||
re ← 0.2·(I2·I2_prev + Q2·Q2_prev) + 0.8·Re_prev
|
||||
im ← 0.2·(I2·Q2_prev - Q2·I2_prev) + 0.8·Im_prev
|
||||
|
||||
// Period extraction
|
||||
if im ≠ 0 and re ≠ 0:
|
||||
period ← 2π / atan2(im, re)
|
||||
else:
|
||||
period ← period_prev
|
||||
|
||||
period ← clamp(period, minPeriod, maxPeriod)
|
||||
period ← 0.33·period + 0.67·period_prev
|
||||
|
||||
// Update state
|
||||
I2_prev ← I2; Q2_prev ← Q2
|
||||
Re_prev ← re; Im_prev ← im
|
||||
period_prev ← period
|
||||
|
||||
emit period
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Meaning |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# HT_DCPERIOD: Ehlers Hilbert Transform Dominant Cycle Period
|
||||
|
||||
> *The Hilbert Transform extracts the dominant cycle period by converting price into an analytic signal and measuring its phase rate.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -68,47 +70,6 @@ $O(1)$ per bar. Fixed Hilbert cascade with circular buffers totaling approximate
|
||||
|
||||
The period range [6, 50] and all smoothing constants are fixed by the TA-Lib specification.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function HT_DCPERIOD(source):
|
||||
A ← 0.0962; B ← 0.5769
|
||||
smoothBuf ← CircularBuffer(7)
|
||||
detBuf, q1Buf, i1Buf ← CircularBuffers
|
||||
|
||||
I2 ← 0; Q2 ← 0
|
||||
Re ← 0; Im ← 0
|
||||
period ← 15 // initial estimate
|
||||
|
||||
for each price in source:
|
||||
// Step 1: WMA smooth
|
||||
smooth ← (4·price + 3·p[1] + 2·p[2] + p[3]) / 10
|
||||
|
||||
// Step 2: Hilbert FIR (adaptive to period)
|
||||
adj ← A + B // coefficient adjustment
|
||||
det ← adj·(smooth[0] - smooth[6]) + B·(smooth[2] - smooth[4])
|
||||
Q1 ← adj·(det[0] - det[6]) + B·(det[2] - det[4])
|
||||
I1 ← det[3]
|
||||
jI ← adj·(I1[0] - I1[6]) + B·(I1[2] - I1[4])
|
||||
jQ ← adj·(Q1[0] - Q1[6]) + B·(Q1[2] - Q1[4])
|
||||
|
||||
// Step 3: Phasor (EMA smoothed)
|
||||
I2 ← 0.2·(I1 - jQ) + 0.8·I2
|
||||
Q2 ← 0.2·(Q1 + jI) + 0.8·Q2
|
||||
|
||||
// Step 4: Homodyne discriminator
|
||||
Re ← 0.2·(I2·I2_prev + Q2·Q2_prev) + 0.8·Re
|
||||
Im ← 0.2·(I2·Q2_prev - Q2·I2_prev) + 0.8·Im
|
||||
|
||||
// Step 5: Period
|
||||
if Im ≠ 0 and Re ≠ 0:
|
||||
p ← 2π / atan(Im / Re)
|
||||
p ← clamp(p, 6, 50)
|
||||
period ← 0.33·p + 0.67·period
|
||||
|
||||
emit period
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Output | Meaning |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# HT_DCPHASE: Ehlers Hilbert Transform Dominant Cycle Phase
|
||||
|
||||
> *Dominant cycle phase tracks where price sits within its current cycle — the angular position of the market's heartbeat.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -64,38 +66,6 @@ $O(P)$ per bar where $P$ is the smoothed period (typically 6-50), due to the DFT
|
||||
|
||||
All internal constants are fixed by the TA-Lib specification.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function HT_DCPHASE(source):
|
||||
// Same Hilbert cascade as HT_DCPERIOD
|
||||
// ... (WMA smooth, Hilbert FIR, phasor, homodyne)
|
||||
// Produces: smoothPeriod, smoothPriceBuf
|
||||
|
||||
for each bar (after warmup):
|
||||
P ← round(smoothPeriod)
|
||||
|
||||
// DFT accumulation over dominant period
|
||||
realPart ← 0; imagPart ← 0
|
||||
for i = 0 to P-1:
|
||||
realPart += sin(2π·i / P) · smoothPriceBuf[t - i]
|
||||
imagPart += cos(2π·i / P) · smoothPriceBuf[t - i]
|
||||
|
||||
// Phase extraction
|
||||
if |imagPart| > 0:
|
||||
dcPhase ← atan(realPart / imagPart) · (180/π)
|
||||
else:
|
||||
dcPhase ← 90 · sign(realPart)
|
||||
|
||||
if imagPart > 0: dcPhase -= 180
|
||||
dcPhase += 90
|
||||
|
||||
// Wrap to [-45, 315]
|
||||
if dcPhase < -45: dcPhase += 360
|
||||
|
||||
emit dcPhase
|
||||
```
|
||||
|
||||
### Phase Quadrant Interpretation
|
||||
|
||||
| Phase Range | Cycle Position |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# HT_PHASOR: Ehlers Hilbert Transform Phasor Components
|
||||
|
||||
> *Phasor components decompose price into in-phase and quadrature parts, mapping the cycle as a rotating vector.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -58,37 +60,6 @@ $O(1)$ per bar. Fixed Hilbert cascade with circular buffers. Warmup: 32 bars (TA
|
||||
|-----------|-------------|---------|------------|
|
||||
| (none) | No user-configurable parameters | | |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function HT_PHASOR(source):
|
||||
A ← 0.0962; B ← 0.5769
|
||||
smoothBuf ← CircularBuffer(7)
|
||||
detBuf, q1Buf, i1Buf ← CircularBuffers
|
||||
|
||||
I2 ← 0; Q2 ← 0
|
||||
|
||||
for each price in source:
|
||||
// WMA smooth
|
||||
smooth ← (4·price + 3·p[1] + 2·p[2] + p[3]) / 10
|
||||
smoothBuf.Add(smooth)
|
||||
|
||||
// Hilbert FIR (adaptive)
|
||||
det ← A·smooth[0] + B·smooth[2] - B·smooth[4] - A·smooth[6]
|
||||
Q1 ← A·det[0] + B·det[2] - B·det[4] - A·det[6]
|
||||
I1 ← det[3]
|
||||
|
||||
// Hilbert of I1 and Q1
|
||||
jI ← A·I1[0] + B·I1[2] - B·I1[4] - A·I1[6]
|
||||
jQ ← A·Q1[0] + B·Q1[2] - B·Q1[4] - A·Q1[6]
|
||||
|
||||
// Phasor components (EMA smoothed)
|
||||
I2 ← 0.2·(I1 - jQ) + 0.8·I2
|
||||
Q2 ← 0.2·(Q1 + jI) + 0.8·Q2
|
||||
|
||||
emit InPhase = I2, Quadrature = Q2
|
||||
```
|
||||
|
||||
### Phasor Crossover Signals
|
||||
|
||||
| Condition | Signal |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# HT_SINE: Ehlers Hilbert Transform SineWave (also known as SINE)
|
||||
|
||||
> *The Hilbert sine wave renders cycle timing visible — crossovers of sine and lead-sine mark turning points.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -70,27 +72,6 @@ $O(P)$ per bar where $P$ is the smoothed period (typically 6-50), due to the DFT
|
||||
|
||||
All constants are fixed by the TA-Lib specification.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function HT_SINE(source):
|
||||
// Full Hilbert cascade (same as HT_DCPHASE)
|
||||
// Produces: smoothPeriod, smoothPriceBuf, dcPhase
|
||||
|
||||
for each bar (after warmup):
|
||||
// Phase from DFT accumulation (see HT_DCPHASE)
|
||||
φ ← computeDCPhase(smoothPeriod, smoothPriceBuf)
|
||||
|
||||
// Convert phase to radians
|
||||
φ_rad ← φ · (π / 180)
|
||||
|
||||
// Dual sine output
|
||||
sine ← sin(φ_rad)
|
||||
leadSine ← sin(φ_rad + π/4) // 45° lead
|
||||
|
||||
emit sine, leadSine
|
||||
```
|
||||
|
||||
### Crossover Signals
|
||||
|
||||
| Pattern | Signal |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# LUNAR: Lunar Phase Indicator
|
||||
|
||||
> *The lunar cycle maps the Moon's phase onto price — an ancient rhythm tested against modern markets.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -82,41 +84,6 @@ $O(1)$ per timestamp. No state required (deterministic from time). Zero warmup.
|
||||
|
||||
The calculation is entirely determined by the input timestamp.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function LUNAR(timestamp):
|
||||
// Julian date
|
||||
JD ← timestamp_to_unix_ms / 86400000 + 2440587.5
|
||||
T ← (JD - 2451545.0) / 36525.0
|
||||
|
||||
// Mean orbital elements (Horner evaluation)
|
||||
Lp ← FMA(T, FMA(T, FMA(T, 1/538841, -0.0015786), 481267.88123421), 218.3164477)
|
||||
D ← FMA(T, FMA(T, FMA(T, 1/545868, -0.0018819), 445267.1114034), 297.8501921)
|
||||
M ← FMA(T, FMA(T, -0.0001536, 35999.0502909), 357.5291092)
|
||||
Mp ← FMA(T, FMA(T, 0.0087414, 477198.8675055), 134.9633964)
|
||||
F ← FMA(T, FMA(T, -0.0036539, 483202.0175233), 93.2720950)
|
||||
|
||||
// Normalize to [0°, 360°)
|
||||
Lp, D, M, Mp, F ← mod(*, 360)
|
||||
|
||||
// Perturbation correction (6 major terms)
|
||||
Σ ← 6288016·sin(Mp) + 1274242·sin(2D - Mp) + 658314·sin(2D)
|
||||
+ 214818·sin(2Mp) + 186986·sin(M) + 109154·sin(2F)
|
||||
λ_moon ← Lp + Σ / 1e6
|
||||
|
||||
// Solar longitude (simplified)
|
||||
L0 ← 280.46646 + 36000.76983·T
|
||||
M_sun ← 357.52911 + 35999.05029·T
|
||||
λ_sun ← L0 + 1.9146·sin(M_sun) + 0.02·sin(2·M_sun)
|
||||
|
||||
// Phase angle and illumination
|
||||
ψ ← λ_moon - λ_sun
|
||||
k ← (1 - cos(ψ)) / 2
|
||||
|
||||
emit k // 0.0 = New Moon, 1.0 = Full Moon
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Value | Phase |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# SOLAR: Solar Cycle Indicator
|
||||
|
||||
> *Solar cycles encode the Sun's rhythmic activity into a tradeable signal, bridging astrophysics and price action.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -74,36 +76,6 @@ $O(1)$ per timestamp. No state required. Zero warmup. The tropical year is appro
|
||||
|
||||
The calculation is entirely determined by the input timestamp.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function SOLAR(timestamp):
|
||||
// Julian date
|
||||
JD ← timestamp_to_unix_ms / 86400000 + 2440587.5
|
||||
T ← (JD - 2451545.0) / 36525.0
|
||||
|
||||
// Geometric mean longitude
|
||||
L0 ← FMA(T, FMA(T, 0.0003032, 36000.76983), 280.46646)
|
||||
L0 ← mod(L0, 360)
|
||||
|
||||
// Mean anomaly
|
||||
M ← FMA(T, FMA(T, -0.0001537, 35999.05029), 357.52911)
|
||||
M ← mod(M, 360)
|
||||
|
||||
// Equation of center
|
||||
C ← FMA(T, FMA(T, -0.000014, -0.004817), 1.914602) · sin(M)
|
||||
+ FMA(T, -0.000101, 0.019993) · sin(2M)
|
||||
+ 0.000289 · sin(3M)
|
||||
|
||||
// True ecliptic longitude
|
||||
λ ← L0 + C
|
||||
|
||||
// Seasonal index
|
||||
solar ← sin(λ · π / 180)
|
||||
|
||||
emit solar
|
||||
```
|
||||
|
||||
### Seasonal Correspondence (Northern Hemisphere)
|
||||
|
||||
| Date (approx.) | $\lambda_{Sun}$ | Solar Value | Season |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# SSFDSP: Ehlers SSF Detrended Synthetic Price
|
||||
|
||||
> *SSF-based detrended synthetic price applies a super smoother before extracting cycles, achieving cleaner periodicity isolation.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Cycle |
|
||||
@@ -70,50 +72,6 @@ $O(1)$ per bar. Two independent 2-pole IIR filters with $O(1)$ memory. Warmup: a
|
||||
|
||||
The SSF has $-3$ dB attenuation at the cutoff period, $-12$ dB/octave rolloff (2-pole), and zero phase lag at the cutoff. This is equivalent to a critically-damped Butterworth filter.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function SSFDSP(source, period):
|
||||
pFast ← max(2, round(period / 4))
|
||||
pSlow ← max(3, round(period / 2))
|
||||
|
||||
// Fast SSF coefficients
|
||||
αf ← √2·π / pFast
|
||||
c2f ← 2·exp(-αf)·cos(αf)
|
||||
c3f ← -exp(-2·αf)
|
||||
c1f ← 1 - c2f - c3f
|
||||
|
||||
// Slow SSF coefficients
|
||||
αs ← √2·π / pSlow
|
||||
c2s ← 2·exp(-αs)·cos(αs)
|
||||
c3s ← -exp(-2·αs)
|
||||
c1s ← 1 - c2s - c3s
|
||||
|
||||
ssfFast_1 ← 0; ssfFast_2 ← 0
|
||||
ssfSlow_1 ← 0; ssfSlow_2 ← 0
|
||||
p_prev ← 0
|
||||
|
||||
for each price in source:
|
||||
// Input averaging
|
||||
avg ← (price + p_prev) / 2
|
||||
|
||||
// Fast SSF update
|
||||
ssfFast ← c1f·avg + c2f·ssfFast_1 + c3f·ssfFast_2
|
||||
|
||||
// Slow SSF update
|
||||
ssfSlow ← c1s·avg + c2s·ssfSlow_1 + c3s·ssfSlow_2
|
||||
|
||||
// SSFDSP
|
||||
ssfdsp ← ssfFast - ssfSlow
|
||||
|
||||
// Shift state
|
||||
ssfFast_2 ← ssfFast_1; ssfFast_1 ← ssfFast
|
||||
ssfSlow_2 ← ssfSlow_1; ssfSlow_1 ← ssfSlow
|
||||
p_prev ← price
|
||||
|
||||
emit ssfdsp
|
||||
```
|
||||
|
||||
### DSP vs SSFDSP
|
||||
|
||||
| Aspect | DSP (EMA-based) | SSFDSP (Super-Smoother) |
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Dynamics
|
||||
|
||||
> "The trend is your friend, but only if you know its strength." Unknown
|
||||
|
||||
Dynamics indicators measure trend strength, speed, and direction. Unlike momentum indicators that measure rate of change, dynamics indicators answer: "Is there a trend, and how strong is it?" Critical for filtering signals and avoiding whipsaws in ranging markets.
|
||||
|
||||
## Indicators
|
||||
|
||||
+2
-43
@@ -1,5 +1,7 @@
|
||||
# ADX: Average Directional Index
|
||||
|
||||
> *ADX measures trend strength without regard to direction — a compass that tells you how hard the wind blows, not where.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -78,49 +80,6 @@ $$ADX = \text{RMA}(DX, N)$$
|
||||
|--------|-----------|---------|------------|
|
||||
| $N$ | period | 14 | $N \geq 2$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
Initialize:
|
||||
α = 1 / period
|
||||
smoothPlusDM = smoothMinusDM = smoothTR = 0
|
||||
adx = 0
|
||||
prevHigh = prevLow = NaN
|
||||
bar_count = 0
|
||||
|
||||
On each bar (high, low, close, isNew):
|
||||
if !isNew: restore previous state
|
||||
|
||||
TR = max(high - low, |high - prevClose|, |low - prevClose|)
|
||||
|
||||
upMove = high - prevHigh
|
||||
downMove = prevLow - low
|
||||
|
||||
+DM = (upMove > downMove AND upMove > 0) ? upMove : 0
|
||||
-DM = (downMove > upMove AND downMove > 0) ? downMove : 0
|
||||
|
||||
// Wilder smoothing (RMA)
|
||||
smoothPlusDM = FMA(smoothPlusDM, 1 - α, α × +DM)
|
||||
smoothMinusDM = FMA(smoothMinusDM, 1 - α, α × -DM)
|
||||
smoothTR = FMA(smoothTR, 1 - α, α × TR)
|
||||
|
||||
// Directional Indicators
|
||||
+DI = 100 × smoothPlusDM / smoothTR
|
||||
-DI = 100 × smoothMinusDM / smoothTR
|
||||
|
||||
// Directional Index
|
||||
diSum = +DI + -DI
|
||||
DX = diSum > 0 ? 100 × |+DI - -DI| / diSum : 0
|
||||
|
||||
// Final smoothing
|
||||
ADX = FMA(ADX, 1 - α, α × DX)
|
||||
|
||||
prevHigh = high
|
||||
prevLow = low
|
||||
prevClose = close
|
||||
output = ADX
|
||||
```
|
||||
|
||||
### The Stability Problem
|
||||
|
||||
Because ADX relies on recursive RMA at multiple stages, convergence is slow. Period 14 needs roughly 40-56 bars before matching TA-Lib to 4 decimal places. The first $2N$ values are mathematically correct but statistically immature — treat them as warmup artifacts.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# ADXR: Average Directional Movement Rating
|
||||
|
||||
> *ADXR smooths ADX over time, filtering out momentary strength spikes to reveal the underlying trend conviction.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -56,34 +58,6 @@ The $N-1$ lag (rather than $N$) matches TA-Lib's reference implementation exactl
|
||||
|
||||
The period controls both the internal ADX calculation and the historical lookback depth.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
Initialize:
|
||||
adx = new Adx(period)
|
||||
adxBuffer = RingBuffer(period)
|
||||
bar_count = 0
|
||||
|
||||
On each bar (high, low, close, isNew):
|
||||
if !isNew: restore previous state
|
||||
|
||||
// Full ADX pipeline
|
||||
adxValue = adx.Update(high, low, close, isNew)
|
||||
|
||||
// Store in history
|
||||
adxBuffer.Add(adxValue)
|
||||
bar_count++
|
||||
|
||||
// ADXR = average of current and (N-1)-lagged ADX
|
||||
if bar_count >= period:
|
||||
historicalAdx = adxBuffer[0] // oldest value in buffer
|
||||
ADXR = (adxValue + historicalAdx) / 2.0
|
||||
else:
|
||||
ADXR = adxValue // insufficient history
|
||||
|
||||
output = ADXR
|
||||
```
|
||||
|
||||
### Lag Analysis
|
||||
|
||||
| Component | Lag Source |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# ALLIGATOR: Williams Alligator
|
||||
|
||||
> *Three displaced moving averages form jaw, teeth, and lips — when they diverge the Alligator feeds, when they converge it sleeps.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -71,40 +73,6 @@ The offsets shift plotted values forward in time for display purposes only. The
|
||||
| $N_l$ | lipsPeriod | 5 | $N_l \geq 1$ |
|
||||
| $O_l$ | lipsOffset | 3 | $O_l \geq 0$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
Initialize:
|
||||
α_jaw = 1 / jawPeriod
|
||||
α_teeth = 1 / teethPeriod
|
||||
α_lips = 1 / lipsPeriod
|
||||
jaw = teeth = lips = first source value
|
||||
e_jaw = e_teeth = e_lips = 1.0 // bias compensation
|
||||
|
||||
On each bar (high, low, close, isNew):
|
||||
if !isNew: restore previous state
|
||||
|
||||
source = (high + low + close) / 3.0
|
||||
|
||||
// SMMA updates with bias compensation
|
||||
jaw = FMA(jaw, 1 - α_jaw, α_jaw × source)
|
||||
e_jaw = e_jaw × (1 - α_jaw)
|
||||
jaw_compensated = jaw / (1 - e_jaw)
|
||||
|
||||
teeth = FMA(teeth, 1 - α_teeth, α_teeth × source)
|
||||
e_teeth = e_teeth × (1 - α_teeth)
|
||||
teeth_compensated = teeth / (1 - e_teeth)
|
||||
|
||||
lips = FMA(lips, 1 - α_lips, α_lips × source)
|
||||
e_lips = e_lips × (1 - α_lips)
|
||||
lips_compensated = lips / (1 - e_lips)
|
||||
|
||||
output:
|
||||
Jaw = jaw_compensated (plot at bar + jawOffset)
|
||||
Teeth = teeth_compensated (plot at bar + teethOffset)
|
||||
Lips = lips_compensated (plot at bar + lipsOffset)
|
||||
```
|
||||
|
||||
### Market Phase Detection
|
||||
|
||||
| Phase | Line Configuration | Action |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# AMAT: Archer Moving Averages Trends
|
||||
|
||||
> *Archer's moving average trends compare fast and slow averages, signaling when short-term momentum confirms the longer-term direction.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -63,52 +65,6 @@ $$\text{Strength}_t = \frac{|\text{Fast}_t - \text{Slow}_t|}{\text{Slow}_t} \tim
|
||||
| $N_f$ | fastPeriod | 10 | $N_f \geq 1$ |
|
||||
| $N_s$ | slowPeriod | 50 | $N_s > N_f$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
Initialize:
|
||||
α_fast = 2 / (fastPeriod + 1)
|
||||
α_slow = 2 / (slowPeriod + 1)
|
||||
ema_fast = ema_slow = 0
|
||||
e_fast = e_slow = 1.0
|
||||
prev_fast = prev_slow = 0
|
||||
bar_count = 0
|
||||
|
||||
On each bar (price, isNew):
|
||||
if !isNew: restore previous state
|
||||
|
||||
// EMA updates
|
||||
ema_fast = FMA(ema_fast, 1 - α_fast, α_fast × price)
|
||||
e_fast = e_fast × (1 - α_fast)
|
||||
fast = ema_fast / (1 - e_fast)
|
||||
|
||||
ema_slow = FMA(ema_slow, 1 - α_slow, α_slow × price)
|
||||
e_slow = e_slow × (1 - α_slow)
|
||||
slow = ema_slow / (1 - e_slow)
|
||||
|
||||
// Direction detection
|
||||
fastDir = fast > prev_fast ? +1 : fast < prev_fast ? -1 : 0
|
||||
slowDir = slow > prev_slow ? +1 : slow < prev_slow ? -1 : 0
|
||||
|
||||
// Triple-confirmation
|
||||
if fast > slow AND fastDir == +1 AND slowDir == +1:
|
||||
trend = +1
|
||||
else if fast < slow AND fastDir == -1 AND slowDir == -1:
|
||||
trend = -1
|
||||
else:
|
||||
trend = 0
|
||||
|
||||
// Strength
|
||||
strength = slow > 0 ? |fast - slow| / slow × 100 : 0
|
||||
|
||||
prev_fast = fast
|
||||
prev_slow = slow
|
||||
|
||||
output:
|
||||
Trend = trend // +1, -1, or 0
|
||||
Strength = strength // percentage
|
||||
```
|
||||
|
||||
### Period Selection Guidelines
|
||||
|
||||
| Use Case | Fast | Slow | Ratio |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# AROON: Aroon Indicator
|
||||
|
||||
> *Aroon measures how recently the highest high and lowest low occurred — recency as a proxy for trend vitality.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -62,51 +64,6 @@ Range: $[-100, +100]$.
|
||||
|--------|-----------|---------|------------|
|
||||
| $N$ | period | 25 | $N \geq 1$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
Initialize:
|
||||
highBuf = RingBuffer(period + 1)
|
||||
lowBuf = RingBuffer(period + 1)
|
||||
bar_count = 0
|
||||
|
||||
On each bar (high, low, isNew):
|
||||
if !isNew: restore previous state
|
||||
|
||||
highBuf.Add(high)
|
||||
lowBuf.Add(low)
|
||||
bar_count++
|
||||
|
||||
// Find index of highest high in buffer
|
||||
maxIdx = 0
|
||||
maxVal = -∞
|
||||
for i = 0 to min(bar_count, period):
|
||||
if highBuf[i] >= maxVal:
|
||||
maxVal = highBuf[i]
|
||||
maxIdx = i
|
||||
|
||||
// Find index of lowest low in buffer
|
||||
minIdx = 0
|
||||
minVal = +∞
|
||||
for i = 0 to min(bar_count, period):
|
||||
if lowBuf[i] <= minVal:
|
||||
minVal = lowBuf[i]
|
||||
minIdx = i
|
||||
|
||||
len = min(bar_count, period)
|
||||
barsSinceHigh = len - maxIdx
|
||||
barsSinceLow = len - minIdx
|
||||
|
||||
AroonUp = (len - barsSinceHigh) / len × 100
|
||||
AroonDown = (len - barsSinceLow) / len × 100
|
||||
AroonOsc = AroonUp - AroonDown
|
||||
|
||||
output:
|
||||
Up = AroonUp
|
||||
Down = AroonDown
|
||||
Oscillator = AroonOsc
|
||||
```
|
||||
|
||||
### Interpretation
|
||||
|
||||
| Condition | Signal |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# AROONOSC: Aroon Oscillator
|
||||
|
||||
> *The Aroon oscillator subtracts Aroon Down from Aroon Up, collapsing two timing signals into a single directional gauge.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -56,36 +58,6 @@ $$\text{AroonOsc} = \text{AroonUp} - \text{AroonDown}$$
|
||||
|--------|-----------|---------|------------|
|
||||
| $N$ | period | 25 | $N \geq 1$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
Initialize:
|
||||
highBuf = RingBuffer(period + 1)
|
||||
lowBuf = RingBuffer(period + 1)
|
||||
bar_count = 0
|
||||
|
||||
On each bar (high, low, isNew):
|
||||
if !isNew: restore previous state
|
||||
|
||||
highBuf.Add(high)
|
||||
lowBuf.Add(low)
|
||||
bar_count++
|
||||
|
||||
len = min(bar_count, period)
|
||||
|
||||
// Scan for extremes
|
||||
maxIdx = index of maximum in highBuf over last (len + 1) entries
|
||||
minIdx = index of minimum in lowBuf over last (len + 1) entries
|
||||
|
||||
barsSinceHigh = len - maxIdx
|
||||
barsSinceLow = len - minIdx
|
||||
|
||||
AroonUp = (len - barsSinceHigh) / len × 100
|
||||
AroonDown = (len - barsSinceLow) / len × 100
|
||||
|
||||
output = AroonUp - AroonDown
|
||||
```
|
||||
|
||||
### Drift Immunity
|
||||
|
||||
Unlike EMA-based oscillators that accumulate rounding errors across thousands of bars, the Aroon Oscillator is computed fresh each bar from a finite window. There is no recursive state that can diverge. This makes it architecturally robust for long-running production systems where indicator drift is a concern.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# CHOP: Choppiness Index
|
||||
|
||||
> *Choppiness index quantifies how range-bound a market is — high values mean sideways, low values mean trending.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -56,50 +58,6 @@ The denominator $\log_{10}(N)$ normalizes the output so that the theoretical max
|
||||
|--------|-----------|---------|------------|
|
||||
| $N$ | period | 14 | $N \geq 2$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
Initialize:
|
||||
trBuf = RingBuffer(period)
|
||||
highBuf = RingBuffer(period)
|
||||
lowBuf = RingBuffer(period)
|
||||
trSum = 0
|
||||
prevClose = NaN
|
||||
logPeriod = log10(period)
|
||||
|
||||
On each bar (high, low, close, isNew):
|
||||
if !isNew: restore previous state
|
||||
|
||||
// True Range
|
||||
if prevClose is valid:
|
||||
TR = max(high - low, |high - prevClose|, |low - prevClose|)
|
||||
else:
|
||||
TR = high - low
|
||||
|
||||
// Rolling sum update
|
||||
if trBuf is full:
|
||||
trSum -= trBuf.Oldest
|
||||
trBuf.Add(TR)
|
||||
trSum += TR
|
||||
|
||||
highBuf.Add(high)
|
||||
lowBuf.Add(low)
|
||||
|
||||
// Channel width
|
||||
maxHigh = Max(highBuf)
|
||||
minLow = Min(lowBuf)
|
||||
channel = maxHigh - minLow
|
||||
|
||||
// Choppiness Index
|
||||
if channel > 0 AND trSum > 0:
|
||||
CHOP = 100 × log10(trSum / channel) / logPeriod
|
||||
else:
|
||||
CHOP = 50 // neutral fallback
|
||||
|
||||
prevClose = close
|
||||
output = Clamp(CHOP, 0, 100)
|
||||
```
|
||||
|
||||
### Interpretation
|
||||
|
||||
| CHOP Value | Market Regime | Strategy Implication |
|
||||
|
||||
+2
-41
@@ -1,5 +1,7 @@
|
||||
# DMX: Directional Movement Index (Jurik)
|
||||
|
||||
> *Jurik's directional movement applies adaptive smoothing to DI lines, reducing whipsaws in the classic DMI framework.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -70,47 +72,6 @@ Positive values indicate bullish directional dominance; negative values indicate
|
||||
|--------|-----------|---------|------------|
|
||||
| $N$ | period | 14 | $N \geq 2$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
Initialize:
|
||||
jmaPlusDM = new JMA(period)
|
||||
jmaMinusDM = new JMA(period)
|
||||
jmaTR = new JMA(period)
|
||||
prevHigh = prevLow = prevClose = NaN
|
||||
|
||||
On each bar (high, low, close, isNew):
|
||||
if !isNew: restore previous state
|
||||
|
||||
// Wilder's directional movement decomposition
|
||||
TR = max(high - low, |high - prevClose|, |low - prevClose|)
|
||||
|
||||
upMove = high - prevHigh
|
||||
downMove = prevLow - low
|
||||
|
||||
+DM = (upMove > downMove AND upMove > 0) ? upMove : 0
|
||||
-DM = (downMove > upMove AND downMove > 0) ? downMove : 0
|
||||
|
||||
// Jurik smoothing (replaces Wilder's RMA)
|
||||
smoothPlusDM = jmaPlusDM.Update(+DM)
|
||||
smoothMinusDM = jmaMinusDM.Update(-DM)
|
||||
smoothTR = jmaTR.Update(TR)
|
||||
|
||||
// Directional indicators
|
||||
if smoothTR > 0:
|
||||
DI_plus = 100 × smoothPlusDM / smoothTR
|
||||
DI_minus = 100 × smoothMinusDM / smoothTR
|
||||
else:
|
||||
DI_plus = DI_minus = 0
|
||||
|
||||
DMX = DI_plus - DI_minus
|
||||
|
||||
prevHigh = high
|
||||
prevLow = low
|
||||
prevClose = close
|
||||
output = DMX
|
||||
```
|
||||
|
||||
### DMX vs DMI Comparison
|
||||
|
||||
| Property | DMI (Wilder) | DMX (Jurik) |
|
||||
|
||||
+2
-43
@@ -1,5 +1,7 @@
|
||||
# DX: Directional Movement Index
|
||||
|
||||
> *Directional movement captures the difference between positive and negative thrust, normalized by true range.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -72,49 +74,6 @@ When $+DI + (-DI) = 0$ (no directional movement), DX = 0.
|
||||
|--------|-----------|---------|------------|
|
||||
| $N$ | period | 14 | $N \geq 2$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
Initialize:
|
||||
α = 1 / period
|
||||
smoothPlusDM = smoothMinusDM = smoothTR = 0
|
||||
prevHigh = prevLow = prevClose = NaN
|
||||
|
||||
On each bar (high, low, close, isNew):
|
||||
if !isNew: restore previous state
|
||||
|
||||
// True Range
|
||||
TR = max(high - low, |high - prevClose|, |low - prevClose|)
|
||||
|
||||
upMove = high - prevHigh
|
||||
downMove = prevLow - low
|
||||
|
||||
+DM = (upMove > downMove AND upMove > 0) ? upMove : 0
|
||||
-DM = (downMove > upMove AND downMove > 0) ? downMove : 0
|
||||
|
||||
// Wilder smoothing
|
||||
smoothPlusDM = FMA(smoothPlusDM, 1 - α, α × +DM)
|
||||
smoothMinusDM = FMA(smoothMinusDM, 1 - α, α × -DM)
|
||||
smoothTR = FMA(smoothTR, 1 - α, α × TR)
|
||||
|
||||
// Directional Indicators
|
||||
+DI = smoothTR > 0 ? 100 × smoothPlusDM / smoothTR : 0
|
||||
-DI = smoothTR > 0 ? 100 × smoothMinusDM / smoothTR : 0
|
||||
|
||||
// DX (raw, no final RMA)
|
||||
diSum = +DI + -DI
|
||||
DX = diSum > 0 ? 100 × |+DI - -DI| / diSum : 0
|
||||
|
||||
prevHigh = high
|
||||
prevLow = low
|
||||
prevClose = close
|
||||
|
||||
output:
|
||||
DX = DX // trend strength (0-100)
|
||||
DiPlus = +DI // bullish directional indicator
|
||||
DiMinus = -DI // bearish directional indicator
|
||||
```
|
||||
|
||||
### DX vs ADX
|
||||
|
||||
| Property | DX | ADX |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# GHLA: Gann High-Low Activator
|
||||
|
||||
> *The simplest indicators are the hardest to argue with. Two averages, one rule, and the market tells you which side of the fence to stand on.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires `period` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "The simplest indicators are the hardest to argue with. Two averages, one rule, and the market tells you which side of the fence to stand on."
|
||||
|
||||
The Gann High-Low Activator (GHLA) is a trend-following stop/reversal indicator that alternates between the Simple Moving Average of Highs and the Simple Moving Average of Lows based on a three-state crossover rule. Developed by Robert Krausz and published in *Technical Analysis of Stocks & Commodities* (February 1998), the indicator produces a single trailing line: SMA(Low) during uptrends (acting as dynamic support) and SMA(High) during downtrends (acting as dynamic resistance). The flip between states occurs only when price closes decisively beyond the opposing SMA, creating a hysteresis zone that filters minor whipsaws. With a default period of 3 bars, GHLA responds aggressively to trend changes while requiring just $O(N)$ additions and one comparison per bar.
|
||||
|
||||
## Historical Context
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# HT_TRENDMODE: Hilbert Transform Trend vs Cycle Mode
|
||||
|
||||
> *Hilbert trend mode classifies the market as trending or cycling — a binary answer from the analytic signal's behavior.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -89,66 +91,6 @@ Criterion 4: Price deviation override
|
||||
|
||||
No user-configurable parameters. The algorithm self-tunes based on the detected dominant cycle period (clamped to 6-50 bars).
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
Initialize:
|
||||
circBuffer = array for Hilbert state
|
||||
smoothPrice = priceHistory = arrays
|
||||
daysInTrend = 0
|
||||
smoothPeriod = 0
|
||||
prevDcPhase = 0
|
||||
bar_count = 0
|
||||
|
||||
On each bar (price, isNew):
|
||||
if !isNew: restore previous state
|
||||
|
||||
// Step 1: 4-bar WMA smooth
|
||||
smooth = (4×price[0] + 3×price[1] + 2×price[2] + price[3]) / 10
|
||||
|
||||
// Step 2: Hilbert Transform (FIR filters)
|
||||
detrender = HilbertFIR(smooth) × adjustedBandwidth
|
||||
Q1 = HilbertFIR(detrender) × adjustedBandwidth
|
||||
I1 = detrender[3]
|
||||
|
||||
// Step 3: Phasor rotation
|
||||
I2 = I1 - jQ_prev; Q2 = Q1 + jI_prev
|
||||
I2 = 0.2×I2 + 0.8×I2_prev; Q2 = 0.2×Q2 + 0.8×Q2_prev
|
||||
|
||||
// Step 4: Homodyne discriminator → period
|
||||
Re = 0.2×(I2×I2_prev + Q2×Q2_prev) + 0.8×Re_prev
|
||||
Im = 0.2×(I2×Q2_prev - Q2×I2_prev) + 0.8×Im_prev
|
||||
period = clamp(360 / (atan(Im/Re) × RAD2DEG), 6, 50)
|
||||
smoothPeriod = 0.33×period + 0.67×smoothPeriod_prev
|
||||
|
||||
// Step 5: DC Phase via DFT
|
||||
dcPeriodInt = floor(smoothPeriod + 0.5)
|
||||
realPart = Σ sin(i × 360/dcPeriodInt) × smooth[i] for i=0..dcPeriodInt-1
|
||||
imagPart = Σ cos(i × 360/dcPeriodInt) × smooth[i]
|
||||
dcPhase = atan(realPart/imagPart)×RAD2DEG + 90 + lagCompensation
|
||||
|
||||
// Step 6: SineWave indicators
|
||||
sine = sin(dcPhase × DEG2RAD)
|
||||
leadSine = sin((dcPhase + 45) × DEG2RAD)
|
||||
|
||||
// Step 7: Trendline (SMA smoothed with WMA)
|
||||
sma = average(price, dcPeriodInt)
|
||||
trendline = (4×sma[0] + 3×sma[1] + 2×sma[2] + sma[3]) / 10
|
||||
|
||||
// Step 8: Four-criteria trend decision
|
||||
trend = 1
|
||||
if sine crosses leadSine: daysInTrend = 0; trend = 0
|
||||
daysInTrend++
|
||||
if daysInTrend < 0.5 × smoothPeriod: trend = 0
|
||||
phaseChange = dcPhase - prevDcPhase
|
||||
expected = 360 / smoothPeriod
|
||||
if phaseChange > 0.67×expected AND phaseChange < 1.5×expected: trend = 0
|
||||
if |smooth - trendline| / trendline >= 0.015: trend = 1
|
||||
|
||||
prevDcPhase = dcPhase
|
||||
output = trend // 1 = trending, 0 = cycling
|
||||
```
|
||||
|
||||
### Decision Criteria Summary
|
||||
|
||||
| Criterion | Purpose |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# ICHIMOKU: Ichimoku Kinko Hyo
|
||||
|
||||
> *Five lines, one cloud, and a time-shifted perspective — Ichimoku maps support, resistance, and momentum in a single glance.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -81,44 +83,6 @@ Confirms trend by comparing current price to the price from 26 bars ago.
|
||||
| $N_s$ | senkouBPeriod | 52 | 2 trading months |
|
||||
| $d$ | displacement | 26 | 1 trading month forward/back |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
Initialize:
|
||||
highBuf = RingBuffer(senkouBPeriod) // covers all windows
|
||||
lowBuf = RingBuffer(senkouBPeriod)
|
||||
bar_count = 0
|
||||
|
||||
On each bar (high, low, close, isNew):
|
||||
if !isNew: restore previous state (including buffer snapshots)
|
||||
|
||||
highBuf.Add(high)
|
||||
lowBuf.Add(low)
|
||||
bar_count++
|
||||
|
||||
// Tenkan-sen (shortest window)
|
||||
tenkan = (Max(highBuf, tenkanPeriod) + Min(lowBuf, tenkanPeriod)) / 2
|
||||
|
||||
// Kijun-sen (medium window)
|
||||
kijun = (Max(highBuf, kijunPeriod) + Min(lowBuf, kijunPeriod)) / 2
|
||||
|
||||
// Senkou Span A (computed now, plotted displacement bars forward)
|
||||
senkouA = (tenkan + kijun) / 2
|
||||
|
||||
// Senkou Span B (longest window, plotted displacement bars forward)
|
||||
senkouB = (Max(highBuf, senkouBPeriod) + Min(lowBuf, senkouBPeriod)) / 2
|
||||
|
||||
// Chikou Span (current close, plotted displacement bars backward)
|
||||
chikou = close
|
||||
|
||||
output:
|
||||
Tenkan = tenkan // plot at current bar
|
||||
Kijun = kijun // plot at current bar
|
||||
SenkouA = senkouA // plot at bar + displacement
|
||||
SenkouB = senkouB // plot at bar + displacement
|
||||
Chikou = chikou // plot at bar - displacement
|
||||
```
|
||||
|
||||
### Cloud (Kumo) Interpretation
|
||||
|
||||
| Condition | Meaning |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# IMPULSE: Elder Impulse System
|
||||
|
||||
> *The Impulse System identifies inflection points where a trend speeds up or slows down.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -15,8 +17,6 @@
|
||||
- Requires `Math.Max(emaPeriod, macdSlow) + macdSignal - 1` bars (default 34) of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "The Impulse System identifies inflection points where a trend speeds up or slows down." -- Alexander Elder, *Come Into My Trading Room*
|
||||
|
||||
The Elder Impulse System combines a 13-period EMA (trend inertia) with the MACD(12,26,9) histogram (momentum acceleration) to classify each bar as bullish (+1), bearish (-1), or neutral (0). Both EMA slope and histogram slope must agree for a directional signal; disagreement forces neutral. The system functions as a permission filter rather than a signal generator, requiring 34 bars warmup and running at O(1) per bar through composition of two child indicators.
|
||||
|
||||
## Historical Context
|
||||
@@ -80,35 +80,6 @@ Both child indicators must be warmed up and at least two comparison values must
|
||||
| macdSlow | int | 26 | > macdFast | MACD slow EMA period |
|
||||
| macdSignal | int | 9 | > 0 | MACD signal smoothing period |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
IMPULSE(close, emaPeriod=13, macdFast=12, macdSlow=26, macdSignal=9):
|
||||
|
||||
// Child indicator updates
|
||||
ema_val = EMA.Update(close, emaPeriod)
|
||||
macd_result = MACD.Update(close, macdFast, macdSlow, macdSignal)
|
||||
hist_val = macd_result.Histogram
|
||||
|
||||
// Slope computation (requires previous values)
|
||||
ema_slope = sign(ema_val - prev_ema)
|
||||
hist_slope = sign(hist_val - prev_hist)
|
||||
|
||||
// Classification
|
||||
if ema_slope > 0 AND hist_slope > 0:
|
||||
signal = +1 // Bullish: both inertia and momentum rising
|
||||
else if ema_slope < 0 AND hist_slope < 0:
|
||||
signal = -1 // Bearish: both inertia and momentum falling
|
||||
else:
|
||||
signal = 0 // Neutral: disagreement between components
|
||||
|
||||
// State update
|
||||
prev_ema = ema_val
|
||||
prev_hist = hist_val
|
||||
|
||||
return signal
|
||||
```
|
||||
|
||||
### Derivative Interpretation
|
||||
|
||||
The system combines two derivatives:
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class MinusDiIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void MinusDiIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new MinusDiIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("-DI - Minus Directional Indicator", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDiIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new MinusDiIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, MinusDiIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDiIndicator_Initialize_CreatesInternal()
|
||||
{
|
||||
var indicator = new MinusDiIndicator { Period = 14 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDiIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MinusDiIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDiIndicator_ShortName_IsCorrect()
|
||||
{
|
||||
var indicator = new MinusDiIndicator { Period = 20 };
|
||||
Assert.Equal("-DI 20", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDiIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new MinusDiIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("MinusDi.Quantower.cs", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class MinusDiIndicator : 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 MinusDi _minusDi = null!;
|
||||
private readonly LineSeries _minusDiSeries;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"-DI {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/dynamics/minusdi/MinusDi.Quantower.cs";
|
||||
|
||||
public MinusDiIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "-DI - Minus Directional Indicator";
|
||||
Description = "Measures downward directional movement as a percentage of true range";
|
||||
|
||||
_minusDiSeries = new LineSeries(name: "-DI", color: Color.Red, width: 2, style: LineStyle.Solid);
|
||||
|
||||
AddLineSeries(_minusDiSeries);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_minusDi = new MinusDi(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TValue result = _minusDi.Update(this.GetInputBar(args), args.IsNewBar());
|
||||
|
||||
_minusDiSeries.SetValue(result.Value, _minusDi.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using OoplesFinance.StockIndicators.Enums;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using QuanTAlib.Tests;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for MinusDi (-DI). Cross-validates against TA-Lib, Skender,
|
||||
/// OoplesFinance, and internal Dx equivalence with multiple periods.
|
||||
/// </summary>
|
||||
public sealed class MinusDiValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
|
||||
public MinusDiValidationTests()
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_data.Dispose();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// TA-Lib Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void MatchesTalib()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.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 = Functions.MinusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDILookback(14);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(7)]
|
||||
[InlineData(21)]
|
||||
[InlineData(28)]
|
||||
public void MatchesTalib_VariousPeriods(int period)
|
||||
{
|
||||
var indicator = new MinusDi(period);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.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 = Functions.MinusDI(hData, lData, cData, 0..^0, outReal, out var outRange, period);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDILookback(period);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Skender Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void MatchesSkender()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
var skenderResults = _data.SkenderQuotes.GetAdx(14).ToList();
|
||||
ValidationHelper.VerifyData(results, skenderResults, x => x.Mdi);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(7)]
|
||||
[InlineData(21)]
|
||||
[InlineData(28)]
|
||||
public void MatchesSkender_VariousPeriods(int period)
|
||||
{
|
||||
var indicator = new MinusDi(period);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
var skenderResults = _data.SkenderQuotes.GetAdx(period).ToList();
|
||||
ValidationHelper.VerifyData(results, skenderResults, x => x.Mdi);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Dx Equivalence
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ExactlyMatchesDx_DiMinus()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
var dx = new Dx(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
dx.Update(_data.Bars[i]);
|
||||
|
||||
Assert.Equal(dx.DiMinus.Value, indicator.Last.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// OoplesFinance Structural Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void MatchesOoples_Structural()
|
||||
{
|
||||
var ooplesData = _data.SkenderQuotes
|
||||
.Select(q => new TickerData
|
||||
{
|
||||
Date = q.Date,
|
||||
Open = (double)q.Open,
|
||||
High = (double)q.High,
|
||||
Low = (double)q.Low,
|
||||
Close = (double)q.Close,
|
||||
Volume = (double)q.Volume
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var stockData = new StockData(ooplesData);
|
||||
var adxResults = stockData.CalculateAverageDirectionalIndex(MovingAvgType.WildersSmoothingMethod, 14);
|
||||
|
||||
var allValues = adxResults.OutputValues.Values.SelectMany(v => v).ToList();
|
||||
int finiteCount = allValues.Count(v => double.IsFinite(v));
|
||||
Assert.True(finiteCount > 100, $"Expected >100 finite Ooples DI values, got {finiteCount}");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Self-Consistency: Batch == Streaming
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void BatchEqualsStreaming()
|
||||
{
|
||||
var batchResults = MinusDi.Batch(_data.Bars, 14);
|
||||
|
||||
var streaming = new MinusDi(14);
|
||||
var streamResults = new List<double>();
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
streamResults.Add(streaming.Update(_data.Bars[i]).Value);
|
||||
}
|
||||
|
||||
Assert.Equal(streamResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < batchResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchMatchesTalib()
|
||||
{
|
||||
var batchResults = MinusDi.Batch(_data.Bars, 14);
|
||||
|
||||
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 = Functions.MinusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDILookback(14);
|
||||
ValidationHelper.VerifyData(batchResults.Select(x => x.Value).ToList(), outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Determinism
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ConsistentAcrossMultipleRuns()
|
||||
{
|
||||
var ind1 = new MinusDi(14);
|
||||
var ind2 = new MinusDi(14);
|
||||
var results1 = new List<double>();
|
||||
var results2 = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind1.Update(_data.Bars[i]);
|
||||
results1.Add(ind1.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind2.Update(_data.Bars[i]);
|
||||
results2.Add(ind2.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(results1[i], results2[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Output Range Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void OutputIsNonNegative()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
Assert.True(indicator.Last.Value >= 0, $"-DI output at bar {i} was {indicator.Last.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputBounded0To100()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
double val = indicator.Last.Value;
|
||||
if (i >= 14)
|
||||
{
|
||||
Assert.True(val >= 0 && val <= 100, $"-DI at bar {i} was {val}, expected [0,100]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Different Periods Produce Different Results
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var short7 = new MinusDi(7);
|
||||
var long28 = new MinusDi(28);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
short7.Update(_data.Bars[i]);
|
||||
long28.Update(_data.Bars[i]);
|
||||
}
|
||||
|
||||
Assert.NotEqual(short7.Last.Value, long28.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,100 @@
|
||||
# MINUS_DI: Minus Directional Indicator
|
||||
|
||||
Measures downward directional movement strength as a percentage (0-100).
|
||||
> *-DI isolates downward directional thrust as a fraction of true range — the bearish arm of Wilder's directional system.*
|
||||
|
||||
## Introduction
|
||||
The Minus Directional Indicator (-DI) measures the strength of downward price movement relative to the true range. It is one of the components of the Directional Movement System developed by J. Welles Wilder Jr.
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
| **Inputs** | OHLCV bar (TBar) |
|
||||
| **Parameters** | `period` (default 14) |
|
||||
| **Outputs** | Single series |
|
||||
| **Output range** | 0 to 100 |
|
||||
| **Warmup** | `period` bars |
|
||||
| **PineScript** | [minusdi.pine](minusdi.pine) |
|
||||
|
||||
When -DI is rising, downward price pressure is increasing. When -DI crosses above +DI, it signals a potential bearish trend. The -DI line is commonly plotted alongside +DI to visualize directional balance.
|
||||
- The Minus Directional Indicator measures the strength of downward price movement relative to true range.
|
||||
- Parameterized by `period` (default 14).
|
||||
- Output range: 0 to 100.
|
||||
- Requires `period` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Dx equivalence.
|
||||
|
||||
## Calculation
|
||||
-DI = Smoothed(-DM) / Smoothed(TR) × 100
|
||||
The Minus Directional Indicator (-DI) is one component of J. Welles Wilder Jr.'s Directional Movement System. It quantifies the fraction of recent true range attributable to downward price extension. The computation smooths both -DM (minus directional movement) and TR (true range) with Wilder's RMA ($\alpha = 1/N$), then divides: $-DI = 100 \times \text{Smooth}(-DM) / \text{Smooth}(TR)$. When -DI rises, downward price pressure is increasing. When -DI crosses above +DI, it signals a potential bearish trend. The -DI line is commonly plotted alongside +DI to visualize directional balance.
|
||||
|
||||
Where:
|
||||
- -DM (Minus Directional Movement) = max(PrevLow - Low, 0) when PrevLow - Low > High - PrevHigh, else 0
|
||||
- TR (True Range) = max(High - Low, |High - PrevClose|, |Low - PrevClose|)
|
||||
- Smoothing uses Wilder's method: Smooth = Smooth - Smooth/N + Input
|
||||
## Historical Context
|
||||
|
||||
## Parameters
|
||||
| Parameter | Default | Range | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Period | 14 | 2-∞ | Wilder smoothing period |
|
||||
J. Welles Wilder Jr. introduced the Directional Movement System in *New Concepts in Technical Trading Systems* (1978). The system decomposes price range into directional components. +DI and -DI are the normalized indicators from which DX and ADX are derived. While most traders focus on ADX for trend strength, +DI and -DI remain essential for determining trend *direction* — a bearish signal occurs when -DI crosses above +DI, bullish when +DI crosses above -DI.
|
||||
|
||||
## Interpretation
|
||||
- **Rising -DI:** Strengthening downward movement
|
||||
- **-DI > +DI:** Bears dominate; potential downtrend
|
||||
- **-DI crossover above +DI:** Bearish signal
|
||||
- **High -DI (>40):** Strong downward momentum
|
||||
## Architecture & Physics
|
||||
|
||||
## References
|
||||
- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978)
|
||||
### 1. Minus Directional Movement
|
||||
|
||||
$$\text{UpMove} = H_t - H_{t-1}, \quad \text{DownMove} = L_{t-1} - L_t$$
|
||||
|
||||
$$-DM = \begin{cases} \text{DownMove} & \text{if DownMove} > \text{UpMove and DownMove} > 0 \\ 0 & \text{otherwise} \end{cases}$$
|
||||
|
||||
### 2. True Range
|
||||
|
||||
$$TR = \max(H_t - L_t,\; |H_t - C_{t-1}|,\; |L_t - C_{t-1}|)$$
|
||||
|
||||
### 3. Wilder Smoothing (RMA)
|
||||
|
||||
$$-DM_{\text{smooth}} = \text{RMA}(-DM, N), \quad TR_{\text{smooth}} = \text{RMA}(TR, N)$$
|
||||
|
||||
### 4. Minus Directional Indicator
|
||||
|
||||
$$-DI = 100 \times \frac{-DM_{\text{smooth}}}{TR_{\text{smooth}}}$$
|
||||
|
||||
When $TR_{\text{smooth}} = 0$ (no price movement), -DI = 0.
|
||||
|
||||
### 5. Complexity
|
||||
|
||||
- **Time:** $O(1)$ per bar — all RMA updates are recursive
|
||||
- **Space:** $O(1)$ — scalar state only (delegates to Dx)
|
||||
- **Warmup:** $N$ bars
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
| Symbol | Parameter | Default | Constraint |
|
||||
|--------|-----------|---------|------------|
|
||||
| $N$ | period | 14 | $N \geq 2$ |
|
||||
|
||||
### Interpretation
|
||||
|
||||
| -DI Value | Signal |
|
||||
|-----------|--------|
|
||||
| Rising -DI | Strengthening downward movement |
|
||||
| -DI > +DI | Bears dominate; potential downtrend |
|
||||
| -DI crossover above +DI | Bearish signal |
|
||||
| High -DI (>40) | Strong downward momentum |
|
||||
|
||||
-DI measures directional *strength*, not absolute direction. Compare +DI vs -DI for directional bias: if $-DI > +DI$, the trend is down.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
-DI is a thin wrapper around Dx. The per-bar cost is identical to Dx (one property extraction after Dx completes its update).
|
||||
|
||||
**Post-warmup steady state (per bar):**
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Dx.Update (full pipeline) | 1 | 75 | 75 |
|
||||
| Property extraction | 1 | 1 | 1 |
|
||||
| **Total** | **2** | — | **~76 cycles** |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 9/10 | Exact Dx delegation; FMA-precise RMA smoothing |
|
||||
| **Timeliness** | 7/10 | N-bar warmup; responds to bar-level changes |
|
||||
| **Smoothness** | 7/10 | Single RMA layer; moderate noise suppression |
|
||||
| **Noise Rejection** | 7/10 | Wilder smoothing filters transient spikes |
|
||||
|
||||
## Resources
|
||||
|
||||
- Wilder, J.W. — *New Concepts in Technical Trading Systems* (Trend Research, 1978)
|
||||
- PineScript reference: `minusdi.pine` in indicator directory
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Minus Directional Indicator (-DI)", "-DI", overlay=false)
|
||||
|
||||
//@function Calculates -DI using Wilder's smoothing with compensated RMA
|
||||
//@param period Number of bars used in the calculation
|
||||
//@returns -DI value (0-100)
|
||||
//@optimized Uses Wilder's smoothing (RMA) with warmup compensation for accurate values from bar 1
|
||||
minusdi(simple int period) =>
|
||||
if period <= 0
|
||||
runtime.error("Period must be greater than 0")
|
||||
float alpha = 1.0 / period
|
||||
float beta = 1.0 - alpha
|
||||
float tr = 0.0
|
||||
float minus_dm = 0.0
|
||||
if na(close[1])
|
||||
tr := high - low
|
||||
else
|
||||
tr := math.max(high - low, math.max(math.abs(high - close[1]), math.abs(low - close[1])))
|
||||
float upMove = high - high[1]
|
||||
float downMove = low[1] - low
|
||||
if downMove > upMove and downMove > 0
|
||||
minus_dm := downMove
|
||||
var bool warmup = true
|
||||
var float e = 1.0
|
||||
var float tr_ema = 0.0
|
||||
var float tr_result = tr
|
||||
var float minus_dm_ema = 0.0
|
||||
var float minus_dm_result = minus_dm
|
||||
tr_ema := alpha * (tr - tr_ema) + tr_ema
|
||||
minus_dm_ema := alpha * (minus_dm - minus_dm_ema) + minus_dm_ema
|
||||
if warmup
|
||||
e *= beta
|
||||
float c = 1.0 / (1.0 - e)
|
||||
tr_result := c * tr_ema
|
||||
minus_dm_result := c * minus_dm_ema
|
||||
warmup := e > 1e-10
|
||||
else
|
||||
tr_result := tr_ema
|
||||
minus_dm_result := minus_dm_ema
|
||||
float minus_di = tr_result != 0.0 ? 100.0 * minus_dm_result / tr_result : 0.0
|
||||
minus_di
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_period = input.int(14, "Period", minval=1, tooltip="Number of bars used in the calculation")
|
||||
|
||||
// Calculation
|
||||
minus_di = minusdi(i_period)
|
||||
|
||||
// Plot
|
||||
plot(minus_di, "-DI", color=color.red, linewidth=2)
|
||||
hline(25, "Threshold", color=color.gray, linestyle=hline.style_dashed)
|
||||
@@ -0,0 +1,73 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class MinusDmIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void MinusDmIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new MinusDmIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("-DM - Minus Directional Movement", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDmIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new MinusDmIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, MinusDmIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDmIndicator_Initialize_CreatesInternal()
|
||||
{
|
||||
var indicator = new MinusDmIndicator { Period = 14 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDmIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MinusDmIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDmIndicator_ShortName_IsCorrect()
|
||||
{
|
||||
var indicator = new MinusDmIndicator { Period = 20 };
|
||||
Assert.Equal("-DM 20", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDmIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new MinusDmIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("MinusDm.Quantower.cs", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class MinusDmIndicator : 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 MinusDm _minusDm = null!;
|
||||
private readonly LineSeries _minusDmSeries;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"-DM {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/dynamics/minusdm/MinusDm.Quantower.cs";
|
||||
|
||||
public MinusDmIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "-DM - Minus Directional Movement";
|
||||
Description = "Wilder-smoothed downward directional movement in price units";
|
||||
|
||||
_minusDmSeries = new LineSeries(name: "-DM", color: Color.Red, width: 2, style: LineStyle.Solid);
|
||||
|
||||
AddLineSeries(_minusDmSeries);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_minusDm = new MinusDm(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TValue result = _minusDm.Update(this.GetInputBar(args), args.IsNewBar());
|
||||
|
||||
_minusDmSeries.SetValue(result.Value, _minusDm.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using OoplesFinance.StockIndicators.Enums;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using QuanTAlib.Tests;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for MinusDm (-DM). Cross-validates against TA-Lib,
|
||||
/// OoplesFinance, and internal Dx equivalence with multiple periods.
|
||||
/// Note: Skender does not expose DM values directly; only DI values via GetAdx().
|
||||
/// </summary>
|
||||
public sealed class MinusDmValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
|
||||
public MinusDmValidationTests()
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_data.Dispose();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// TA-Lib Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void MatchesTalib()
|
||||
{
|
||||
var indicator = new MinusDm(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.MinusDM(hData, lData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDMLookback(14);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(7)]
|
||||
[InlineData(21)]
|
||||
[InlineData(28)]
|
||||
public void MatchesTalib_VariousPeriods(int period)
|
||||
{
|
||||
var indicator = new MinusDm(period);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.MinusDM(hData, lData, 0..^0, outReal, out var outRange, period);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDMLookback(period);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Dx Equivalence
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ExactlyMatchesDx_DmMinus()
|
||||
{
|
||||
var indicator = new MinusDm(14);
|
||||
var dx = new Dx(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
dx.Update(_data.Bars[i]);
|
||||
|
||||
Assert.Equal(dx.DmMinus.Value, indicator.Last.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// OoplesFinance Structural Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void MatchesOoples_Structural()
|
||||
{
|
||||
var ooplesData = _data.SkenderQuotes
|
||||
.Select(q => new TickerData
|
||||
{
|
||||
Date = q.Date,
|
||||
Open = (double)q.Open,
|
||||
High = (double)q.High,
|
||||
Low = (double)q.Low,
|
||||
Close = (double)q.Close,
|
||||
Volume = (double)q.Volume
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var stockData = new StockData(ooplesData);
|
||||
var adxResults = stockData.CalculateAverageDirectionalIndex(MovingAvgType.WildersSmoothingMethod, 14);
|
||||
|
||||
var allValues = adxResults.OutputValues.Values.SelectMany(v => v).ToList();
|
||||
int finiteCount = allValues.Count(v => double.IsFinite(v));
|
||||
Assert.True(finiteCount > 100, $"Expected >100 finite Ooples ADX/DI values, got {finiteCount}");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Self-Consistency: Batch == Streaming
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void BatchEqualsStreaming()
|
||||
{
|
||||
var batchResults = MinusDm.Batch(_data.Bars, 14);
|
||||
|
||||
var streaming = new MinusDm(14);
|
||||
var streamResults = new List<double>();
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
streamResults.Add(streaming.Update(_data.Bars[i]).Value);
|
||||
}
|
||||
|
||||
Assert.Equal(streamResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < batchResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchMatchesTalib()
|
||||
{
|
||||
var batchResults = MinusDm.Batch(_data.Bars, 14);
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.MinusDM(hData, lData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDMLookback(14);
|
||||
ValidationHelper.VerifyData(batchResults.Select(x => x.Value).ToList(), outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Determinism
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ConsistentAcrossMultipleRuns()
|
||||
{
|
||||
var ind1 = new MinusDm(14);
|
||||
var ind2 = new MinusDm(14);
|
||||
var results1 = new List<double>();
|
||||
var results2 = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind1.Update(_data.Bars[i]);
|
||||
results1.Add(ind1.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind2.Update(_data.Bars[i]);
|
||||
results2.Add(ind2.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(results1[i], results2[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Output Range Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void OutputIsNonNegative()
|
||||
{
|
||||
var indicator = new MinusDm(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
Assert.True(indicator.Last.Value >= 0, $"-DM output at bar {i} was {indicator.Last.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Different Periods Produce Different Results
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var short7 = new MinusDm(7);
|
||||
var long28 = new MinusDm(28);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
short7.Update(_data.Bars[i]);
|
||||
long28.Update(_data.Bars[i]);
|
||||
}
|
||||
|
||||
Assert.NotEqual(short7.Last.Value, long28.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,92 @@
|
||||
# MINUS_DM: Minus Directional Movement
|
||||
|
||||
Wilder-smoothed downward directional movement in price units (≥0).
|
||||
> *-DM captures the raw downward price extension, smoothed by Wilder's RMA — the building block before normalization to -DI.*
|
||||
|
||||
## Introduction
|
||||
Minus Directional Movement (-DM) measures the magnitude of downward price movement, smoothed using Wilder's method. Unlike -DI which normalizes by true range to produce a percentage, -DM outputs raw smoothed values in price units.
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
| **Inputs** | OHLCV bar (TBar) |
|
||||
| **Parameters** | `period` (default 14) |
|
||||
| **Outputs** | Single series |
|
||||
| **Output range** | ≥ 0 (price units) |
|
||||
| **Warmup** | `period` bars |
|
||||
| **PineScript** | [minusdm.pine](minusdm.pine) |
|
||||
|
||||
-DM captures when the previous bar's low exceeds the current bar's low by more than the current bar's high exceeds the previous bar's high. It is the raw building block of the Directional Movement System.
|
||||
- Minus Directional Movement outputs the Wilder-smoothed downward directional movement in price units.
|
||||
- Parameterized by `period` (default 14).
|
||||
- Output range: ≥ 0 (price units, scales with instrument).
|
||||
- Requires `period` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib and Dx equivalence.
|
||||
|
||||
## Calculation
|
||||
-DM = max(PrevLow - Low, 0) when PrevLow - Low > High - PrevHigh, else 0
|
||||
Minus Directional Movement (-DM) measures the magnitude of downward price movement, smoothed using Wilder's method. Unlike -DI which normalizes by true range to produce a percentage, -DM outputs raw smoothed values in price units. -DM captures when the previous bar's low exceeds the current bar's low by more than the current bar's high exceeds the previous bar's high. It is the raw building block of the Directional Movement System — the unnormalized signal before division by True Range converts it to -DI.
|
||||
|
||||
Smoothed using Wilder's method: Smooth = Smooth - Smooth/N + Input
|
||||
## Historical Context
|
||||
|
||||
## Parameters
|
||||
| Parameter | Default | Range | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Period | 14 | 2-∞ | Wilder smoothing period |
|
||||
J. Welles Wilder Jr. designed the Directional Movement System as a pipeline in *New Concepts in Technical Trading Systems* (1978). -DM is the first computational stage for downward movement: a binary event detector that fires when downward range expansion dominates. Wilder's insight was that direction should be measured by range *extension*, not by close-to-close returns. A bar that pushes to a new low by more than it pushes to a new high registers as negative directional movement. The smoothed -DM series shows how much downward thrust is being sustained over the lookback period, in absolute price units.
|
||||
|
||||
## Interpretation
|
||||
- **Rising -DM:** Increasing downward price extension
|
||||
- **-DM > +DM:** Downward movement exceeds upward movement
|
||||
- **Zero -DM:** No downward directional movement on the bar
|
||||
- Values are in price units and scale with the instrument
|
||||
## Architecture & Physics
|
||||
|
||||
## References
|
||||
- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978)
|
||||
### 1. Minus Directional Movement (Raw)
|
||||
|
||||
$$\text{UpMove} = H_t - H_{t-1}, \quad \text{DownMove} = L_{t-1} - L_t$$
|
||||
|
||||
$$-DM = \begin{cases} \text{DownMove} & \text{if DownMove} > \text{UpMove and DownMove} > 0 \\ 0 & \text{otherwise} \end{cases}$$
|
||||
|
||||
Only one of +DM or -DM can be non-zero per bar — the dominant direction wins.
|
||||
|
||||
### 2. Wilder Smoothing (RMA)
|
||||
|
||||
$$-DM_{\text{smooth}} = \text{RMA}(-DM, N), \quad \alpha = 1/N$$
|
||||
|
||||
### 3. Complexity
|
||||
|
||||
- **Time:** $O(1)$ per bar — recursive RMA update
|
||||
- **Space:** $O(1)$ — scalar state only (delegates to Dx)
|
||||
- **Warmup:** $N$ bars
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
| Symbol | Parameter | Default | Constraint |
|
||||
|--------|-----------|---------|------------|
|
||||
| $N$ | period | 14 | $N \geq 2$ |
|
||||
|
||||
### Interpretation
|
||||
|
||||
| -DM Behavior | Signal |
|
||||
|--------------|--------|
|
||||
| Rising -DM | Increasing downward price extension |
|
||||
| -DM > +DM | Downward movement exceeds upward movement |
|
||||
| Zero -DM | No downward directional movement on the bar |
|
||||
| Values scale with instrument | Compare within same instrument only |
|
||||
|
||||
-DM values are in price units and scale with the instrument. They cannot be compared across different instruments without normalization (which is what -DI provides).
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
-DM is a thin wrapper around Dx. The per-bar cost is identical to Dx (one property extraction after Dx completes its update).
|
||||
|
||||
**Post-warmup steady state (per bar):**
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Dx.Update (full pipeline) | 1 | 75 | 75 |
|
||||
| Property extraction | 1 | 1 | 1 |
|
||||
| **Total** | **2** | — | **~76 cycles** |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 9/10 | Exact Dx delegation; FMA-precise RMA smoothing |
|
||||
| **Timeliness** | 7/10 | N-bar warmup; responds to bar-level changes |
|
||||
| **Smoothness** | 7/10 | Single RMA layer; moderate noise suppression |
|
||||
| **Noise Rejection** | 7/10 | Wilder smoothing filters transient spikes |
|
||||
|
||||
## Resources
|
||||
|
||||
- Wilder, J.W. — *New Concepts in Technical Trading Systems* (Trend Research, 1978)
|
||||
- PineScript reference: `minusdm.pine` in indicator directory
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Minus Directional Movement (-DM)", "-DM", overlay=false)
|
||||
|
||||
//@function Calculates Wilder-smoothed -DM using compensated RMA
|
||||
//@param period Number of bars used in the calculation
|
||||
//@returns Smoothed -DM value in price units (≥0)
|
||||
//@optimized Uses Wilder's smoothing (RMA) with warmup compensation for accurate values from bar 1
|
||||
minusdm(simple int period) =>
|
||||
if period <= 0
|
||||
runtime.error("Period must be greater than 0")
|
||||
float alpha = 1.0 / period
|
||||
float beta = 1.0 - alpha
|
||||
float minus_dm = 0.0
|
||||
if not na(low[1])
|
||||
float upMove = high - high[1]
|
||||
float downMove = low[1] - low
|
||||
if downMove > upMove and downMove > 0
|
||||
minus_dm := downMove
|
||||
var bool warmup = true
|
||||
var float e = 1.0
|
||||
var float minus_dm_ema = 0.0
|
||||
var float minus_dm_result = minus_dm
|
||||
minus_dm_ema := alpha * (minus_dm - minus_dm_ema) + minus_dm_ema
|
||||
if warmup
|
||||
e *= beta
|
||||
float c = 1.0 / (1.0 - e)
|
||||
minus_dm_result := c * minus_dm_ema
|
||||
warmup := e > 1e-10
|
||||
else
|
||||
minus_dm_result := minus_dm_ema
|
||||
minus_dm_result
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_period = input.int(14, "Period", minval=1, tooltip="Number of bars used in the calculation")
|
||||
|
||||
// Calculation
|
||||
minus_dm_value = minusdm(i_period)
|
||||
|
||||
// Plot
|
||||
plot(minus_dm_value, "-DM", color=color.red, linewidth=2)
|
||||
@@ -1,5 +1,7 @@
|
||||
# PFE: Polarized Fractal Efficiency
|
||||
|
||||
> *The shortest distance between two points is a straight line. The market never takes the shortest distance. PFE measures how badly it misses.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires `period + 1` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "The shortest distance between two points is a straight line. The market never takes the shortest distance. PFE measures how badly it misses."
|
||||
|
||||
Polarized Fractal Efficiency (PFE) quantifies trend strength by comparing the Euclidean distance a price series actually travels bar-to-bar against the straight-line distance between the endpoints over the same window. The ratio, scaled to [-100, +100] and smoothed with an EMA, distinguishes efficient trending motion (values near ±100) from fractal, self-similar noise (values near 0). Created by Hans Hannula and published in *Technical Analysis of Stocks & Commodities* (January 1994), PFE applies fractal geometry to price action without requiring Hurst exponent estimation or rescaled-range analysis. With default parameters (period=10, smooth=5), the indicator needs 11 close values for the first raw reading plus 5 bars of EMA convergence, totaling ~16 bars of warmup. The core loop executes $N$ square roots per bar, making it $O(N)$ per update in streaming mode.
|
||||
|
||||
## Historical Context
|
||||
|
||||
@@ -0,0 +1,917 @@
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Combined unit tests for PlusDi, MinusDi, PlusDm, MinusDm.
|
||||
/// All four are thin Dx-composition wrappers extracting a single property.
|
||||
/// </summary>
|
||||
public class DiDmTests
|
||||
{
|
||||
// ═══════════════════════════════════════════════
|
||||
// A. Constructor / Parameter Tests
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_Constructor_InvalidPeriod_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new PlusDi(0));
|
||||
Assert.Throws<ArgumentException>(() => new PlusDi(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_Constructor_InvalidPeriod_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new MinusDi(0));
|
||||
Assert.Throws<ArgumentException>(() => new MinusDi(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_Constructor_InvalidPeriod_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new PlusDm(0));
|
||||
Assert.Throws<ArgumentException>(() => new PlusDm(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_Constructor_InvalidPeriod_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new MinusDm(0));
|
||||
Assert.Throws<ArgumentException>(() => new MinusDm(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_DefaultPeriod_Is14()
|
||||
{
|
||||
var indicator = new PlusDi();
|
||||
Assert.Equal(14, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_DefaultPeriod_Is14()
|
||||
{
|
||||
var indicator = new MinusDi();
|
||||
Assert.Equal(14, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_DefaultPeriod_Is14()
|
||||
{
|
||||
var indicator = new PlusDm();
|
||||
Assert.Equal(14, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_DefaultPeriod_Is14()
|
||||
{
|
||||
var indicator = new MinusDm();
|
||||
Assert.Equal(14, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_Name_ContainsPeriod()
|
||||
{
|
||||
var indicator = new PlusDi(20);
|
||||
Assert.Contains("20", indicator.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_Name_ContainsPeriod()
|
||||
{
|
||||
var indicator = new MinusDi(20);
|
||||
Assert.Contains("20", indicator.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_Name_ContainsPeriod()
|
||||
{
|
||||
var indicator = new PlusDm(20);
|
||||
Assert.Contains("20", indicator.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_Name_ContainsPeriod()
|
||||
{
|
||||
var indicator = new MinusDm(20);
|
||||
Assert.Contains("20", indicator.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// B. Basic Calculation Tests
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var indicator = new PlusDm(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var indicator = new MinusDm(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// C. IsHot / WarmupPeriod Tests
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
Assert.False(indicator.IsHot);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
if (indicator.IsHot)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
Assert.False(indicator.IsHot);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
if (indicator.IsHot)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var indicator = new PlusDm(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
Assert.False(indicator.IsHot);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
if (indicator.IsHot)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var indicator = new MinusDm(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
Assert.False(indicator.IsHot);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
if (indicator.IsHot)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// D. Bar Correction (isNew=false) Tests
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_BarCorrection_MatchesFreshInstance()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
indicator.Update(bars[99]);
|
||||
|
||||
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 1.0, bars[99].Low - 1.0, bars[99].Close, bars[99].Volume);
|
||||
var val2 = indicator.Update(modifiedBar, isNew: false);
|
||||
|
||||
var fresh = new PlusDi(14);
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
fresh.Update(bars[i]);
|
||||
}
|
||||
var val3 = fresh.Update(modifiedBar);
|
||||
|
||||
Assert.Equal(val3.Value, val2.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_BarCorrection_MatchesFreshInstance()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
indicator.Update(bars[99]);
|
||||
|
||||
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 1.0, bars[99].Low - 1.0, bars[99].Close, bars[99].Volume);
|
||||
var val2 = indicator.Update(modifiedBar, isNew: false);
|
||||
|
||||
var fresh = new MinusDi(14);
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
fresh.Update(bars[i]);
|
||||
}
|
||||
var val3 = fresh.Update(modifiedBar);
|
||||
|
||||
Assert.Equal(val3.Value, val2.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_BarCorrection_MatchesFreshInstance()
|
||||
{
|
||||
var indicator = new PlusDm(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
indicator.Update(bars[99]);
|
||||
|
||||
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 1.0, bars[99].Low - 1.0, bars[99].Close, bars[99].Volume);
|
||||
var val2 = indicator.Update(modifiedBar, isNew: false);
|
||||
|
||||
var fresh = new PlusDm(14);
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
fresh.Update(bars[i]);
|
||||
}
|
||||
var val3 = fresh.Update(modifiedBar);
|
||||
|
||||
Assert.Equal(val3.Value, val2.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_BarCorrection_MatchesFreshInstance()
|
||||
{
|
||||
var indicator = new MinusDm(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
indicator.Update(bars[99]);
|
||||
|
||||
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 1.0, bars[99].Low - 1.0, bars[99].Close, bars[99].Volume);
|
||||
var val2 = indicator.Update(modifiedBar, isNew: false);
|
||||
|
||||
var fresh = new MinusDm(14);
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
fresh.Update(bars[i]);
|
||||
}
|
||||
var val3 = fresh.Update(modifiedBar);
|
||||
|
||||
Assert.Equal(val3.Value, val2.Value, 1e-9);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// E. Reset Tests
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_Reset_ClearsState()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
indicator.Reset();
|
||||
Assert.Equal(0, indicator.Last.Value);
|
||||
Assert.False(indicator.IsHot);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_Reset_ClearsState()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
indicator.Reset();
|
||||
Assert.Equal(0, indicator.Last.Value);
|
||||
Assert.False(indicator.IsHot);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_Reset_ClearsState()
|
||||
{
|
||||
var indicator = new PlusDm(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
indicator.Reset();
|
||||
Assert.Equal(0, indicator.Last.Value);
|
||||
Assert.False(indicator.IsHot);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_Reset_ClearsState()
|
||||
{
|
||||
var indicator = new MinusDm(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
indicator.Reset();
|
||||
Assert.Equal(0, indicator.Last.Value);
|
||||
Assert.False(indicator.IsHot);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// F. Batch / Streaming Consistency Tests
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_Batch_MatchesStreaming()
|
||||
{
|
||||
var gbm = new GBM(seed: 123);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var batchResult = PlusDi.Batch(bars, 14);
|
||||
|
||||
var streaming = new PlusDi(14);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streaming.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(batchResult.Last.Value, streaming.Last.Value, 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_Batch_MatchesStreaming()
|
||||
{
|
||||
var gbm = new GBM(seed: 123);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var batchResult = MinusDi.Batch(bars, 14);
|
||||
|
||||
var streaming = new MinusDi(14);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streaming.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(batchResult.Last.Value, streaming.Last.Value, 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_Batch_MatchesStreaming()
|
||||
{
|
||||
var gbm = new GBM(seed: 123);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var batchResult = PlusDm.Batch(bars, 14);
|
||||
|
||||
var streaming = new PlusDm(14);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streaming.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(batchResult.Last.Value, streaming.Last.Value, 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_Batch_MatchesStreaming()
|
||||
{
|
||||
var gbm = new GBM(seed: 123);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var batchResult = MinusDm.Batch(bars, 14);
|
||||
|
||||
var streaming = new MinusDm(14);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streaming.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(batchResult.Last.Value, streaming.Last.Value, 9);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// G. Event / Pub Tests
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_Pub_FiresOnNewBar()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
int fireCount = 0;
|
||||
|
||||
indicator.Pub += (object? _, in TValueEventArgs _e) => { fireCount++; };
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(bars.Count, fireCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_Pub_DoesNotFireOnCorrection()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
int fireCount = 0;
|
||||
|
||||
indicator.Pub += (object? _, in TValueEventArgs _e) => { fireCount++; };
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
int countAfterNewBars = fireCount;
|
||||
|
||||
// Bar correction should not fire
|
||||
var modifiedBar = new TBar(bars[^1].Time, bars[^1].Open, bars[^1].High + 1.0, bars[^1].Low - 1.0, bars[^1].Close, bars[^1].Volume);
|
||||
indicator.Update(modifiedBar, isNew: false);
|
||||
|
||||
Assert.Equal(countAfterNewBars, fireCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_Pub_FiresOnNewBar()
|
||||
{
|
||||
var indicator = new PlusDm(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
int fireCount = 0;
|
||||
|
||||
indicator.Pub += (object? _, in TValueEventArgs _e) => { fireCount++; };
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(bars.Count, fireCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_Pub_DoesNotFireOnCorrection()
|
||||
{
|
||||
var indicator = new MinusDm(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
int fireCount = 0;
|
||||
|
||||
indicator.Pub += (object? _, in TValueEventArgs _e) => { fireCount++; };
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
int countAfterNewBars = fireCount;
|
||||
|
||||
var modifiedBar = new TBar(bars[^1].Time, bars[^1].Open, bars[^1].High + 1.0, bars[^1].Low - 1.0, bars[^1].Close, bars[^1].Volume);
|
||||
indicator.Update(modifiedBar, isNew: false);
|
||||
|
||||
Assert.Equal(countAfterNewBars, fireCount);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// H. Prime / Calculate / Chainability Tests
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_Prime_SetsState()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
indicator.Prime(bars);
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_Prime_SetsState()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
indicator.Prime(bars);
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_Calculate_ReturnsTupleWithIndicator()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var (results, ind) = PlusDm.Calculate(bars, 14);
|
||||
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
Assert.True(ind.IsHot);
|
||||
Assert.Equal(results[^1].Value, ind.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_Calculate_ReturnsTupleWithIndicator()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var (results, ind) = MinusDm.Calculate(bars, 14);
|
||||
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
Assert.True(ind.IsHot);
|
||||
Assert.Equal(results[^1].Value, ind.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_TBarSeriesConstructor_Works()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var indicator = new PlusDi(bars, 14);
|
||||
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_TBarSeriesConstructor_Works()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var indicator = new MinusDi(bars, 14);
|
||||
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_ScalarUpdate_ReturnsLastUnchanged()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
var before = indicator.Last;
|
||||
var result = indicator.Update(new TValue(DateTime.UtcNow, 42.0));
|
||||
|
||||
Assert.Equal(before.Value, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_ScalarUpdate_ReturnsLastUnchanged()
|
||||
{
|
||||
var indicator = new MinusDm(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator.Update(bars[i]);
|
||||
}
|
||||
|
||||
var before = indicator.Last;
|
||||
var result = indicator.Update(new TValue(DateTime.UtcNow, 42.0));
|
||||
|
||||
Assert.Equal(before.Value, result.Value);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Range / Value Constraint Tests
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_OutputRange_0to100()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = indicator.Update(bars[i]);
|
||||
if (indicator.IsHot)
|
||||
{
|
||||
Assert.InRange(result.Value, 0, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_OutputRange_0to100()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = indicator.Update(bars[i]);
|
||||
if (indicator.IsHot)
|
||||
{
|
||||
Assert.InRange(result.Value, 0, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_NonNegative()
|
||||
{
|
||||
var indicator = new PlusDm(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = indicator.Update(bars[i]);
|
||||
if (indicator.IsHot)
|
||||
{
|
||||
Assert.True(result.Value >= 0, $"+DM should be non-negative, got {result.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_NonNegative()
|
||||
{
|
||||
var indicator = new MinusDm(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = indicator.Update(bars[i]);
|
||||
if (indicator.IsHot)
|
||||
{
|
||||
Assert.True(result.Value >= 0, $"-DM should be non-negative, got {result.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Dx Equivalence Tests
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_MatchesDx_DiPlus()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var plusDi = new PlusDi(14);
|
||||
var dx = new Dx(14);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
plusDi.Update(bars[i]);
|
||||
dx.Update(bars[i]);
|
||||
|
||||
Assert.Equal(dx.DiPlus.Value, plusDi.Last.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_MatchesDx_DiMinus()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var minusDi = new MinusDi(14);
|
||||
var dx = new Dx(14);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
minusDi.Update(bars[i]);
|
||||
dx.Update(bars[i]);
|
||||
|
||||
Assert.Equal(dx.DiMinus.Value, minusDi.Last.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_MatchesDx_DmPlus()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var plusDm = new PlusDm(14);
|
||||
var dx = new Dx(14);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
plusDm.Update(bars[i]);
|
||||
dx.Update(bars[i]);
|
||||
|
||||
Assert.Equal(dx.DmPlus.Value, plusDm.Last.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_MatchesDx_DmMinus()
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var minusDm = new MinusDm(14);
|
||||
var dx = new Dx(14);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
minusDm.Update(bars[i]);
|
||||
dx.Update(bars[i]);
|
||||
|
||||
Assert.Equal(dx.DmMinus.Value, minusDm.Last.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Determinism Tests
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void AllFour_Deterministic_SameSeedSameResult()
|
||||
{
|
||||
var gbm1 = new GBM(seed: 99);
|
||||
var bars1 = gbm1.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var gbm2 = new GBM(seed: 99);
|
||||
var bars2 = gbm2.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var pdi1 = new PlusDi(14);
|
||||
var pdi2 = new PlusDi(14);
|
||||
var mdi1 = new MinusDi(14);
|
||||
var mdi2 = new MinusDi(14);
|
||||
var pdm1 = new PlusDm(14);
|
||||
var pdm2 = new PlusDm(14);
|
||||
var mdm1 = new MinusDm(14);
|
||||
var mdm2 = new MinusDm(14);
|
||||
|
||||
for (int i = 0; i < bars1.Count; i++)
|
||||
{
|
||||
pdi1.Update(bars1[i]);
|
||||
pdi2.Update(bars2[i]);
|
||||
mdi1.Update(bars1[i]);
|
||||
mdi2.Update(bars2[i]);
|
||||
pdm1.Update(bars1[i]);
|
||||
pdm2.Update(bars2[i]);
|
||||
mdm1.Update(bars1[i]);
|
||||
mdm2.Update(bars2[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(pdi1.Last.Value, pdi2.Last.Value, 1e-12);
|
||||
Assert.Equal(mdi1.Last.Value, mdi2.Last.Value, 1e-12);
|
||||
Assert.Equal(pdm1.Last.Value, pdm2.Last.Value, 1e-12);
|
||||
Assert.Equal(mdm1.Last.Value, mdm2.Last.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,818 @@
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using OoplesFinance.StockIndicators.Enums;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using QuanTAlib.Tests;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Combined validation tests for PlusDi, MinusDi, PlusDm, MinusDm.
|
||||
/// Cross-validates against TA-Lib, Skender, Ooples, and Dx equivalence.
|
||||
/// </summary>
|
||||
public sealed class DiDmValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
|
||||
public DiDmValidationTests()
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_data.Dispose();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// TA-Lib Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_MatchesTalib()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.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 = Functions.PlusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDILookback(14);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_MatchesTalib()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.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 = Functions.MinusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDILookback(14);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_MatchesTalib()
|
||||
{
|
||||
var indicator = new PlusDm(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.PlusDM(hData, lData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDMLookback(14);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_MatchesTalib()
|
||||
{
|
||||
var indicator = new MinusDm(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.MinusDM(hData, lData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDMLookback(14);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Skender Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_MatchesSkender()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
var skenderResults = _data.SkenderQuotes.GetAdx(14).ToList();
|
||||
|
||||
ValidationHelper.VerifyData(results, skenderResults, x => x.Pdi);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_MatchesSkender()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
var skenderResults = _data.SkenderQuotes.GetAdx(14).ToList();
|
||||
|
||||
ValidationHelper.VerifyData(results, skenderResults, x => x.Mdi);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Dx Equivalence
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_ExactlyMatchesDx_DiPlus()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var dx = new Dx(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
dx.Update(_data.Bars[i]);
|
||||
|
||||
Assert.Equal(dx.DiPlus.Value, indicator.Last.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_ExactlyMatchesDx_DiMinus()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
var dx = new Dx(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
dx.Update(_data.Bars[i]);
|
||||
|
||||
Assert.Equal(dx.DiMinus.Value, indicator.Last.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_ExactlyMatchesDx_DmPlus()
|
||||
{
|
||||
var indicator = new PlusDm(14);
|
||||
var dx = new Dx(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
dx.Update(_data.Bars[i]);
|
||||
|
||||
Assert.Equal(dx.DmPlus.Value, indicator.Last.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_ExactlyMatchesDx_DmMinus()
|
||||
{
|
||||
var indicator = new MinusDm(14);
|
||||
var dx = new Dx(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
dx.Update(_data.Bars[i]);
|
||||
|
||||
Assert.Equal(dx.DmMinus.Value, indicator.Last.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Self-Consistency: Batch == Streaming
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_BatchEqualsStreaming()
|
||||
{
|
||||
var batchResults = PlusDi.Batch(_data.Bars, 14);
|
||||
|
||||
var streaming = new PlusDi(14);
|
||||
var streamResults = new List<double>();
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
streamResults.Add(streaming.Update(_data.Bars[i]).Value);
|
||||
}
|
||||
|
||||
Assert.Equal(streamResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < batchResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_BatchEqualsStreaming()
|
||||
{
|
||||
var batchResults = MinusDi.Batch(_data.Bars, 14);
|
||||
|
||||
var streaming = new MinusDi(14);
|
||||
var streamResults = new List<double>();
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
streamResults.Add(streaming.Update(_data.Bars[i]).Value);
|
||||
}
|
||||
|
||||
Assert.Equal(streamResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < batchResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_BatchEqualsStreaming()
|
||||
{
|
||||
var batchResults = PlusDm.Batch(_data.Bars, 14);
|
||||
|
||||
var streaming = new PlusDm(14);
|
||||
var streamResults = new List<double>();
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
streamResults.Add(streaming.Update(_data.Bars[i]).Value);
|
||||
}
|
||||
|
||||
Assert.Equal(streamResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < batchResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_BatchEqualsStreaming()
|
||||
{
|
||||
var batchResults = MinusDm.Batch(_data.Bars, 14);
|
||||
|
||||
var streaming = new MinusDm(14);
|
||||
var streamResults = new List<double>();
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
streamResults.Add(streaming.Update(_data.Bars[i]).Value);
|
||||
}
|
||||
|
||||
Assert.Equal(streamResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < batchResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Multi-Period TALib Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Theory]
|
||||
[InlineData(7)]
|
||||
[InlineData(21)]
|
||||
[InlineData(28)]
|
||||
public void PlusDi_MatchesTalib_VariousPeriods(int period)
|
||||
{
|
||||
var indicator = new PlusDi(period);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.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 = Functions.PlusDI(hData, lData, cData, 0..^0, outReal, out var outRange, period);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDILookback(period);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(7)]
|
||||
[InlineData(21)]
|
||||
[InlineData(28)]
|
||||
public void MinusDi_MatchesTalib_VariousPeriods(int period)
|
||||
{
|
||||
var indicator = new MinusDi(period);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.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 = Functions.MinusDI(hData, lData, cData, 0..^0, outReal, out var outRange, period);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDILookback(period);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(7)]
|
||||
[InlineData(21)]
|
||||
[InlineData(28)]
|
||||
public void PlusDm_MatchesTalib_VariousPeriods(int period)
|
||||
{
|
||||
var indicator = new PlusDm(period);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.PlusDM(hData, lData, 0..^0, outReal, out var outRange, period);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDMLookback(period);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(7)]
|
||||
[InlineData(21)]
|
||||
[InlineData(28)]
|
||||
public void MinusDm_MatchesTalib_VariousPeriods(int period)
|
||||
{
|
||||
var indicator = new MinusDm(period);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.MinusDM(hData, lData, 0..^0, outReal, out var outRange, period);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDMLookback(period);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Multi-Period Skender Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Theory]
|
||||
[InlineData(7)]
|
||||
[InlineData(21)]
|
||||
[InlineData(28)]
|
||||
public void PlusDi_MatchesSkender_VariousPeriods(int period)
|
||||
{
|
||||
var indicator = new PlusDi(period);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
var skenderResults = _data.SkenderQuotes.GetAdx(period).ToList();
|
||||
ValidationHelper.VerifyData(results, skenderResults, x => x.Pdi);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(7)]
|
||||
[InlineData(21)]
|
||||
[InlineData(28)]
|
||||
public void MinusDi_MatchesSkender_VariousPeriods(int period)
|
||||
{
|
||||
var indicator = new MinusDi(period);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
var skenderResults = _data.SkenderQuotes.GetAdx(period).ToList();
|
||||
ValidationHelper.VerifyData(results, skenderResults, x => x.Mdi);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Determinism: Consistent Across Multiple Runs
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_ConsistentAcrossMultipleRuns()
|
||||
{
|
||||
var ind1 = new PlusDi(14);
|
||||
var ind2 = new PlusDi(14);
|
||||
var results1 = new List<double>();
|
||||
var results2 = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind1.Update(_data.Bars[i]);
|
||||
results1.Add(ind1.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind2.Update(_data.Bars[i]);
|
||||
results2.Add(ind2.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(results1[i], results2[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_ConsistentAcrossMultipleRuns()
|
||||
{
|
||||
var ind1 = new MinusDi(14);
|
||||
var ind2 = new MinusDi(14);
|
||||
var results1 = new List<double>();
|
||||
var results2 = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind1.Update(_data.Bars[i]);
|
||||
results1.Add(ind1.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind2.Update(_data.Bars[i]);
|
||||
results2.Add(ind2.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(results1[i], results2[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_ConsistentAcrossMultipleRuns()
|
||||
{
|
||||
var ind1 = new PlusDm(14);
|
||||
var ind2 = new PlusDm(14);
|
||||
var results1 = new List<double>();
|
||||
var results2 = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind1.Update(_data.Bars[i]);
|
||||
results1.Add(ind1.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind2.Update(_data.Bars[i]);
|
||||
results2.Add(ind2.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(results1[i], results2[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_ConsistentAcrossMultipleRuns()
|
||||
{
|
||||
var ind1 = new MinusDm(14);
|
||||
var ind2 = new MinusDm(14);
|
||||
var results1 = new List<double>();
|
||||
var results2 = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind1.Update(_data.Bars[i]);
|
||||
results1.Add(ind1.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind2.Update(_data.Bars[i]);
|
||||
results2.Add(ind2.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(results1[i], results2[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Non-Negative Output Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_OutputIsNonNegative()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
Assert.True(indicator.Last.Value >= 0, $"PlusDi output at bar {i} was {indicator.Last.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_OutputIsNonNegative()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
Assert.True(indicator.Last.Value >= 0, $"MinusDi output at bar {i} was {indicator.Last.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_OutputIsNonNegative()
|
||||
{
|
||||
var indicator = new PlusDm(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
Assert.True(indicator.Last.Value >= 0, $"PlusDm output at bar {i} was {indicator.Last.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_OutputIsNonNegative()
|
||||
{
|
||||
var indicator = new MinusDm(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
Assert.True(indicator.Last.Value >= 0, $"MinusDm output at bar {i} was {indicator.Last.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// DI values bounded 0-100
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_OutputBounded0To100()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
double val = indicator.Last.Value;
|
||||
if (i >= 14)
|
||||
{
|
||||
Assert.True(val >= 0 && val <= 100, $"PlusDi at bar {i} was {val}, expected [0,100]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_OutputBounded0To100()
|
||||
{
|
||||
var indicator = new MinusDi(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
double val = indicator.Last.Value;
|
||||
if (i >= 14)
|
||||
{
|
||||
Assert.True(val >= 0 && val <= 100, $"MinusDi at bar {i} was {val}, expected [0,100]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Different Periods Produce Different Results
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var short14 = new PlusDi(7);
|
||||
var long28 = new PlusDi(28);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
short14.Update(_data.Bars[i]);
|
||||
long28.Update(_data.Bars[i]);
|
||||
}
|
||||
|
||||
Assert.NotEqual(short14.Last.Value, long28.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var short14 = new MinusDi(7);
|
||||
var long28 = new MinusDi(28);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
short14.Update(_data.Bars[i]);
|
||||
long28.Update(_data.Bars[i]);
|
||||
}
|
||||
|
||||
Assert.NotEqual(short14.Last.Value, long28.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var short14 = new PlusDm(7);
|
||||
var long28 = new PlusDm(28);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
short14.Update(_data.Bars[i]);
|
||||
long28.Update(_data.Bars[i]);
|
||||
}
|
||||
|
||||
Assert.NotEqual(short14.Last.Value, long28.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var short14 = new MinusDm(7);
|
||||
var long28 = new MinusDm(28);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
short14.Update(_data.Bars[i]);
|
||||
long28.Update(_data.Bars[i]);
|
||||
}
|
||||
|
||||
Assert.NotEqual(short14.Last.Value, long28.Last.Value);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// OoplesFinance Structural Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void DiDm_MatchesOoples_Structural()
|
||||
{
|
||||
// OoplesFinance.CalculateAverageDirectionalIndex produces Di+/Di- as part of ADX
|
||||
var ooplesData = _data.SkenderQuotes
|
||||
.Select(q => new TickerData
|
||||
{
|
||||
Date = q.Date,
|
||||
Open = (double)q.Open,
|
||||
High = (double)q.High,
|
||||
Low = (double)q.Low,
|
||||
Close = (double)q.Close,
|
||||
Volume = (double)q.Volume
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var stockData = new StockData(ooplesData);
|
||||
var adxResults = stockData.CalculateAverageDirectionalIndex(MovingAvgType.WildersSmoothingMethod, 14);
|
||||
|
||||
// Verify the Ooples ADX calculation produces finite DI values
|
||||
var allValues = adxResults.OutputValues.Values.SelectMany(v => v).ToList();
|
||||
int finiteCount = allValues.Count(v => double.IsFinite(v));
|
||||
Assert.True(finiteCount > 100, $"Expected >100 finite Ooples DI/DM values, got {finiteCount}");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Batch Matches TALib
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PlusDi_BatchMatchesTalib()
|
||||
{
|
||||
var batchResults = PlusDi.Batch(_data.Bars, 14);
|
||||
|
||||
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 = Functions.PlusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDILookback(14);
|
||||
ValidationHelper.VerifyData(batchResults.Select(x => x.Value).ToList(), outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDi_BatchMatchesTalib()
|
||||
{
|
||||
var batchResults = MinusDi.Batch(_data.Bars, 14);
|
||||
|
||||
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 = Functions.MinusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDILookback(14);
|
||||
ValidationHelper.VerifyData(batchResults.Select(x => x.Value).ToList(), outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDm_BatchMatchesTalib()
|
||||
{
|
||||
var batchResults = PlusDm.Batch(_data.Bars, 14);
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.PlusDM(hData, lData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDMLookback(14);
|
||||
ValidationHelper.VerifyData(batchResults.Select(x => x.Value).ToList(), outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinusDm_BatchMatchesTalib()
|
||||
{
|
||||
var batchResults = MinusDm.Batch(_data.Bars, 14);
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.MinusDM(hData, lData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.MinusDMLookback(14);
|
||||
ValidationHelper.VerifyData(batchResults.Select(x => x.Value).ToList(), outReal, outRange, lookback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class PlusDiIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void PlusDiIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new PlusDiIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("+DI - Plus Directional Indicator", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDiIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new PlusDiIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, PlusDiIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDiIndicator_Initialize_CreatesInternal()
|
||||
{
|
||||
var indicator = new PlusDiIndicator { Period = 14 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDiIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PlusDiIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDiIndicator_ShortName_IsCorrect()
|
||||
{
|
||||
var indicator = new PlusDiIndicator { Period = 20 };
|
||||
Assert.Equal("+DI 20", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDiIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new PlusDiIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("PlusDi.Quantower.cs", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class PlusDiIndicator : 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 PlusDi _plusDi = null!;
|
||||
private readonly LineSeries _plusDiSeries;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"+DI {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/dynamics/plusdi/PlusDi.Quantower.cs";
|
||||
|
||||
public PlusDiIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "+DI - Plus Directional Indicator";
|
||||
Description = "Measures upward directional movement as a percentage of true range";
|
||||
|
||||
_plusDiSeries = new LineSeries(name: "+DI", color: Color.Green, width: 2, style: LineStyle.Solid);
|
||||
|
||||
AddLineSeries(_plusDiSeries);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_plusDi = new PlusDi(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TValue result = _plusDi.Update(this.GetInputBar(args), args.IsNewBar());
|
||||
|
||||
_plusDiSeries.SetValue(result.Value, _plusDi.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using OoplesFinance.StockIndicators.Enums;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using QuanTAlib.Tests;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for PlusDi (+DI). Cross-validates against TA-Lib, Skender,
|
||||
/// OoplesFinance, and internal Dx equivalence with multiple periods.
|
||||
/// </summary>
|
||||
public sealed class PlusDiValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
|
||||
public PlusDiValidationTests()
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_data.Dispose();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// TA-Lib Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void MatchesTalib()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.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 = Functions.PlusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDILookback(14);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(7)]
|
||||
[InlineData(21)]
|
||||
[InlineData(28)]
|
||||
public void MatchesTalib_VariousPeriods(int period)
|
||||
{
|
||||
var indicator = new PlusDi(period);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.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 = Functions.PlusDI(hData, lData, cData, 0..^0, outReal, out var outRange, period);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDILookback(period);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Skender Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void MatchesSkender()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
var skenderResults = _data.SkenderQuotes.GetAdx(14).ToList();
|
||||
ValidationHelper.VerifyData(results, skenderResults, x => x.Pdi);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(7)]
|
||||
[InlineData(21)]
|
||||
[InlineData(28)]
|
||||
public void MatchesSkender_VariousPeriods(int period)
|
||||
{
|
||||
var indicator = new PlusDi(period);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
var skenderResults = _data.SkenderQuotes.GetAdx(period).ToList();
|
||||
ValidationHelper.VerifyData(results, skenderResults, x => x.Pdi);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Dx Equivalence
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ExactlyMatchesDx_DiPlus()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
var dx = new Dx(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
dx.Update(_data.Bars[i]);
|
||||
|
||||
Assert.Equal(dx.DiPlus.Value, indicator.Last.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// OoplesFinance Structural Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void MatchesOoples_Structural()
|
||||
{
|
||||
var ooplesData = _data.SkenderQuotes
|
||||
.Select(q => new TickerData
|
||||
{
|
||||
Date = q.Date,
|
||||
Open = (double)q.Open,
|
||||
High = (double)q.High,
|
||||
Low = (double)q.Low,
|
||||
Close = (double)q.Close,
|
||||
Volume = (double)q.Volume
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var stockData = new StockData(ooplesData);
|
||||
var adxResults = stockData.CalculateAverageDirectionalIndex(MovingAvgType.WildersSmoothingMethod, 14);
|
||||
|
||||
var allValues = adxResults.OutputValues.Values.SelectMany(v => v).ToList();
|
||||
int finiteCount = allValues.Count(v => double.IsFinite(v));
|
||||
Assert.True(finiteCount > 100, $"Expected >100 finite Ooples DI values, got {finiteCount}");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Self-Consistency: Batch == Streaming
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void BatchEqualsStreaming()
|
||||
{
|
||||
var batchResults = PlusDi.Batch(_data.Bars, 14);
|
||||
|
||||
var streaming = new PlusDi(14);
|
||||
var streamResults = new List<double>();
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
streamResults.Add(streaming.Update(_data.Bars[i]).Value);
|
||||
}
|
||||
|
||||
Assert.Equal(streamResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < batchResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchMatchesTalib()
|
||||
{
|
||||
var batchResults = PlusDi.Batch(_data.Bars, 14);
|
||||
|
||||
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 = Functions.PlusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDILookback(14);
|
||||
ValidationHelper.VerifyData(batchResults.Select(x => x.Value).ToList(), outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Determinism
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ConsistentAcrossMultipleRuns()
|
||||
{
|
||||
var ind1 = new PlusDi(14);
|
||||
var ind2 = new PlusDi(14);
|
||||
var results1 = new List<double>();
|
||||
var results2 = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind1.Update(_data.Bars[i]);
|
||||
results1.Add(ind1.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind2.Update(_data.Bars[i]);
|
||||
results2.Add(ind2.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(results1[i], results2[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Output Range Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void OutputIsNonNegative()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
Assert.True(indicator.Last.Value >= 0, $"+DI output at bar {i} was {indicator.Last.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputBounded0To100()
|
||||
{
|
||||
var indicator = new PlusDi(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
double val = indicator.Last.Value;
|
||||
if (i >= 14)
|
||||
{
|
||||
Assert.True(val >= 0 && val <= 100, $"+DI at bar {i} was {val}, expected [0,100]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Different Periods Produce Different Results
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var short7 = new PlusDi(7);
|
||||
var long28 = new PlusDi(28);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
short7.Update(_data.Bars[i]);
|
||||
long28.Update(_data.Bars[i]);
|
||||
}
|
||||
|
||||
Assert.NotEqual(short7.Last.Value, long28.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,100 @@
|
||||
# PLUS_DI: Plus Directional Indicator
|
||||
|
||||
Measures upward directional movement strength as a percentage (0-100).
|
||||
> *+DI isolates upward directional thrust as a fraction of true range — the bullish arm of Wilder's directional system.*
|
||||
|
||||
## Introduction
|
||||
The Plus Directional Indicator (+DI) measures the strength of upward price movement relative to the true range. It is one of the components of the Directional Movement System developed by J. Welles Wilder Jr.
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
| **Inputs** | OHLCV bar (TBar) |
|
||||
| **Parameters** | `period` (default 14) |
|
||||
| **Outputs** | Single series |
|
||||
| **Output range** | 0 to 100 |
|
||||
| **Warmup** | `period` bars |
|
||||
| **PineScript** | [plusdi.pine](plusdi.pine) |
|
||||
|
||||
When +DI is rising, upward price pressure is increasing. When +DI crosses above -DI, it signals a potential bullish trend. The +DI line is commonly plotted alongside -DI to visualize directional balance.
|
||||
- The Plus Directional Indicator measures the strength of upward price movement relative to true range.
|
||||
- Parameterized by `period` (default 14).
|
||||
- Output range: 0 to 100.
|
||||
- Requires `period` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Dx equivalence.
|
||||
|
||||
## Calculation
|
||||
+DI = Smoothed(+DM) / Smoothed(TR) × 100
|
||||
The Plus Directional Indicator (+DI) is one component of J. Welles Wilder Jr.'s Directional Movement System. It quantifies the fraction of recent true range attributable to upward price extension. The computation smooths both +DM (plus directional movement) and TR (true range) with Wilder's RMA ($\alpha = 1/N$), then divides: $+DI = 100 \times \text{Smooth}(+DM) / \text{Smooth}(TR)$. When +DI rises, upward price pressure is increasing. When +DI crosses above -DI, it signals a potential bullish trend. The +DI line is commonly plotted alongside -DI to visualize directional balance.
|
||||
|
||||
Where:
|
||||
- +DM (Plus Directional Movement) = max(High - PrevHigh, 0) when High - PrevHigh > PrevLow - Low, else 0
|
||||
- TR (True Range) = max(High - Low, |High - PrevClose|, |Low - PrevClose|)
|
||||
- Smoothing uses Wilder's method: Smooth = Smooth - Smooth/N + Input
|
||||
## Historical Context
|
||||
|
||||
## Parameters
|
||||
| Parameter | Default | Range | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Period | 14 | 2-∞ | Wilder smoothing period |
|
||||
J. Welles Wilder Jr. introduced the Directional Movement System in *New Concepts in Technical Trading Systems* (1978). The system decomposes price range into directional components. +DI and -DI are the normalized indicators from which DX and ADX are derived. While most traders focus on ADX for trend strength, +DI and -DI remain essential for determining trend *direction* — a bullish signal occurs when +DI crosses above -DI, bearish when -DI crosses above +DI.
|
||||
|
||||
## Interpretation
|
||||
- **Rising +DI:** Strengthening upward movement
|
||||
- **+DI > -DI:** Bulls dominate; potential uptrend
|
||||
- **+DI crossover above -DI:** Bullish signal
|
||||
- **High +DI (>40):** Strong upward momentum
|
||||
## Architecture & Physics
|
||||
|
||||
## References
|
||||
- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978)
|
||||
### 1. Plus Directional Movement
|
||||
|
||||
$$\text{UpMove} = H_t - H_{t-1}, \quad \text{DownMove} = L_{t-1} - L_t$$
|
||||
|
||||
$$+DM = \begin{cases} \text{UpMove} & \text{if UpMove} > \text{DownMove and UpMove} > 0 \\ 0 & \text{otherwise} \end{cases}$$
|
||||
|
||||
### 2. True Range
|
||||
|
||||
$$TR = \max(H_t - L_t,\; |H_t - C_{t-1}|,\; |L_t - C_{t-1}|)$$
|
||||
|
||||
### 3. Wilder Smoothing (RMA)
|
||||
|
||||
$$+DM_{\text{smooth}} = \text{RMA}(+DM, N), \quad TR_{\text{smooth}} = \text{RMA}(TR, N)$$
|
||||
|
||||
### 4. Plus Directional Indicator
|
||||
|
||||
$$+DI = 100 \times \frac{+DM_{\text{smooth}}}{TR_{\text{smooth}}}$$
|
||||
|
||||
When $TR_{\text{smooth}} = 0$ (no price movement), +DI = 0.
|
||||
|
||||
### 5. Complexity
|
||||
|
||||
- **Time:** $O(1)$ per bar — all RMA updates are recursive
|
||||
- **Space:** $O(1)$ — scalar state only (delegates to Dx)
|
||||
- **Warmup:** $N$ bars
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
| Symbol | Parameter | Default | Constraint |
|
||||
|--------|-----------|---------|------------|
|
||||
| $N$ | period | 14 | $N \geq 2$ |
|
||||
|
||||
### Interpretation
|
||||
|
||||
| +DI Value | Signal |
|
||||
|-----------|--------|
|
||||
| Rising +DI | Strengthening upward movement |
|
||||
| +DI > -DI | Bulls dominate; potential uptrend |
|
||||
| +DI crossover above -DI | Bullish signal |
|
||||
| High +DI (>40) | Strong upward momentum |
|
||||
|
||||
+DI measures directional *strength*, not absolute direction. Compare +DI vs -DI for directional bias: if $+DI > -DI$, the trend is up.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
+DI is a thin wrapper around Dx. The per-bar cost is identical to Dx (one property extraction after Dx completes its update).
|
||||
|
||||
**Post-warmup steady state (per bar):**
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Dx.Update (full pipeline) | 1 | 75 | 75 |
|
||||
| Property extraction | 1 | 1 | 1 |
|
||||
| **Total** | **2** | — | **~76 cycles** |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 9/10 | Exact Dx delegation; FMA-precise RMA smoothing |
|
||||
| **Timeliness** | 7/10 | N-bar warmup; responds to bar-level changes |
|
||||
| **Smoothness** | 7/10 | Single RMA layer; moderate noise suppression |
|
||||
| **Noise Rejection** | 7/10 | Wilder smoothing filters transient spikes |
|
||||
|
||||
## Resources
|
||||
|
||||
- Wilder, J.W. — *New Concepts in Technical Trading Systems* (Trend Research, 1978)
|
||||
- PineScript reference: `plusdi.pine` in indicator directory
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Plus Directional Indicator (+DI)", "+DI", overlay=false)
|
||||
|
||||
//@function Calculates +DI using Wilder's smoothing with compensated RMA
|
||||
//@param period Number of bars used in the calculation
|
||||
//@returns +DI value (0-100)
|
||||
//@optimized Uses Wilder's smoothing (RMA) with warmup compensation for accurate values from bar 1
|
||||
plusdi(simple int period) =>
|
||||
if period <= 0
|
||||
runtime.error("Period must be greater than 0")
|
||||
float alpha = 1.0 / period
|
||||
float beta = 1.0 - alpha
|
||||
float tr = 0.0
|
||||
float plus_dm = 0.0
|
||||
if na(close[1])
|
||||
tr := high - low
|
||||
else
|
||||
tr := math.max(high - low, math.max(math.abs(high - close[1]), math.abs(low - close[1])))
|
||||
float upMove = high - high[1]
|
||||
float downMove = low[1] - low
|
||||
if upMove > downMove and upMove > 0
|
||||
plus_dm := upMove
|
||||
var bool warmup = true
|
||||
var float e = 1.0
|
||||
var float tr_ema = 0.0
|
||||
var float tr_result = tr
|
||||
var float plus_dm_ema = 0.0
|
||||
var float plus_dm_result = plus_dm
|
||||
tr_ema := alpha * (tr - tr_ema) + tr_ema
|
||||
plus_dm_ema := alpha * (plus_dm - plus_dm_ema) + plus_dm_ema
|
||||
if warmup
|
||||
e *= beta
|
||||
float c = 1.0 / (1.0 - e)
|
||||
tr_result := c * tr_ema
|
||||
plus_dm_result := c * plus_dm_ema
|
||||
warmup := e > 1e-10
|
||||
else
|
||||
tr_result := tr_ema
|
||||
plus_dm_result := plus_dm_ema
|
||||
float plus_di = tr_result != 0.0 ? 100.0 * plus_dm_result / tr_result : 0.0
|
||||
plus_di
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_period = input.int(14, "Period", minval=1, tooltip="Number of bars used in the calculation")
|
||||
|
||||
// Calculation
|
||||
plus_di = plusdi(i_period)
|
||||
|
||||
// Plot
|
||||
plot(plus_di, "+DI", color=color.green, linewidth=2)
|
||||
hline(25, "Threshold", color=color.gray, linestyle=hline.style_dashed)
|
||||
@@ -0,0 +1,73 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class PlusDmIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void PlusDmIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new PlusDmIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("+DM - Plus Directional Movement", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDmIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new PlusDmIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, PlusDmIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDmIndicator_Initialize_CreatesInternal()
|
||||
{
|
||||
var indicator = new PlusDmIndicator { Period = 14 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDmIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PlusDmIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDmIndicator_ShortName_IsCorrect()
|
||||
{
|
||||
var indicator = new PlusDmIndicator { Period = 20 };
|
||||
Assert.Equal("+DM 20", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlusDmIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new PlusDmIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("PlusDm.Quantower.cs", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class PlusDmIndicator : 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 PlusDm _plusDm = null!;
|
||||
private readonly LineSeries _plusDmSeries;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"+DM {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/dynamics/plusdm/PlusDm.Quantower.cs";
|
||||
|
||||
public PlusDmIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "+DM - Plus Directional Movement";
|
||||
Description = "Wilder-smoothed upward directional movement in price units";
|
||||
|
||||
_plusDmSeries = new LineSeries(name: "+DM", color: Color.Green, width: 2, style: LineStyle.Solid);
|
||||
|
||||
AddLineSeries(_plusDmSeries);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_plusDm = new PlusDm(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TValue result = _plusDm.Update(this.GetInputBar(args), args.IsNewBar());
|
||||
|
||||
_plusDmSeries.SetValue(result.Value, _plusDm.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using OoplesFinance.StockIndicators.Enums;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using QuanTAlib.Tests;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for PlusDm (+DM). Cross-validates against TA-Lib,
|
||||
/// OoplesFinance, and internal Dx equivalence with multiple periods.
|
||||
/// Note: Skender does not expose DM values directly; only DI values via GetAdx().
|
||||
/// </summary>
|
||||
public sealed class PlusDmValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
|
||||
public PlusDmValidationTests()
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_data.Dispose();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// TA-Lib Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void MatchesTalib()
|
||||
{
|
||||
var indicator = new PlusDm(14);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.PlusDM(hData, lData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDMLookback(14);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(7)]
|
||||
[InlineData(21)]
|
||||
[InlineData(28)]
|
||||
public void MatchesTalib_VariousPeriods(int period)
|
||||
{
|
||||
var indicator = new PlusDm(period);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
results.Add(indicator.Last.Value);
|
||||
}
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.PlusDM(hData, lData, 0..^0, outReal, out var outRange, period);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDMLookback(period);
|
||||
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Dx Equivalence
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ExactlyMatchesDx_DmPlus()
|
||||
{
|
||||
var indicator = new PlusDm(14);
|
||||
var dx = new Dx(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
dx.Update(_data.Bars[i]);
|
||||
|
||||
Assert.Equal(dx.DmPlus.Value, indicator.Last.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// OoplesFinance Structural Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void MatchesOoples_Structural()
|
||||
{
|
||||
var ooplesData = _data.SkenderQuotes
|
||||
.Select(q => new TickerData
|
||||
{
|
||||
Date = q.Date,
|
||||
Open = (double)q.Open,
|
||||
High = (double)q.High,
|
||||
Low = (double)q.Low,
|
||||
Close = (double)q.Close,
|
||||
Volume = (double)q.Volume
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var stockData = new StockData(ooplesData);
|
||||
var adxResults = stockData.CalculateAverageDirectionalIndex(MovingAvgType.WildersSmoothingMethod, 14);
|
||||
|
||||
var allValues = adxResults.OutputValues.Values.SelectMany(v => v).ToList();
|
||||
int finiteCount = allValues.Count(v => double.IsFinite(v));
|
||||
Assert.True(finiteCount > 100, $"Expected >100 finite Ooples ADX/DI values, got {finiteCount}");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Self-Consistency: Batch == Streaming
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void BatchEqualsStreaming()
|
||||
{
|
||||
var batchResults = PlusDm.Batch(_data.Bars, 14);
|
||||
|
||||
var streaming = new PlusDm(14);
|
||||
var streamResults = new List<double>();
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
streamResults.Add(streaming.Update(_data.Bars[i]).Value);
|
||||
}
|
||||
|
||||
Assert.Equal(streamResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < batchResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchMatchesTalib()
|
||||
{
|
||||
var batchResults = PlusDm.Batch(_data.Bars, 14);
|
||||
|
||||
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] outReal = new double[_data.Bars.Count];
|
||||
|
||||
var retCode = Functions.PlusDM(hData, lData, 0..^0, outReal, out var outRange, 14);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.PlusDMLookback(14);
|
||||
ValidationHelper.VerifyData(batchResults.Select(x => x.Value).ToList(), outReal, outRange, lookback);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Determinism
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ConsistentAcrossMultipleRuns()
|
||||
{
|
||||
var ind1 = new PlusDm(14);
|
||||
var ind2 = new PlusDm(14);
|
||||
var results1 = new List<double>();
|
||||
var results2 = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind1.Update(_data.Bars[i]);
|
||||
results1.Add(ind1.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
ind2.Update(_data.Bars[i]);
|
||||
results2.Add(ind2.Last.Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(results1[i], results2[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Output Range Validation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void OutputIsNonNegative()
|
||||
{
|
||||
var indicator = new PlusDm(14);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
indicator.Update(_data.Bars[i]);
|
||||
Assert.True(indicator.Last.Value >= 0, $"+DM output at bar {i} was {indicator.Last.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Different Periods Produce Different Results
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var short7 = new PlusDm(7);
|
||||
var long28 = new PlusDm(28);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
short7.Update(_data.Bars[i]);
|
||||
long28.Update(_data.Bars[i]);
|
||||
}
|
||||
|
||||
Assert.NotEqual(short7.Last.Value, long28.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,92 @@
|
||||
# PLUS_DM: Plus Directional Movement
|
||||
|
||||
Wilder-smoothed upward directional movement in price units (≥0).
|
||||
> *+DM captures the raw upward price extension, smoothed by Wilder's RMA — the building block before normalization to +DI.*
|
||||
|
||||
## Introduction
|
||||
Plus Directional Movement (+DM) measures the magnitude of upward price movement, smoothed using Wilder's method. Unlike +DI which normalizes by true range to produce a percentage, +DM outputs raw smoothed values in price units.
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
| **Inputs** | OHLCV bar (TBar) |
|
||||
| **Parameters** | `period` (default 14) |
|
||||
| **Outputs** | Single series |
|
||||
| **Output range** | ≥ 0 (price units) |
|
||||
| **Warmup** | `period` bars |
|
||||
| **PineScript** | [plusdm.pine](plusdm.pine) |
|
||||
|
||||
+DM captures when the current bar's high exceeds the previous bar's high by more than the previous bar's low exceeds the current bar's low. It is the raw building block of the Directional Movement System.
|
||||
- Plus Directional Movement outputs the Wilder-smoothed upward directional movement in price units.
|
||||
- Parameterized by `period` (default 14).
|
||||
- Output range: ≥ 0 (price units, scales with instrument).
|
||||
- Requires `period` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib and Dx equivalence.
|
||||
|
||||
## Calculation
|
||||
+DM = max(High - PrevHigh, 0) when High - PrevHigh > PrevLow - Low, else 0
|
||||
Plus Directional Movement (+DM) measures the magnitude of upward price movement, smoothed using Wilder's method. Unlike +DI which normalizes by true range to produce a percentage, +DM outputs raw smoothed values in price units. +DM captures when the current bar's high exceeds the previous bar's high by more than the previous bar's low exceeds the current bar's low. It is the raw building block of the Directional Movement System — the unnormalized signal before division by True Range converts it to +DI.
|
||||
|
||||
Smoothed using Wilder's method: Smooth = Smooth - Smooth/N + Input
|
||||
## Historical Context
|
||||
|
||||
## Parameters
|
||||
| Parameter | Default | Range | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Period | 14 | 2-∞ | Wilder smoothing period |
|
||||
J. Welles Wilder Jr. designed the Directional Movement System as a pipeline in *New Concepts in Technical Trading Systems* (1978). +DM is the first computational stage: a binary event detector that fires when upward range expansion dominates. Wilder's insight was that direction should be measured by range *extension*, not by close-to-close returns. A bar that pushes to a new high by more than it pushes to a new low registers as positive directional movement. The smoothed +DM series shows how much upward thrust is being sustained over the lookback period, in absolute price units.
|
||||
|
||||
## Interpretation
|
||||
- **Rising +DM:** Increasing upward price extension
|
||||
- **+DM > -DM:** Upward movement exceeds downward movement
|
||||
- **Zero +DM:** No upward directional movement on the bar
|
||||
- Values are in price units and scale with the instrument
|
||||
## Architecture & Physics
|
||||
|
||||
## References
|
||||
- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978)
|
||||
### 1. Plus Directional Movement (Raw)
|
||||
|
||||
$$\text{UpMove} = H_t - H_{t-1}, \quad \text{DownMove} = L_{t-1} - L_t$$
|
||||
|
||||
$$+DM = \begin{cases} \text{UpMove} & \text{if UpMove} > \text{DownMove and UpMove} > 0 \\ 0 & \text{otherwise} \end{cases}$$
|
||||
|
||||
Only one of +DM or -DM can be non-zero per bar — the dominant direction wins.
|
||||
|
||||
### 2. Wilder Smoothing (RMA)
|
||||
|
||||
$$+DM_{\text{smooth}} = \text{RMA}(+DM, N), \quad \alpha = 1/N$$
|
||||
|
||||
### 3. Complexity
|
||||
|
||||
- **Time:** $O(1)$ per bar — recursive RMA update
|
||||
- **Space:** $O(1)$ — scalar state only (delegates to Dx)
|
||||
- **Warmup:** $N$ bars
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
| Symbol | Parameter | Default | Constraint |
|
||||
|--------|-----------|---------|------------|
|
||||
| $N$ | period | 14 | $N \geq 2$ |
|
||||
|
||||
### Interpretation
|
||||
|
||||
| +DM Behavior | Signal |
|
||||
|--------------|--------|
|
||||
| Rising +DM | Increasing upward price extension |
|
||||
| +DM > -DM | Upward movement exceeds downward movement |
|
||||
| Zero +DM | No upward directional movement on the bar |
|
||||
| Values scale with instrument | Compare within same instrument only |
|
||||
|
||||
+DM values are in price units and scale with the instrument. They cannot be compared across different instruments without normalization (which is what +DI provides).
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
+DM is a thin wrapper around Dx. The per-bar cost is identical to Dx (one property extraction after Dx completes its update).
|
||||
|
||||
**Post-warmup steady state (per bar):**
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Dx.Update (full pipeline) | 1 | 75 | 75 |
|
||||
| Property extraction | 1 | 1 | 1 |
|
||||
| **Total** | **2** | — | **~76 cycles** |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 9/10 | Exact Dx delegation; FMA-precise RMA smoothing |
|
||||
| **Timeliness** | 7/10 | N-bar warmup; responds to bar-level changes |
|
||||
| **Smoothness** | 7/10 | Single RMA layer; moderate noise suppression |
|
||||
| **Noise Rejection** | 7/10 | Wilder smoothing filters transient spikes |
|
||||
|
||||
## Resources
|
||||
|
||||
- Wilder, J.W. — *New Concepts in Technical Trading Systems* (Trend Research, 1978)
|
||||
- PineScript reference: `plusdm.pine` in indicator directory
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Plus Directional Movement (+DM)", "+DM", overlay=false)
|
||||
|
||||
//@function Calculates Wilder-smoothed +DM using compensated RMA
|
||||
//@param period Number of bars used in the calculation
|
||||
//@returns Smoothed +DM value in price units (≥0)
|
||||
//@optimized Uses Wilder's smoothing (RMA) with warmup compensation for accurate values from bar 1
|
||||
plusdm(simple int period) =>
|
||||
if period <= 0
|
||||
runtime.error("Period must be greater than 0")
|
||||
float alpha = 1.0 / period
|
||||
float beta = 1.0 - alpha
|
||||
float plus_dm = 0.0
|
||||
if not na(high[1])
|
||||
float upMove = high - high[1]
|
||||
float downMove = low[1] - low
|
||||
if upMove > downMove and upMove > 0
|
||||
plus_dm := upMove
|
||||
var bool warmup = true
|
||||
var float e = 1.0
|
||||
var float plus_dm_ema = 0.0
|
||||
var float plus_dm_result = plus_dm
|
||||
plus_dm_ema := alpha * (plus_dm - plus_dm_ema) + plus_dm_ema
|
||||
if warmup
|
||||
e *= beta
|
||||
float c = 1.0 / (1.0 - e)
|
||||
plus_dm_result := c * plus_dm_ema
|
||||
warmup := e > 1e-10
|
||||
else
|
||||
plus_dm_result := plus_dm_ema
|
||||
plus_dm_result
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_period = input.int(14, "Period", minval=1, tooltip="Number of bars used in the calculation")
|
||||
|
||||
// Calculation
|
||||
plus_dm_value = plusdm(i_period)
|
||||
|
||||
// Plot
|
||||
plot(plus_dm_value, "+DM", color=color.green, linewidth=2)
|
||||
@@ -1,5 +1,7 @@
|
||||
# QSTICK: Qstick Indicator
|
||||
|
||||
> *The average candlestick body reveals the market's true conviction.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires `period` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "The average candlestick body reveals the market's true conviction."
|
||||
|
||||
The Qstick indicator, developed by Tushar Chande, computes a moving average of the close-minus-open difference over a lookback period, quantifying whether bars are predominantly bullish or bearish. Positive values indicate closes above opens (buying pressure); negative values indicate closes below opens (selling pressure). It supports both SMA (O(N) space via ring buffer) and EMA (O(1) space) smoothing modes and requires TBar input for open/close access.
|
||||
|
||||
## Historical Context
|
||||
@@ -61,33 +61,6 @@ EMA mode uses O(1) space but weights recent bars more heavily than SMA.
|
||||
| period | int | 14 | > 0 | Lookback period for moving average |
|
||||
| useEma | bool | false | — | Use EMA (true) or SMA (false) |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
QSTICK(bar, period=14, useEma=false):
|
||||
|
||||
diff = bar.Close - bar.Open
|
||||
|
||||
if useEma:
|
||||
// EMA mode
|
||||
alpha = 2.0 / (period + 1)
|
||||
if count == 0:
|
||||
ema_val = diff
|
||||
else:
|
||||
ema_val = FMA(alpha, diff - ema_val, ema_val) // alpha*(diff-ema)+ema
|
||||
result = ema_val
|
||||
|
||||
else:
|
||||
// SMA mode with ring buffer
|
||||
if buffer is full:
|
||||
running_sum -= buffer.oldest
|
||||
buffer.add(diff)
|
||||
running_sum += diff
|
||||
result = running_sum / min(count, period)
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
### Zero-Crossing Interpretation
|
||||
|
||||
| Condition | Meaning |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# RAVI: Chande Range Action Verification Index
|
||||
|
||||
> *The simplest question in technical analysis is also the most important: is this market trending or not? RAVI answers it with two moving averages and a division.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires `longPeriod` bars (default 65) of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "The simplest question in technical analysis is also the most important: is this market trending or not? RAVI answers it with two moving averages and a division."
|
||||
|
||||
RAVI (Range Action Verification Index) measures trend strength by computing the absolute percentage divergence between a short-period SMA and a long-period SMA. Created by Tushar Chande and published in *Beyond Technical Analysis* (Wiley, 2001), the indicator classifies markets into trending (RAVI > 3%) and ranging (RAVI < 3%) regimes using a single threshold. With default parameters (short=7, long=65), RAVI requires 65 bars of warmup for the first valid reading. The core computation is three operations per bar in streaming mode: two running-sum updates and one division. No square roots, no exponentials, no recursion. The entire indicator reduces to normalized SMA spread, making it one of the cheapest dynamics classifiers available.
|
||||
|
||||
## Historical Context
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# SUPER: SuperTrend
|
||||
|
||||
> *It's not an indicator; it's a trailing stop with a marketing budget.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires `> period + 1` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "It's not an indicator; it's a trailing stop with a marketing budget."
|
||||
|
||||
SuperTrend is a trend-following overlay that uses ATR-scaled bands around the HL2 midpoint, switching between upper and lower bands based on close price breakouts. A ratchet mechanism prevents the active band from moving against the trend, creating a step-like trailing stop that adapts to volatility. The indicator is a two-state machine (bullish/bearish) with O(1) per-bar updates and zero allocations in the hot path.
|
||||
|
||||
## Historical Context
|
||||
@@ -74,46 +74,6 @@ The output is the active stop level. Crossing the active band flips the state.
|
||||
| atrPeriod | int | 10 | > 0 | ATR lookback period |
|
||||
| multiplier | double | 3.0 | > 0 | ATR multiplier for band width |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
SUPERTREND(bar, atrPeriod=10, multiplier=3.0):
|
||||
|
||||
// ATR update (Wilder's smoothing or SMA)
|
||||
atr = ATR.Update(bar, atrPeriod)
|
||||
|
||||
// Basic bands
|
||||
hl2 = (bar.High + bar.Low) / 2
|
||||
upper_basic = hl2 + multiplier * atr
|
||||
lower_basic = hl2 - multiplier * atr
|
||||
|
||||
// Ratchet: upper can only decrease, lower can only increase
|
||||
if prev_close <= prev_upper_final:
|
||||
upper_final = min(upper_basic, prev_upper_final)
|
||||
else:
|
||||
upper_final = upper_basic
|
||||
|
||||
if prev_close >= prev_lower_final:
|
||||
lower_final = max(lower_basic, prev_lower_final)
|
||||
else:
|
||||
lower_final = lower_basic
|
||||
|
||||
// State machine transition
|
||||
if bar.Close > upper_final:
|
||||
is_bullish = true
|
||||
else if bar.Close < lower_final:
|
||||
is_bullish = false
|
||||
// else: retain previous state (hysteresis)
|
||||
|
||||
// Output active stop level
|
||||
if is_bullish:
|
||||
supertrend = lower_final
|
||||
else:
|
||||
supertrend = upper_final
|
||||
|
||||
return supertrend
|
||||
```
|
||||
|
||||
### Band Behavior by State
|
||||
|
||||
| State | Active Band | Ratchet Direction | Flip Condition |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# TTM_SQUEEZE: TTM Squeeze
|
||||
|
||||
> *Volatility compression is the market holding its breath before screaming.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -15,8 +17,6 @@
|
||||
- Requires `Math.Max(Math.Max(bbPeriod, kcPeriod), momPeriod)` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "Volatility compression is the market holding its breath before screaming."
|
||||
|
||||
John Carter's TTM Squeeze detects low-volatility compression by comparing Bollinger Band width against Keltner Channel width: when BB fits inside KC, a "squeeze" is on, signaling imminent breakout. The momentum component uses linear regression of price deviation from the Donchian midline to indicate direction. The indicator outputs a boolean squeeze state plus a continuous momentum histogram, requiring BB(20,2.0) and KC(20,1.5) as default parameters with a combined warmup of 20 bars.
|
||||
|
||||
## Historical Context
|
||||
@@ -85,40 +85,6 @@ The linear regression extracts the trend component of the deviation, filtering n
|
||||
| kcLength | int | 20 | > 1 | Keltner Channel period |
|
||||
| kcMult | double | 1.5 | > 0 | KC ATR multiplier |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
TTM_SQUEEZE(bar, bbLen=20, bbMult=2.0, kcLen=20, kcMult=1.5):
|
||||
|
||||
// Bollinger Bands
|
||||
sma_val = SMA(close, bbLen)
|
||||
stddev = StdDev(close, bbLen)
|
||||
bb_upper = sma_val + bbMult * stddev
|
||||
bb_lower = sma_val - bbMult * stddev
|
||||
|
||||
// Keltner Channel
|
||||
ema_val = EMA(close, kcLen)
|
||||
atr_val = ATR(bar, kcLen)
|
||||
kc_upper = ema_val + kcMult * atr_val
|
||||
kc_lower = ema_val - kcMult * atr_val
|
||||
|
||||
// Squeeze state
|
||||
squeeze_on = (bb_lower > kc_lower) AND (bb_upper < kc_upper)
|
||||
|
||||
// Momentum via linear regression of deviation
|
||||
highest_high = Highest(high, bbLen)
|
||||
lowest_low = Lowest(low, bbLen)
|
||||
midline = (highest_high + lowest_low) / 2
|
||||
delta = close - (midline + sma_val) / 2
|
||||
momentum = LinReg(delta, bbLen)
|
||||
|
||||
// Momentum direction
|
||||
momentum_rising = momentum > prev_momentum
|
||||
momentum_positive = momentum > 0
|
||||
|
||||
return (momentum, squeeze_on, momentum_rising, momentum_positive)
|
||||
```
|
||||
|
||||
### Squeeze-Fire Signal
|
||||
|
||||
The critical trading signal occurs on the transition bar:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# TTM_TREND: TTM Trend
|
||||
|
||||
> *The simplest trend indicator is the one you actually follow.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires `> 2` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "The simplest trend indicator is the one you actually follow."
|
||||
|
||||
John Carter's TTM Trend uses a fast EMA (default period 6) applied to typical price (HLC/3) to determine short-term trend direction via slope sign. Output is a ternary trend state: +1 (bullish, EMA rising), -1 (bearish, EMA falling), or 0 (neutral, EMA unchanged). The indicator requires only 2 bars warmup, runs at O(1) per bar with O(1) space, and produces zero allocations in the hot path.
|
||||
|
||||
## Historical Context
|
||||
@@ -75,36 +75,6 @@ This percentage rate-of-change quantifies how aggressively the trend is moving.
|
||||
|:----------|:-----|:--------|:-----------|:------------|
|
||||
| period | int | 6 | > 0 | EMA lookback period (very fast by default) |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
TTM_TREND(bar, period=6):
|
||||
|
||||
tp = (bar.High + bar.Low + bar.Close) / 3
|
||||
alpha = 2.0 / (period + 1)
|
||||
|
||||
if count == 0:
|
||||
ema_val = tp
|
||||
else:
|
||||
ema_val = FMA(alpha, tp - ema_val, ema_val)
|
||||
|
||||
// Trend direction from slope sign
|
||||
if count >= 1:
|
||||
if ema_val > prev_ema:
|
||||
trend = +1
|
||||
else if ema_val < prev_ema:
|
||||
trend = -1
|
||||
else:
|
||||
trend = 0
|
||||
|
||||
strength = abs(ema_val - prev_ema) / prev_ema * 100
|
||||
|
||||
prev_ema = ema_val
|
||||
count += 1
|
||||
|
||||
return (ema_val, trend, strength)
|
||||
```
|
||||
|
||||
### Period Selection
|
||||
|
||||
The default period of 6 makes TTM Trend extremely fast-reacting. The EMA half-life is approximately $\ln(2) / \ln(1 + 2/N) \approx 2.4$ bars for $N = 6$. This means the indicator responds within 2-3 bars of a price shift. Longer periods (12, 20) reduce whipsaws but delay detection. Carter's design intent was maximum responsiveness, with noise filtering delegated to companion indicators (Squeeze, Wave).
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# VHF: Vertical Horizontal Filter
|
||||
|
||||
> *Before you ask which way the market is going, ask whether it is going anywhere at all. VHF answers the second question with a ratio and a ruler.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires `period + 1` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "Before you ask which way the market is going, ask whether it is going anywhere at all. VHF answers the second question with a ratio and a ruler."
|
||||
|
||||
VHF (Vertical Horizontal Filter) measures trend strength by dividing the price range over $N$ periods by the total absolute bar-to-bar path distance over the same window. Created by Adam White and published in the August 1991 issue of *Futures* magazine, VHF produces a single positive value where higher readings indicate trending conditions and lower readings indicate choppy, range-bound markets. With the default period of 28, the indicator requires 29 close values for the first valid output. The core computation in streaming mode is O(1) per bar when implemented with deque-based min/max tracking and a running sum of absolute changes. No square roots, no exponentials, no recursion. VHF is one of the simplest and cheapest trend-strength classifiers available, requiring approximately 12 operations per bar at steady state.
|
||||
|
||||
## Historical Context
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# VORTEX: Vortex Indicator
|
||||
|
||||
> *When bulls and bears clash, the Vortex measures the violence.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Dynamic |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires `period` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "When bulls and bears clash, the Vortex measures the violence."
|
||||
|
||||
The Vortex Indicator measures upward and downward trend momentum by computing the ratio of positive and negative vortex movements to true range over a rolling window. VI+ captures the distance from current high to previous low (upward force); VI- captures the distance from current low to previous high (downward force). Both are normalized by summed true range, producing two lines that oscillate around 1.0. Crossovers signal trend changes. The implementation uses three ring buffers with running sums for O(1) streaming updates.
|
||||
|
||||
## Historical Context
|
||||
@@ -81,45 +81,6 @@ This yields O(1) per-bar updates after the initial warmup fill.
|
||||
|:----------|:-----|:--------|:-----------|:------------|
|
||||
| period | int | 14 | > 0 | Rolling window for VM and TR sums |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
VORTEX(bar, period=14):
|
||||
|
||||
// Vortex movements (require previous bar)
|
||||
vm_plus = abs(bar.High - prev_bar.Low)
|
||||
vm_minus = abs(bar.Low - prev_bar.High)
|
||||
|
||||
// True range
|
||||
tr = max(bar.High - bar.Low,
|
||||
abs(bar.High - prev_bar.Close),
|
||||
abs(bar.Low - prev_bar.Close))
|
||||
|
||||
// Ring buffer updates with running sums
|
||||
if buffer_full:
|
||||
sum_vm_plus -= vm_plus_buffer.oldest
|
||||
sum_vm_minus -= vm_minus_buffer.oldest
|
||||
sum_tr -= tr_buffer.oldest
|
||||
|
||||
vm_plus_buffer.add(vm_plus)
|
||||
vm_minus_buffer.add(vm_minus)
|
||||
tr_buffer.add(tr)
|
||||
|
||||
sum_vm_plus += vm_plus
|
||||
sum_vm_minus += vm_minus
|
||||
sum_tr += tr
|
||||
|
||||
// Vortex indicators
|
||||
if sum_tr > 0:
|
||||
vi_plus = sum_vm_plus / sum_tr
|
||||
vi_minus = sum_vm_minus / sum_tr
|
||||
else:
|
||||
vi_plus = 0
|
||||
vi_minus = 0
|
||||
|
||||
return (vi_plus, vi_minus)
|
||||
```
|
||||
|
||||
### Reference Line at 1.0
|
||||
|
||||
The value 1.0 serves as a natural reference:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Errors
|
||||
|
||||
> "All models are wrong. Error metrics tell you how wrong." Adapted from George Box
|
||||
|
||||
Error metrics and loss functions for model/strategy evaluation. All error indicators accept two input series (actual and predicted values) and compute rolling error metrics over a configurable period.
|
||||
|
||||
## Indicators
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Huber: Huber Loss
|
||||
|
||||
> *The Goldilocks of loss functions: not too sensitive, not too robust, just right.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Error Metric |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires 1 bar of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "The Goldilocks of loss functions: not too sensitive, not too robust, just right."
|
||||
|
||||
Huber Loss is a hybrid loss function that combines the best properties of Mean Squared Error (MSE) and Mean Absolute Error (MAE). For small errors, it behaves quadratically like MSE; for large errors, it behaves linearly like MAE.
|
||||
|
||||
## Historical Context
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Log-Cosh: Logarithm of Hyperbolic Cosine Loss
|
||||
|
||||
> *The smooth operator that acts like L2 for small errors and L1 for large ones.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Error Metric |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires 1 bar of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "The smooth operator that acts like L2 for small errors and L1 for large ones."
|
||||
|
||||
Log-Cosh Loss combines the best properties of L1 (absolute) and L2 (squared) error metrics through the logarithm of the hyperbolic cosine function. It provides smooth gradients everywhere while remaining robust to outliers.
|
||||
|
||||
## Historical Context
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# MAAPE: Mean Arctangent Absolute Percentage Error
|
||||
|
||||
> *When percentage errors need boundaries, arctangent provides the walls.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Error Metric |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires 1 bar of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "When percentage errors need boundaries, arctangent provides the walls."
|
||||
|
||||
Mean Arctangent Absolute Percentage Error (MAAPE) transforms percentage errors through the arctangent function, naturally bounding the metric between 0 and π/2. This eliminates the unbounded nature of MAPE while preserving its scale-independence.
|
||||
|
||||
## Historical Context
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# MAE: Mean Absolute Error
|
||||
|
||||
> *When you need to know how wrong you are on average, without the drama of squared errors.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Error Metric |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires 1 bar of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "When you need to know how wrong you are on average, without the drama of squared errors."
|
||||
|
||||
Mean Absolute Error (MAE) measures the average magnitude of errors in a set of predictions, without considering their direction. It represents the average of the absolute differences between actual and predicted values.
|
||||
|
||||
## Historical Context
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# MAPD: Mean Absolute Percentage Deviation
|
||||
|
||||
> *Like MAPE, but divides by what you predicted instead of what actually happened.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Error Metric |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires 1 bar of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "Like MAPE, but divides by what you predicted instead of what actually happened."
|
||||
|
||||
Mean Absolute Percentage Deviation (MAPD) measures the average absolute percentage difference between actual and predicted values, using the predicted value as the denominator. This is the key difference from MAPE, which uses the actual value.
|
||||
|
||||
## Historical Context
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# MAPE: Mean Absolute Percentage Error
|
||||
|
||||
> *The metric that lets you compare apples to oranges, as long as you don't have any zeros.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Error Metric |
|
||||
@@ -16,8 +18,6 @@
|
||||
- Requires 1 bar of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "The metric that lets you compare apples to oranges, as long as you don't have any zeros."
|
||||
|
||||
Mean Absolute Percentage Error (MAPE) measures the average absolute percentage difference between actual and predicted values. It expresses accuracy as a percentage, making it scale-independent and easy to interpret.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user