Technical analysis (ta.*)

ta.* indicators: series helpers, bar mode, interpret↔compile parity, and submodule layout.

This page

Technical analysis (ta.*)

Abstract

The ta.* namespace is the largest pure-compute surface in PYNE: moving averages, oscillators, volatility bands, volume studies, pattern helpers, and cross/rise/fall predicates. Implementations live under technical_submodules/ and are composed into TechnicalAnalysisMixin. Numerical behavior is validated against TradingView reference series (see numerical validation); the design goal is IEEE-limit parity, not “approximate TA.” Interpret and compile (Numba) hosts share the same formulas for the kernels listed under Interpret ↔ compile parity.

Conceptual model

Diagram

Rendering…

Hosts in production set bar mode so indicator calls return scalars for the active bar, matching how Pine expressions compose (ta.ema(a) - ta.ema(b)).

Incremental hot path

When _pine_bar_mode and _pine_ta_incremental are enabled (Runtime default; disable with PYNE_TA_INCREMENTAL=0), hot ta.* builtins update call-site state once per bar instead of recomputing full history. Call sites are indexed like crossovers (_ta_call_i reset each bar). Nested forms such as ta.ema(ta.sma(close, 14), 10) stay correct because each call keeps its own slot. Golden suite: tests/test_ta_incremental.py (incremental last values ≡ full recompute).

FamilyIncremental builtins (non-exhaustive)
Moving averagessma, ema, rma, wma, hma, vwma, swma, kama, dema, tema, alma
Oscillatorsrsi, macd, stoch/stochrsi, cci, cmo, tsi, roc, wpr, aroon, dpo, kst
Volatility / structureatr, tr, stdev, bb, highest/lowest, linreg, adx/dmi, supertrend, kc, sar, donchian
Volume (0.3.10–0.3.11)obv, wad/wvad, cmf, klinger, mfi, vwap, accdist, nvi, pvi
Otherchange/mom/cum, median, percentrank, rising/falling, barssince, pivots

ta.nvi / ta.pvi are incremental (O(1)/bar; PYNE_TA_INCREMENTAL=0 full-list). ta.atr is Wilder RMA of true range on interpret (full + incremental) and Numba (ta.rma(ta.tr, length) — 0.3.4+). Supertrend is locked mid±factor·ATR (not the TV band ratchet).

Interface surface

Dispatch keys (non-exhaustive; map is authoritative in technical.py):

FamilyExamples
Moving averagesta.sma, ta.ema, ta.wma, ta.rma, ta.hma, ta.vwma, ta.swma, ta.alma
Oscillatorsta.rsi, ta.stoch, ta.cci, ta.cmo, ta.mfi, ta.roc, ta.wpr, ta.tsi, ta.rci
Trend / channelsta.macd, ta.adx, ta.dmi, ta.supertrend, ta.sar, ta.linreg
Volatilityta.atr, ta.tr, ta.bb, ta.bbw, ta.kc, ta.kcw, ta.stdev, ta.variance
Volumeta.obv, ta.vwap, ta.mfi (shared), volume submodule helpers
Structureta.highest / lowest / highestbars / lowestbars, ta.pivothigh / pivotlow
Predicatesta.crossover, ta.crossunder, ta.cross, ta.rising, ta.falling
Stats / otherta.change, ta.mom, ta.cum, ta.median, ta.mode, percentiles, ta.barssince, ta.valuewhen, ta.correlation

Argument conventions

  • Typical form: ta.fn(series, length).
  • Some functions allow period-only calls (e.g. ta.highest(20)) defaulting source to high / context series via _expect_series(..., allow_period_only=True).
  • Multi-value returns (e.g. MACD, BB, Supertrend) unpack as tuples/lists; assignment uses StatementEvaluator unpack rules.
  • Fractional lengths floor to int (TradingView-compatible).

Stateful crosses

ta.crossover / ta.crossunder need previous-bar pairs. In bar-mode runs the host resets a call-site index (_cross_call_i) each bar so multiple cross calls in one script keep independent state.

Internals

PathRole
src/pynescript/ast/evaluator/builtins/technical.pyDispatch map aggregation
.../technical_submodules/core.pySeries coerce, expect helpers, bar finalize, incremental kernels
.../moving_averages.py, oscillators.py, volatility.py, volume.py, …Kernels + inc wiring
.../common.py, basic.py, advanced.py, patterns.py, strategies.py, synthesizer.py, economics.pyAdditional families
src/pynescript/compiler/numba_builtins.pyCompile-path Numba kernels (must track interpret formulas)
tests/test_ta_indicators_*.py, tests/test_indicators.pyRegression
tests/test_ta_incremental.pyInc ≡ full recompute golden (full CI gate)
tests/test_first_party_ta_goldens.pyDual-host first-party goldens (ATR / Supertrend / Keltner)
tests/test_interp_compile_parity.pyAlways-on dual-host smoke + full-corpus mark
scripts/compare_interp_compile.pyCorpus harness (report under .cache/interp_compile_parity.json)
tests/test_compiler_numba.py (TestInterpCompilePlotParityFixes, highestbars offsets, …)Targeted formula locks
docs/numerical_validation_report.mdPublished precision summary (TV / IEEE bounds)

History for wrapper series is reversed to chronological order and may be truncated (_SERIES_MAX) before full kernels run. Incremental path only needs series[-1] per call, so truncation does not freeze state.

Interpret ↔ compile parity

