Skip to content

hmm_bull_bear returns constant state 1 on price input (unusable as regime filter); undocumented {1,2} states on returns #34

Description

@lavs9

Summary

pl.col(...).ta.hmm_bull_bear() returns a constant 1 for every bar when fed a price series, regardless of the underlying trend — an all-up series and an all-down series both classify as 100% state 1. The indicator only produces varying output when fed returns, and even then emits states labelled {1, 2} rather than the 0/1 (bear/bull) a caller would expect. This makes it unusable as a regime filter on price data without undocumented preprocessing.

Environment

  • quantwave 0.7.0
  • polars 1.40.1
  • Python 3.12, macOS (arm64)

Minimal reproduction

import polars as pl, quantwave, numpy as np
np.random.seed(1)

def series(drift, n=400, vol=0.01, p0=100.0):
    p = [p0]
    for _ in range(n): p.append(p[-1] * (1 + np.random.normal(drift, vol)))
    return p

def counts(vals):
    v = np.asarray(vals, float); v = v[~np.isnan(v)]
    u, c = np.unique(v, return_counts=True); return dict(zip(u.astype(int).tolist(), c.tolist()))

def hmm(x):
    return (pl.DataFrame({"c": [float(v) for v in x]})
            .select(pl.col("c").ta.hmm_bull_bear().alias("r")).to_series().to_numpy())

up, dn = series(0.004), series(-0.004)
print("PRICE  uptrend  ->", counts(hmm(up)))
print("PRICE  downtrend->", counts(hmm(dn)))
print("RETURN uptrend  ->", counts(hmm(np.diff(np.log(up)))))
print("RETURN downtrend->", counts(hmm(np.diff(np.log(dn)))))

Actual output

PRICE  uptrend  -> {1: 401}
PRICE  downtrend-> {1: 401}
RETURN uptrend  -> {1: 398, 2: 2}
RETURN downtrend-> {1: 361, 2: 39}

Same behaviour on real data: run against ~4,100 daily closes of the NIFTY 50 index, hmm_bull_bear returned 1 for all 4,106 bars.

Expected

  • On a price series with a clear regime change, the classifier should switch states (bull vs bear), not return a single constant state.
  • State labels should be documented and ideally normalised (e.g. 0 = bear, 1 = bull). The current {1, 2} labelling (only on returns input) is undocumented and easy to misread.

Observations / questions

  1. Input expectation is undocumented. It appears the indicator wants returns, not price levels — but .ta accessors generally take price/close. If returns are required, that should be stated (and price input should probably error or be internally differenced rather than silently collapsing to a constant).
  2. State semantics. On returns input it emits {1, 2}, with 2 appearing far more often in the downtrend (39 vs 2), suggesting 2 ≈ bear — but this isn't documented.
  3. Look-ahead. If the HMM is fit in batch over the whole series, the classification at bar t uses future data. Given the library's "bit-identical streaming & batch" guarantee, it would help to confirm hmm_bull_bear is causal (streaming-safe) for backtesting use.

Impact

Discovered while dogfooding quantwave as the compute layer for a momentum backtest, using hmm_bull_bear as a market-regime gate on NIFTY 50. Because it returned a constant on price input, the gate never triggered defensively; when coerced via returns it fired on ~8% of days and degraded every backtest metric — consistent with an unreliable/constant signal rather than a working regime classifier.

Minor, likely-by-design (noting for docs)

  • stddev(N) uses population std (ddof=0); pandas .std() defaults to ddof=1. TA-Lib convention, but a documented note would prevent parity surprises.
  • roc(N) is scaled ×100 (TA-Lib convention); rocp(N) gives the raw price/price_N - 1 ratio.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions