Troubleshooting

Sorted by what you actually see, not by cause. Most of these are pomata behaving exactly as designed — the fix is usually one line.

My output is all null (or null for far longer than I expected)

That is the warm-up. A window of length n cannot produce a value until it has seen n observations, so the first n - 1 rows are null — and the chained averages stack that up: dema warms up over 2 * (n - 1) rows, t3 over 6 * (n - 1), the cycle indicators over sixty-odd. If the whole column is null, your series is shorter than the warm-up the function owes. This is not a bug and it is not negotiable: seeding a window with a fabricated value would be a lie that compounds downstream. Each function documents its exact warm-up length.

A second, sneakier cause: a null in the input. A leading null does not consume warm-up budget, and an interior null propagates through a recursion — so a column with gaps warms up later than a clean one.

My multi-asset result is contaminated across tickers

You left off .over. Without it, a window or a recursion runs down the entire column and happily averages the end of one ticker into the start of the next.

# wrong: AAA's last bars leak into BBB's first
frame.with_columns(sma=sma(pl.col("close"), 20))

# right: each ticker is computed on its own
frame.with_columns(sma=sma(pl.col("close"), 20).over("ticker"))

The same applies to the .shift(1) on your signal and to anything else with memory: if it spans bars, it needs the .over. Sort by ticker then time first, so the groups are contiguous.

A whole metric came back NaN

A NaN reached it — and a NaN is not a missing value, it is a real number that contaminates everything it touches. One NaN in a returns series is enough to turn a Sharpe ratio into NaN, because the mean and the standard deviation both see it. pomata will not quietly drop it for you; that would hide a real problem in your data.

Track down where it was born. The usual sources are a 0 / 0 (a flat window, a zero denominator), an inf - inf, or an inf that arrived from an upstream divide-by-zero. A null, by contrast, is skipped or carried across and will not poison a reduction — so if you meant “missing”, make sure you have a null, not a NaN.

Eager and lazy give me different numbers

They should not — the suite pins eager and lazy to bit-for-bit agreement on every function, so pomata is almost never the difference. Look upstream first: a different row order reaching the two paths, an unsorted join, or a non-deterministic source (a set iteration, an unsorted group_by without maintain_order). Pin the order before the pomata call and the two paths converge.

A rolling statistic loses precision on an extreme series

Known and documented. A two-pass rolling sum can shed digits when an entire window collapses onto the float-precision floor of a much larger value that recently passed through it — it takes a deliberately adversarial path (a price dropping many orders of magnitude bar to bar) to trigger. It is noted on the affected indicators, and the oscillators with a hard bound are clamped rather than left to drift. trust has the full account.