Numba kernels are separate code from the interpreter, but the dual-host contract is same formula, same warm-up mask, same na rules for the surface below. Drift is a bug, not an allowed “approx compile.” Prove regressions with the interp↔compile harness and the numerical validation page (TV / IEEE bounds).

TopicShared behavior
ta.rsiWilder on both hosts: SMA seed of the first period deltas, then RMA of gains/losses. First finite bar is index period (period deltas need period+1 prices). Not a simple window average.
ta.rocStandard TV formula 100 * (src - src[length]) / src[length]. Warm-up is na, never an early 0.0. Zero or missing baseline → na. Interpret no longer used a wrong lookback denominator or early zero.
ta.wmaRequires a full non-na window of length N. Nested forms such as ta.wma(ta.roc(...), …) stay na until the inner series has produced N finite samples (no partial-window reweight).
ta.cumRunning sum; Pine na / IEEE NaN treated as 0 (skipped contribution), matching TradingView cumulative-sum semantics.
ta.highestbars / ta.lowestbarsReturn negative bars-back offsets: 0 if the extreme is the current bar, -1 one bar ago, …, -(length-1) at the far edge. Short history (i + 1 < length) returns -1. Matches Aroon-style scripts that index with high[ta.highestbars(...)] / math.abs(ta.lowestbars(...)).
math.avg (multi-arg)Arithmetic mean of the arguments. Any na argument → na (do not skip). Not a rolling ta.sma.
ta.linregLeast-squares fit over the window; endpoint at x = n−1 when offset=0 (TV): mean_y + slope * ((n−1) − mean_x) / compile intercept + slope * (n − 1 − offset). Length < 2 → na.
ta.mfiWarm-up aligned on both hosts: needs length + 1 typical-price samples (direction vs previous bar). Equal typical prices contribute to neither side; only-pos / only-neg MF → 100 / 0 (or 50 when both empty).
ta.rciRank Correlation Index (Spearman of time vs value ranks) is implemented on the compile path (numba_rci) as well as interpret; stays in numeric mode when called.

0.3.4+: ta.atr is Wilder RMA of TR on interpret + Numba (not EMA-of-TR). EMA dual-host seed is full-list SMA seed matching incremental/Numba. First-party dual-host goldens (ATR / Supertrend / Keltner) and test_ta_incremental gate residual drift. Any remaining gaps are tracked under missing features—not as silent “close enough” for the rows above.

Parity harness

# Always-on smoke (stable scripts under tests/test_interp_compile_parity.py)
pytest tests/test_interp_compile_parity.py -q

# Corpus compare (default ~50 scripts × 1000 bars; writes report JSON)
python scripts/compare_interp_compile.py --bars 1000 --limit 50
python scripts/compare_interp_compile.py --glob 'average_*.pine' --bars 200

# Optional longer pytest path
pytest tests/test_interp_compile_parity.py -m interp_compile_full

# Formula locks (RSI/ROC/WMA/cum/math.avg/highestbars, …)
pytest tests/test_compiler_numba.py -k 'InterpCompilePlotParity or highestbars_lowestbars_negative' -q

Report path: .cache/interp_compile_parity.json. Methodology and acceptable relative-error bands vs TradingView: Numerical validation.

Invariants & edge cases

  1. Warm-up → na. Insufficient bars (e.g. SMA length (N) needs (N) samples; RSI needs period+1 prices) yield None / leading na in full-series mode.
  2. NA in window. Many kernels propagate na if any window element is missing (SMA-like sums, WMA, MFI money-flow samples). ta.cum is the TV exception (na → 0 contribution).
  3. Default sources. ta.atr pulls high/low/close from current_series / context when not passed explicitly.
  4. Compile kernels must track interpret. Numba numba_* implementations are separate source; for the parity table above they are formula-locked by dual-host tests—do not “approximate” for speed.
  5. No chart look-ahead. Kernels only see history available at the current bar index; request.security lookahead is a separate concern.
  6. Incremental ≡ full recompute (oracle). ATR seed is already Wilder RMA on both hosts; changing seed rules is a correctness project, not a silent perf flag.

Worked examples

Classic overlay

//@version=6
indicator("SMA 14")
v = ta.sma(close, 14)
plot(v)

In bar mode each visit returns one float (or na); the host stacks them into a series for the response envelope.

Cross entry signal

//@version=6
strategy("X")
fast = ta.ema(close, 12)
slow = ta.ema(close, 26)
if ta.crossover(fast, slow)
    strategy.entry("L", strategy.long)

Cross state is bar-local; ensure the runtime resets cross call indices when reusing an evaluator.

Multi-value unpack

[macdLine, signal, hist] = ta.macd(close, 12, 26, 9)
plot(hist)

Failure modes

SymptomCause
Always naLength longer than available history; or series not updated
Wrong vs TV by large marginWrong source series; mock OHLCV; or non-bar-mode list composition bug
Cross never firesCall-index state not reset / shared incorrectly across bars
Compile diverges from interpretKernel drift in numba_builtins.py (RSI must stay Wilder; ROC warm-up must stay na; WMA full window; highestbars sign; math.avg na rule)—re-run the parity harness
Early ROC / nested WMA finite too soonLegacy quirks: ROC returned 0.0 before lookback, or WMA reweighted partial/na windows—fixed on both hosts
high[ta.highestbars(...)] mostly naOffsets are negative; only offset 0 indexes the current bar without math.abs
math.avg finite when an arg is naBug: multi-arg avg must propagate na, not skip

See also