9.7 KiB
NMA: Natural Moving Average
Jim Sloman looked at how volatility distributes across a window and asked: if the most volatile bars are recent, should the filter not respond faster? NMA derives its smoothing constant from the volatility profile itself, weighted by a square-root kernel that emphasizes recent action.
| Property | Value |
|---|---|
| Category | Trend (IIR MA) |
| Inputs | Source (close) |
| Parameters | period |
| Outputs | Single series (Nma) |
| Output range | Tracks input |
| Warmup | period bars |
| PineScript | nma.pine |
| Signature | nma_signature |
- NMA is an adaptive IIR filter whose smoothing ratio is derived from a volatility-weighted square-root kernel analysis of log-price movements over a...
- Similar: KAMA, VIDYA | Complementary: Noise filters | Trading note: Noise-elimination MA; adapts to signal-to-noise ratio.
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
NMA is an adaptive IIR filter whose smoothing ratio is derived from a volatility-weighted square-root kernel analysis of log-price movements over a lookback window. When volatility concentrates in recent bars, the ratio approaches 1.0 (fast tracking). When volatility is spread uniformly, the ratio approaches 1/\sqrt{N} (heavy smoothing). The square-root kernel (\sqrt{i+1} - \sqrt{i}) gives a concave-down weighting that gently emphasizes recency, while the log-price transformation normalizes for price level, making the adaptation scale-invariant.
Historical Context
Jim Sloman introduced the Natural Moving Average in Ocean Theory (pages 63-70), a book that applied chaos and complexity theory metaphors to financial markets. The NMA was designed as a "natural" filter that lets the market's own volatility structure determine the smoothing rate, rather than imposing an arbitrary period.
The core innovation is the square-root differencing kernel \sqrt{i+1} - \sqrt{i} as the weighting function for volatility. This kernel has the property that its cumulative sum \sqrt{N} grows sublinearly, meaning each additional bar in the lookback contributes less weight than the previous one. This creates a "diminishing returns" effect: extending the lookback adds context without drowning out recent information.
The log-price transformation (\ln(\text{price}) \times 1000) serves two purposes: (1) it makes the volatility measure proportional to percentage moves rather than absolute dollar moves, and (2) the scaling factor of 1000 brings typical values into a numerically convenient range for the ratio computation.
NMA belongs to the family of adaptive moving averages alongside KAMA, VIDYA, and ADXVMA, but uses a unique adaptation mechanism based on the spatial distribution of volatility rather than a single efficiency or strength metric.
Architecture & Physics
1. Log-Price Buffer
A circular buffer of size N+1 stores \ln(\text{price}) \times 1000 for each bar, providing the lookback data for volatility computation.
2. Volatility-Weighted Square-Root Ratio
For each bar i in the lookback:
o_i = |\ln_i - \ln_{i+1}|
\text{num} = \sum_{i=0}^{N-1} o_i \cdot \left(\sqrt{i+1} - \sqrt{i}\right)
\text{denom} = \sum_{i=0}^{N-1} o_i
\text{ratio} = \frac{\text{num}}{\text{denom}}
3. Adaptive EMA Step
\text{NMA}_t = \text{NMA}_{t-1} + \text{ratio} \times (x_t - \text{NMA}_{t-1})
Mathematical Foundation
Log-price volatility:
o_i = \left|\ln(x_{t-i}) - \ln(x_{t-i-1})\right| \times 1000
Square-root kernel weights:
\phi_i = \sqrt{i+1} - \sqrt{i} = \frac{1}{\sqrt{i+1} + \sqrt{i}}
Note: \phi_i \approx \frac{1}{2\sqrt{i}} for large i, confirming the 1/\sqrt{i} decay rate.
Adaptive ratio:
r = \frac{\sum_{i=0}^{N-1} o_i \cdot \phi_i}{\sum_{i=0}^{N-1} o_i}
Ratio bounds:
- If all volatility is at
i = 0(most recent):r = \phi_0 = \sqrt{1} - \sqrt{0} = 1 - If volatility is uniform:
r = \frac{\sum \phi_i}{N} = \frac{\sqrt{N}}{N} = \frac{1}{\sqrt{N}} - For
N = 40: uniform ratio\approx 0.158, equivalent to EMA period\approx 11
IIR update:
\text{NMA}_t = \text{NMA}_{t-1} + r_t \cdot (x_t - \text{NMA}_{t-1})
Default parameters: period = 40, minPeriod = 1.
Pseudo-code (streaming):
// Store scaled log-price
lnBuf[head] = log(src) * 1000
// Compute volatility-weighted ratio
num = 0; denom = 0
for i = 0 to bars-1:
oi = |lnBuf[t-i] - lnBuf[t-i-1]|
num += oi * (sqrt(i+1) - sqrt(i))
denom += oi
ratio = denom != 0 ? num/denom : 0
// Adaptive EMA step
result = result + ratio * (src - result)
Performance Profile
Operation Count (Streaming Mode)
| Operation | Count per Update | Notes |
|---|---|---|
| Log | 1 | Math.Log(price) |
| Abs | N |
` |
| Multiply | N |
o_i \times \phi_i |
| Add | 2N + 1 |
Numerator sum + denominator sum + EMA step |
| Divide | 1 | num / denom |
| FMA | 1 | FusedMultiplyAdd(prev, decay, ratio * price) |
| Total | \approx 4N + 4 |
N = \text{period} |
For period = 40: approximately 164 FLOPs per streaming update.
Batch Mode (SIMD Analysis)
The inner ComputeRatio() loop walks backward through the ring buffer with data-dependent indexing, which resists SIMD vectorization. The batch Calculate(Span) method uses the same scalar loop per bar.
SIMD opportunity exists for the sqrt-weight precomputation (done once in the constructor), but not for the per-bar ratio computation due to the sequential buffer access pattern.
| Metric | Score |
|---|---|
| Streaming latency | 8/10 (O(N) per bar, but small constant) |
| Batch throughput | 5/10 (O(N*M) total, no SIMD in hot loop) |
| Memory efficiency | 9/10 (single RingBuffer + precomputed weights) |
| Warmup speed | 9/10 (hot after N bars) |
| Numerical stability | 7/10 (log-scale amplifies FP drift in corrections; mitigated by CopyFrom pattern) |
Memory Layout
| Field | Type | Size | Purpose |
|---|---|---|---|
_lnBuf |
RingBuffer | ~40B + (N+1)x8B | Circular log-price buffer |
_p_lnBuf |
RingBuffer | ~40B + (N+1)x8B | Backup buffer for bar correction |
_sqrtWeights |
double[] | Nx8B | Precomputed \sqrt{i+1} - \sqrt{i} |
_state |
State | 32B | Current NMA, last NMA, bar count, flags |
_p_state |
State | 32B | Previous state for rollback |
| Total | ~144B + 3Nx8B |
For period = 40: approximately 144 + 984 = 1128 bytes per instance.
Bar Correction Pattern
NMA requires full buffer copy (CopyFrom) for bar correction rather than the lighter Snapshot/Restore used by simpler indicators. The reason: ComputeRatio() reads all buffer positions during backward traversal, so a single-value restore is insufficient.
if (isNew) { _p_state = _state; _p_lnBuf.CopyFrom(_lnBuf); }
else { _state = _p_state; _lnBuf.CopyFrom(_p_lnBuf); }
_ = _lnBuf.Add(lnVal); // always Add() since CopyFrom restores pre-Add state
Validation
| Library | Batch | Streaming | Span | Notes |
|---|---|---|---|---|
| Skender | N/A | N/A | N/A | Not available |
| TA-Lib | N/A | N/A | N/A | Not available |
| Tulip | N/A | N/A | N/A | Not available |
| Ooples | N/A | N/A | N/A | Not available |
NMA is a proprietary indicator from Sloman's Ocean Theory. No reference implementations exist in standard TA libraries. Validation relies on:
- Internal consistency: batch == streaming == span == eventing (4-mode consistency test)
- Mathematical verification: ratio bounds
[1/\sqrt{N}, 1]confirmed - Edge cases: NaN/Infinity handling, bar correction precision
Common Pitfalls
-
Log of non-positive prices: If
price <= 0,Math.Logreturns-InfinityorNaN. The implementation guards withprice > 0 ? Math.Log(price) * 1000 : 0.0. -
Bar correction drift with Snapshot/Restore: RingBuffer's
Snapshot()/Restore()only saves one buffer position. NMA'sComputeRatio()reads ALL positions, soCopyFrom()is mandatory. Using Snapshot/Restore produces ~1% drift after corrections. -
Zero denominator in ratio: When all adjacent log-prices are identical (
o_i = 0for alli), the denominator is zero. The implementation returnsratio = 0, causing NMA to hold its previous value. -
Period = 1 degeneracy: With a single-bar lookback,
ComputeRatio()has zero iterations and returns 0. NMA becomes a constant after initialization. Useperiod >= 2for meaningful adaptation. -
Log-scale amplification: The
\times 1000scaling factor amplifies differences between log-prices. While this improves numerical resolution for the ratio computation, it also amplifies floating-point errors during buffer operations. -
Memory cost of CopyFrom: Each bar correction copies the entire buffer array (
N+1doubles = 328 bytes for period 40). This is ~8x more expensive than Snapshot/Restore but necessary for correctness. -
No external validation available: Unlike SMA, EMA, or KAMA, there are no reference implementations to validate against. All correctness assurance comes from internal consistency tests and mathematical bound verification.
Resources
- Sloman, J. Ocean Theory. Pages 63-70. (Original NMA description.)
- Kaufman, P.J. (2013). Trading Systems and Methods, 5th ed. Wiley. Chapter 7: Adaptive Moving Averages.
- Chande, T.S. & Kroll, S. (1994). The New Technical Trader. Wiley. (Adaptive filter framework.)