mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-21 20:18:05 +00:00
Remove multiple Pine Script indicators: SSFDSP, STARCHANNEL, STBANDS, STC, UBANDS, UCHANNEL, VWAPBANDS, and VWAPSD. These indicators were deleted to streamline the library and remove unused or redundant code.
This commit is contained in:
@@ -24,4 +24,5 @@ Finite Impulse Response (FIR) trend indicators. These use fixed-length windows w
|
||||
| [SINEMA](sinema/Sinema.md) | Sine-Weighted MA | Sine wave weighting. Smooth bell-shaped emphasis. |
|
||||
| [SMA](sma/Sma.md) | Simple MA | Equal weights. Baseline reference. Lag = (N-1)/2. |
|
||||
| [TRIMA](trima/Trima.md) | Triangular MA | Triangular weights. SMA of SMA. Emphasizes middle. |
|
||||
| [TSF](tsf/Tsf.md) | Time Series Forecast | Linear regression projected one step ahead. Extrapolates trend. |
|
||||
| [WMA](wma/Wma.md) | Weighted MA | Linear weights. Recent prices weighted more. Lag < SMA. |
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Cubic Regression Moving Average (CRMA)", "CRMA", overlay=true)
|
||||
|
||||
//@function Computes Cubic Regression Moving Average — fits a degree-3 polynomial
|
||||
// y = a0 + a1*x + a2*x² + a3*x³ to the most recent `period` bars via
|
||||
// normal equations with Gaussian elimination, returns the fitted endpoint.
|
||||
//@param source Series to analyze
|
||||
//@param period Lookback window for the cubic regression
|
||||
//@returns Fitted value at the most recent bar (x = 0)
|
||||
//@reference Polynomial least-squares regression (degree 3), evaluated at endpoint
|
||||
//@optimized O(period) per bar for accumulating sums; O(1) for 4×4 solve
|
||||
crma(series float source, simple int period) =>
|
||||
if period < 4
|
||||
runtime.error("Period must be at least 4 for cubic regression")
|
||||
|
||||
float price = nz(source)
|
||||
|
||||
// --- Circular buffer for rolling window ---
|
||||
var array<float> buffer = array.new_float(period, na)
|
||||
var int head = 0
|
||||
array.set(buffer, head, price)
|
||||
head := (head + 1) % period
|
||||
|
||||
int p = math.min(bar_index + 1, period)
|
||||
if p < 4
|
||||
price
|
||||
else
|
||||
// --- Accumulate power sums and cross-products ---
|
||||
// Normal equations for degree-3 polynomial: M * a = rhs
|
||||
// M[i][j] = Σ x^(i+j), rhs[i] = Σ x^i * y for i,j = 0..3
|
||||
// x = 0 (newest) to p-1 (oldest), so a0 = fitted value at newest bar
|
||||
float s0 = 0.0, s1 = 0.0, s2 = 0.0, s3 = 0.0
|
||||
float s4 = 0.0, s5 = 0.0, s6 = 0.0
|
||||
float r0 = 0.0, r1 = 0.0, r2 = 0.0, r3 = 0.0
|
||||
|
||||
int idx = (head - 1 + period) % period
|
||||
for i = 0 to p - 1
|
||||
float val = array.get(buffer, idx)
|
||||
float v = na(val) ? price : val
|
||||
float x = float(i)
|
||||
float x2 = x * x
|
||||
float x3 = x2 * x
|
||||
|
||||
s0 += 1.0 // Σ x^0 = count
|
||||
s1 += x // Σ x^1
|
||||
s2 += x2 // Σ x^2
|
||||
s3 += x3 // Σ x^3
|
||||
s4 += x2 * x2 // Σ x^4
|
||||
s5 += x2 * x3 // Σ x^5
|
||||
s6 += x3 * x3 // Σ x^6
|
||||
|
||||
r0 += v // Σ y
|
||||
r1 += x * v // Σ x*y
|
||||
r2 += x2 * v // Σ x²*y
|
||||
r3 += x3 * v // Σ x³*y
|
||||
|
||||
idx := (idx - 1 + period) % period
|
||||
|
||||
// --- Build 4×4 augmented matrix (row-major, 4 rows × 5 cols) ---
|
||||
var matrix<float> m = matrix.new<float>(4, 5, 0.0)
|
||||
|
||||
// Row 0: [s0, s1, s2, s3 | r0]
|
||||
matrix.set(m, 0, 0, s0), matrix.set(m, 0, 1, s1), matrix.set(m, 0, 2, s2), matrix.set(m, 0, 3, s3), matrix.set(m, 0, 4, r0)
|
||||
// Row 1: [s1, s2, s3, s4 | r1]
|
||||
matrix.set(m, 1, 0, s1), matrix.set(m, 1, 1, s2), matrix.set(m, 1, 2, s3), matrix.set(m, 1, 3, s4), matrix.set(m, 1, 4, r1)
|
||||
// Row 2: [s2, s3, s4, s5 | r2]
|
||||
matrix.set(m, 2, 0, s2), matrix.set(m, 2, 1, s3), matrix.set(m, 2, 2, s4), matrix.set(m, 2, 3, s5), matrix.set(m, 2, 4, r2)
|
||||
// Row 3: [s3, s4, s5, s6 | r3]
|
||||
matrix.set(m, 3, 0, s3), matrix.set(m, 3, 1, s4), matrix.set(m, 3, 2, s5), matrix.set(m, 3, 3, s6), matrix.set(m, 3, 4, r3)
|
||||
|
||||
// --- Gaussian elimination with partial pivoting ---
|
||||
bool singular = false
|
||||
for col = 0 to 3
|
||||
// Find pivot row
|
||||
int pivot_row = col
|
||||
float pivot_max = math.abs(matrix.get(m, col, col))
|
||||
for row = col + 1 to 3
|
||||
float absval = math.abs(matrix.get(m, row, col))
|
||||
if absval > pivot_max
|
||||
pivot_max := absval
|
||||
pivot_row := row
|
||||
|
||||
if pivot_max < 1e-12
|
||||
singular := true
|
||||
break
|
||||
|
||||
// Swap rows if needed
|
||||
if pivot_row != col
|
||||
for k = col to 4
|
||||
float tmp = matrix.get(m, col, k)
|
||||
matrix.set(m, col, k, matrix.get(m, pivot_row, k))
|
||||
matrix.set(m, pivot_row, k, tmp)
|
||||
|
||||
// Eliminate below
|
||||
float diag = matrix.get(m, col, col)
|
||||
for row = col + 1 to 3
|
||||
float factor = matrix.get(m, row, col) / diag
|
||||
for k = col to 4
|
||||
matrix.set(m, row, k, matrix.get(m, row, k) - factor * matrix.get(m, col, k))
|
||||
|
||||
float result = price
|
||||
if not singular
|
||||
// Back-substitution
|
||||
var array<float> a = array.new_float(4, 0.0)
|
||||
for row = 3 to 0
|
||||
float val = matrix.get(m, row, 4)
|
||||
for k = row + 1 to 3
|
||||
val -= matrix.get(m, row, k) * array.get(a, k)
|
||||
array.set(a, row, val / matrix.get(m, row, row))
|
||||
|
||||
// a[0] is the fitted value at x = 0 (most recent bar)
|
||||
result := array.get(a, 0)
|
||||
|
||||
result
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_period = input.int(14, "Period", minval=4)
|
||||
i_source = input.source(close, "Source")
|
||||
|
||||
// Calculation
|
||||
crma_value = crma(i_source, i_period)
|
||||
|
||||
// Plot
|
||||
plot(crma_value, "CRMA", color=color.yellow, linewidth=2)
|
||||
@@ -0,0 +1,80 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Henderson Moving Average (HEND)", "HEND", overlay=true)
|
||||
|
||||
//@function Computes Henderson Moving Average — a symmetric FIR filter that preserves
|
||||
// cubic polynomial trends without distortion, from the X-11 seasonal adjustment
|
||||
// framework. Weights are derived from the closed-form Henderson formula and
|
||||
// can be negative at the edges (bandpass-like property).
|
||||
//@param source Series to smooth
|
||||
//@param period Lookback window (must be odd >= 5)
|
||||
//@returns Henderson-weighted moving average
|
||||
//@reference Henderson, R. (1916). "Note on Graduation by Adjusted Average."
|
||||
// Transactions of the Actuarial Society of America, 17, 43-48.
|
||||
//@reference Hyndman, R.J. (2011). "Moving Averages" (International Encyclopedia of
|
||||
// Statistical Science). Springer.
|
||||
//@optimized O(period) per bar for convolution; weights precomputed once
|
||||
hend(series float source, simple int period) =>
|
||||
if period < 5
|
||||
runtime.error("Period must be at least 5 for Henderson filter")
|
||||
if period % 2 == 0
|
||||
runtime.error("Period must be odd for Henderson filter")
|
||||
|
||||
float price = nz(source)
|
||||
|
||||
// --- Circular buffer for rolling window ---
|
||||
var array<float> buffer = array.new_float(period, na)
|
||||
var int head = 0
|
||||
array.set(buffer, head, price)
|
||||
head := (head + 1) % period
|
||||
|
||||
// --- Precompute Henderson weights once ---
|
||||
// Formula: w(k) = 315 * [(n-1)^2 - k^2] * [n^2 - k^2] * [(n+1)^2 - k^2]
|
||||
// * [3*n^2 - 16 - 11*k^2]
|
||||
// / {8 * n * (n^2-1) * (4*n^2 - 1) * (4*n^2 - 9) * (4*n^2 - 25)}
|
||||
// where n = (period + 3) / 2, k ranges from -(period-1)/2 to (period-1)/2
|
||||
var array<float> weights = array.new_float(0)
|
||||
if barstate.isfirst
|
||||
int half = (period - 1) / 2
|
||||
float n = (period + 3) / 2.0
|
||||
float n2 = n * n
|
||||
float nm1_2 = (n - 1) * (n - 1)
|
||||
float np1_2 = (n + 1) * (n + 1)
|
||||
float denom = 8.0 * n * (n2 - 1) * (4 * n2 - 1) * (4 * n2 - 9) * (4 * n2 - 25)
|
||||
|
||||
float wsum = 0.0
|
||||
for k = -half to half
|
||||
float k2 = float(k * k)
|
||||
float w = 315.0 * (nm1_2 - k2) * (n2 - k2) * (np1_2 - k2) * (3 * n2 - 16 - 11 * k2) / denom
|
||||
array.push(weights, w)
|
||||
wsum += w
|
||||
|
||||
// Normalize weights to sum exactly 1.0 (handles floating-point drift)
|
||||
if wsum != 0
|
||||
for j = 0 to period - 1
|
||||
array.set(weights, j, array.get(weights, j) / wsum)
|
||||
|
||||
int count = math.min(bar_index + 1, period)
|
||||
if count < period
|
||||
price
|
||||
else
|
||||
// --- Apply Henderson convolution via circular buffer ---
|
||||
// Buffer position: head points to next-write = oldest entry
|
||||
// Weight[0] corresponds to oldest bar, Weight[period-1] to newest
|
||||
float result = 0.0
|
||||
for j = 0 to period - 1
|
||||
int idx = (head + j) % period
|
||||
float val = nz(array.get(buffer, idx))
|
||||
result += val * array.get(weights, j)
|
||||
result
|
||||
|
||||
// ── Inputs ──────────────────────────────────────────────────────────────
|
||||
src = input.source(close, "Source")
|
||||
per = input.int(7, "Period", minval=5, step=2, tooltip="Must be odd, >= 5")
|
||||
|
||||
// Enforce odd period at input level
|
||||
period_adj = per % 2 == 0 ? per + 1 : per
|
||||
|
||||
// ── Plot ────────────────────────────────────────────────────────────────
|
||||
plot(hend(src, period_adj), "HEND", color.new(color.yellow, 0), 2)
|
||||
@@ -0,0 +1,68 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Integral of Linear Regression Slope (ILRS)", "ILRS", overlay=true)
|
||||
|
||||
//@function Computes the Integral of Linear Regression Slope — cumulative sum of the
|
||||
// least-squares slope computed over a rolling window. Tracks accumulated
|
||||
// trend direction as a price-overlay smoothing filter.
|
||||
//@param source Series to analyze
|
||||
//@param period Lookback window for slope calculation (>= 2)
|
||||
//@returns Cumulative integral of the rolling linear regression slope
|
||||
//@reference John Ehlers, "Rocket Science for Traders" (Wiley, 2001).
|
||||
//@reference Concept: ILRS is the discrete integral (running sum) of the LinReg slope,
|
||||
// producing a smoother trend follower than LSMA. Equivalent to filtering
|
||||
// the first derivative and reconstructing via integration.
|
||||
//@optimized O(period) per bar for slope via circular buffer accumulation
|
||||
ilrs(series float source, simple int period) =>
|
||||
if period < 2
|
||||
runtime.error("Period must be at least 2")
|
||||
|
||||
float price = nz(source)
|
||||
|
||||
// --- Circular buffer for rolling window ---
|
||||
var array<float> buffer = array.new_float(period, na)
|
||||
var int head = 0
|
||||
array.set(buffer, head, price)
|
||||
head := (head + 1) % period
|
||||
|
||||
// --- Running integral state ---
|
||||
var float integral = na
|
||||
|
||||
int count = math.min(bar_index + 1, period)
|
||||
if count < 2
|
||||
integral := price
|
||||
integral
|
||||
else
|
||||
// --- Compute linear regression slope over the buffer ---
|
||||
// x-indices: 0, 1, ..., n-1 (oldest to newest)
|
||||
// Analytical x-sums: ΣX = n(n-1)/2, ΣX² = n(n-1)(2n-1)/6
|
||||
float n = count
|
||||
float sumX = 0.5 * (n - 1) * n
|
||||
float sumX2 = (n - 1) * n * (2 * n - 1) / 6.0
|
||||
|
||||
// Accumulate y-sums from circular buffer
|
||||
int start = count < period ? 0 : head
|
||||
float sumY = 0.0
|
||||
float sumXY = 0.0
|
||||
for i = 0 to int(n) - 1
|
||||
int idx = (start + i) % period
|
||||
float val = nz(array.get(buffer, idx))
|
||||
sumY += val
|
||||
sumXY += i * val
|
||||
|
||||
float denomX = n * sumX2 - sumX * sumX
|
||||
float slope = denomX != 0 ? (n * sumXY - sumX * sumY) / denomX : 0.0
|
||||
|
||||
// --- Integrate: ILRS = ILRS[1] + slope ---
|
||||
if na(integral)
|
||||
integral := price
|
||||
integral := integral + slope
|
||||
integral
|
||||
|
||||
// ── Inputs ──────────────────────────────────────────────────────────────
|
||||
src = input.source(close, "Source")
|
||||
per = input.int(14, "Period", minval=2)
|
||||
|
||||
// ── Plot ────────────────────────────────────────────────────────────────
|
||||
plot(ilrs(src, per), "ILRS", color.new(color.yellow, 0), 2)
|
||||
@@ -0,0 +1,85 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Kaiser Window Moving Average (KAISER)", "KAISER", overlay=true)
|
||||
|
||||
//@function Computes Kaiser Window Moving Average — a symmetric FIR filter using the
|
||||
// Kaiser-Bessel window function for optimal sidelobe attenuation. The beta
|
||||
// parameter controls the trade-off between main lobe width and sidelobe level.
|
||||
//@param source Series to smooth
|
||||
//@param period Lookback window (>= 2)
|
||||
//@param beta Kaiser shape parameter controlling sidelobe attenuation (default 3.0).
|
||||
// Higher beta = smoother (more attenuation) but wider transition band.
|
||||
// beta=0 reduces to rectangular (SMA), beta~5.65 approximates Blackman.
|
||||
//@returns Kaiser-weighted moving average
|
||||
//@reference Kaiser, J.F. & Schafer, R.W. (1980). "On the Use of the I0-Sinh Window
|
||||
// for Spectrum Analysis." IEEE Trans. Acoust., Speech, Signal Process.
|
||||
//@optimized O(period) per bar for convolution; weights precomputed once via I0 series
|
||||
kaiser(series float source, simple int period, simple float beta = 3.0) =>
|
||||
if period < 2
|
||||
runtime.error("Period must be at least 2")
|
||||
|
||||
float price = nz(source)
|
||||
|
||||
// --- Circular buffer for rolling window ---
|
||||
var array<float> buffer = array.new_float(period, na)
|
||||
var int head = 0
|
||||
array.set(buffer, head, price)
|
||||
head := (head + 1) % period
|
||||
|
||||
// --- Precompute Kaiser window weights once ---
|
||||
// Kaiser window: w(k) = I0(beta * sqrt(1 - ((2k/(N-1)) - 1)^2)) / I0(beta)
|
||||
// I0(x) = modified Bessel function of the first kind, order 0
|
||||
// Approximated via power series: I0(x) = sum_{m=0}^{M} [(x/2)^m / m!]^2
|
||||
var array<float> weights = array.new_float(0)
|
||||
if barstate.isfirst
|
||||
// I0 approximation via power series (25 terms — sufficient for double precision)
|
||||
bessel_i0(float x) =>
|
||||
float sum = 1.0
|
||||
float term = 1.0
|
||||
float half_x = x / 2.0
|
||||
for m = 1 to 25
|
||||
term *= (half_x / m)
|
||||
sum += term * term
|
||||
sum
|
||||
|
||||
float i0_beta = bessel_i0(beta)
|
||||
float N = period - 1
|
||||
|
||||
float wsum = 0.0
|
||||
for k = 0 to period - 1
|
||||
float t = N > 0 ? (2.0 * k / N) - 1.0 : 0.0
|
||||
float arg_sq = 1.0 - t * t
|
||||
// Clamp to avoid sqrt of negative due to floating-point
|
||||
float arg = arg_sq > 0 ? math.sqrt(arg_sq) : 0.0
|
||||
float w = i0_beta > 0 ? bessel_i0(beta * arg) / i0_beta : 1.0
|
||||
array.push(weights, w)
|
||||
wsum += w
|
||||
|
||||
// Normalize weights to sum exactly 1.0
|
||||
if wsum > 0
|
||||
for j = 0 to period - 1
|
||||
array.set(weights, j, array.get(weights, j) / wsum)
|
||||
|
||||
int count = math.min(bar_index + 1, period)
|
||||
if count < period
|
||||
price
|
||||
else
|
||||
// --- Apply Kaiser window convolution via circular buffer ---
|
||||
// Buffer: head points to next-write = oldest entry
|
||||
// Weight[0] = oldest bar, Weight[period-1] = newest
|
||||
float result = 0.0
|
||||
for j = 0 to period - 1
|
||||
int idx = (head + j) % period
|
||||
float val = nz(array.get(buffer, idx))
|
||||
result += val * array.get(weights, j)
|
||||
result
|
||||
|
||||
// ── Inputs ──────────────────────────────────────────────────────────────
|
||||
src = input.source(close, "Source")
|
||||
per = input.int(14, "Period", minval=2)
|
||||
bet = input.float(3.0, "Beta (shape)", minval=0, maxval=20, step=0.1,
|
||||
tooltip="0=rectangular(SMA), 3=good general, 5.65≈Blackman, 8.6=Hamming-like")
|
||||
|
||||
// ── Plot ────────────────────────────────────────────────────────────────
|
||||
plot(kaiser(src, per, bet), "KAISER", color.new(color.yellow, 0), 2)
|
||||
@@ -0,0 +1,74 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Lanczos (Sinc) Window Moving Average (LANCZOS)", "LANCZOS", overlay=true)
|
||||
|
||||
//@function Computes Lanczos Window Moving Average — a symmetric FIR filter using the
|
||||
// normalized sinc function as the window shape. The Lanczos window is the
|
||||
// central lobe of a sinc function, providing excellent frequency-domain
|
||||
// characteristics with minimal Gibbs phenomenon ringing.
|
||||
//@param source Series to smooth
|
||||
//@param period Lookback window (>= 2)
|
||||
//@returns Lanczos-windowed moving average
|
||||
//@reference Lanczos, C. (1956). "Applied Analysis." Prentice-Hall.
|
||||
//@reference The Lanczos window w(k) = sinc(2k/(N-1) - 1) where sinc(x) = sin(πx)/(πx).
|
||||
// This is the simplest sinc-based window; higher-order Lanczos kernels use
|
||||
// sinc(x) * sinc(x/a) for Lanczos-a resampling (a=2 or 3 typical).
|
||||
//@optimized O(period) per bar for convolution; weights precomputed once
|
||||
lanczos(series float source, simple int period) =>
|
||||
if period < 2
|
||||
runtime.error("Period must be at least 2")
|
||||
|
||||
float price = nz(source)
|
||||
|
||||
// --- Circular buffer for rolling window ---
|
||||
var array<float> buffer = array.new_float(period, na)
|
||||
var int head = 0
|
||||
array.set(buffer, head, price)
|
||||
head := (head + 1) % period
|
||||
|
||||
// --- Precompute Lanczos (sinc) window weights once ---
|
||||
// Lanczos window: w(k) = sinc(2k/(N-1) - 1)
|
||||
// sinc(x) = sin(π·x) / (π·x) for x != 0, sinc(0) = 1
|
||||
var array<float> weights = array.new_float(0)
|
||||
if barstate.isfirst
|
||||
float N = period - 1
|
||||
float wsum = 0.0
|
||||
for k = 0 to period - 1
|
||||
float x = N > 0 ? (2.0 * k / N) - 1.0 : 0.0
|
||||
float w = 0.0
|
||||
if math.abs(x) < 1e-10
|
||||
w := 1.0 // sinc(0) = 1
|
||||
else
|
||||
float pi_x = math.pi * x
|
||||
w := math.sin(pi_x) / pi_x
|
||||
// Clamp negative weights to 0 for pure Lanczos window
|
||||
// (sinc sidelobes are negative but we keep them for fidelity)
|
||||
array.push(weights, w)
|
||||
wsum += w
|
||||
|
||||
// Normalize weights to sum exactly 1.0
|
||||
if wsum > 0
|
||||
for j = 0 to period - 1
|
||||
array.set(weights, j, array.get(weights, j) / wsum)
|
||||
|
||||
int count = math.min(bar_index + 1, period)
|
||||
if count < period
|
||||
price
|
||||
else
|
||||
// --- Apply Lanczos convolution via circular buffer ---
|
||||
// Buffer: head points to next-write = oldest entry
|
||||
// Weight[0] = oldest bar, Weight[period-1] = newest
|
||||
float result = 0.0
|
||||
for j = 0 to period - 1
|
||||
int idx = (head + j) % period
|
||||
float val = nz(array.get(buffer, idx))
|
||||
result += val * array.get(weights, j)
|
||||
result
|
||||
|
||||
// ── Inputs ──────────────────────────────────────────────────────────────
|
||||
src = input.source(close, "Source")
|
||||
per = input.int(14, "Period", minval=2)
|
||||
|
||||
// ── Plot ────────────────────────────────────────────────────────────────
|
||||
plot(lanczos(src, per), "LANCZOS", color.new(color.yellow, 0), 2)
|
||||
@@ -0,0 +1,76 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Parzen Window Moving Average (PARZEN)", "PARZEN", overlay=true)
|
||||
|
||||
//@function Computes Parzen (de la Vallée-Poussin) Window Moving Average — a symmetric
|
||||
// FIR filter using the Parzen window function. The Parzen window is a piecewise
|
||||
// cubic polynomial with zero sidelobe discontinuity, giving excellent sidelobe
|
||||
// suppression (-24 dB/octave rolloff). It is the convolution of two triangular
|
||||
// (Bartlett) windows at half-length, producing a smooth bell-shaped kernel.
|
||||
// w(k) = 1 - 6u² + 6|u|³ for |u| ≤ 0.5
|
||||
// w(k) = 2(1 - |u|)³ for 0.5 < |u| ≤ 1.0
|
||||
// where u = 2k/(N-1) normalized to [-1,1] center-symmetric.
|
||||
//@param source Series to smooth
|
||||
//@param period Lookback window (>= 2)
|
||||
//@returns Parzen-weighted moving average
|
||||
//@reference Parzen, E. (1961). "Mathematical Considerations in the Estimation of Spectra."
|
||||
// Technometrics, 3(2), 167–190.
|
||||
//@optimized O(period) per bar for convolution; weights precomputed once
|
||||
export parzen(series float source, simple int period) =>
|
||||
if period < 2
|
||||
runtime.error("Period must be at least 2")
|
||||
|
||||
float price = nz(source)
|
||||
|
||||
// --- Circular buffer for rolling window ---
|
||||
var array<float> buffer = array.new_float(period, na)
|
||||
var int head = 0
|
||||
array.set(buffer, head, price)
|
||||
head := (head + 1) % period
|
||||
|
||||
// --- Precompute Parzen window weights once ---
|
||||
var array<float> weights = array.new_float(0)
|
||||
if barstate.isfirst
|
||||
float half_N = (period - 1) / 2.0
|
||||
float wsum = 0.0
|
||||
for k = 0 to period - 1
|
||||
// u normalized to [-1, 1] centered on the middle of the window
|
||||
float u = half_N > 0 ? (k - half_N) / half_N : 0.0
|
||||
float abs_u = math.abs(u)
|
||||
float w = 0.0
|
||||
if abs_u <= 0.5
|
||||
// Inner region: cubic spline
|
||||
w := 1.0 - 6.0 * abs_u * abs_u + 6.0 * abs_u * abs_u * abs_u
|
||||
else if abs_u <= 1.0
|
||||
// Outer region: cubic taper to zero
|
||||
float t = 1.0 - abs_u
|
||||
w := 2.0 * t * t * t
|
||||
array.push(weights, w)
|
||||
wsum += w
|
||||
|
||||
// Normalize weights to sum exactly 1.0
|
||||
if wsum > 0
|
||||
for j = 0 to period - 1
|
||||
array.set(weights, j, array.get(weights, j) / wsum)
|
||||
|
||||
int count = math.min(bar_index + 1, period)
|
||||
if count < period
|
||||
price
|
||||
else
|
||||
// --- Apply Parzen window convolution via circular buffer ---
|
||||
// Buffer: head points to next-write = oldest entry
|
||||
// Weight[0] = oldest bar, Weight[period-1] = newest
|
||||
float result = 0.0
|
||||
for j = 0 to period - 1
|
||||
int idx = (head + j) % period
|
||||
float val = nz(array.get(buffer, idx))
|
||||
result += val * array.get(weights, j)
|
||||
result
|
||||
|
||||
// ── Inputs ──
|
||||
src = input.source(close, "Source")
|
||||
per = input.int(14, "Period", minval = 2)
|
||||
|
||||
// ── Plot ──
|
||||
plot(parzen(src, per), "PARZEN", color.yellow, 2)
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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("Quadratic Regression Moving Average (QRMA)", "QRMA", overlay = true)
|
||||
|
||||
//@function Quadratic Regression Moving Average — fits a second-degree polynomial
|
||||
// y = a + b·x + c·x² to the lookback window via ordinary least squares and
|
||||
// returns the fitted value at the most recent bar (the endpoint). Captures
|
||||
// both linear trends and curvature, providing better tracking of parabolic
|
||||
// price movements than linear regression (LSMA). Uses x = 0..N-1 convention
|
||||
// where x=0 is the oldest bar and the endpoint is evaluated at x=N-1.
|
||||
//@param source Series to smooth
|
||||
//@param period Lookback window (>= 3, need at least 3 points for quadratic fit)
|
||||
//@returns Quadratic regression endpoint value
|
||||
//@reference Savitzky, A. & Golay, M.J.E. (1964). Analytical Chemistry, 36(8), 1627–1639.
|
||||
// (Quadratic regression is a special case of Savitzky-Golay degree=2 smoothing.)
|
||||
//@optimized O(period) per bar via circular buffer; sums precomputed for normal equations
|
||||
export qrma(series float source, simple int period) =>
|
||||
if period < 3
|
||||
runtime.error("Period must be at least 3")
|
||||
|
||||
float price = nz(source)
|
||||
|
||||
// --- Circular buffer ---
|
||||
var array<float> buffer = array.new_float(period, 0.0)
|
||||
var int head = 0
|
||||
array.set(buffer, head, price)
|
||||
head := (head + 1) % period
|
||||
|
||||
int count = math.min(bar_index + 1, period)
|
||||
if count < period
|
||||
price
|
||||
else
|
||||
// --- Precompute constant sums for x = 0, 1, ..., N-1 ---
|
||||
// These only depend on period (N) and could be precomputed once,
|
||||
// but Pine Script handles them efficiently inline.
|
||||
float n = period
|
||||
|
||||
// Σx = N(N-1)/2
|
||||
float sx = n * (n - 1.0) / 2.0
|
||||
// Σx² = N(N-1)(2N-1)/6
|
||||
float sx2 = n * (n - 1.0) * (2.0 * n - 1.0) / 6.0
|
||||
// Σx³ = [N(N-1)/2]²
|
||||
float sx3 = sx * sx
|
||||
// Σx⁴ = N(N-1)(2N-1)(3N²-3N-1)/30
|
||||
float sx4 = n * (n - 1.0) * (2.0 * n - 1.0) * (3.0 * n * n - 3.0 * n - 1.0) / 30.0
|
||||
|
||||
// --- Compute data-dependent sums ---
|
||||
// Buffer: head points to next-write = oldest entry
|
||||
float sy = 0.0
|
||||
float sxy = 0.0
|
||||
float sx2y = 0.0
|
||||
for j = 0 to period - 1
|
||||
int idx = (head + j) % period
|
||||
float val = array.get(buffer, idx)
|
||||
float x = j // x=0 is oldest, x=N-1 is newest
|
||||
sy += val
|
||||
sxy += x * val
|
||||
sx2y += x * x * val
|
||||
|
||||
// --- Solve 3x3 normal equations via Cramer's rule ---
|
||||
// [n Σx Σx²] [a] [Σy ]
|
||||
// [Σx Σx² Σx³] [b] = [Σxy ]
|
||||
// [Σx² Σx³ Σx⁴] [c] [Σx²y]
|
||||
|
||||
float d00 = n
|
||||
float d01 = sx
|
||||
float d02 = sx2
|
||||
float d10 = sx
|
||||
float d11 = sx2
|
||||
float d12 = sx3
|
||||
float d20 = sx2
|
||||
float d21 = sx3
|
||||
float d22 = sx4
|
||||
|
||||
// Determinant of coefficient matrix
|
||||
float det = d00 * (d11 * d22 - d12 * d21) -
|
||||
d01 * (d10 * d22 - d12 * d20) +
|
||||
d02 * (d10 * d21 - d11 * d20)
|
||||
|
||||
if math.abs(det) < 1e-20
|
||||
price
|
||||
else
|
||||
// Solve for a, b, c using Cramer's rule
|
||||
float det_a = sy * (d11 * d22 - d12 * d21) -
|
||||
d01 * (sxy * d22 - sx2y * d21) +
|
||||
d02 * (sxy * d21 - sx2y * d11)
|
||||
float det_b = d00 * (sxy * d22 - sx2y * d21) -
|
||||
sy * (d10 * d22 - d12 * d20) +
|
||||
d02 * (d10 * sx2y - sxy * d20)
|
||||
float det_c = d00 * (d11 * sx2y - sxy * d21) -
|
||||
d01 * (d10 * sx2y - sxy * d20) +
|
||||
sy * (d10 * d21 - d11 * d20)
|
||||
|
||||
float a = det_a / det
|
||||
float b = det_b / det
|
||||
float c = det_c / det
|
||||
|
||||
// Evaluate at x = N-1 (newest bar = endpoint)
|
||||
float x_end = n - 1.0
|
||||
a + b * x_end + c * x_end * x_end
|
||||
|
||||
// ── Inputs ──
|
||||
int p_period = input.int(14, "Period", minval = 3)
|
||||
float p_src = input.source(close, "Source")
|
||||
|
||||
// ── Calculation ──
|
||||
float out = qrma(p_src, p_period)
|
||||
|
||||
// ── Plot ──
|
||||
plot(out, "QRMA", color.yellow, 2)
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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("RWMA: Range Weighted Moving Average", shorttitle="RWMA", overlay=true)
|
||||
|
||||
// @function Calculates the Range Weighted Moving Average.
|
||||
// Each bar's contribution is weighted by its range (high - low),
|
||||
// giving more influence to volatile bars and less to narrow-range bars.
|
||||
// RWMA = Σ(close[i] × range[i]) / Σ(range[i]) over the lookback period.
|
||||
// @param src Series to smooth (typically close).
|
||||
// @param high_src High price series.
|
||||
// @param low_src Low price series.
|
||||
// @param period Lookback window length. Must be > 0.
|
||||
// @returns The range-weighted moving average value.
|
||||
export rwma(series float src, series float high_src, series float low_src, simple int period) =>
|
||||
float sumWV = 0.0
|
||||
float sumW = 0.0
|
||||
for i = 0 to period - 1
|
||||
float rng = high_src[i] - low_src[i]
|
||||
float w = math.max(rng, 0.0)
|
||||
sumWV += src[i] * w
|
||||
sumW += w
|
||||
sumW > 0.0 ? sumWV / sumW : src
|
||||
|
||||
// ── Inputs ──────────────────────────────────────────────
|
||||
p = input.int(14, "Period", minval=1)
|
||||
|
||||
// ── Calculation ─────────────────────────────────────────
|
||||
result = rwma(close, high, low, p)
|
||||
|
||||
// ── Plot ────────────────────────────────────────────────
|
||||
plot(result, "RWMA", color=color.yellow, linewidth=2)
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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("SP15: Spencer 15-Point Moving Average", shorttitle="SP15", overlay=true)
|
||||
|
||||
// @function Calculates the Spencer 15-Point Moving Average.
|
||||
// A symmetric FIR filter with fixed coefficients designed by John Spencer
|
||||
// for seasonal adjustment of economic time series. The 15 weights are:
|
||||
// [-3, -6, -5, 3, 21, 46, 67, 74, 67, 46, 21, 3, -5, -6, -3] / 320.
|
||||
// The filter has zero response at frequencies 2π/4 and 2π/5 (periods 4 and 5),
|
||||
// making it effective at removing quarterly and quintile seasonal components.
|
||||
// Centered at lag 7 bars.
|
||||
// @param src Series to smooth.
|
||||
// @returns The Spencer 15-point weighted average.
|
||||
export sp15(series float src) =>
|
||||
// Spencer 15-point weights (symmetric, sum = 320)
|
||||
float w0 = -3.0
|
||||
float w1 = -6.0
|
||||
float w2 = -5.0
|
||||
float w3 = 3.0
|
||||
float w4 = 21.0
|
||||
float w5 = 46.0
|
||||
float w6 = 67.0
|
||||
float w7 = 74.0
|
||||
// Symmetric: w8=w6, w9=w5, ..., w14=w0
|
||||
float total = w0 * (src[0] + src[14]) + w1 * (src[1] + src[13]) + w2 * (src[2] + src[12]) + w3 * (src[3] + src[11]) + w4 * (src[4] + src[10]) + w5 * (src[5] + src[9]) + w6 * (src[6] + src[8]) + w7 * src[7]
|
||||
total / 320.0
|
||||
|
||||
// ── Calculation ─────────────────────────────────────────
|
||||
result = sp15(close)
|
||||
|
||||
// ── Plot ────────────────────────────────────────────────
|
||||
plot(result, "SP15", color=color.yellow, linewidth=2)
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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("SWMA: Symmetric Weighted Moving Average", shorttitle="SWMA", overlay=true)
|
||||
|
||||
// @function Calculates the Symmetric Weighted Moving Average.
|
||||
// Uses triangular/symmetric weights that peak at the center of the window.
|
||||
// For period N, weight at position i (0-based from newest) is:
|
||||
// w(i) = (N/2 + 1) - |i - N/2| (triangular shape)
|
||||
// This is equivalent to convolving two rectangular windows (SMA of SMA)
|
||||
// and produces a smooth, low-lag FIR filter with zero phase distortion
|
||||
// at the center of the window.
|
||||
// For period=4 (Pine's built-in ta.swma): weights are [1, 2, 2, 1] / 6.
|
||||
// @param src Series to smooth.
|
||||
// @param period Window length. Must be >= 2.
|
||||
// @returns The symmetric weighted moving average value.
|
||||
export swma(series float src, simple int period) =>
|
||||
float sumWV = 0.0
|
||||
float sumW = 0.0
|
||||
float half = (period - 1) / 2.0
|
||||
for i = 0 to period - 1
|
||||
float w = half + 1.0 - math.abs(i - half)
|
||||
sumWV += src[i] * w
|
||||
sumW += w
|
||||
sumWV / sumW
|
||||
|
||||
// ── Inputs ──────────────────────────────────────────────
|
||||
p = input.int(4, "Period", minval=2)
|
||||
|
||||
// ── Calculation ─────────────────────────────────────────
|
||||
result = swma(close, p)
|
||||
|
||||
// ── Plot ────────────────────────────────────────────────
|
||||
plot(result, "SWMA", color=color.yellow, linewidth=2)
|
||||
@@ -0,0 +1,159 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TsfIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void TsfIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new TsfIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("TSF - Time Series Forecast", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TsfIndicator_MinHistoryDepths_IsZero()
|
||||
{
|
||||
var indicator = new TsfIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, TsfIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TsfIndicator_ShortName_IncludesPeriodAndSource()
|
||||
{
|
||||
var indicator = new TsfIndicator { Period = 15 };
|
||||
|
||||
Assert.Contains("TSF", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TsfIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new TsfIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Tsf.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TsfIndicator_Initialize_CreatesInternalTsf()
|
||||
{
|
||||
var indicator = new TsfIndicator { Period = 10 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TsfIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TsfIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TsfIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TsfIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TsfIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new TsfIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TsfIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new TsfIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 104, 103, 105 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TsfIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new TsfIndicator { Period = 3, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TsfIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new TsfIndicator { Period = 5 };
|
||||
Assert.Equal(5, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(0, TsfIndicator.MinHistoryDepths);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class TsfIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Tsf _tsf = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"TSF {Period}:{_sourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends_FIR/tsf/Tsf.Quantower.cs";
|
||||
|
||||
public TsfIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "TSF - Time Series Forecast";
|
||||
Description = "Time Series Forecast (Linear Regression one-step-ahead projection)";
|
||||
_series = new LineSeries(name: $"TSF {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_tsf = new Tsf(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _tsf.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew).Value;
|
||||
_series.SetValue(value, _tsf.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TsfTests
|
||||
{
|
||||
private static TSeries MakeSeries(int count = 500)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
}
|
||||
|
||||
// ── A) Constructor validation ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex0 = Assert.Throws<ArgumentException>(() => new Tsf(0));
|
||||
Assert.Equal("period", ex0.ParamName);
|
||||
|
||||
var exNeg = Assert.Throws<ArgumentException>(() => new Tsf(-1));
|
||||
Assert.Equal("period", exNeg.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidParameters_SetsProperties()
|
||||
{
|
||||
var tsf = new Tsf(14);
|
||||
Assert.Equal("Tsf(14)", tsf.Name);
|
||||
Assert.False(tsf.IsHot);
|
||||
Assert.Equal(14, tsf.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NullSource_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new Tsf(null!, 14));
|
||||
}
|
||||
|
||||
// ── B) Basic calculation ───────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_SingleValue_ReturnsSameValue()
|
||||
{
|
||||
var tsf = new Tsf(14);
|
||||
var result = tsf.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(100, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Last_IsAccessible()
|
||||
{
|
||||
var tsf = new Tsf(5);
|
||||
var series = MakeSeries(20);
|
||||
foreach (var item in series)
|
||||
{
|
||||
tsf.Update(item);
|
||||
}
|
||||
Assert.True(double.IsFinite(tsf.Last.Value));
|
||||
Assert.True(tsf.IsHot);
|
||||
Assert.Contains("Tsf", tsf.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_LinearTrend_ReturnsNextValue()
|
||||
{
|
||||
// For a perfect linear trend y = x,
|
||||
// TSF should return x+1 (one step forecast) after warmup
|
||||
const int period = 10;
|
||||
var tsf = new Tsf(period);
|
||||
|
||||
for (int i = 0; i < period * 2; i++)
|
||||
{
|
||||
var result = tsf.Update(new TValue(DateTime.UtcNow, i));
|
||||
if (i >= period)
|
||||
{
|
||||
// TSF forecasts one step ahead: should be i+1
|
||||
Assert.Equal(i + 1, result.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantValue_ReturnsSameValue()
|
||||
{
|
||||
const int period = 10;
|
||||
var tsf = new Tsf(period);
|
||||
const double value = 123.45;
|
||||
|
||||
for (int i = 0; i < period * 2; i++)
|
||||
{
|
||||
var result = tsf.Update(new TValue(DateTime.UtcNow, value));
|
||||
Assert.Equal(value, result.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_LinearSlope_ForecastsCorrectly()
|
||||
{
|
||||
// y = 2x + 5
|
||||
// At bar i, the next bar's value should be 2*(i+1) + 5
|
||||
const int period = 8;
|
||||
var tsf = new Tsf(period);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double y = 2.0 * i + 5.0;
|
||||
var result = tsf.Update(new TValue(DateTime.UtcNow, y));
|
||||
|
||||
if (i >= period)
|
||||
{
|
||||
double expected = 2.0 * (i + 1) + 5.0;
|
||||
Assert.Equal(expected, result.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── C) State + bar correction ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var tsf = new Tsf(5);
|
||||
var result = tsf.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var tsf = new Tsf(5);
|
||||
var series = MakeSeries(20);
|
||||
|
||||
foreach (var item in series)
|
||||
{
|
||||
tsf.Update(item, isNew: true);
|
||||
}
|
||||
|
||||
double valueBefore = tsf.Last.Value;
|
||||
tsf.Update(new TValue(DateTime.UtcNow, series[^1].Value * 1.1), isNew: false);
|
||||
double valueAfter = tsf.Last.Value;
|
||||
|
||||
Assert.NotEqual(valueBefore, valueAfter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var tsf = new Tsf(10);
|
||||
var series = MakeSeries(50);
|
||||
|
||||
// Feed N values
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
tsf.Update(series[i], isNew: true);
|
||||
}
|
||||
double expectedValue = tsf.Last.Value;
|
||||
|
||||
// Feed M corrections with isNew: false
|
||||
for (int j = 0; j < 5; j++)
|
||||
{
|
||||
tsf.Update(new TValue(DateTime.UtcNow, 999.0 + j), isNew: false);
|
||||
}
|
||||
|
||||
// Restore original value
|
||||
tsf.Update(series[29], isNew: false);
|
||||
Assert.Equal(expectedValue, tsf.Last.Value, 1e-6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var tsf = new Tsf(10);
|
||||
var series = MakeSeries(50);
|
||||
foreach (var item in series)
|
||||
{
|
||||
tsf.Update(item);
|
||||
}
|
||||
|
||||
Assert.True(tsf.IsHot);
|
||||
tsf.Reset();
|
||||
Assert.False(tsf.IsHot);
|
||||
|
||||
// Re-feed same data should produce identical results
|
||||
var tsf2 = new Tsf(10);
|
||||
foreach (var item in series)
|
||||
{
|
||||
tsf.Update(item);
|
||||
tsf2.Update(item);
|
||||
}
|
||||
Assert.Equal(tsf2.Last.Value, tsf.Last.Value, 1e-12);
|
||||
}
|
||||
|
||||
// ── D) Warmup/convergence ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
var tsf = new Tsf(10);
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
tsf.Update(new TValue(DateTime.UtcNow, i));
|
||||
Assert.False(tsf.IsHot);
|
||||
}
|
||||
tsf.Update(new TValue(DateTime.UtcNow, 9));
|
||||
Assert.True(tsf.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_IsPeriodDependent()
|
||||
{
|
||||
foreach (int period in new[] { 5, 10, 20, 50 })
|
||||
{
|
||||
var tsf = new Tsf(period);
|
||||
for (int i = 0; i < period - 1; i++)
|
||||
{
|
||||
tsf.Update(new TValue(DateTime.UtcNow, i));
|
||||
Assert.False(tsf.IsHot);
|
||||
}
|
||||
tsf.Update(new TValue(DateTime.UtcNow, period - 1));
|
||||
Assert.True(tsf.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
// ── E) Robustness (NaN/Infinity) ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var tsf = new Tsf(5);
|
||||
var series = MakeSeries(20);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tsf.Update(series[i]);
|
||||
}
|
||||
|
||||
_ = tsf.Last.Value;
|
||||
tsf.Update(new TValue(DateTime.UtcNow, double.NaN), isNew: true);
|
||||
Assert.True(double.IsFinite(tsf.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var tsf = new Tsf(5);
|
||||
var series = MakeSeries(20);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tsf.Update(series[i]);
|
||||
}
|
||||
|
||||
tsf.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity), isNew: true);
|
||||
Assert.True(double.IsFinite(tsf.Last.Value));
|
||||
|
||||
tsf.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity), isNew: true);
|
||||
Assert.True(double.IsFinite(tsf.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleNaN_ContinuesWithLastValid()
|
||||
{
|
||||
var tsf = new Tsf(5);
|
||||
var series = MakeSeries(20);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tsf.Update(series[i]);
|
||||
}
|
||||
|
||||
for (int j = 0; j < 5; j++)
|
||||
{
|
||||
tsf.Update(new TValue(DateTime.UtcNow, double.NaN), isNew: true);
|
||||
Assert.True(double.IsFinite(tsf.Last.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_HandlesNaN()
|
||||
{
|
||||
double[] input = { 1, 2, 3, double.NaN, 5, 6, 7, 8, 9, 10 };
|
||||
double[] output = new double[input.Length];
|
||||
Tsf.Batch(input.AsSpan(), output.AsSpan(), 5);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
}
|
||||
}
|
||||
|
||||
// ── F) Consistency (all 4 modes match) ─────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
int period = 14;
|
||||
var series = MakeSeries(500);
|
||||
|
||||
// 1. Batch (TSeries)
|
||||
var batchResult = Tsf.Batch(series, period);
|
||||
|
||||
// 2. Span
|
||||
double[] spanOutput = new double[series.Count];
|
||||
Tsf.Batch(series.Values, spanOutput.AsSpan(), period);
|
||||
|
||||
// 3. Streaming
|
||||
var streamTsf = new Tsf(period);
|
||||
var streamResults = new List<double>();
|
||||
foreach (var item in series)
|
||||
{
|
||||
streamResults.Add(streamTsf.Update(item).Value);
|
||||
}
|
||||
|
||||
// 4. Eventing
|
||||
var pubSource = new TSeries();
|
||||
var eventTsf = new Tsf(pubSource, period);
|
||||
foreach (var item in series)
|
||||
{
|
||||
pubSource.Add(item);
|
||||
}
|
||||
|
||||
// Compare last values
|
||||
double batchLast = batchResult.Values[^1];
|
||||
double spanLast = spanOutput[^1];
|
||||
double streamLast = streamResults[^1];
|
||||
double eventLast = eventTsf.Last.Value;
|
||||
|
||||
Assert.Equal(batchLast, spanLast, 1e-9);
|
||||
Assert.Equal(batchLast, streamLast, 1e-9);
|
||||
Assert.Equal(batchLast, eventLast, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
int period = 10;
|
||||
var series = MakeSeries(200);
|
||||
|
||||
// Batch
|
||||
var batchResult = Tsf.Batch(series, period);
|
||||
|
||||
// Iterative
|
||||
var tsf = new Tsf(period);
|
||||
TSeries streamResult = tsf.Update(series);
|
||||
|
||||
int compareCount = Math.Min(100, series.Count);
|
||||
int start = series.Count - compareCount;
|
||||
|
||||
for (int i = start; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], streamResult.Values[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
// ── G) Span API tests ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SpanCalc_ValidatesInput_LengthMismatch()
|
||||
{
|
||||
double[] input = { 1, 2, 3, 4, 5 };
|
||||
double[] output = new double[3];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Tsf.Batch(input.AsSpan(), output.AsSpan(), 3));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalc_ValidatesInput_InvalidPeriod()
|
||||
{
|
||||
double[] input = { 1, 2, 3, 4, 5 };
|
||||
double[] output = new double[5];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Tsf.Batch(input.AsSpan(), output.AsSpan(), 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalc_MatchesTSeriesCalc()
|
||||
{
|
||||
int period = 20;
|
||||
var series = MakeSeries(500);
|
||||
|
||||
var batchResult = Tsf.Batch(series, period);
|
||||
double[] spanOutput = new double[series.Count];
|
||||
Tsf.Batch(series.Values, spanOutput.AsSpan(), period);
|
||||
|
||||
int compareCount = 100;
|
||||
int start = series.Count - compareCount;
|
||||
for (int i = start; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], spanOutput[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalc_EmptyInput_NoException()
|
||||
{
|
||||
double[] input = Array.Empty<double>();
|
||||
double[] output = Array.Empty<double>();
|
||||
Tsf.Batch(input.AsSpan(), output.AsSpan(), 5);
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalc_LargeDataset_NoStackOverflow()
|
||||
{
|
||||
int size = 10_000;
|
||||
double[] input = new double[size];
|
||||
double[] output = new double[size];
|
||||
|
||||
var gbm = new GBM(100, 0.05, 0.2, seed: 99);
|
||||
var series = gbm.Fetch(size, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
input[i] = series.Values[i];
|
||||
}
|
||||
|
||||
Tsf.Batch(input.AsSpan(), output.AsSpan(), 300);
|
||||
|
||||
Assert.True(double.IsFinite(output[^1]));
|
||||
}
|
||||
|
||||
// ── H) Chainability ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Pub_FiresOnUpdate()
|
||||
{
|
||||
var tsf = new Tsf(5);
|
||||
int fireCount = 0;
|
||||
tsf.Pub += (object? _, in TValueEventArgs _) => fireCount++;
|
||||
|
||||
var series = MakeSeries(20);
|
||||
foreach (var item in series)
|
||||
{
|
||||
tsf.Update(item);
|
||||
}
|
||||
|
||||
Assert.Equal(series.Count, fireCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventChaining_Works()
|
||||
{
|
||||
int period = 5;
|
||||
var source = new TSeries();
|
||||
var tsf = new Tsf(source, period);
|
||||
|
||||
var series = MakeSeries(50);
|
||||
foreach (var item in series)
|
||||
{
|
||||
source.Add(item);
|
||||
}
|
||||
|
||||
Assert.True(tsf.IsHot);
|
||||
Assert.True(double.IsFinite(tsf.Last.Value));
|
||||
}
|
||||
|
||||
// ── TSF-specific tests ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TSF_EqualsLSMA_PlusSlope()
|
||||
{
|
||||
// TSF = LSMA(offset=0) + slope
|
||||
// Which is the same as LSMA(offset=1)?
|
||||
// Yes: LSMA uses result = b - m * offset
|
||||
// LSMA(offset=1) = b - m*1 = b - m = TSF
|
||||
const int period = 14;
|
||||
var series = MakeSeries(500);
|
||||
|
||||
var lsma = new Lsma(period, offset: 1);
|
||||
var tsf = new Tsf(period);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var lsmaResult = lsma.Update(series[i]);
|
||||
var tsfResult = tsf.Update(series[i]);
|
||||
|
||||
Assert.Equal(lsmaResult.Value, tsfResult.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsBothResultsAndIndicator()
|
||||
{
|
||||
var series = MakeSeries(100);
|
||||
var (results, indicator) = Tsf.Calculate(series, 10);
|
||||
|
||||
Assert.True(results.Count > 0);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(results[^1].Value, indicator.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class TsfValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public TsfValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_testData.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cross-validate against LSMA(offset=1) ─────────────────────────
|
||||
// TSF = LSMA with offset=1. This is a mathematical identity.
|
||||
|
||||
[Fact]
|
||||
public void Validate_LSMA_Batch()
|
||||
{
|
||||
int[] periods = { 5, 10, 14, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var tsf = new global::QuanTAlib.Tsf(period);
|
||||
var tsfResult = tsf.Update(_testData.Data);
|
||||
|
||||
var lsma = new global::QuanTAlib.Lsma(period, offset: 1);
|
||||
var lsmaResult = lsma.Update(_testData.Data);
|
||||
|
||||
int compareCount = 100;
|
||||
int start = tsfResult.Count - compareCount;
|
||||
|
||||
for (int i = start; i < tsfResult.Count; i++)
|
||||
{
|
||||
Assert.Equal(lsmaResult.Values[i], tsfResult.Values[i], 1e-9);
|
||||
}
|
||||
}
|
||||
_output.WriteLine("TSF Batch validated successfully against LSMA(offset=1)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_LSMA_Streaming()
|
||||
{
|
||||
int[] periods = { 5, 10, 14, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var tsf = new global::QuanTAlib.Tsf(period);
|
||||
var lsma = new global::QuanTAlib.Lsma(period, offset: 1);
|
||||
|
||||
var tsfResults = new List<double>();
|
||||
var lsmaResults = new List<double>();
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
tsfResults.Add(tsf.Update(item).Value);
|
||||
lsmaResults.Add(lsma.Update(item).Value);
|
||||
}
|
||||
|
||||
int compareCount = 100;
|
||||
int start = tsfResults.Count - compareCount;
|
||||
|
||||
for (int i = start; i < tsfResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(lsmaResults[i], tsfResults[i], 1e-9);
|
||||
}
|
||||
}
|
||||
_output.WriteLine("TSF Streaming validated successfully against LSMA(offset=1)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_LSMA_Span()
|
||||
{
|
||||
int[] periods = { 5, 10, 14, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
double[] tsfOutput = new double[_testData.RawData.Length];
|
||||
double[] lsmaOutput = new double[_testData.RawData.Length];
|
||||
|
||||
global::QuanTAlib.Tsf.Batch(_testData.RawData.Span, tsfOutput.AsSpan(), period);
|
||||
global::QuanTAlib.Lsma.Batch(_testData.RawData.Span, lsmaOutput.AsSpan(), period, offset: 1);
|
||||
|
||||
int compareCount = 100;
|
||||
int start = tsfOutput.Length - compareCount;
|
||||
|
||||
for (int i = start; i < tsfOutput.Length; i++)
|
||||
{
|
||||
Assert.Equal(lsmaOutput[i], tsfOutput[i], 1e-9);
|
||||
}
|
||||
}
|
||||
_output.WriteLine("TSF Span validated successfully against LSMA(offset=1)");
|
||||
}
|
||||
|
||||
// ── Self-consistency checks ────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Validate_Batch_Streaming_Consistency()
|
||||
{
|
||||
const int period = 14;
|
||||
|
||||
// Batch
|
||||
var batchResult = global::QuanTAlib.Tsf.Batch(_testData.Data, period);
|
||||
|
||||
// Streaming
|
||||
var tsf = new global::QuanTAlib.Tsf(period);
|
||||
var streamResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
streamResults.Add(tsf.Update(item).Value);
|
||||
}
|
||||
|
||||
int compareCount = 100;
|
||||
int start = batchResult.Count - compareCount;
|
||||
for (int i = start; i < batchResult.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], streamResults[i], 1e-6);
|
||||
}
|
||||
_output.WriteLine("TSF Batch vs Streaming consistency verified");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DifferentPeriods()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var result = global::QuanTAlib.Tsf.Batch(_testData.Data, period);
|
||||
Assert.True(result.Count == _testData.Data.Count);
|
||||
Assert.True(double.IsFinite(result.Values[^1]));
|
||||
}
|
||||
_output.WriteLine("TSF different periods validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
const int period = 14;
|
||||
var (results, indicator) = global::QuanTAlib.Tsf.Calculate(_testData.Data, period);
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.True(results.Count == _testData.Data.Count);
|
||||
Assert.Equal(results.Values[^1], indicator.Last.Value);
|
||||
_output.WriteLine("TSF Calculate returns hot indicator verified");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_BarCorrection_Consistency()
|
||||
{
|
||||
const int period = 14;
|
||||
|
||||
// Feed initial data
|
||||
var tsf = new global::QuanTAlib.Tsf(period);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
tsf.Update(_testData.Data[i], isNew: true);
|
||||
}
|
||||
double expectedLast = tsf.Last.Value;
|
||||
|
||||
// Apply multiple corrections, then restore
|
||||
for (int j = 0; j < 5; j++)
|
||||
{
|
||||
tsf.Update(new TValue(DateTime.UtcNow, 999.0), isNew: false);
|
||||
}
|
||||
tsf.Update(_testData.Data[99], isNew: false);
|
||||
|
||||
Assert.Equal(expectedLast, tsf.Last.Value, 1e-6);
|
||||
_output.WriteLine("TSF bar correction consistency verified");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// TSF: Time Series Forecast
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Projects the linear regression line one step forward, forecasting the
|
||||
/// next bar's value based on the least-squares trend over the lookback period.
|
||||
///
|
||||
/// Calculation: <c>TSF = slope × period + intercept</c> (standard convention)
|
||||
/// or equivalently <c>TSF = b − m</c> (reversed-x convention where b = current bar value).
|
||||
///
|
||||
/// Uses O(1) incremental running sums (SumY, SumXY) identical to LSMA.
|
||||
/// Relationship: TSF = LSMA(offset=0) + slope = LSMA(offset=1).
|
||||
/// </remarks>
|
||||
/// <seealso href="Tsf.md">Detailed documentation</seealso>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Tsf : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly RingBuffer _buffer;
|
||||
|
||||
private readonly double _sumX;
|
||||
private readonly double _denominator;
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
private ITValuePublisher? _source;
|
||||
private int _disposed;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double SumY, double SumXY, double LastVal, double LastValidValue);
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
private int _tickCount;
|
||||
private bool _isNew;
|
||||
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
public override bool IsHot => _buffer.IsFull;
|
||||
public bool IsNew => _isNew;
|
||||
|
||||
/// <summary>
|
||||
/// Creates TSF with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Lookback period for linear regression (must be > 0)</param>
|
||||
public Tsf(int period = 14)
|
||||
{
|
||||
if (period <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
}
|
||||
|
||||
_period = period;
|
||||
_buffer = new RingBuffer(period);
|
||||
Name = $"Tsf({period})";
|
||||
WarmupPeriod = period;
|
||||
_handler = Handle;
|
||||
|
||||
// Precompute constants (reversed-x convention: x=0=newest, x=n-1=oldest)
|
||||
// sumX = 0 + 1 + ... + (n-1) = n(n-1)/2
|
||||
_sumX = 0.5 * period * (period - 1);
|
||||
|
||||
// sumX2 = 0^2 + ... + (n-1)^2 = (n-1)n(2n-1)/6
|
||||
double sumX2 = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
|
||||
|
||||
// denominator = n * sumX2 - sumX^2
|
||||
_denominator = period * sumX2 - _sumX * _sumX;
|
||||
_s.LastValidValue = double.NaN;
|
||||
}
|
||||
|
||||
public Tsf(ITValuePublisher source, int period = 14) : this(period)
|
||||
{
|
||||
_source = source ?? throw new ArgumentNullException(nameof(source));
|
||||
_source.Pub += _handler;
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetValidValue(double input)
|
||||
{
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
_s.LastValidValue = input;
|
||||
return input;
|
||||
}
|
||||
return _s.LastValidValue;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void UpdateState(double val)
|
||||
{
|
||||
if (_buffer.IsFull)
|
||||
{
|
||||
double oldest = _buffer.Oldest;
|
||||
double prevSumY = _s.SumY;
|
||||
|
||||
// O(1) update for SumXY (reversed-x convention)
|
||||
// New value enters at x=0, existing values shift x+1, oldest drops off
|
||||
// sumXY_new = sumXY_old + sumY_prev - n * oldest
|
||||
_s.SumXY = Math.FusedMultiplyAdd(-_period, oldest, _s.SumXY + prevSumY);
|
||||
|
||||
// O(1) update for SumY
|
||||
_s.SumY = _s.SumY - oldest + val;
|
||||
|
||||
_buffer.Add(val);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_buffer.Count > 0)
|
||||
{
|
||||
_s.SumXY += _s.SumY;
|
||||
}
|
||||
_s.SumY += val;
|
||||
_buffer.Add(val);
|
||||
}
|
||||
|
||||
_tickCount++;
|
||||
if (_buffer.IsFull && _tickCount >= ResyncInterval)
|
||||
{
|
||||
_tickCount = 0;
|
||||
Resync();
|
||||
}
|
||||
}
|
||||
|
||||
private void Resync()
|
||||
{
|
||||
_s.SumY = _buffer.Sum;
|
||||
_s.SumXY = 0;
|
||||
var span = _buffer.GetSpan();
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
int x = span.Length - 1 - i;
|
||||
_s.SumXY = Math.FusedMultiplyAdd(x, span[i], _s.SumXY);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
_isNew = isNew;
|
||||
if (isNew)
|
||||
{
|
||||
double val = GetValidValue(input.Value);
|
||||
UpdateState(val);
|
||||
|
||||
_s.LastVal = val;
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s.LastValidValue = _ps.LastValidValue;
|
||||
double val = GetValidValue(input.Value);
|
||||
|
||||
// For isNew=false, update the current bar without advancing.
|
||||
// SumXY remains constant (depends on previous window state).
|
||||
// SumY updates to reflect the change in the newest value.
|
||||
_s.SumY = _ps.SumY - _ps.LastVal + val;
|
||||
_s.SumXY = _ps.SumXY;
|
||||
|
||||
_buffer.UpdateNewest(val);
|
||||
_s.LastVal = val;
|
||||
}
|
||||
|
||||
double result;
|
||||
if (_buffer.Count <= 1)
|
||||
{
|
||||
result = _buffer.Newest;
|
||||
}
|
||||
else
|
||||
{
|
||||
double n = _buffer.Count;
|
||||
double sx = _sumX;
|
||||
double denom = _denominator;
|
||||
|
||||
if (!_buffer.IsFull)
|
||||
{
|
||||
// Recalculate constants for smaller n during warmup
|
||||
sx = 0.5 * n * (n - 1);
|
||||
double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
|
||||
denom = n * sx2 - sx * sx;
|
||||
}
|
||||
|
||||
if (Math.Abs(denom) < 1e-10)
|
||||
{
|
||||
result = _buffer.Newest;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reversed-x convention: m is negative for uptrend
|
||||
double m = Math.FusedMultiplyAdd(n, _s.SumXY, -sx * _s.SumY) / denom;
|
||||
double b = Math.FusedMultiplyAdd(-m, sx, _s.SumY) / n;
|
||||
|
||||
// b = value at x=0 (current bar endpoint)
|
||||
// TSF = forecast one step ahead = b - m
|
||||
// (In reversed-x, stepping forward means x=-1, so y = b - m*(-1)... wait)
|
||||
// Actually: b - m * offset, where offset=1 projects one step ahead
|
||||
// TSF = b - m * 1 = b - m
|
||||
result = b - m;
|
||||
}
|
||||
}
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return new TSeries([], []);
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
double initialLastValid = _s.LastValidValue;
|
||||
Batch(source.Values, vSpan, _period, initialLastValid);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Restore state by replaying the last 'period' bars
|
||||
int windowSize = Math.Min(len, _period);
|
||||
int startIndex = len - windowSize;
|
||||
|
||||
Reset();
|
||||
|
||||
if (startIndex > 0)
|
||||
{
|
||||
for (int i = startIndex - 1; i >= 0; i--)
|
||||
{
|
||||
if (double.IsFinite(source.Values[i]))
|
||||
{
|
||||
_s.LastValidValue = source.Values[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_s.LastValidValue = initialLastValid;
|
||||
}
|
||||
|
||||
double lastProcessedValue = _s.LastValidValue;
|
||||
for (int i = startIndex; i < len; i++)
|
||||
{
|
||||
double val = GetValidValue(source.Values[i]);
|
||||
UpdateState(val);
|
||||
lastProcessedValue = val;
|
||||
}
|
||||
|
||||
_s.LastVal = lastProcessedValue;
|
||||
_ps = _s;
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (var value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.MinValue, value));
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Batch(TSeries source, int period = 14)
|
||||
{
|
||||
var tsf = new Tsf(period);
|
||||
return tsf.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates TSF in-place, writing results to pre-allocated output span.
|
||||
/// Zero-allocation method for maximum performance.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period = 14, double initialLastValid = double.NaN)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
||||
}
|
||||
|
||||
if (period <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int StackAllocThreshold = 256;
|
||||
Span<double> buffer = period <= StackAllocThreshold
|
||||
? stackalloc double[period]
|
||||
: new double[period];
|
||||
|
||||
double sumY = 0;
|
||||
double sumXY = 0;
|
||||
double lastValid = initialLastValid;
|
||||
int bufferIndex = 0;
|
||||
int count = 0;
|
||||
|
||||
// Precalculate constants for full period
|
||||
double fullSumX = 0.5 * period * (period - 1);
|
||||
double fullSumX2 = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
|
||||
double fullDenom = period * fullSumX2 - fullSumX * fullSumX;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
lastValid = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
val = lastValid;
|
||||
}
|
||||
|
||||
if (count < period)
|
||||
{
|
||||
// Warmup phase
|
||||
buffer[count] = val;
|
||||
count++;
|
||||
|
||||
if (count > 1)
|
||||
{
|
||||
sumXY += sumY;
|
||||
}
|
||||
sumY += val;
|
||||
|
||||
if (count <= 1)
|
||||
{
|
||||
output[i] = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
double n = count;
|
||||
double sx = 0.5 * n * (n - 1);
|
||||
double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
|
||||
double denom = n * sx2 - sx * sx;
|
||||
|
||||
if (Math.Abs(denom) < 1e-10)
|
||||
{
|
||||
output[i] = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
double m = Math.FusedMultiplyAdd(n, sumXY, -sx * sumY) / denom;
|
||||
double b = Math.FusedMultiplyAdd(-m, sx, sumY) / n;
|
||||
// TSF = b - m (one step ahead forecast)
|
||||
output[i] = b - m;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == period)
|
||||
{
|
||||
bufferIndex = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Full buffer phase — O(1) update
|
||||
double oldest = buffer[bufferIndex];
|
||||
double prevSumY = sumY;
|
||||
|
||||
sumXY = Math.FusedMultiplyAdd(-period, oldest, sumXY + prevSumY);
|
||||
sumY = sumY - oldest + val;
|
||||
buffer[bufferIndex] = val;
|
||||
|
||||
bufferIndex++;
|
||||
if (bufferIndex >= period)
|
||||
{
|
||||
bufferIndex = 0;
|
||||
}
|
||||
|
||||
double m = Math.FusedMultiplyAdd(period, sumXY, -fullSumX * sumY) / fullDenom;
|
||||
double b = Math.FusedMultiplyAdd(-m, fullSumX, sumY) / period;
|
||||
// TSF = b - m (one step ahead forecast)
|
||||
output[i] = b - m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Results, Tsf Indicator) Calculate(TSeries source, int period = 14)
|
||||
{
|
||||
var indicator = new Tsf(period);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_s = default;
|
||||
_s.LastValidValue = double.NaN;
|
||||
_ps = default;
|
||||
Last = default;
|
||||
_tickCount = 0;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0 && _source != null)
|
||||
{
|
||||
_source.Pub -= _handler;
|
||||
_source = null;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
# TSF: Time Series Forecast
|
||||
|
||||
> "The best prediction of the future is the trend that's already in motion — extended by exactly one step."
|
||||
|
||||
TSF projects the least-squares regression line one bar forward, providing a statistically grounded forecast of the next bar's value. Unlike simple moving averages that smooth past data, TSF answers the question: "If the current trend continues, where will price be next?" This makes it inherently leading rather than lagging, though the forecast degrades quickly beyond one step.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Time Series Forecast originates from classical linear regression applied to financial time series. The concept appeared in TA-Lib as `TA_TSF` and has been a standard offering in technical analysis software since the 1990s. Tushar Chande's *The New Technical Trader* (1994) formalized several regression-based indicators including the closely related Chande Forecast Oscillator (CFO), which measures the percentage error between the current price and the TSF value.
|
||||
|
||||
TSF is mathematically identical to the Least Squares Moving Average (LSMA) evaluated one step beyond the window endpoint. Where LSMA answers "what is the trend value now?", TSF answers "what will the trend value be next bar?" The relationship is exact: `TSF = LSMA + slope`, where slope is the per-bar rate of change of the regression line.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. O(1) Incremental Linear Regression
|
||||
|
||||
The implementation uses running sums (`SumY`, `SumXY`) with a reversed-x convention where `x=0` corresponds to the newest bar. This allows O(1) updates without maintaining the full regression matrix.
|
||||
|
||||
**Constants (precomputed once):**
|
||||
|
||||
$$\Sigma_x = \frac{n(n-1)}{2}, \quad \Sigma_{x^2} = \frac{(n-1) \cdot n \cdot (2n-1)}{6}$$
|
||||
|
||||
$$D = n \cdot \Sigma_{x^2} - \Sigma_x^2$$
|
||||
|
||||
### 2. O(1) Sum Updates
|
||||
|
||||
When a new value enters and the oldest drops:
|
||||
|
||||
$$\Sigma_{xy}^{new} = \Sigma_{xy}^{old} + \Sigma_y^{old} - n \cdot v_{oldest}$$
|
||||
|
||||
$$\Sigma_y^{new} = \Sigma_y^{old} - v_{oldest} + v_{new}$$
|
||||
|
||||
### 3. Regression Parameters
|
||||
|
||||
$$m = \frac{n \cdot \Sigma_{xy} - \Sigma_x \cdot \Sigma_y}{D}$$
|
||||
|
||||
$$b = \frac{\Sigma_y - m \cdot \Sigma_x}{n}$$
|
||||
|
||||
In the reversed-x convention, `b` is the regression value at the current bar (x=0), and `m` is negative for uptrends.
|
||||
|
||||
### 4. TSF Calculation
|
||||
|
||||
$$\text{TSF} = b - m$$
|
||||
|
||||
This projects one step forward from the current bar. Equivalently, in standard convention (x=0=oldest):
|
||||
|
||||
$$\text{TSF} = \text{slope} \cdot n + \text{intercept}$$
|
||||
|
||||
### 5. Resync Guard
|
||||
|
||||
After every 1000 ticks, running sums are recomputed from the buffer to prevent floating-point drift accumulation.
|
||||
|
||||
## Mathematical Precision & Implementation Philosophy
|
||||
|
||||
### Relationship to Other Indicators
|
||||
|
||||
| Indicator | Formula | Interpretation |
|
||||
|-----------|---------|----------------|
|
||||
| **LSMA** (offset=0) | `b` | Regression value at current bar |
|
||||
| **TSF** | `b - m` | Regression value one step ahead |
|
||||
| **LSMA** (offset=1) | `b - m × 1` | Same as TSF |
|
||||
| **CFO** | `100 × (price - TSF_at_current) / price` | Forecast error as percentage |
|
||||
| **Inertia** | `price - TSF_at_current` | Raw forecast error (residual) |
|
||||
|
||||
### FMA Usage
|
||||
|
||||
All critical multiplications use `Math.FusedMultiplyAdd` for precision, including:
|
||||
- SumXY O(1) update: `FMA(-period, oldest, sumXY + prevSumY)`
|
||||
- Slope calculation: `FMA(n, sumXY, -sumX × sumY)`
|
||||
- Intercept calculation: `FMA(-m, sumX, sumY)`
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
|-----------|-------|---------------|----------|
|
||||
| ADD/SUB | 4 | 1 | 4 |
|
||||
| MUL | 0 | 3 | 0 |
|
||||
| DIV | 2 | 12 | 24 |
|
||||
| FMA | 3 | 5 | 15 |
|
||||
| CMP | 1 | 1 | 1 |
|
||||
| **Total** | **10** | | **~44** |
|
||||
|
||||
### Batch Mode (SIMD Analysis)
|
||||
|
||||
The O(1) running-sum algorithm is inherently serial due to data dependencies. Batch mode uses `stackalloc` for small buffers (≤256 elements) to avoid heap allocation.
|
||||
|
||||
| Mode | Per-Bar Cost | Notes |
|
||||
|------|-------------|-------|
|
||||
| Streaming | ~44 cycles | O(1) update |
|
||||
| Batch (Span) | ~44 cycles | Same algorithm, zero-alloc |
|
||||
| Batch (TSeries) | ~44 cycles + state restore | Replays last N bars |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score (1-10) | Justification |
|
||||
|--------|-------------|---------------|
|
||||
| Accuracy | 9 | Exact OLS regression, FMA precision |
|
||||
| Timeliness | 10 | Leading indicator (projects forward) |
|
||||
| Overshoot | 7 | Extrapolation amplifies noise |
|
||||
| Smoothness | 5 | Less smooth than LSMA (forecast adds slope) |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| LSMA(offset=1) | ✅ | Mathematical identity, exact match |
|
||||
| TA-Lib | 🔲 | `TA_TSF` available in TALib.NETCore |
|
||||
| Skender | ❌ | No direct TSF method |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **TSF is not LSMA.** LSMA = regression value at the current bar. TSF = one step ahead. The difference equals the regression slope. Using TSF as a smoothing average will produce systematically biased results.
|
||||
|
||||
2. **Single-step forecast only.** TSF projects exactly one bar forward. Multi-step extrapolation (TSF at offset=2, 3, ...) accumulates error quadratically. For multi-step forecasting, use AFIRMA or dedicated time-series models.
|
||||
|
||||
3. **Warmup = period bars.** The indicator needs a full window of data before regression is meaningful. During warmup, TSF returns raw input values.
|
||||
|
||||
4. **Noise amplification.** Because TSF adds the slope to the endpoint value, it amplifies short-term noise. Use longer periods (20+) for less noisy forecasts, or combine with a smoother like LSMA.
|
||||
|
||||
5. **Bar correction support.** The `isNew=false` pathway correctly rolls back state using the `_ps` (previous state) pattern. Always use `isNew=false` for intra-bar updates in live trading.
|
||||
|
||||
6. **Resync interval.** Running sums are recomputed every 1000 ticks to prevent floating-point drift. This adds negligible overhead but ensures long-running accuracy.
|
||||
|
||||
## References
|
||||
|
||||
- Tushar Chande, *The New Technical Trader*, 1994
|
||||
- TA-Lib: `TA_TSF` function (www.ta-lib.org)
|
||||
- PineScript: `ta.linreg(source, length, -1)` (offset=-1 = one step ahead)
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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("TUKEY_W: Tukey (Tapered Cosine) Window MA", shorttitle="TUKEY_W", overlay=true)
|
||||
|
||||
// @function Calculates the Tukey (Tapered Cosine) Window Moving Average.
|
||||
// The Tukey window has cosine-tapered edges and a flat-top center.
|
||||
// Parameter alpha controls the taper fraction:
|
||||
// alpha=0 → rectangular window (SMA)
|
||||
// alpha=1 → Hann window (full cosine taper)
|
||||
// alpha=0.5 → half tapered, half flat (typical default)
|
||||
// For sample n in [0, N-1]:
|
||||
// if n < alpha*(N-1)/2: w(n) = 0.5*(1 - cos(2π*n / (alpha*(N-1))))
|
||||
// if n > (N-1)*(1 - alpha/2): w(n) = 0.5*(1 - cos(2π*(N-1-n) / (alpha*(N-1))))
|
||||
// else: w(n) = 1.0 (flat-top center)
|
||||
// @param src Series to smooth.
|
||||
// @param period Window length. Must be >= 2.
|
||||
// @param alpha Taper fraction (0 = rectangular, 1 = Hann). Default 0.5.
|
||||
// @returns The Tukey window weighted average.
|
||||
export tukey_w(series float src, simple int period, simple float alpha) =>
|
||||
float sumWV = 0.0
|
||||
float sumW = 0.0
|
||||
int N = period - 1
|
||||
float aN = alpha * N
|
||||
for i = 0 to N
|
||||
float w = 1.0
|
||||
if aN > 0.0
|
||||
if i < aN / 2.0
|
||||
w := 0.5 * (1.0 - math.cos(2.0 * math.pi * i / aN))
|
||||
else if i > N - aN / 2.0
|
||||
w := 0.5 * (1.0 - math.cos(2.0 * math.pi * (N - i) / aN))
|
||||
sumWV += src[i] * w
|
||||
sumW += w
|
||||
sumWV / sumW
|
||||
|
||||
// ── Inputs ──────────────────────────────────────────────
|
||||
p = input.int(20, "Period", minval=2)
|
||||
a = input.float(0.5, "Alpha (taper fraction)", minval=0.0, maxval=1.0, step=0.05, tooltip="0=SMA, 1=Hann, 0.5=half taper")
|
||||
|
||||
// ── Calculation ─────────────────────────────────────────
|
||||
result = tukey_w(close, p, a)
|
||||
|
||||
// ── Plot ────────────────────────────────────────────────
|
||||
plot(result, "TUKEY_W", color=color.yellow, linewidth=2)
|
||||
Reference in New Issue
Block a user