Repository navigation
Released 2026-10-10. Full details in CHANGELOG.md.
On PyPI as ta_py 2.1.0.
⚠️ Read this before upgradingSeven functions return different numbers, and three of them return a different number of rows. Each was verified against the implementation it was ported from (ta.js, or the Pine original) and each fix has a test that fails against the old code — but nothing raises, so an existing pipeline will consume the new values silently. They were found by running identical data through all three ports and comparing: ta.py disagreed with ta.js and ta.go, and in every case ta.py was the one that was wrong.
function what changed rocvalues: returned a fraction, now percent ( 100 * (last - reference) / reference).kstandcopare built on it and move with it.kstsignature: positional parameters were interleaved ( r1, s1, r2, s2, …), now grouped (r1..r4, s1..s4, sig) as in ta.js. Defaults unchanged; positional callers are not.harow count: n + 1→n. The loop re-processed bar 0, so the second row was a duplicate over the seed.aroon_up,aroon_oscvalues: inverted — 0 when the window's high was its newest bar, 100 when it was the oldest. aroon_downwas already correct.psarvalues: the downtrend extreme tracked the high instead of the low, so the SAR sat above the market through every decline. zigzagvalues: five bookkeeping deviations from ta.js — pivots were delayed a bar, legs were reset from ±infinity, and the flat-input branch mixed its indices. hullvalues: the half-length and the final smoothing window were truncated instead of rounded, so the output differed from ta.js at every length that is 1 mod 4 (5, 9, 13, 17, …). er,ar,kellyone-sided input: raised ZeroDivisionError; now returns the geometric mean of the side that exists, and a Kelly fraction of 1 / negative.If you index results by bar, or compare them against stored values, re-run before trusting them.
Added
-
occ(data, basis_len=8, basis_type="SMMA", delay=0)— Open Close Cross, from JustUncleL's Pine (source), requested in #4. Moving averages ofopenand ofclose; the trend is whichever is on top and a flip is the signal. Rows are[open, close]and the output aligns to the input bars as[close basis, open basis, direction], NaN until both bases exist.delayis the Pine'sdelayOffset, which shifts both bases right.Eight of the Pine's twelve
basisTypeoptions are supported:SMA,EMA,DEMA,TEMA,WMA,SMMA,HullMA,TMA. The other four are omitted rather than approximated —LSMAtakes an offset parameterlsmadoes not have,VWMAneeds volume in the data row, andALMAandSSMA(Ehlers SuperSmoother) are not implemented in this library. TradingView's alert conditions, alternate-resolution mode and divergence plot are platform features, not computations, and have no counterpart in a library that returns series.Two caveats worth knowing. The Pine's variant definitions were checked against the existing primitives for DEMA, TEMA and Hull across even and odd lengths (
~1e-11, float noise), so those are the same averages. And with an oddbasis_len, Hull's half-length is rounded to even in Python where Pine rounds half away from zero, so that one combination may differ; every other type is unaffected.
Fixed
-
hullrounded where the reference rounds half up. Its half-length came fromround(l / 2), and Python'sround()is half-to-even: for lengths wherel / 2is a half-integer with an even integer part — 5, 9, 13, 17, … — it gave 2 where ta.js'sMath.roundgives 3, so the whole line differed. Measured against ta.js on one shared series, lengths 2-20: differing at 5, 9, 13 and 17 before, none after. Found while checkingocc'sHullMAbasis, which inherits it. -
alligator/gatorno longer match TradingView by accident. Both were returning their rows newest-first and truncated ton - jl - js + 2rows, so they could not be lined up with the input series at all, and each line was displaced byshift - 1bars instead ofshift.gator's upper histogram also had the wrong sign — it plottedjaw - teethinstead of|jaw - teeth|. Reported in #10. -
kamanow computes Kaufman's adaptive average correctly. Four bugs, all inherited from the ta.js original: the smoothing constant dropped its slow term (- 2/(l3+1) + 2/(l3+1)cancelled, leavingER * fast); the volatility window summedl1-1one-bar changes instead ofl1, which let the efficiency ratio exceed 1 and divided by zero on flat input; the seed was the SMA of the last window rather than the first, so the opening value was computed from data at the end of the series; and the loop started one bar late, emittingn - l1values instead ofn - l1 + 1. Nothing raises before or after — every call simply returns different numbers.The recursion now matches tulind, the reference the benchmarks compare against: bit-identical on short, flat, trending and gapping series, max Δ 1.4e-14 on a 100k random walk. Carrying the volatility sum across bars instead of re-summing it per bar also makes it ~2.5× faster. Fixes the same defect as Bitvested/ta.js#22, mirrored in ta.go.
-
halftrendnever flipped, and read its inputs from the wrong bars. Reported in #7. Three defects against the Pine it was ported from (everget's HalfTrend):- the flip tests read
sma(...)[0]— the SMA at the start of the window, roughlyatrlenbars old — instead of the SMA at the current bar. Both flip conditions therefore compared stale values and never fired: on 4901 bars of the reporter's 5-minute data the indicator produced zero trend changes and stayed on one line. - the price extremes behind
maxlow/minhighwere taken over the ATR window (atrlenbars) instead ofamplitudebars, and the down-flip comparedclosewith<where the Pine uses>. - the window was
data[i-atrlen:i], excluding the current bar, so every value was one bar stale.
The rewrite follows the Pine line by line: one row per input bar, extremes over
amplitude, the state machine evaluated at the current bar, and an ATR that uses Pine's own seeding (Wilder's RMA seeded with the mean of the firstatrlentrue ranges, which is not whatta_py.atrdoes). On the reporter's data it now flips 160 times. Pinned bytests/test_halftrend.py, which fails against the previous implementation. - the flip tests read
Changed — BREAKING for zigzag consumers
zigzagnow follows the ta.js algorithm, which the audit showed it did not. Five deviations, all silent: extending a running pivotcontinued past the bar's move test, delaying every pivot by a bar; the leg reset seededmin/maxwith ±infinity where the original seeds them with the new extreme; the flat-branch bounds compareddata[i][0]but storeddata[i][1]; andZeroDivisionErrorfallbacks replaced the original's IEEE arithmetic. On a 300-bar series it disagreed with ta.js and ta.go in 186 of 298 values; it now matches both exactly (max Δ0.0), on high/low pairs and flat input alike.zigzag__pairspinned the old interpolation and is re-pinned.
Changed — BREAKING for psar consumers
psartracked the wrong extreme in a downtrend. Where it should have taken the lowest low as the extreme point, it took the high, so the stop-and-reverse sat above the market through a decline and every subsequent value was wrong. On a 300-bar series it disagreed with ta.js and ta.go in 63 values; it now matches both exactly (max Δ0.0).tests/test_indicators.pypins an 18-bar decline in which the fix moves 12 values.
Changed — BREAKING for er, ar and kelly consumers
-
A one-sided series is no longer an error.
erdivided by the count of the side that may be empty, so a series with no losses raisedZeroDivisionError, andarandkellyraised with it. An empty side carries weight zero, so its geometric mean is never taken and contributes nothing:er([0.02, 0.01, 0.05, 0.03])is now0.0274(the geometric mean of the wins),arreturns finite residuals, andkellyreturns1.0for an all-winning series and-1.0276for an all-losing one. Mixed series are unchanged to the last bit.The same input produced three different answers across the ports — NaN in ta.js (JS evaluates
1 ** Infinityas NaN), a finite value in ta.go (wheremath.Pow(1, +Inf)is 1), an exception here — and all three now return these numbers, with the empty side handled explicitly rather than left to each language's exponentiation edge case.
Changed — BREAKING for roc, kst, ha and aroon consumers
Found by a cross-language audit (identical series through 60 indicators in ta.js, ta.go and ta.py): ta.py disagreed with both siblings on these. Nothing raises — every call returns different numbers.
rocnow returns percent, not a fraction.100 * (last - reference) / reference, matching ta.js, ta.go and the usual convention;ta.roc([1, 2, 3, 4], 3)was[2, 1]and is now[200.0, 100.0]. A zero reference yieldsNaNinstead of raisingZeroDivisionError.kstandcopare built onrocand move with it.kst's positional parameters are reordered to(data, r1, r2, r3, r4, s1, s2, s3, s4, sig)— rates first, then smoothing — which is what ta.js accepts. The old signature interleaved them (r1, s1, r2, s2, …), so a positional call that used to meanr1=5, s1=5, r2=7…now meansr1=5, r2=5, r3=7…. The defaults are unchanged in effect, and callers passing keywords are unaffected.hareturnsnrows forninputs, notn + 1. Its loop re-processed bar 0, so the second row was a duplicate computation over the seed.len(out) == len(data)now holds, as it does in ta.js and ta.go.aroon_upwas inverted. It returned100 - aroon_up, i.e. 0 when the window's highest high was its newest bar and 100 when it was the oldest — the opposite of the standard, of ta.js and of ta.go.aroon_downwas already correct;aroon_oscfollowsaroon_up. The pinnedaroonfixtures never caught it because they placed the extreme at a tie and at the window's middle, the two positions where an inverted Aroon returns the same number;tests/test_indicators.pynow pins the discriminating cases. A window of one bar has no width, soaroon_up/aroon_downreturnNaNthere instead of 0, matching the siblings.
Changed — BREAKING for halftrend consumers
halftrendreturns one row per input bar instead ofn - atrlen, solen(out) == len(data)and rowicovers bari. The ATR channel isNaNuntil the ATR window is full (baratrlen - 1); the trend line itself is defined from bar 0. Callers that index-shifted the old output to line it up with their data — as in the issue report — no longer need to.
Changed — BREAKING for alligator / gator consumers
-
alligator(data, ...)/gator(data, ...)output is now aligned to the input bars.len(out) == len(data)and rowicovers bari. Each line is displaced right by its own shift, matching TradingView'splot(..., offset=shift), so rowiholds the SMMA value computed at bari - shift. A line is NaN until barlength - 1 + shift(defaults: jaw at 20, teeth at 12, lips at 7).gatoris now derived fromalligator's displaced lines, which also makes it the four-argument-free way to reach TradingView's Gator Oscillator: passjs=ts=ls=0.Nothing raises — every caller below gets different numbers, not an error:
- Length changed from
n - jl - js + 2(with defaults,n - 19) ton. Anything keyed offlen(out), sliced by it, or zipped against a same-length series behaves differently. - Row order reversed.
out[0]used to be the newest value andout[-1]the oldest; nowout[0]is bar 0 andout[-1]is the newest row, which itself holds the value computed 8 bars earlier. - Leading rows are NaN where they used to be numbers.
sum,mean,minor any comparison over the whole array now propagates NaN, andout[0][0]isnanrather than the latest jaw. gator's upper histogram changed sign fromjaw - teeth(negative) to|jaw - teeth|(non-negative). The lower histogram is unchanged.
The old output was unusable for its stated purpose — that is issue #10 — but it was published in 2.0.0, so code written against it will change behaviour silently. See
MIGRATION.mdfor the before/after and for how to recover the old array shape if you depended on it. - Length changed from
data = [8, 7, 8, 9, 7, 8, 9, 6, 7, 8, 6, 8, 10, 8, 7, 9, 8, 7, 9, 6, 7, 9]
# 2.0.0 — 3 rows, newest first, not aligned to `data`
ta_py.alligator(data)
# → [[7.726445152480656, 7.813665029359981, 7.6378735707815935],
# [7.78698224852071, 7.929902890697122, 8.047341963476992],
# [7.769230769230769, 7.919889017939568, 7.809177454346241]]
# unreleased — 22 rows, one per bar of `data`
ta_py.alligator(data)
# → [[nan, nan, nan], ... 19 more ..., [7.769230769230769, 7.919889017939568, 7.809177454346241],
# [7.78698224852071, 7.929902890697122, 8.047341963476992]]