mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +00:00
Add TRAMA implementation and comprehensive tests
- Implemented the TRAMA (Trend Regularity Adaptive Moving Average) class with adaptive EMA logic. - Added unit tests for TRAMA functionality, including constructor validation, basic calculations, state management, and robustness checks. - Created validation tests to ensure consistency across different modes of operation (streaming, batch, and static calculations). - Enhanced documentation for TRAMA, including performance profiles and quality metrics. - Updated workspace configuration by removing unnecessary folder references.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# RAIN: Rainbow Moving Average
|
||||
|
||||
> "Mel Widner applied SMA ten times recursively, then weighted the layers like a rainbow: brightest at the top, fading toward the base. Ten colors of smoothing, one composite average that sees both fast and slow structure simultaneously."
|
||||
|
||||
RAIN recursively applies SMA 10 times, producing 10 layers of progressively smoother price representation, then computes a weighted average across all layers. Layers 1-4 receive weights 5, 4, 3, 2 (emphasizing the more responsive layers), while layers 5-10 each receive weight 1, for a total divisor of 20. This multi-scale composition produces a moving average that responds to short-term price changes through the lightly smoothed upper layers while maintaining stability through the heavily smoothed lower layers.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Mel Widner published "Rainbow Charts" in *Technical Analysis of Stocks & Commodities* (1998), introducing the concept of recursive SMA application as both a visualization technique and a composite smoothing method. The thinkorswim platform later standardized the weight vector as $[5, 4, 3, 2, 1, 1, 1, 1, 1, 1]$, which became the canonical RAIN MA.
|
||||
|
||||
The recursive SMA application has a deep mathematical interpretation: applying SMA $k$ times is equivalent to convolving the rectangular kernel with itself $k$ times, which produces a B-spline kernel of order $k$. Thus RAIN's 10 layers correspond to B-splines of orders 1 through 10, and the weighted average blends these spline approximations. The B-spline interpretation explains why higher layers are smoother: each convolution adds a degree of polynomial reproduction and reduces the spectral sidelobe level.
|
||||
|
||||
The weight vector $[5, 4, 3, 2, 1, 1, 1, 1, 1, 1]$ with sum 20 was chosen empirically rather than derived from optimization theory. The declining weights for layers 1-4 bias the output toward the more responsive layers, making RAIN track trends more closely than a uniform average of all 10 layers would.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Ten Cascaded SMA Layers
|
||||
|
||||
Each layer is an SMA applied to the previous layer's output:
|
||||
|
||||
$$
|
||||
\text{MA}_1 = \text{SMA}(x, N), \quad \text{MA}_k = \text{SMA}(\text{MA}_{k-1}, N), \quad k = 2, \ldots, 10
|
||||
$$
|
||||
|
||||
### 2. O(1) Running-Sum SMA
|
||||
|
||||
Each of the 10 SMA layers uses a circular buffer with a running sum, giving O(1) per-bar update cost per layer. Total cost: O(10) per bar, with O($10 \times N$) memory for the 10 buffers.
|
||||
|
||||
### 3. Weighted Composite
|
||||
|
||||
$$
|
||||
\text{RAIN} = \frac{5 \cdot \text{MA}_1 + 4 \cdot \text{MA}_2 + 3 \cdot \text{MA}_3 + 2 \cdot \text{MA}_4 + \sum_{k=5}^{10} \text{MA}_k}{20}
|
||||
$$
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
**Layer computation (recursive SMA):**
|
||||
|
||||
$$
|
||||
\text{MA}_1[t] = \frac{1}{N}\sum_{i=0}^{N-1} x_{t-i}
|
||||
$$
|
||||
|
||||
$$
|
||||
\text{MA}_k[t] = \frac{1}{N}\sum_{i=0}^{N-1} \text{MA}_{k-1}[t-i], \quad k = 2, \ldots, 10
|
||||
$$
|
||||
|
||||
**Equivalent kernel:** The $k$-fold SMA is the $k$-th order B-spline kernel:
|
||||
|
||||
$$
|
||||
B_k(x) = \underbrace{B_0 * B_0 * \cdots * B_0}_{k \text{ times}}(x)
|
||||
$$
|
||||
|
||||
where $B_0$ is the rectangular pulse.
|
||||
|
||||
**Weighted output:**
|
||||
|
||||
$$
|
||||
\text{RAIN} = \frac{\sum_{k=1}^{10} w_k \cdot \text{MA}_k}{20}
|
||||
$$
|
||||
|
||||
with weights $\mathbf{w} = [5, 4, 3, 2, 1, 1, 1, 1, 1, 1]$.
|
||||
|
||||
**Group delay:** Each SMA layer adds $(N-1)/2$ bars of lag. However, the weighted composite lag is:
|
||||
|
||||
$$
|
||||
\bar{d} = \frac{\sum w_k \cdot k \cdot (N-1)/2}{\sum w_k}
|
||||
$$
|
||||
|
||||
For $N = 2$: $\bar{d} \approx 1.85$ bars. The upper-layer weighting significantly reduces the effective lag below what layer 10 alone would produce.
|
||||
|
||||
**Default parameters:** `period = 2`, `fixed layers = 10`, `minPeriod = 1`.
|
||||
|
||||
**Pseudo-code (streaming):**
|
||||
|
||||
```
|
||||
// 10 circular buffers with running sums
|
||||
for layer = 1 to 10:
|
||||
sum[layer] -= buf[layer][head]
|
||||
sum[layer] += input[layer] // input is price for layer 1, MA[layer-1] for others
|
||||
buf[layer][head] = input[layer]
|
||||
MA[layer] = sum[layer] / count
|
||||
|
||||
head = (head + 1) % period
|
||||
|
||||
return (5*MA[1] + 4*MA[2] + 3*MA[3] + 2*MA[4] + MA[5] + MA[6] + MA[7] + MA[8] + MA[9] + MA[10]) / 20
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- Widner, M. (1998). "Rainbow Charts." *Technical Analysis of Stocks & Commodities*.
|
||||
- thinkorswim / TD Ameritrade. "RainbowAverage" study documentation.
|
||||
- Schoenberg, I.J. (1946). "Contributions to the Problem of Approximation of Equidistant Data by Analytic Functions." *Quarterly of Applied Mathematics*, 4(1), 45-99. (B-spline theory underlying recursive SMA.)
|
||||
@@ -0,0 +1,132 @@
|
||||
// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0
|
||||
// https://mozilla.org/MPL/2.0/
|
||||
// © QuanTAlib
|
||||
|
||||
//@version=6
|
||||
indicator("Rainbow Moving Average (RAIN)", "RAIN", overlay = true)
|
||||
|
||||
//@function Rainbow Moving Average — recursively applies SMA 10 times, then computes
|
||||
// a weighted average of all 10 layers. Layers 1–4 receive weights 5,4,3,2
|
||||
// and layers 5–10 each receive weight 1, for a total divisor of 20.
|
||||
// The recursive SMA application creates progressively smoother versions
|
||||
// of price, and the weighting scheme emphasizes the less-smoothed (more
|
||||
// responsive) layers. Developed by Mel Widner, published in Technical
|
||||
// Analysis of Stocks & Commodities (TASC), 1998.
|
||||
//@param source Series to smooth
|
||||
//@param period Lookback window for each SMA layer (>= 1)
|
||||
//@returns Weighted average of 10 recursive SMA layers
|
||||
//@reference Widner, M. (1998). "Rainbow Charts." Technical Analysis of Stocks & Commodities.
|
||||
//@reference thinkorswim RainbowAverage study documentation.
|
||||
//@optimized O(10 × period) per bar; each SMA uses circular buffer with O(1) running sum
|
||||
export rain(series float source, simple int period) =>
|
||||
if period < 1
|
||||
runtime.error("Period must be at least 1")
|
||||
|
||||
float price = nz(source)
|
||||
|
||||
// --- 10 circular buffers for 10 SMA layers ---
|
||||
var array<float> buf1 = array.new_float(period, 0.0)
|
||||
var array<float> buf2 = array.new_float(period, 0.0)
|
||||
var array<float> buf3 = array.new_float(period, 0.0)
|
||||
var array<float> buf4 = array.new_float(period, 0.0)
|
||||
var array<float> buf5 = array.new_float(period, 0.0)
|
||||
var array<float> buf6 = array.new_float(period, 0.0)
|
||||
var array<float> buf7 = array.new_float(period, 0.0)
|
||||
var array<float> buf8 = array.new_float(period, 0.0)
|
||||
var array<float> buf9 = array.new_float(period, 0.0)
|
||||
var array<float> buf10 = array.new_float(period, 0.0)
|
||||
|
||||
// --- Running sums for O(1) SMA ---
|
||||
var float sum1 = 0.0
|
||||
var float sum2 = 0.0
|
||||
var float sum3 = 0.0
|
||||
var float sum4 = 0.0
|
||||
var float sum5 = 0.0
|
||||
var float sum6 = 0.0
|
||||
var float sum7 = 0.0
|
||||
var float sum8 = 0.0
|
||||
var float sum9 = 0.0
|
||||
var float sum10 = 0.0
|
||||
|
||||
// --- Shared head pointer (all buffers same size, same cadence) ---
|
||||
var int head = 0
|
||||
int count = math.min(bar_index + 1, period)
|
||||
float n = count
|
||||
|
||||
// --- Layer 1: SMA(price, period) ---
|
||||
sum1 -= array.get(buf1, head)
|
||||
sum1 += price
|
||||
array.set(buf1, head, price)
|
||||
float ma1 = sum1 / n
|
||||
|
||||
// --- Layer 2: SMA(ma1, period) ---
|
||||
sum2 -= array.get(buf2, head)
|
||||
sum2 += ma1
|
||||
array.set(buf2, head, ma1)
|
||||
float ma2 = sum2 / n
|
||||
|
||||
// --- Layer 3: SMA(ma2, period) ---
|
||||
sum3 -= array.get(buf3, head)
|
||||
sum3 += ma2
|
||||
array.set(buf3, head, ma2)
|
||||
float ma3 = sum3 / n
|
||||
|
||||
// --- Layer 4: SMA(ma3, period) ---
|
||||
sum4 -= array.get(buf4, head)
|
||||
sum4 += ma3
|
||||
array.set(buf4, head, ma3)
|
||||
float ma4 = sum4 / n
|
||||
|
||||
// --- Layer 5: SMA(ma4, period) ---
|
||||
sum5 -= array.get(buf5, head)
|
||||
sum5 += ma4
|
||||
array.set(buf5, head, ma4)
|
||||
float ma5 = sum5 / n
|
||||
|
||||
// --- Layer 6: SMA(ma5, period) ---
|
||||
sum6 -= array.get(buf6, head)
|
||||
sum6 += ma5
|
||||
array.set(buf6, head, ma5)
|
||||
float ma6 = sum6 / n
|
||||
|
||||
// --- Layer 7: SMA(ma6, period) ---
|
||||
sum7 -= array.get(buf7, head)
|
||||
sum7 += ma6
|
||||
array.set(buf7, head, ma6)
|
||||
float ma7 = sum7 / n
|
||||
|
||||
// --- Layer 8: SMA(ma7, period) ---
|
||||
sum8 -= array.get(buf8, head)
|
||||
sum8 += ma7
|
||||
array.set(buf8, head, ma7)
|
||||
float ma8 = sum8 / n
|
||||
|
||||
// --- Layer 9: SMA(ma8, period) ---
|
||||
sum9 -= array.get(buf9, head)
|
||||
sum9 += ma8
|
||||
array.set(buf9, head, ma8)
|
||||
float ma9 = sum9 / n
|
||||
|
||||
// --- Layer 10: SMA(ma9, period) ---
|
||||
sum10 -= array.get(buf10, head)
|
||||
sum10 += ma9
|
||||
array.set(buf10, head, ma9)
|
||||
float ma10 = sum10 / n
|
||||
|
||||
// --- Advance shared head ---
|
||||
head := (head + 1) % period
|
||||
|
||||
// --- Weighted average (Widner/thinkorswim weights) ---
|
||||
// Layers 1-4: weights 5,4,3,2; Layers 5-10: weight 1 each
|
||||
// Total weight = 5 + 4 + 3 + 2 + 1 + 1 + 1 + 1 + 1 + 1 = 20
|
||||
(5.0 * ma1 + 4.0 * ma2 + 3.0 * ma3 + 2.0 * ma4 + ma5 + ma6 + ma7 + ma8 + ma9 + ma10) / 20.0
|
||||
|
||||
// ── Inputs ──
|
||||
int p_period = input.int(2, "Period", minval = 1)
|
||||
float p_src = input.source(close, "Source")
|
||||
|
||||
// ── Calculation ──
|
||||
float out = rain(p_src, p_period)
|
||||
|
||||
// ── Plot ──
|
||||
plot(out, "RAIN", color.yellow, 2)
|
||||
Reference in New Issue
Block a user