mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 11:38:05 +00:00
feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Average Daily Range (ADR)", "ADR", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Average True Range (ATR)", "ATR", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Average True Range Normalized (ATRN)", "ATRN", overlay=false, format=format.percent, precision=2)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Bollinger Band Width (BBW)", "BBW", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Bollinger Band Width Normalized (BBWN)", "BBWN", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Bollinger Band Width Percentile (BBWP)", "BBWP", overlay=false, format=format.percent)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Close-to-Close Volatility (CCV)", "CCV", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Conditional Volatility (CV)", "CV", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Chaikin's Volatility (CVI)", "CVI", overlay=false)
|
||||
|
||||
@@ -12,9 +12,8 @@ namespace QuanTAlib;
|
||||
/// <remarks>
|
||||
/// <b>Calculation steps:</b>
|
||||
/// <list type="number">
|
||||
/// <item>highDiff = |High - prevHigh|, lowDiff = |prevLow - Low|</item>
|
||||
/// <item>Inside bar (High < prevHigh AND Low > prevLow) → Temperature = 0</item>
|
||||
/// <item>Otherwise Temperature = max(highDiff, lowDiff)</item>
|
||||
/// <item>highDiff = max(High − prevHigh, 0), lowDiff = max(prevLow − Low, 0)</item>
|
||||
/// <item>Temperature = max(highDiff, lowDiff)</item>
|
||||
/// <item>Signal = EMA(Temperature, period) with bias compensation</item>
|
||||
/// </list>
|
||||
///
|
||||
@@ -271,10 +270,9 @@ public sealed class Etherm : AbstractBase
|
||||
prevL = lastValidLow;
|
||||
}
|
||||
|
||||
double highDiff = Math.Abs(h - prevH);
|
||||
double lowDiff = Math.Abs(prevL - l);
|
||||
bool isInsideBar = h < prevH && l > prevL;
|
||||
temp = isInsideBar ? 0 : Math.Max(highDiff, lowDiff);
|
||||
double highDiff = Math.Max(h - prevH, 0.0);
|
||||
double lowDiff = Math.Max(prevL - l, 0.0);
|
||||
temp = Math.Max(highDiff, lowDiff);
|
||||
}
|
||||
|
||||
if (!double.IsFinite(temp) || temp < 0)
|
||||
@@ -345,10 +343,9 @@ public sealed class Etherm : AbstractBase
|
||||
}
|
||||
else
|
||||
{
|
||||
double highDiff = Math.Abs(high - s.PrevHigh);
|
||||
double lowDiff = Math.Abs(s.PrevLow - low);
|
||||
bool isInsideBar = high < s.PrevHigh && low > s.PrevLow;
|
||||
temp = isInsideBar ? 0 : Math.Max(highDiff, lowDiff);
|
||||
double highDiff = Math.Max(high - s.PrevHigh, 0.0);
|
||||
double lowDiff = Math.Max(s.PrevLow - low, 0.0);
|
||||
temp = Math.Max(highDiff, lowDiff);
|
||||
}
|
||||
|
||||
// NaN/Infinity safety on computed temp
|
||||
|
||||
+70
-220
@@ -2,275 +2,125 @@
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Volatility |
|
||||
| **Inputs** | OHLCV bar (TBar) |
|
||||
| **Parameters** | `period` (default 22) |
|
||||
| **Outputs** | Single series (Etherm) |
|
||||
| **Output range** | $\geq 0$ |
|
||||
| **Warmup** | `period` bars |
|
||||
| **Category** | Volatility |
|
||||
| **Inputs** | OHLCV bar (TBar) |
|
||||
| **Parameters** | `period` (default 22) |
|
||||
| **Outputs** | Temperature + Signal (EMA) |
|
||||
| **Output range** | $\geq 0$ |
|
||||
| **Warmup** | `period` bars |
|
||||
|
||||
### TL;DR
|
||||
|
||||
- Elder's Thermometer (ETHERM) measures how far today's price bar extends beyond yesterday's range, capturing the maximum absolute expansion in eithe...
|
||||
- Parameterized by `period` (default 22).
|
||||
- Output range: $\geq 0$.
|
||||
- Elder's Thermometer (ETHERM) measures how far today's price bar protrudes beyond yesterday's range, capturing the maximum outward extension in either direction.
|
||||
- Parameterized by `period` (default 22) for the EMA signal line.
|
||||
- Output range: $\geq 0$ (same units as price).
|
||||
- Requires `period` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
|
||||
> "Markets run a fever before they crash. The thermometer tells you when to reach for the aspirin."
|
||||
|
||||
Elder's Thermometer (ETHERM) measures how far today's price bar extends beyond yesterday's range, capturing the maximum absolute expansion in either direction. Developed by Dr. Alexander Elder and described in *Come Into My Trading Room* (2002, p.162), the indicator distinguishes between sleepy, quiet periods and hot episodes when market crowds become excited. The raw thermometer reading is smoothed with an EMA to produce a signal line; when temperature spikes to triple the signal, it flags an explosive move worth fading. At 5 operations per bar for the raw value and O(1) EMA update, ETHERM is among the cheapest volatility measures to compute.
|
||||
Elder's Thermometer (ETHERM) measures bar-to-bar range extension — the maximum outward protrusion of the current bar beyond the previous bar's high or low. Developed by Dr. Alexander Elder, it captures only outward expansions; inward contractions clamp to zero. An EMA signal line with bias compensation provides a smoothed reference for detecting explosive moves (temperature significantly exceeding the signal).
|
||||
|
||||
## Historical Context
|
||||
|
||||
Dr. Alexander Elder, a psychiatrist-turned-trader who emigrated from the Soviet Union in the 1970s, built his reputation on applying behavioral psychology to market analysis. His first book *Trading for a Living* (1993) introduced the Elder-Ray Index and the Triple Screen system. His second, *Come Into My Trading Room* (2002), added the Market Thermometer on page 162, filling a gap he identified: existing volatility tools (ATR, Bollinger Width) measured absolute dispersion, but none specifically isolated the *bar-to-bar range extension* that characterizes crowd excitement.
|
||||
|
||||
Elder's insight was deceptively simple. Adjacent bars in a quiet market overlap. The high barely exceeds yesterday's high; the low barely undercuts yesterday's low. When the crowd gets excited, bars start pushing outside previous ranges. The thermometer captures exactly this phenomenon: how many price units did today's bar extend beyond yesterday's boundaries?
|
||||
|
||||
The formula differs from True Range in a critical way. TR measures the total possible price excursion including gaps (max of H-L, |H-prevC|, |L-prevC|). ETHERM ignores the close entirely and focuses on high-to-high and low-to-low comparisons. A stock that gaps up 5 points but trades within a 1-point range registers TR=5 but ETHERM near zero (assuming yesterday's high was close to today's high). The two indicators answer different questions: TR asks "how far could price have traveled?" while ETHERM asks "how much did today's bar escape yesterday's?"
|
||||
|
||||
Several implementations exist across platforms. The ProRealCode and MotiveWave versions match Elder's original formula precisely. The LightningChart JS version diverges significantly, comparing current bars to N-periods-ago bars rather than the previous bar. This QuanTAlib implementation follows Elder's original specification: previous bar comparison with inside-bar detection.
|
||||
Dr. Alexander Elder introduced the Market Thermometer in *Come Into My Trading Room* (2002) as part of his Triple Screen trading system refinements. Elder observed that bars extending well beyond the prior bar's range signaled heightened volatility — the market "running a fever." The thermometer provides a simple, bar-level volatility measure that distinguishes between outward breakouts and inward consolidation, making it ideal for stop placement and position sizing decisions.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
ETHERM has three components: raw temperature calculation, EMA signal smoothing, and threshold detection.
|
||||
ETHERM is a **two-stage pipeline**: a per-bar range-extension measurement followed by an exponential smoother.
|
||||
|
||||
### 1. Raw Temperature Calculation
|
||||
**Stage 1 — Temperature:** For each bar, compute how far the high protrudes above the previous high and how far the low protrudes below the previous low. Only outward extensions count; inward contractions clamp to zero. The temperature is the larger of the two protrusions.
|
||||
|
||||
The thermometer measures the maximum absolute extension beyond the previous bar:
|
||||
**Stage 2 — Signal:** A bias-compensated EMA of the temperature provides a smoothed baseline. The bias compensation ensures accuracy from the first bar by dividing out the geometric decay factor $e_t$, converging to a standard EMA as $e_t \to 0$.
|
||||
|
||||
$$
|
||||
\text{highDiff}_t = |H_t - H_{t-1}|
|
||||
$$
|
||||
### Transfer Function
|
||||
|
||||
$$
|
||||
\text{lowDiff}_t = |L_{t-1} - L_t|
|
||||
$$
|
||||
The signal line is a standard EMA applied to the temperature series:
|
||||
|
||||
Three cases determine the output:
|
||||
$$H(z) = \frac{\alpha}{1 - \beta z^{-1}}, \quad \alpha = \frac{2}{N+1}, \quad \beta = 1 - \alpha$$
|
||||
|
||||
$$
|
||||
T_t = \begin{cases}
|
||||
0 & \text{if } H_t < H_{t-1} \text{ AND } L_t > L_{t-1} \text{ (inside bar)} \\
|
||||
\max(\text{highDiff}_t, \text{lowDiff}_t) & \text{otherwise}
|
||||
\end{cases}
|
||||
$$
|
||||
### Half-Life
|
||||
|
||||
The inside bar case is significant. When today's entire range fits within yesterday's range, there is zero range extension in either direction. The crowd is dormant.
|
||||
$$t_{1/2} = \frac{-\ln 2}{\ln \beta}$$
|
||||
|
||||
### 2. EMA Signal Line
|
||||
For `period = 22`: $\beta \approx 0.913$, $t_{1/2} \approx 7.6$ bars.
|
||||
|
||||
The raw temperature is smoothed with an exponential moving average:
|
||||
### Warmup Period
|
||||
|
||||
$$
|
||||
\alpha = \frac{2}{N + 1}
|
||||
$$
|
||||
|
||||
$$
|
||||
S_t = \alpha \cdot T_t + (1 - \alpha) \cdot S_{t-1}
|
||||
$$
|
||||
|
||||
Default period $N = 22$ (approximately one trading month). The EMA provides a baseline "normal temperature" against which spikes and troughs are measured.
|
||||
|
||||
### 3. Threshold Detection
|
||||
|
||||
Elder defined two key thresholds:
|
||||
|
||||
**Explosive move:** When the thermometer reaches or exceeds the signal multiplied by a factor (default 3.0):
|
||||
|
||||
$$
|
||||
\text{Explosive} = T_t \geq S_t \times M
|
||||
$$
|
||||
|
||||
where $M$ is the multiplier (default 3.0).
|
||||
|
||||
**Idle market:** When the thermometer remains below the signal for a sustained number of consecutive bars (Elder suggested 5-7 bars). This is a secondary signal not computed in the indicator itself but observable from the histogram.
|
||||
|
||||
### 4. First Bar Handling
|
||||
|
||||
For the first bar (no previous bar available):
|
||||
|
||||
$$
|
||||
T_0 = 0
|
||||
$$
|
||||
|
||||
Using `nz(high[1], high)` maps the previous high to today's high, making highDiff and lowDiff both zero. This is correct: with no history, there is no range extension to measure.
|
||||
QuanTAlib uses bias-compensated EMA, which converges after approximately `period` bars. During warmup, outputs are produced but `IsHot` returns false until the compensator $e_t \leq 0.05$.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Why Absolute Values?
|
||||
### Step 1: Outward Protrusions
|
||||
|
||||
Consider a bar where today's high is 102 and yesterday's high was 105. The extension is $|102 - 105| = 3$. Without the absolute value, the result would be $-3$, hiding the magnitude. Elder's thermometer cares about *size* of escape, not direction. A 3-point high compression and a 3-point low extension represent equal amounts of crowd activity.
|
||||
$$\text{highDiff}_t = \max(H_t - H_{t-1},\; 0)$$
|
||||
$$\text{lowDiff}_t = \max(L_{t-1} - L_t,\; 0)$$
|
||||
|
||||
### Relationship to True Range
|
||||
### Step 2: Temperature
|
||||
|
||||
True Range and ETHERM share a structural similarity but measure different phenomena:
|
||||
$$T_t = \max(\text{highDiff}_t,\; \text{lowDiff}_t)$$
|
||||
|
||||
| Scenario | TR | ETHERM |
|
||||
| :--- | :--- | :--- |
|
||||
| No gap, wide bar | $H - L$ | $\max(\|H-H_{-1}\|, \|L_{-1}-L\|)$ |
|
||||
| Large gap up, narrow bar | $H - C_{-1}$ (large) | Near 0 (similar H-to-H) |
|
||||
| Breakout bar exceeding prior range | $H - L$ | Large (extension detected) |
|
||||
| Inside bar | $H - L$ (positive) | 0 (no extension) |
|
||||
### Step 3: EMA Signal with Bias Compensation
|
||||
|
||||
ETHERM specifically detects range *expansion*. TR detects total price travel. A market that gaps and then consolidates shows high TR but low ETHERM.
|
||||
$$\text{ema}_t = \beta \cdot \text{ema}_{t-1} + \alpha \cdot T_t$$
|
||||
|
||||
### EMA Warmup Compensation
|
||||
$$e_t = \beta \cdot e_{t-1}, \quad e_0 = 1$$
|
||||
|
||||
The PineScript reference implementation uses warmup-compensated EMA to eliminate initialization bias:
|
||||
$$\text{Signal}_t = \begin{cases} \frac{\text{ema}_t}{1 - e_t} & \text{if } e_t > \epsilon \\ \text{ema}_t & \text{otherwise} \end{cases}$$
|
||||
|
||||
$$
|
||||
e_t = e_{t-1} \cdot (1 - \alpha), \quad e_0 = 1
|
||||
$$
|
||||
|
||||
$$
|
||||
S_{compensated} = \frac{S_{raw}}{1 - e_t} \quad \text{when } e_t > \epsilon
|
||||
$$
|
||||
|
||||
This ensures accurate signal values from the first bar rather than waiting for the EMA to "fill up."
|
||||
|
||||
### Convergence
|
||||
|
||||
For EMA period $N = 22$, $\alpha = 2/23 \approx 0.087$:
|
||||
|
||||
$$
|
||||
\text{WarmupPeriod} \approx \frac{\ln(0.05)}{\ln(1 - \alpha)} \approx \frac{-3.0}{-0.091} \approx 33 \text{ bars}
|
||||
$$
|
||||
|
||||
After 33 bars, the initialization bias drops below 5%.
|
||||
|
||||
### Inside Bar Probability
|
||||
|
||||
In typical equity markets, inside bars occur approximately 15-25% of trading days. The zero-temperature reading for inside bars creates a natural floor that keeps the EMA signal from rising without genuine range extension. This asymmetry is intentional: Elder wanted the thermometer to measure heat, not cold.
|
||||
where $N$ = `period`, $H_t$ = High, $L_t$ = Low, $\epsilon = 10^{-10}$.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
### Operation Count (per bar)
|
||||
|
||||
Per-bar operations:
|
||||
| Operation | Count | Notes |
|
||||
| --------------- | ----- | ---------------------------------- |
|
||||
| Subtract | 2 | High/low diffs |
|
||||
| Max | 3 | Clamp to 0 (×2), final max |
|
||||
| FMA | 1 | EMA update |
|
||||
| Multiply | 2 | $\alpha \cdot T$, $\beta \cdot e$ |
|
||||
| Division | 1 | Bias compensation |
|
||||
| Compare/branch | 2 | Finite check, bias threshold |
|
||||
| **Total** | ~11 | O(1) per bar, no allocations |
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| SUB | 2 | 1 | 2 |
|
||||
| ABS | 2 | 1 | 2 |
|
||||
| CMP | 3 | 1 | 3 |
|
||||
| MAX | 1 | 1 | 1 |
|
||||
| FMA | 1 | 5 | 5 |
|
||||
| MUL | 2 | 3 | 6 |
|
||||
| DIV | 1 | 15 | 15 |
|
||||
| **Total** | **12** | | **~34 cycles** |
|
||||
### SIMD Applicability
|
||||
|
||||
ETHERM is extremely lightweight. No logarithms, no square roots, no transcendental functions. The EMA update dominates at ~60% of total cost.
|
||||
Not beneficial — the recursive EMA dependency prevents vectorization. Each bar depends on the previous bar's state.
|
||||
|
||||
### Batch Mode (512 values, SIMD/FMA)
|
||||
### Memory Layout
|
||||
|
||||
| Operation | Scalar Ops | SIMD Ops (AVX2) | Speedup |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Subtractions (H-prevH, prevL-L) | 1024 | 128 | 8x |
|
||||
| Absolute values | 1024 | 128 | 8x |
|
||||
| Comparisons + MAX | 1536 | 192 | 8x |
|
||||
| EMA update | 512 | 512 | 1x (sequential) |
|
||||
|
||||
The raw temperature calculation vectorizes perfectly. The EMA is inherently sequential (each value depends on the previous), limiting overall batch speedup to roughly 3-4x.
|
||||
|
||||
### Memory Profile
|
||||
|
||||
- **Per instance:** ~64 bytes (state struct with prevHigh, prevLow, EMA state, warmup)
|
||||
- **No ring buffer required** (only needs previous bar's high and low)
|
||||
- **100 instances:** ~6.4 KB
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Exact calculation, no approximations |
|
||||
| **Timeliness** | 9/10 | Minimal lag; raw value is instantaneous, EMA adds slight delay |
|
||||
| **Smoothness** | 4/10 | Raw thermometer is spiky by design; signal line smooths |
|
||||
| **Simplicity** | 9/10 | Two subtractions, two abs, one max, one EMA |
|
||||
| **Interpretability** | 8/10 | Direct physical meaning: price units of range extension |
|
||||
| Field | Type | Bytes | Purpose |
|
||||
| -------------- | -------- | ----- | ---------------------------- |
|
||||
| `PrevHigh` | `double` | 8 | Previous bar's high |
|
||||
| `PrevLow` | `double` | 8 | Previous bar's low |
|
||||
| `Ema` | `double` | 8 | Running EMA of temperature |
|
||||
| `E` | `double` | 8 | Bias compensator |
|
||||
| `LastValidHigh`| `double` | 8 | NaN fallback for high |
|
||||
| `LastValidLow` | `double` | 8 | NaN fallback for low |
|
||||
| `LastValidTemp`| `double` | 8 | NaN fallback for temperature |
|
||||
| `Count` | `int` | 4 | Bar counter |
|
||||
| **Total** | | 60 | Single cache line |
|
||||
|
||||
## Validation
|
||||
|
||||
ETHERM is not widely implemented in major open-source libraries under a standard name. Most implementations are custom scripts.
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib** | N/A | Not implemented |
|
||||
| **Skender** | N/A | Not implemented |
|
||||
| **Tulip** | N/A | Not implemented |
|
||||
| **OoplesFinance** | N/A | Not implemented |
|
||||
| **PineScript** | ✅ | Matches etherm.pine reference |
|
||||
| **ProRealCode** | ✅ | Matches Elder's original formula |
|
||||
| **MotiveWave** | ✅ | Confirms formula: "highest absolute difference" |
|
||||
| **Manual** | ✅ | Validated against Elder p.162 formula |
|
||||
|
||||
The absence from standard libraries is unsurprising. ETHERM was published in a trading book, not an academic paper. It lacks the institutional pedigree of Wilder's indicators (ATR, RSI) or Bollinger's Bands. The algorithm is simple enough that most platforms implement it as a custom script rather than a built-in function.
|
||||
| Library | Match | Notes |
|
||||
| -------- | ----- | ---------------------------------------- |
|
||||
| TA-Lib | — | No Elder Thermometer function |
|
||||
| Skender | — | No direct equivalent |
|
||||
| Tulip | — | No direct equivalent |
|
||||
| Self | ✓ | Batch ⟷ streaming ⟷ span consistency |
|
||||
| Pine | ✓ | `etherm.pine` matches C# output |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Confusing ETHERM with ATR**: ATR measures total price excursion including gaps (uses close). ETHERM measures bar-to-bar range extension (ignores close entirely). A large gap-up with a narrow range produces high ATR but near-zero ETHERM. Using one where the other is intended produces meaningfully wrong signals.
|
||||
|
||||
2. **Inside bar handling**: Some implementations omit the inside bar check, computing `max(highDiff, lowDiff)` even when both differences are negative (meaning compression, not expansion). This incorrectly reports range contraction as if it were expansion. Elder's original formula explicitly returns zero for inside bars.
|
||||
|
||||
3. **Absolute value omission**: The formula requires absolute values of the differences. When `Low_today > Low_yesterday`, `Low_yesterday - Low_today` is negative. Without `abs()`, the max function may select highDiff by default even when lowDiff is the dominant extension. Approximately 10-15% of signals will be wrong.
|
||||
|
||||
4. **EMA period sensitivity**: Elder's default of 22 bars (roughly one trading month) works for daily charts. On 5-minute charts, 22 bars spans less than 2 hours. For intraday use, scale the period proportionally: ~250 for 5-min, ~50 for hourly. Using period 22 on intraday data produces an overly responsive signal line.
|
||||
|
||||
5. **Multiplier calibration**: The default 3.0 multiplier for explosive moves was designed for daily equity data in the late 1990s. Crypto and high-volatility assets may need higher multipliers (4.0-5.0) to avoid false positives. Low-volatility instruments (bonds, utilities) may need lower multipliers (2.0-2.5). Test the multiplier against historical data before relying on it.
|
||||
|
||||
6. **Zero-temperature clustering**: Inside bars cluster during consolidation. Extended periods of zero readings followed by a breakout bar produce a spike that appears dramatic relative to the suppressed EMA. This is feature, not bug: Elder designed the indicator to flag exactly this transition. But traders should be aware that the spike magnitude reflects the prior calm as much as the current excitement.
|
||||
|
||||
7. **No directional information**: ETHERM measures magnitude of range extension but not direction. A 10-point extension could be bullish (new highs) or bearish (new lows). Pair ETHERM with directional indicators (Elder-Ray, Impulse System) for complete context.
|
||||
|
||||
## Trading Applications
|
||||
|
||||
### Entry Timing
|
||||
|
||||
Elder's primary recommendation: enter positions when Thermometer < Signal:
|
||||
|
||||
```text
|
||||
If system generates entry signal AND ETHERM < Signal:
|
||||
Execute entry (low slippage environment)
|
||||
If system generates entry signal AND ETHERM > Signal:
|
||||
Wait or reduce size (hot market, slippage likely)
|
||||
```
|
||||
|
||||
### Profit-Taking on Spikes
|
||||
|
||||
Exit (or take partial profits) when Thermometer >= Signal x 3:
|
||||
|
||||
```text
|
||||
If ETHERM >= Signal × multiplier:
|
||||
Take profits on existing positions
|
||||
Panics are short-lived; cash in before reversion
|
||||
```
|
||||
|
||||
### Volatility Regime Filter
|
||||
|
||||
Track consecutive bars below the signal line:
|
||||
|
||||
```text
|
||||
If ETHERM < Signal for 7+ consecutive bars:
|
||||
Market is idle/consolidating
|
||||
Prepare for potential breakout
|
||||
Tighten stops or reduce position size
|
||||
```
|
||||
|
||||
## Relationship to Other Indicators
|
||||
|
||||
| Indicator | Relationship to ETHERM |
|
||||
| :--- | :--- |
|
||||
| **TR** | TR measures total price travel (with gaps); ETHERM measures range extension only |
|
||||
| **ATR** | Smoothed TR; both measure volatility but from different perspectives |
|
||||
| **Elder-Ray** | Bull/Bear Power measures distance from EMA; complements ETHERM's range extension |
|
||||
| **Impulse System** | Directional classification; pair with ETHERM for timing |
|
||||
| **Bollinger Width** | Measures band expansion/contraction; slower-moving volatility gauge |
|
||||
| **ADX** | Trend strength; ETHERM measures volatility regardless of trend |
|
||||
1. **Using close-only data** — ETHERM requires High and Low prices. When fed a single value (TValue), it treats H=L, producing zero temperature. Always use `Update(TBar)`.
|
||||
2. **Confusing temperature with signal** — The `Value` property returns the raw temperature (current bar only); the `Signal` property returns the smoothed EMA. Use signal for trend comparisons.
|
||||
3. **Inside bars** — Both protrusions clamp to zero, so inside bars always produce temperature = 0. This is by design, not a bug.
|
||||
4. **First bar** — No previous bar exists, so temperature = 0. The EMA signal starts building from the second bar.
|
||||
5. **Explosive threshold** — A common strategy is to flag bars where temperature exceeds `Signal × multiplier` (e.g., 3×) as explosive moves.
|
||||
|
||||
## References
|
||||
|
||||
- Elder, A. (2002). *Come Into My Trading Room: A Complete Guide to Trading*. John Wiley & Sons. pp. 162-164.
|
||||
- Elder, A. (1993). *Trading for a Living: Psychology, Trading Tactics, Money Management*. John Wiley & Sons.
|
||||
- Elder, A. (2014). *The New Trading for a Living*. John Wiley & Sons. (Updated treatment of the Thermometer.)
|
||||
- LazyBear. (2015). "Elder's Market Thermometer." TradingView Community Scripts.
|
||||
- MotiveWave Documentation. "Elders Thermometer (THER)." docs.motivewave.com.
|
||||
- **Elder, Alexander** (2002). *Come Into My Trading Room: A Complete Guide to Trading*, Wiley. p. 162.
|
||||
- **Elder, Alexander** (1993). *Trading for a Living*, Wiley. (Earlier discussion of volatility-based stops.)
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Elder's Thermometer", "ETHERM", overlay=false)
|
||||
@@ -12,14 +12,13 @@ etherm(simple int period) =>
|
||||
runtime.error("Period must be greater than 0")
|
||||
|
||||
// Step 1: Calculate raw thermometer value
|
||||
// Temperature = max(abs(High - prevHigh), abs(prevLow - Low))
|
||||
// Inside bar (High < prevHigh AND Low > prevLow) => 0
|
||||
// Temperature = max of upward high protrusion and downward low protrusion
|
||||
// Only outward extensions count; contractions clamp to zero
|
||||
float prevHigh = nz(high[1], high)
|
||||
float prevLow = nz(low[1], low)
|
||||
float highDiff = math.abs(high - prevHigh)
|
||||
float lowDiff = math.abs(prevLow - low)
|
||||
bool isInsideBar = high < prevHigh and low > prevLow
|
||||
float temp = isInsideBar ? 0.0 : math.max(highDiff, lowDiff)
|
||||
float highDiff = math.max(high - prevHigh, 0.0)
|
||||
float lowDiff = math.max(prevLow - low, 0.0)
|
||||
float temp = math.max(highDiff, lowDiff)
|
||||
|
||||
// Step 2: EMA of thermometer with warmup compensation
|
||||
float alpha = 2.0 / float(period + 1)
|
||||
@@ -51,4 +50,4 @@ color thermColor = isExplosive ? color.red : isHot ? color.orange : color.new(co
|
||||
|
||||
// Plot
|
||||
plot(thermValue, "Thermometer", color=thermColor, style=plot.style_histogram, linewidth=2)
|
||||
plot(signalValue, "Signal", color=color.yellow, linewidth=2)
|
||||
plot(signalValue, "Signal", color=color.yellow, linewidth=2)
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Exponential Weighted MA Volatility", "EWMA Volty", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Garman-Klass Volatility (GKV)", "GKV", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("High-Low Volatility (HLV)", "HLV", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Historical Volatility (HV)", "HV", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Jurik Volatility", "Jvolty", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("JVOLTYN - Normalized Jurik Volatility", shorttitle="JVOLTYN", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Mass Index (MASSI)", "MASSI", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Normalized Average True Range", "NATR", overlay=false, format=format.percent, precision=2)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Rogers-Satchell Volatility (RSV)", "RSV", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Realized Volatility (RV)", "RV", overlay=false)
|
||||
|
||||
@@ -108,12 +108,35 @@ public class RviTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithTBar_UsesClosePrice()
|
||||
public void Update_WithTBar_UsesHighAndLow()
|
||||
{
|
||||
var rvi = new Rvi();
|
||||
var bar = new TBar(DateTime.UtcNow, 98, 102, 97, 100, 1000);
|
||||
var result = rvi.Update(bar);
|
||||
Assert.Equal(50.0, result.Value, Tolerance); // First value
|
||||
Assert.Equal(50.0, result.Value, Tolerance); // First value is always neutral
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithTBar_RevisedDiffersFromOriginal()
|
||||
{
|
||||
// The revised RVI (high+low avg) should differ from original (close-only)
|
||||
// Use oscillating close with asymmetric high/low
|
||||
var rviBar = new Rvi(stdevLength: 5, rmaLength: 5);
|
||||
var rviClose = new Rvi(stdevLength: 5, rmaLength: 5);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var time = DateTime.UtcNow.AddSeconds(i);
|
||||
double close = 100.0 + Math.Sin(i * 0.5) * 3.0; // oscillating
|
||||
double high = close + 2.0 + Math.Sin(i * 0.3) * 1.5; // asymmetric highs
|
||||
double low = close - 1.0 - Math.Cos(i * 0.7) * 0.8; // asymmetric lows
|
||||
|
||||
rviBar.Update(new TBar(time, close - 0.5, high, low, close, 1000));
|
||||
rviClose.Update(new TValue(time, close));
|
||||
}
|
||||
|
||||
// With asymmetric high/low, revised RVI should differ from close-only
|
||||
Assert.NotEqual(rviBar.Last.Value, rviClose.Last.Value, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+174
-251
@@ -1,5 +1,5 @@
|
||||
// Relative Volatility Index (RVI) Indicator
|
||||
// Measures the direction of volatility using standard deviation and RMA smoothing
|
||||
// Relative Volatility Index (RVI) Indicator — Revised (1995) version
|
||||
// Averages original RVI computed on High and Low series separately
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -7,29 +7,23 @@ using System.Runtime.InteropServices;
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// RVI: Relative Volatility Index
|
||||
/// Measures the direction of volatility by comparing upward and downward price movements
|
||||
/// weighted by their standard deviations, smoothed with Wilder's RMA.
|
||||
/// RVI: Relative Volatility Index (Revised)
|
||||
/// Computes original RVI on the High series and on the Low series, then averages.
|
||||
/// Each channel classifies stddev direction based on its own price change.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Calculation steps:</b>
|
||||
/// <b>Calculation steps (per channel — High and Low independently):</b>
|
||||
/// <list type="number">
|
||||
/// <item>Calculate population standard deviation of prices over stdevLength</item>
|
||||
/// <item>Calculate population standard deviation over stdevLength</item>
|
||||
/// <item>Classify by price change: if up, upStd = stddev; if down, downStd = stddev</item>
|
||||
/// <item>Smooth upStd and downStd with RMA (Wilder's smoothing with bias correction)</item>
|
||||
/// <item>RVI = 100 × avgUpStd / (avgUpStd + avgDownStd)</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Key characteristics:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>Oscillator ranging from 0 to 100</item>
|
||||
/// <item>Values above 50 indicate upward volatility momentum</item>
|
||||
/// <item>Values below 50 indicate downward volatility momentum</item>
|
||||
/// <item>Often used to confirm RSI signals or as a standalone indicator</item>
|
||||
/// <item>channelRVI = 100 × avgUpStd / (avgUpStd + avgDownStd)</item>
|
||||
/// </list>
|
||||
/// <b>Final:</b> RVI = (RVI_high + RVI_low) / 2
|
||||
///
|
||||
/// <b>Sources:</b>
|
||||
/// Donald Dorsey (1993). "The Relative Volatility Index". Technical Analysis of Stocks & Commodities.
|
||||
/// Donald Dorsey (1993, original; 1995, revised). Technical Analysis of Stocks & Commodities.
|
||||
/// FM Labs: https://www.fmlabs.com/reference/RVI.htm
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Rvi : AbstractBase
|
||||
@@ -39,10 +33,11 @@ public sealed class Rvi : AbstractBase
|
||||
private readonly int _stdevLength;
|
||||
private readonly int _rmaLength;
|
||||
private readonly double _alpha;
|
||||
private readonly RingBuffer _priceBuffer;
|
||||
private readonly RingBuffer _hiBuf;
|
||||
private readonly RingBuffer _loBuf;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
private record struct ChState(
|
||||
double PrevPrice,
|
||||
double Sum,
|
||||
double SumSq,
|
||||
@@ -50,46 +45,34 @@ public sealed class Rvi : AbstractBase
|
||||
double EUp,
|
||||
double RawRmaDown,
|
||||
double EDown,
|
||||
double LastValue,
|
||||
int FillCount
|
||||
);
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Rvi class.
|
||||
/// </summary>
|
||||
/// <param name="stdevLength">The lookback period for standard deviation calculation (default 10).</param>
|
||||
/// <param name="rmaLength">The lookback period for RMA smoothing (default 14).</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when stdevLength is less than 2, or rmaLength is less than 1.
|
||||
/// </exception>
|
||||
private ChState _hi, _phi;
|
||||
private ChState _lo, _plo;
|
||||
private double _lastValue, _pLastValue;
|
||||
|
||||
public Rvi(int stdevLength = 10, int rmaLength = 14)
|
||||
{
|
||||
if (stdevLength < 2)
|
||||
{
|
||||
throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength));
|
||||
}
|
||||
if (rmaLength < 1)
|
||||
{
|
||||
throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength));
|
||||
}
|
||||
|
||||
_stdevLength = stdevLength;
|
||||
_rmaLength = rmaLength;
|
||||
_alpha = 1.0 / rmaLength;
|
||||
_priceBuffer = new RingBuffer(stdevLength);
|
||||
_hiBuf = new RingBuffer(stdevLength);
|
||||
_loBuf = new RingBuffer(stdevLength);
|
||||
WarmupPeriod = stdevLength + rmaLength;
|
||||
Name = $"Rvi({stdevLength},{rmaLength})";
|
||||
_s = new State(double.NaN, 0, 0, 0, 1.0, 0, 1.0, 50.0, 0);
|
||||
_ps = _s;
|
||||
|
||||
var init = new ChState(double.NaN, 0, 0, 0, 1.0, 0, 1.0, 0);
|
||||
_hi = _phi = init;
|
||||
_lo = _plo = init;
|
||||
_lastValue = _pLastValue = 50.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Rvi class with a source.
|
||||
/// </summary>
|
||||
/// <param name="source">The data source for chaining.</param>
|
||||
/// <param name="stdevLength">The lookback period for standard deviation calculation (default 10).</param>
|
||||
/// <param name="rmaLength">The lookback period for RMA smoothing (default 14).</param>
|
||||
public Rvi(ITValuePublisher source, int stdevLength = 10, int rmaLength = 14)
|
||||
: this(stdevLength, rmaLength)
|
||||
{
|
||||
@@ -98,56 +81,27 @@ public sealed class Rvi : AbstractBase
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has enough data for valid results.
|
||||
/// </summary>
|
||||
public override bool IsHot => _s.FillCount >= _stdevLength;
|
||||
public override bool IsHot => _hi.FillCount >= _stdevLength;
|
||||
|
||||
/// <summary>
|
||||
/// The lookback period for standard deviation calculation.
|
||||
/// </summary>
|
||||
public int StdevLength => _stdevLength;
|
||||
|
||||
/// <summary>
|
||||
/// The lookback period for RMA smoothing.
|
||||
/// </summary>
|
||||
public int RmaLength => _rmaLength;
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a new price value.
|
||||
/// </summary>
|
||||
/// <param name="input">The input price value.</param>
|
||||
/// <param name="isNew">Whether this is a new bar or an update.</param>
|
||||
/// <returns>The calculated RVI value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(input.Time, input.Value, isNew);
|
||||
return UpdateCore(input.Time, input.Value, input.Value, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a new bar (uses Close price).
|
||||
/// </summary>
|
||||
/// <param name="bar">The input bar.</param>
|
||||
/// <param name="isNew">Whether this is a new bar or an update.</param>
|
||||
/// <returns>The calculated RVI value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar bar, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(bar.Time, bar.Close, isNew);
|
||||
return UpdateCore(bar.Time, bar.High, bar.Low, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a bar series.
|
||||
/// </summary>
|
||||
/// <param name="source">The source bar series.</param>
|
||||
/// <returns>A TSeries containing the RVI values.</returns>
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
@@ -158,31 +112,29 @@ public sealed class Rvi : AbstractBase
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
// Extract close prices
|
||||
Span<double> closes = len <= 128 ? stackalloc double[len] : new double[len];
|
||||
Span<double> highs = len <= 128 ? stackalloc double[len] : new double[len];
|
||||
Span<double> lows = len <= 128 ? stackalloc double[len] : new double[len];
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
closes[i] = source[i].Close;
|
||||
highs[i] = source[i].High;
|
||||
lows[i] = source[i].Low;
|
||||
tSpan[i] = source[i].Time;
|
||||
}
|
||||
|
||||
Batch(closes, vSpan, _stdevLength, _rmaLength);
|
||||
BatchDual(highs, lows, vSpan, _stdevLength, _rmaLength);
|
||||
|
||||
// Update internal state
|
||||
// Sync internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source[i].Time, source[i].Close), isNew: true);
|
||||
}
|
||||
Update(source[i], isNew: true);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
@@ -193,49 +145,64 @@ public sealed class Rvi : AbstractBase
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
// Single-price series: same value to both channels
|
||||
Batch(source.Values, vSpan, _stdevLength, _rmaLength);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Update internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private TValue UpdateCore(long timeTicks, double price, bool isNew)
|
||||
private TValue UpdateCore(long timeTicks, double hiPrice, double loPrice, bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
_priceBuffer.Snapshot();
|
||||
_phi = _hi;
|
||||
_plo = _lo;
|
||||
_pLastValue = _lastValue;
|
||||
_hiBuf.Snapshot();
|
||||
_loBuf.Snapshot();
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
_priceBuffer.Restore();
|
||||
_hi = _phi;
|
||||
_lo = _plo;
|
||||
_lastValue = _pLastValue;
|
||||
_hiBuf.Restore();
|
||||
_loBuf.Restore();
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
// Handle non-finite price
|
||||
if (!double.IsFinite(price))
|
||||
// Handle non-finite
|
||||
if (!double.IsFinite(hiPrice) || !double.IsFinite(loPrice))
|
||||
{
|
||||
Last = new TValue(timeTicks, s.LastValue);
|
||||
Last = new TValue(timeTicks, _lastValue);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
double rviValue;
|
||||
double rviHi = UpdateChannel(ref _hi, _hiBuf, hiPrice);
|
||||
double rviLo = UpdateChannel(ref _lo, _loBuf, loPrice);
|
||||
double rviValue = (rviHi + rviLo) * 0.5;
|
||||
|
||||
// Need previous price for direction
|
||||
if (!double.IsFinite(rviValue))
|
||||
rviValue = _lastValue;
|
||||
else
|
||||
_lastValue = rviValue;
|
||||
|
||||
Last = new TValue(timeTicks, rviValue);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double UpdateChannel(ref ChState s, RingBuffer buf, double price)
|
||||
{
|
||||
if (double.IsNaN(s.PrevPrice))
|
||||
{
|
||||
// First price - add to buffer but no RVI yet
|
||||
_priceBuffer.Add(price);
|
||||
buf.Add(price);
|
||||
s = s with
|
||||
{
|
||||
PrevPrice = price,
|
||||
@@ -243,137 +210,87 @@ public sealed class Rvi : AbstractBase
|
||||
SumSq = price * price,
|
||||
FillCount = 1
|
||||
};
|
||||
rviValue = 50.0; // Neutral
|
||||
return 50.0;
|
||||
}
|
||||
else
|
||||
|
||||
double priceChange = price - s.PrevPrice;
|
||||
|
||||
double oldSum = s.Sum;
|
||||
double oldSumSq = s.SumSq;
|
||||
int oldCount = s.FillCount;
|
||||
|
||||
if (buf.Count == _stdevLength)
|
||||
{
|
||||
// Calculate price change direction
|
||||
double priceChange = price - s.PrevPrice;
|
||||
|
||||
// Update price buffer for stddev calculation
|
||||
double oldSum = s.Sum;
|
||||
double oldSumSq = s.SumSq;
|
||||
int oldCount = s.FillCount;
|
||||
|
||||
// Remove oldest if buffer full
|
||||
if (_priceBuffer.Count == _stdevLength)
|
||||
{
|
||||
double oldest = _priceBuffer[0];
|
||||
oldSum -= oldest;
|
||||
oldSumSq -= oldest * oldest;
|
||||
oldCount--;
|
||||
}
|
||||
|
||||
// Add new price
|
||||
_priceBuffer.Add(price);
|
||||
double newSum = oldSum + price;
|
||||
double newSumSq = oldSumSq + (price * price);
|
||||
int newCount = oldCount + 1;
|
||||
|
||||
// Calculate population stddev
|
||||
double currentStdDev = 0.0;
|
||||
if (newCount > 1)
|
||||
{
|
||||
double mean = newSum / newCount;
|
||||
double variance = (newSumSq / newCount) - (mean * mean);
|
||||
variance = Math.Max(0.0, variance);
|
||||
currentStdDev = Math.Sqrt(variance);
|
||||
}
|
||||
|
||||
// Classify stddev by direction
|
||||
double upStdVal = 0.0;
|
||||
double downStdVal = 0.0;
|
||||
|
||||
if (priceChange > 0)
|
||||
{
|
||||
upStdVal = currentStdDev;
|
||||
}
|
||||
else if (priceChange < 0)
|
||||
{
|
||||
downStdVal = currentStdDev;
|
||||
}
|
||||
// If priceChange == 0, both stay 0
|
||||
|
||||
// RMA with bias correction for upward stddev
|
||||
double rawRmaUp = s.RawRmaUp;
|
||||
double eUp = s.EUp;
|
||||
|
||||
rawRmaUp = Math.FusedMultiplyAdd(rawRmaUp, _rmaLength - 1, upStdVal) / _rmaLength;
|
||||
eUp = (1 - _alpha) * eUp;
|
||||
double avgUpStd = eUp > Epsilon ? rawRmaUp / (1.0 - eUp) : rawRmaUp;
|
||||
|
||||
// RMA with bias correction for downward stddev
|
||||
double rawRmaDown = s.RawRmaDown;
|
||||
double eDown = s.EDown;
|
||||
|
||||
rawRmaDown = Math.FusedMultiplyAdd(rawRmaDown, _rmaLength - 1, downStdVal) / _rmaLength;
|
||||
eDown = (1 - _alpha) * eDown;
|
||||
double avgDownStd = eDown > Epsilon ? rawRmaDown / (1.0 - eDown) : rawRmaDown;
|
||||
|
||||
// Calculate RVI
|
||||
double sumAvgStd = avgUpStd + avgDownStd;
|
||||
rviValue = sumAvgStd > Epsilon ? (100.0 * avgUpStd / sumAvgStd) : 50.0;
|
||||
|
||||
s = s with
|
||||
{
|
||||
PrevPrice = price,
|
||||
Sum = newSum,
|
||||
SumSq = newSumSq,
|
||||
RawRmaUp = rawRmaUp,
|
||||
EUp = eUp,
|
||||
RawRmaDown = rawRmaDown,
|
||||
EDown = eDown,
|
||||
FillCount = newCount
|
||||
};
|
||||
double oldest = buf[0];
|
||||
oldSum -= oldest;
|
||||
oldSumSq -= oldest * oldest;
|
||||
oldCount--;
|
||||
}
|
||||
|
||||
if (!double.IsFinite(rviValue))
|
||||
buf.Add(price);
|
||||
double newSum = oldSum + price;
|
||||
double newSumSq = oldSumSq + (price * price);
|
||||
int newCount = oldCount + 1;
|
||||
|
||||
double currentStdDev = 0.0;
|
||||
if (newCount > 1)
|
||||
{
|
||||
rviValue = s.LastValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
s = s with { LastValue = rviValue };
|
||||
double mean = newSum / newCount;
|
||||
double variance = (newSumSq / newCount) - (mean * mean);
|
||||
variance = Math.Max(0.0, variance);
|
||||
currentStdDev = Math.Sqrt(variance);
|
||||
}
|
||||
|
||||
_s = s;
|
||||
double upStdVal = 0.0;
|
||||
double downStdVal = 0.0;
|
||||
if (priceChange > 0)
|
||||
upStdVal = currentStdDev;
|
||||
else if (priceChange < 0)
|
||||
downStdVal = currentStdDev;
|
||||
|
||||
Last = new TValue(timeTicks, rviValue);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
double rawRmaUp = Math.FusedMultiplyAdd(s.RawRmaUp, _rmaLength - 1, upStdVal) / _rmaLength;
|
||||
double eUp = (1 - _alpha) * s.EUp;
|
||||
double avgUpStd = eUp > Epsilon ? rawRmaUp / (1.0 - eUp) : rawRmaUp;
|
||||
|
||||
double rawRmaDown = Math.FusedMultiplyAdd(s.RawRmaDown, _rmaLength - 1, downStdVal) / _rmaLength;
|
||||
double eDown = (1 - _alpha) * s.EDown;
|
||||
double avgDownStd = eDown > Epsilon ? rawRmaDown / (1.0 - eDown) : rawRmaDown;
|
||||
|
||||
double sumAvgStd = avgUpStd + avgDownStd;
|
||||
double rvi = sumAvgStd > Epsilon ? (100.0 * avgUpStd / sumAvgStd) : 50.0;
|
||||
|
||||
s = new ChState(price, newSum, newSumSq, rawRmaUp, eUp, rawRmaDown, eDown, newCount);
|
||||
return rvi;
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, source[i]), isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_s = new State(double.NaN, 0, 0, 0, 1.0, 0, 1.0, 50.0, 0);
|
||||
_ps = _s;
|
||||
_priceBuffer.Clear();
|
||||
var init = new ChState(double.NaN, 0, 0, 0, 1.0, 0, 1.0, 0);
|
||||
_hi = _phi = init;
|
||||
_lo = _plo = init;
|
||||
_lastValue = _pLastValue = 50.0;
|
||||
_hiBuf.Clear();
|
||||
_loBuf.Clear();
|
||||
Last = default;
|
||||
}
|
||||
|
||||
// --- Static Batch methods ---
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Relative Volatility Index for a price series (static).
|
||||
/// Batch RVI for a single-price series (same value to both channels → original behavior).
|
||||
/// </summary>
|
||||
/// <param name="source">The source price series.</param>
|
||||
/// <param name="stdevLength">The lookback period for standard deviation.</param>
|
||||
/// <param name="rmaLength">The lookback period for RMA smoothing.</param>
|
||||
/// <returns>A TSeries containing the RVI values.</returns>
|
||||
public static TSeries Batch(TSeries source, int stdevLength = 10, int rmaLength = 14)
|
||||
{
|
||||
if (stdevLength < 2)
|
||||
{
|
||||
throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength));
|
||||
}
|
||||
if (rmaLength < 1)
|
||||
{
|
||||
throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength));
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
@@ -381,17 +298,14 @@ public sealed class Rvi : AbstractBase
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Batch(source.Values, vSpan, stdevLength, rmaLength);
|
||||
source.Times.CopyTo(tSpan);
|
||||
Batch(source.Values, CollectionsMarshal.AsSpan(v), stdevLength, rmaLength);
|
||||
source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates RVI for a bar series (static).
|
||||
/// Batch RVI for a bar series (revised: high+low average).
|
||||
/// </summary>
|
||||
public static TSeries Batch(TBarSeries source, int stdevLength = 10, int rmaLength = 14)
|
||||
{
|
||||
@@ -400,12 +314,8 @@ public sealed class Rvi : AbstractBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using spans.
|
||||
/// Span-based batch for single-price series. Same price to both channels → original behavior.
|
||||
/// </summary>
|
||||
/// <param name="prices">Price values.</param>
|
||||
/// <param name="output">Output RVI values.</param>
|
||||
/// <param name="stdevLength">The lookback period for standard deviation.</param>
|
||||
/// <param name="rmaLength">The lookback period for RMA smoothing.</param>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> prices,
|
||||
Span<double> output,
|
||||
@@ -413,27 +323,64 @@ public sealed class Rvi : AbstractBase
|
||||
int rmaLength = 14)
|
||||
{
|
||||
if (stdevLength < 2)
|
||||
{
|
||||
throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength));
|
||||
}
|
||||
if (rmaLength < 1)
|
||||
{
|
||||
throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength));
|
||||
}
|
||||
if (output.Length < prices.Length)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as prices span", nameof(output));
|
||||
}
|
||||
|
||||
// Single-price: feed same data to both channels, average = original
|
||||
BatchDual(prices, prices, output, stdevLength, rmaLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Span-based batch for dual-channel (high + low) revised RVI.
|
||||
/// </summary>
|
||||
public static void BatchDual(
|
||||
ReadOnlySpan<double> highs,
|
||||
ReadOnlySpan<double> lows,
|
||||
Span<double> output,
|
||||
int stdevLength = 10,
|
||||
int rmaLength = 14)
|
||||
{
|
||||
if (stdevLength < 2)
|
||||
throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength));
|
||||
if (rmaLength < 1)
|
||||
throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength));
|
||||
|
||||
int len = highs.Length;
|
||||
if (len == 0)
|
||||
return;
|
||||
if (output.Length < len)
|
||||
throw new ArgumentException("Output span must be at least as long as input span", nameof(output));
|
||||
|
||||
// Allocate temp buffers for each channel's RVI output
|
||||
Span<double> rviHi = len <= 256 ? stackalloc double[len] : new double[len];
|
||||
Span<double> rviLo = len <= 256 ? stackalloc double[len] : new double[len];
|
||||
|
||||
BatchSingleChannel(highs, rviHi, stdevLength, rmaLength);
|
||||
BatchSingleChannel(lows, rviLo, stdevLength, rmaLength);
|
||||
|
||||
// Average
|
||||
for (int i = 0; i < len; i++)
|
||||
output[i] = (rviHi[i] + rviLo[i]) * 0.5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes original (single-channel) RVI for one price series.
|
||||
/// </summary>
|
||||
private static void BatchSingleChannel(
|
||||
ReadOnlySpan<double> prices,
|
||||
Span<double> output,
|
||||
int stdevLength,
|
||||
int rmaLength)
|
||||
{
|
||||
int len = prices.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double alpha = 1.0 / rmaLength;
|
||||
|
||||
// Price buffer for stddev
|
||||
Span<double> priceBuffer = stdevLength <= 256 ? stackalloc double[stdevLength] : new double[stdevLength];
|
||||
int head = 0;
|
||||
int count = 0;
|
||||
@@ -442,7 +389,6 @@ public sealed class Rvi : AbstractBase
|
||||
double prevPrice = double.NaN;
|
||||
double lastValue = 50.0;
|
||||
|
||||
// RMA state
|
||||
double rawRmaUp = 0;
|
||||
double eUp = 1.0;
|
||||
double rawRmaDown = 0;
|
||||
@@ -452,21 +398,16 @@ public sealed class Rvi : AbstractBase
|
||||
{
|
||||
double price = prices[i];
|
||||
|
||||
// First price
|
||||
if (double.IsNaN(prevPrice))
|
||||
{
|
||||
// Handle invalid first price - output neutral and continue
|
||||
if (!double.IsFinite(price))
|
||||
{
|
||||
output[i] = lastValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to buffer
|
||||
if (count < stdevLength)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
double oldest = priceBuffer[head];
|
||||
@@ -483,22 +424,17 @@ public sealed class Rvi : AbstractBase
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle invalid price
|
||||
if (!double.IsFinite(price))
|
||||
{
|
||||
output[i] = lastValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Price change direction
|
||||
double priceChange = price - prevPrice;
|
||||
prevPrice = price;
|
||||
|
||||
// Update buffer
|
||||
if (count < stdevLength)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
double oldest = priceBuffer[head];
|
||||
@@ -510,7 +446,6 @@ public sealed class Rvi : AbstractBase
|
||||
sum += price;
|
||||
sumSq += price * price;
|
||||
|
||||
// Population stddev
|
||||
double currentStdDev = 0.0;
|
||||
if (count > 1)
|
||||
{
|
||||
@@ -520,19 +455,13 @@ public sealed class Rvi : AbstractBase
|
||||
currentStdDev = Math.Sqrt(variance);
|
||||
}
|
||||
|
||||
// Classify by direction
|
||||
double upStdVal = 0.0;
|
||||
double downStdVal = 0.0;
|
||||
if (priceChange > 0)
|
||||
{
|
||||
upStdVal = currentStdDev;
|
||||
}
|
||||
else if (priceChange < 0)
|
||||
{
|
||||
downStdVal = currentStdDev;
|
||||
}
|
||||
|
||||
// RMA with bias correction
|
||||
rawRmaUp = Math.FusedMultiplyAdd(rawRmaUp, rmaLength - 1, upStdVal) / rmaLength;
|
||||
eUp = (1 - alpha) * eUp;
|
||||
double avgUpStd = eUp > Epsilon ? rawRmaUp / (1.0 - eUp) : rawRmaUp;
|
||||
@@ -541,18 +470,13 @@ public sealed class Rvi : AbstractBase
|
||||
eDown = (1 - alpha) * eDown;
|
||||
double avgDownStd = eDown > Epsilon ? rawRmaDown / (1.0 - eDown) : rawRmaDown;
|
||||
|
||||
// RVI
|
||||
double sumAvgStd = avgUpStd + avgDownStd;
|
||||
double rviValue = sumAvgStd > Epsilon ? (100.0 * avgUpStd / sumAvgStd) : 50.0;
|
||||
|
||||
if (!double.IsFinite(rviValue))
|
||||
{
|
||||
rviValue = lastValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastValue = rviValue;
|
||||
}
|
||||
|
||||
output[i] = rviValue;
|
||||
}
|
||||
@@ -564,5 +488,4 @@ public sealed class Rvi : AbstractBase
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+40
-15
@@ -3,7 +3,7 @@
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Volatility |
|
||||
| **Inputs** | OHLCV bar (TBar) |
|
||||
| **Inputs** | OHLCV bar (TBar) or single price (TValue) |
|
||||
| **Parameters** | `stdevLength` (default 10), `rmaLength` (default 14) |
|
||||
| **Outputs** | Single series (Rvi) |
|
||||
| **Output range** | $0$ to $100$ |
|
||||
@@ -11,15 +11,16 @@
|
||||
|
||||
### TL;DR
|
||||
|
||||
- The Relative Volatility Index (RVI) is a directional volatility oscillator that distinguishes between upward and downward price volatility.
|
||||
- Parameterized by `stdevlength` (default 10), `rmalength` (default 14).
|
||||
- The Relative Volatility Index (RVI) implements Dorsey's **revised (1995)** version: computes original RVI separately on High and Low series, then averages.
|
||||
- When fed single-price data (TValue), both channels receive the same value, reducing to the original (1993) formula.
|
||||
- Parameterized by `stdevLength` (default 10), `rmaLength` (default 14).
|
||||
- Output range: $0$ to $100$.
|
||||
- Requires 1 bar of warmup before first valid output (IsHot = true).
|
||||
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
|
||||
- Requires `stdevLength` bars of warmup before first valid output (IsHot = true).
|
||||
- Validated against FM Labs revised RVI specification.
|
||||
|
||||
> "Not all volatility is created equal—upward volatility feels like profit, downward volatility feels like loss. RVI separates these psychological experiences into a quantifiable measure."
|
||||
|
||||
The Relative Volatility Index (RVI) is a directional volatility oscillator that distinguishes between upward and downward price volatility. Originally developed by Donald Dorsey in 1993, RVI measures the standard deviation of closing prices and categorizes this volatility based on whether prices are rising or falling. The result is an oscillator bounded between 0 and 100, where values above 50 indicate upward volatility dominance and values below 50 indicate downward volatility dominance.
|
||||
The Relative Volatility Index (RVI) is a directional volatility oscillator that distinguishes between upward and downward price volatility. Originally developed by Donald Dorsey in 1993 using close prices only, RVI was **revised in 1995** to compute separate RVI values on the High and Low price series and average them. This implementation follows the revised version: when fed OHLCV bars (TBar), it runs independent RVI channels on High and Low; when fed single prices (TValue), both channels receive the same value, reducing to the original formula.
|
||||
|
||||
## Historical Context
|
||||
|
||||
@@ -27,13 +28,29 @@ Donald Dorsey introduced the Relative Volatility Index in the June 1993 issue of
|
||||
|
||||
The key innovation was separating volatility into directional components. Traditional volatility measures (standard deviation, ATR) treat upward and downward price movements identically. Dorsey recognized that traders experience these movements differently: upward volatility in a long position feels like opportunity, while downward volatility feels like risk.
|
||||
|
||||
The original 1993 formula used a 10-period standard deviation and 14-period Wilder's smoothing (RMA). This implementation follows the PineScript reference which uses bias-corrected RMA to ensure proper warmup behavior during the initial periods.
|
||||
The original 1993 formula used a 10-period standard deviation of closing prices with 14-period Wilder's smoothing (RMA). In 1995, Dorsey revised the formula to average RVI computed independently on the High and Low series, capturing volatility structure across the full price range rather than just closes.
|
||||
|
||||
FM Labs documents both versions: the original (close-only) and the revised (high+low average). This implementation follows the **revised** version with bias-corrected RMA for proper warmup behavior.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Rolling Population Standard Deviation
|
||||
### 1. Dual-Channel Architecture (Revised 1995)
|
||||
|
||||
First, compute the population standard deviation of closing prices over `stdevLength` periods:
|
||||
The revised RVI computes the original RVI algorithm independently on two channels:
|
||||
- **High channel:** uses bar High prices
|
||||
- **Low channel:** uses bar Low prices
|
||||
|
||||
The final RVI is their average:
|
||||
|
||||
$$
|
||||
\text{RVI}_{\text{revised}} = \frac{\text{RVI}_{\text{high}} + \text{RVI}_{\text{low}}}{2}
|
||||
$$
|
||||
|
||||
When fed single prices (TValue), both channels receive the same value: $\text{RVI} = \frac{\text{RVI}_p + \text{RVI}_p}{2} = \text{RVI}_p$ (original behavior).
|
||||
|
||||
### 2. Per-Channel: Rolling Population Standard Deviation
|
||||
|
||||
For each channel, compute the population standard deviation over `stdevLength` periods:
|
||||
|
||||
$$
|
||||
\sigma_t = \sqrt{\frac{\sum_{i=0}^{n-1}(P_{t-i} - \bar{P})^2}{n}}
|
||||
@@ -51,7 +68,7 @@ $$
|
||||
\sigma_t = \sqrt{\frac{\sum P_i^2}{n} - \left(\frac{\sum P_i}{n}\right)^2}
|
||||
$$
|
||||
|
||||
### 2. Directional Classification
|
||||
### 3. Directional Classification
|
||||
|
||||
Based on price change direction, assign the volatility to either upward or downward:
|
||||
|
||||
@@ -71,7 +88,7 @@ $$
|
||||
|
||||
Note: When $P_t = P_{t-1}$ (unchanged), both upStd and downStd are zero. The volatility is "orphaned" rather than assigned to either direction.
|
||||
|
||||
### 3. Bias-Corrected RMA Smoothing
|
||||
### 4. Bias-Corrected RMA Smoothing
|
||||
|
||||
Both directional volatilities are smoothed using Wilder's RMA (Exponential Moving Average with $\alpha = 1/n$) with bias correction for proper warmup:
|
||||
|
||||
@@ -100,15 +117,21 @@ where $\alpha = 1/\text{rmaLength}$ and $\epsilon = 10^{-10}$.
|
||||
|
||||
This bias correction compensates for the zero initialization of raw RMA, preventing artificially low values during warmup.
|
||||
|
||||
### 4. Final RVI Calculation
|
||||
### 5. Per-Channel RVI
|
||||
|
||||
$$
|
||||
\text{RVI}_t = \begin{cases}
|
||||
\text{RVI}_{\text{channel}} = \begin{cases}
|
||||
100 \times \frac{\text{avgUpStd}_t}{\text{avgUpStd}_t + \text{avgDownStd}_t} & \text{if sum} > 0 \\
|
||||
50 & \text{otherwise}
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
### 6. Final Revised RVI
|
||||
|
||||
$$
|
||||
\text{RVI}_t = \frac{\text{RVI}_{\text{high}} + \text{RVI}_{\text{low}}}{2}
|
||||
$$
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Relationship to RSI
|
||||
@@ -192,13 +215,14 @@ Dominant cost: five divisions (63%) for variance calculation and RMA updates.
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **FM Labs** | ✅ | Matches revised (1995) dual-channel specification |
|
||||
| **TA-Lib** | N/A | Not implemented |
|
||||
| **Skender** | N/A | Not implemented |
|
||||
| **Tulip** | N/A | Not implemented |
|
||||
| **OoplesFinance** | ❔ | Different algorithm (RSI-based) |
|
||||
| **PineScript** | ✅ | Matches rvi.pine reference |
|
||||
| **PineScript** | ✅ | Matches rvi.pine reference (original algorithm per channel) |
|
||||
|
||||
Note: Some libraries implement "RVI" as a different indicator (often RSI applied to volatility). This implementation follows Dorsey's original design using directional standard deviation.
|
||||
Note: Some libraries implement "RVI" as a different indicator (often RSI applied to volatility). FM Labs distinguishes between original (1993, close-only) and revised (1995, high+low average). This implementation follows the **revised** version.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
@@ -261,4 +285,5 @@ Price making lower lows + RVI making higher lows: Bullish divergence
|
||||
|
||||
- Dorsey, D. (1993). "The Relative Volatility Index." *Technical Analysis of Stocks & Commodities*, 11(6), 253-256.
|
||||
- Dorsey, D. (1995). "Refining the Relative Volatility Index." *Technical Analysis of Stocks & Commodities*, 13(9).
|
||||
- FM Labs. "Relative Volatility Index." https://www.fmlabs.com/reference/RVI.htm (Original vs Revised versions).
|
||||
- TradingView. (2024). "PineScript Reference Implementation." rvi.pine source file.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Relative Volatility Index (RVI)", shorttitle="RVI", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("True Range", "TR", overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ulcer Index (UI)", shorttitle="UI", format=format.price, precision=2, overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Volatility of Volatility (VOV)", shorttitle="VOV", format=format.price, precision=4, overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Volatility Ratio (VR)", shorttitle="VR", format=format.price, precision=2, overlay=false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The MIT License (MIT)
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Yang-Zhang Volatility (YZV)", shorttitle="YZV", overlay=false)
|
||||
|
||||
Reference in New Issue
Block a user