]> Piment Noir Git Repositories - freqai-strategies.git/commit
perf(quickadapter): harmonize causal availability for provably-stable zero-weight...
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Thu, 30 Jul 2026 19:19:16 +0000 (21:19 +0200)
committerGitHub <noreply@github.com>
Thu, 30 Jul 2026 19:19:16 +0000 (21:19 +0200)
commit30898b98c447ab45dbbc178d3b6a8545fe21b548
tree813d6c4348d0631414dd5df7f12b4da7745069af
parent6da29070d951562eaf82f080fceeff23899c7a6c
perf(quickadapter): harmonize causal availability for provably-stable zero-weight imputations (#186)

* perf(quickadapter): harmonize causal availability for provably-stable zero-weight imputations

Release trailing single-metric and combined leading non-finite runs whose
imputation is provably fixed at 0.0 under the causal prefix at their true
stabilization candle instead of the frame boundary, matching the existing
single-metric leading treatment (#158).

- Gap 1: a trailing single-metric non-finite run imputes to 0.0 and stabilizes
  at the first finite pivot's confirmation; release it there. An all-non-finite
  metric keeps the nonzero legacy default (1.0) and stays deferred to n, so a
  dedicated trailing_stable_mask distinguishes it from dependency_mask.
- Gap 2: for combined with every component leading with a non-finite run, the
  aggregate is stably zero over [0, min_c first_finite_c); release it at the
  max-over-components confirmation candle (unequal run lengths must not leak),
  guarded by an empirical combined_weights[:S] == 0.0 prefix-stability check.

The producer now returns a LabelWeightImputationMasks dataclass carrying both
stable masks and a stable_release_index; min bounds the stable run while max
sets the release candle, so the release index cannot be derived from the mask
alone. The non-causal path and existing single-metric leading behavior remain
bit-for-bit unchanged.

Closes #169

* refactor(quickadapter): harden causal availability release from review feedback

Address self-review findings on the #169 harmonization without changing
production behavior (bit-for-bit over a monotone battery vs the prior commit).

- Release the combined leading run via the max over the release prefix
  weight_availability[: stable_release_index + 1] instead of a single lookup.
  Both are equal when weight_availability is monotone (guaranteed by the zigzag
  confirmation watermark), but the prefix max stays leak-free even if that
  invariant is ever violated, since every contributing component confirms at or
  before stable_release_index (= max_c first_finite_c for combined).
- Guard the release on idx.size == raw_idx.size so a dropped (out-of-range)
  pivot, which would desync the producer-space release index from the filtered
  weight_availability, conservatively defers to the frame boundary.
- Rename all_components_finite -> every_component_has_finite: the flag is true
  when no component is entirely non-finite, not when every value is finite.
- Refine docstrings to state the prefix-max release and that trailing pivots
  are additionally bounded by their own label availability via the max fold.

* docs(quickadapter): align LabelWeightImputationMasks docstring with prefix-max release

The dataclass docstring still described the pre-hardening single-lookup release
(weight_availability[stable_release_index]). Match the consumer/producer
docstrings: the release candle is the max over the release prefix
weight_availability[: stable_release_index + 1], where stable_release_index is
first_finite for a single metric and the max over components for combined.

* fix(quickadapter): stop causal leak of non-terminal trailing pivot weights

trailing_stable_mask marked the whole trailing non-finite run
(last_finite+1 .. n) and released it at weight_availability[first_finite]. Only
the terminal pivot (no closing swing) is provably 0.0 there: it is always the
last pivot so its trailing-boundary classification cannot flip. A non-terminal
pivot inside a longer trailing run — reachable when an interior swing metric is
non-finite (e.g. volume_rate / efficiency_ratio / volume_weighted_efficiency_
ratio on a zero-volume or flat window) — could still become interior (median,
!= 0.0) until its own later swing confirms, so releasing it at the first finite
pivot's confirmation leaked future information (under-purge).

Restrict trailing_stable_mask to the terminal pivot; a non-terminal pivot in a
length>=2 trailing run stays deferred to the frame boundary n via
dependency_mask (conservative, no leak). Behaviour is unchanged for the common
length-1 trailing run (terminal only), and for the leading, combined, and
non-causal paths. Docstrings aligned (terminal-pivot wording; stable_release_
index == -1 semantics clarified).

* docs(quickadapter): correct stable_release_index and prefix-max descriptions

Third-round review accuracy fixes (documentation only, no behaviour change):

- stable_release_index: the docstrings claimed "-1 when both stable masks are
  empty", but a single metric with any finite value reports first_finite (>= 0)
  even when both masks are empty (all-finite or interior-only-NaN). It is -1
  only when no pivot is finite. The consumer independently gates the release on
  a non-empty stable mask, so a non-negative index with empty masks is inert.
- Consumer release comment: the prefix max equals
  weight_availability[stable_release_index] only under the production invariant
  (zigzag confirmation watermark => monotone weight_availability), not
  unconditionally; state that explicitly and note the prefix max stays leak-free
  (at worst defers later) if the invariant is ever violated.

* docs(quickadapter): deduplicate imputation-masks docstrings

* docs(quickadapter): tighten imputation-masks safety-invariant comments

* fix(quickadapter): stop causal leak of terminal pivot-weight imputation

The terminal non-finite pivot's 0.0 imputation is not causal-prefix stable:
add_pivot backfills its swing metric once a later closing pivot arrives, so
the 0.0 becomes a nonzero interior median. Releasing it (and skipping its
Gaussian band) before the frame boundary n leaked the fact that no later
pivot occurs. Defer the terminal pivot to n via dependency_mask and remove
the now-always-empty trailing_stable plumbing (dataclass field, consumer
parameter, and caller wiring). The leading-run and combined leading release
(both provably prefix-stable) are unchanged.

Empirically verified against the prior revision: bit-for-bit availability on
all non-terminal cases; terminal cases only ever defer later (leak-free).

* refactor(quickadapter): make stable_release_index -1 when no leading run

Return stable_release_index = -1 whenever leading_stable_mask is empty
(single-metric with first_finite == 0: all-finite or interior-only-NaN),
so the field holds the invariant 'index >= 0 iff a leading run is
released' instead of relying on the consumer's mask-emptiness gate. The
consumer already gates the release on leading_stable_mask.any(), so the
availability output is bit-for-bit unchanged (verified across single,
combined, and uniform cases). Also align docstrings/terminology
(terminal vs trailing pivot) and tighten the stable_release_index docs.

* docs(quickadapter): correct all-non-finite combined-component rationale

The comment claimed an all-non-finite component (imputed to the nonzero
default 1.0) makes the aggregate leading run non-zero, but geometric_mean
and harmonic_mean annihilate to 0.0 when any component is 0.0 (verified),
so that rationale is false. The real reason the leading release is blocked
is that an all-non-finite component never confirms a finite weight in-frame
and thus has no first_finite release candle; the pivots defer to n
conservatively. Comment-only; no behavior change.

* docs(quickadapter): tighten LabelWeightImputationMasks docstring

Remove the consumer release-prefix formula duplicated from
compute_label_weight_known_at_lookahead (single source of truth), drop the
vague 'shared' wording, and name both the producer and consumer of the
dataclass. Docstring-only; no behavior change.
quickadapter/user_data/strategies/QuickAdapterV3.py
quickadapter/user_data/strategies/Utils.py