mixle.inference.price_forecast module

forecast_price – commodity-price / cost forecasting with horizon-matched conformal intervals.

Wraps the generic HMM front door (mixle.inference.forecast.forecast()) with a recalibration pass so each reported [lo, hi] lead uses held-out residuals from that same lead:

pf = forecast_price(model, price_history, horizon=12, level=0.9)
pf.mean, pf.lo, pf.hi   # (horizon,) point forecast + calibrated band
pf.paths                # (horizon, n) Monte-Carlo scenario draws for downstream DCF (J2)

The forecast itself is exact where the model is exact (HMM state marginals p_T A^h) and Monte-Carlo only where it has to be (emission quantiles for arbitrary, possibly skewed or multimodal, price/cost emission families) – see mixle.inference.forecast. What this module adds is the recalibration: reserve the most recent cal_frac of history and, within that window, run a rolling-origin backtest of the SAME horizon-step-ahead point forecast the caller is about to receive, scoring each origin’s forecast against what actually happened. That gives a sample of real horizon-step-ahead residuals at the depth that matters (rather than mixing in easier short-horizon or harder long-horizon errors), which recalibrates the requested band via split conformal (mixle.inference.conformal.split_conformal()). This is what makes the band eligible for marginal coverage under the usual held-out exchangeability assumption. Time-series dependence or distribution shift can violate that assumption, so the result records the calibration count and assumptions rather than claiming unconditional real-world coverage.

class PriceForecast(mean, lo, hi, paths, level, calibration_count=0, interval_method='horizon_matched_split_conformal', coverage_assumptions=('held_out_exchangeability', 'stable_data_generating_process'), model_fit_length=0)[source]

Bases: NamedTuple

A price/cost forecast with a horizon-matched interval calibration receipt.

Parameters:
mean: ndarray

Alias for field number 0

lo: ndarray

Alias for field number 1

hi: ndarray

Alias for field number 2

paths: ndarray

Alias for field number 3

level: float

Alias for field number 4

calibration_count: int

Alias for field number 5

interval_method: str

Alias for field number 6

coverage_assumptions: tuple[str, ...]

Alias for field number 7

model_fit_length: int

Alias for field number 8

forecast_price(model, history, horizon, *, level=0.9, cal_frac=0.3, seed=0, model_fit_length=None)[source]

Forecast horizon steps of a price/cost series with a conformally-calibrated band.

Parameters:
  • model (Any) – a fitted HiddenMarkovModelDistribution over the price/cost series (any scalar emission family with a sampler – see mixle.inference.forecast.forecast()).

  • history (Any) – the observed series to condition on (one sequence, oldest first).

  • horizon (int) – number of future steps to forecast.

  • level (float) – central-interval mass (0.9 -> the calibrated 5%..95% band).

  • cal_frac (float) – the fraction of history (most recent) reserved for calibration. Within that reserved window, a rolling-origin backtest of the same horizon-step-ahead forecast is scored against the real outcomes to build the calibration residuals.

  • seed (int) – reproducibility for the calibration and requested-horizon Monte Carlo draws.

  • model_fit_length (int | None) – REQUIRED DECLARATION (STAT-RR17-06) – how many leading observations of history the fitted model consumed (0 when it was built from prior knowledge or other data; len(history) when it was fitted on everything). The rolling-origin calibration reuses the model at every pseudo-origin without refitting, so when the model was fitted through ANY calibration-window outcome the residuals are partly in-sample and the band is optimistic: the receipt’s interval_method and coverage_assumptions then disclose exactly that instead of claiming a held-out guarantee (a spy measured a first origin at 13 with a model fitted through 19 – six future outcomes exposed). The clean alternative is refitting per origin, which this API deliberately does not fake by silently truncating your model.

Returns:

A PriceForecast with the calibrated (lo, hi) intervals, the point forecast mean, the raw per-step predictive draws paths (for Monte-Carlo DCF scenario analysis downstream), and level.

Return type:

PriceForecast

What (lo, hi) is: one MARGINAL interval PER LEAD, each with coverage AT LEAST level at its own horizon depth – NOT a simultaneous band, and not exact: split conformal is a lower-bound guarantee, so each lead can be conservative. Whole-path coverage is therefore NOT controlled in either direction by the per-lead guarantee: independent misses at exactly nominal would compound toward level**horizon, while conservative, strongly dependent leads can push joint coverage ABOVE level (an earlier version of this text said joint coverage “never exceeds” the requested level, which is false for a lower-bound guarantee – STAT-RR21-13). A plotted “tube” must not be read as containing the path with level probability in either direction; for path-level statements, work from paths directly.

Read calibration_count before trusting the exact level. At the defaults the reserved window yields on the order of 20 residuals per lead; split conformal’s finite-sample quantile moves in steps of 1/(n_cal + 1), so small counts make the interval coarse and typically conservative – and consecutive rolling origins are one step apart, so their horizon-step windows OVERLAP and the residuals are serially dependent: the exchangeability behind the conformal guarantee holds only approximately across overlapping origins. More history (or a larger cal_frac) buys both more residuals and less overlap distortion.