Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: ml4t/engineer
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.1.1
Choose a base ref
...
head repository: ml4t/engineer
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.1.2
Choose a head ref
  • 1 commit
  • 46 files changed
  • 1 contributor

Commits on Aug 10, 2026

  1. fix: a gap in the input never produces a silently wrong feature value (…

    …#32)
    
    * fix: a missing observation no longer produces a wrong feature value
    
    Three defects with one shape: an indicator handed a series with a gap returned
    a finite number that was simply wrong, with nothing raised and nothing null.
    
    amihud_illiquidity
      Amihud (2002) averages |R| / DVOL over the D periods the asset traded and
      divides by D. The implementation nulled untraded bars and took
      `rolling_mean(period)` over the result, which propagates a null across the
      whole following window, so it instead required every bar in the window to
      have traded. On daily equities the two agree because a listed name trades
      every day. On the NASDAQ-100 minute panel at period=30 they do not: 0.64%
      untraded bars produced a 9.08% null share, a 14x amplification. Now the
      denominator counts the bars in the window that traded.
    
    momentum.rsi
      Wilder's average is recursive and had no notion of a gap. Compiled with
      `fastmath=True`, which licenses LLVM to assume no NaN, one null in the input
      returned exactly 100.0 for 87.5% of the remaining series: median 49.55 ->
      100.0 on a 4,000-point random walk with a single hole at index 500. The
      series is now split on NaN and each gap-free run is seeded on its own, so a
      gap costs the missing bar plus `period` warmup observations and the
      oscillator then resumes. Measured on the same series: the gap nulls indices
      500-509 and the tail matches the clean series exactly.
    
    fastmath, across the feature kernels
      `fastmath=True` is what turned NaN propagation into silent garbage
      everywhere, not only in RSI. Feeding one null into every momentum,
      volatility and volume feature and comparing against the clean series, 20 of
      91 feature-column pairs returned a finite value that differed from the
      correct one, up to 91.9 for mfi and 1.6e4 for the A/D line. Removing
      `fastmath` leaves 6; fixing obv, ad and the polars A/D path leaves 0. Every
      feature now either recovers from a gap or returns NaN.
    
      obv froze its level forever, because both comparisons against a NaN previous
      close are false. ad_polars used `cum_sum`, which skips nulls and carries the
      level on as if the bar had been observed, while ad_numba propagates and the
      docstring says it propagates. Both now propagate.
    
      Cost, 5M rows: rsi 42.7 -> 53.8 ms, atr 51.4 -> 90.3 ms, adx unchanged. No
      TA-Lib exactness test moved.
    
    tests/features/test_missing_observation_handling.py carries the property for
    all 18 recursive and cumulative features, and fails on 23 cases without these
    fixes. Note for anyone bisecting this: numba's `cache=True` does not invalidate
    when a jitted *callee* changes, so delete src/**/__pycache__/*.nb[ic] after
    editing a kernel or the old machine code is silently reused.
    
    * fix: compile a rolling kernel before the window runs, not inside it
    
    `polars.Expr.rolling_map` does not advance its window while the callback is being
    compiled. A feature whose callback calls a lazily-jitted Numba kernel therefore
    returned the first window's value for the whole series the first time a process
    evaluated it. Nothing raised and no value was NaN: the column simply came out
    near-constant, and every later evaluation in that process was correct.
    
    Measured on a 600-row random walk, distinct values returned on the first evaluation
    against the second, each in a fresh interpreter with an empty Numba cache:
    
      regime.hurst_exponent                    2  ->  501
      statistics.coefficient_of_variation      2  ->  576
      statistics.rolling_cv_zscore             2  ->  551
      statistics.rolling_drift                 2  ->  576
      ml.rolling_entropy_lz                    2  ->   94
      ml.rolling_entropy_plugin                2  ->   46
    
    `warm_rolling_callback` runs the callback once on a probe window of the right
    length, so compilation happens before `rolling_map` starts. It is applied at all
    ten sites whose callback calls a kernel, including the four that measured clean, so
    no site depends on which kernels another feature happened to compile first.
    
    This is what made `etfs/03_financial_features` fail its withheld-rows check on CI
    and pass when re-run by hand. The notebook builds the panel, then rebuilds it with
    the holdout withheld, and compares value by value against a 1e-12 tolerance. The
    first build compiled and got a constant `hurst_100`; the second found the kernel
    compiled and got the real one, so the columns differed by up to 0.177 and the
    assertion named `hurst_100` as fitted across the sample. Reproducing by hand always
    warmed the cache first, which is why five separate reproductions returned a maximum
    difference of 0.0, and why it correlated with machine load: a loaded box was one
    building fresh worktrees and fresh caches.
    
    There is no leak. The withheld build reads no later rows. But the seal check was
    reporting something real, and a constant feature column in any fresh environment -
    CI, a new venv, a reader's first run - is worse than the failure that surfaced it.
    
    tests/features/test_rolling_map_first_call.py runs each of the twelve
    rolling_map-backed features in its own interpreter with its own NUMBA_CACHE_DIR and
    compares the first evaluation against the second. Six fail without this change.
    
    * docs: record the gap-handling fixes, and the two shipped datasets that carried them
    
    The RSI entry names the production evidence rather than the reproduction script:
    a crypto premium index with 537 nulls at 79.0% saturation, and a CME crude-oil
    series where one null on 2020-04-20 - the day the contract settled negative -
    saturated 12.6% of the product's rsi_14 for the following five years.
    stefan-jansen authored Aug 10, 2026
    Configuration menu
    Copy the full SHA
    62ce7b8 View commit details
    Browse the repository at this point in the history
Loading