]> Piment Noir Git Repositories - freqai-strategies.git/log
freqai-strategies.git
8 weeks agofix(quickadapter): avoid duplicate backtest exit reserve (#152)
Jérôme Benoit [Wed, 29 Jul 2026 15:09:09 +0000 (17:09 +0200)] 
fix(quickadapter): avoid duplicate backtest exit reserve (#152)

The partial-exit minimum bound previously multiplied `min_stake` by the
stoploss/rate factor `max(current_exit_rate/current_entry_rate,
1/(1-|stoploss|))` in every runmode. In backtest/hyperopt, Freqtrade already
passes the adjusted minimum it guards the remainder against, so that factor
duplicated the stoploss reserve. Apply the factor only in live/dry-run
(is_trade_runmode == TRADE_MODES = {LIVE, DRY_RUN}), where the callback supplies
min_entry_stake (stoploss 0.0) but the guard uses the larger min_exit_stake;
keep the 0.1% numerical clearance in both modes. Live/dry-run behavior is
bit-for-bit unchanged. Freqtrade remains responsible for amount precision and
final pre-order minimum validation.

Also rename the local bound to `min_remaining_position_value` (it is the minimum
acceptable remaining position value, not an exit stake).

Fixes #138

8 weeks agochore(quickadapter): remove committed test harness (tests are kept external to the...
Jérôme Benoit [Wed, 29 Jul 2026 14:15:47 +0000 (16:15 +0200)] 
chore(quickadapter): remove committed test harness (tests are kept external to the repo)

8 weeks agofix(quickadapter): make causal k-NN weight availability exact (#151)
Jérôme Benoit [Wed, 29 Jul 2026 13:57:20 +0000 (15:57 +0200)] 
fix(quickadapter): make causal k-NN weight availability exact (#151)

Make the causal availability of the label-WEIGHT column exact for adaptive
k-NN Gaussian-fill bandwidths. Compute production 1D k-th-neighbor distances in
O(M log M), and propagate the earliest complete confirmation group at which each
clipped k-NN sigma is provably stable, over the confirmable Zigzag suffix
geometry: a confirmed pivot needs at least five slope observations, so
successive pivots are at least six candles apart and the last future candidate
is n-6. Initial-orientation replay is handled atomically via a position-only
successor bound (last replayed pivot + 6); later groups use the exact
confirmation frontier. Floor/ceiling/interior/singleton stability is decided on
the effective rank min(k, pivot_count-1) jointly with that geometry.

Truncate causal Gaussian fills to the finite support ceil(4*fill_sigma_candles)
tracked by availability; causal_mode=false keeps the legacy dense unbounded
Gaussian bit-identical. Pure-Gaussian uniform pivot centers keep their own label
availability (fixed, constant-clipped, adaptive); only adaptive off-center bands
additionally wait for sigma availability. Thread each label column's
weighting_config to availability.

Add the first quickadapter test harness, locking the Zigzag geometry invariants
the availability proof relies on (min pivot spacing, first-future bound, k-NN
availability bounds), runnable in the freqtrade container.

Fixes #131

8 weeks agodocs: streamline copilot instructions
Jérôme Benoit [Wed, 29 Jul 2026 12:59:10 +0000 (14:59 +0200)] 
docs: streamline copilot instructions

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
8 weeks agofix(quickadapter): recheck weight support after feature pipeline (#150)
Jérôme Benoit [Tue, 28 Jul 2026 23:57:36 +0000 (01:57 +0200)] 
fix(quickadapter): recheck weight support after feature pipeline (#150)

Label-weight support checks (min_pivot_equivalent_count,
min_positive_label_weight_fraction, min_effective_sample_size Kish ESS)
previously ran only before feature_pipeline.fit_transform, which can remove
training rows and renormalize the surviving weights below those thresholds
without re-invoking support_policy.

Recheck support post-pipeline on the surviving rows: carry the raw base and
label weight components through fit_transform as temporary object-keyed label
columns (row-filtered in lockstep with X/y/sample_weight, since DataSieve
resets indexes), pop them, restore the label schema, and re-gate via
support_policy. _sanitize_pipeline_weights fails closed on any malformed
post-pipeline weight shape (None/scalar/2-D/wrong-length). Base-only weights
carry no label support and skip the re-gate. Selection training and final
refit share the path; eval (val/test) weights still bypass support_policy but
now fail closed on shape.

No tunable, canonical default, or public API change.

Fixes #93

8 weeks agoMerge pull request #167 from jerome-benoit/codex/quickadapter-166-config-deprecations
Jérôme Benoit [Tue, 28 Jul 2026 21:46:54 +0000 (23:46 +0200)] 
Merge pull request #167 from jerome-benoit/codex/quickadapter-166-config-deprecations

refactor(quickadapter): unify config deprecation handling

8 weeks agorefactor(quickadapter): remove legacy migration view
Jérôme Benoit [Tue, 28 Jul 2026 21:15:04 +0000 (23:15 +0200)] 
refactor(quickadapter): remove legacy migration view

8 weeks agorefactor(quickadapter): unify config deprecation handling
Jérôme Benoit [Tue, 28 Jul 2026 21:03:54 +0000 (23:03 +0200)] 
refactor(quickadapter): unify config deprecation handling

8 weeks agofix(quickadapter): use directional PnL momentum gate (#149)
Jérôme Benoit [Tue, 28 Jul 2026 20:35:17 +0000 (22:35 +0200)] 
fix(quickadapter): use directional PnL momentum gate (#149)

Replace the velocity/acceleration t-statistic exit gate with a deterministic
mean per-candle PnL velocity direction rule: once the take-profit target is
reached and the recent window is complete, allow the exit iff recent[-1] <
recent[0] (equivalent to strictly negative mean velocity via telescoping).

Remove the now-dead machinery: decline_quantile, t-critical/effective-df,
acceleration and zero-variance fallbacks, get_pnl_momentum,
get_trade_unrealized_pnl_history, the thresholds_calibration config specs, and
the unrealized_pnl_timeframe_minutes field. A legacy
exit_pricing.thresholds_calibration setting now warns as obsolete and ignored.

Fail-open only where the horizon is unmeasurable (missing candle date,
incomplete window, non-finite value in the selected window); complete finite
windows are deterministic (falling exits; flat or rising blocks).

Fixes #128

8 weeks agofix(quickadapter): make epsilon fill causal (#148)
Jérôme Benoit [Tue, 28 Jul 2026 17:41:09 +0000 (19:41 +0200)] 
fix(quickadapter): make epsilon fill causal (#148)

* fix(quickadapter): make epsilon fill causal

Derive each causal row epsilon baseline from pivot weights available with its label while preserving the global non-causal formulation.

* fix(quickadapter): resolve undefined as_dict in set_freqai_targets

Replace the unimported as_dict() call with the dominant
self.freqai_info.get("feature_parameters", {}) idiom, matching the sibling
get_causal_mode call site and the other feature_parameters lookups in the
file. Fixes the F821/NameError that aborted set_freqai_targets on every fit,
and removes the idiom divergence.

Reformat the README configuration tunables table via prettier (table
re-padding only; no cell content changed).

8 weeks agofeat(quickadapter): make label study schema reset configurable (#147)
Jérôme Benoit [Tue, 28 Jul 2026 16:26:06 +0000 (18:26 +0200)] 
feat(quickadapter): make label study schema reset configurable (#147)

* feat(quickadapter): make label study schema reset configurable

Add a boolean reset_label_study_on_schema_mismatch option that preserves the current reset behavior by default and lets operators retain incompatible label studies with a warning. Avoid rewriting incompatible selection metadata when preservation is selected, and fail closed when study inspection or deletion fails.

Refs #87

* docs(quickadapter): clarify reset_label_study_on_schema_mismatch fail-closed behavior

The `true` value keeps the historical destructive reset but no longer matches
main byte-for-byte on error paths: a study inspection error (either value) or a
deletion error (under `true`) now fails closed and aborts the optimization
instead of silently recreating or reusing the study. Reword the README tunable
description accordingly and align terminology with the neighboring entries.

* docs(quickadapter): tighten reset_label_study_on_schema_mismatch abort wording

Say the fail-closed paths abort study creation, matching the actual mechanism:
optuna_create_study returns None before the study is created, and the caller
then skips the optimization cycle.

8 weeks agofix(quickadapter): preserve take-profit JSON history (#145)
Jérôme Benoit [Tue, 28 Jul 2026 15:15:27 +0000 (17:15 +0200)] 
fix(quickadapter): preserve take-profit JSON history (#145)

* fix(quickadapter): preserve take-profit JSON history

* refactor(quickadapter): drop over-engineered take-profit history migration

Remove the history_v2 versioned key, its legacy-migration branches, the
scalar cd_type/JSON-string recovery paths and the getter write side effect.
The history custom-data row has only ever been written as a dict, so its
cd_type is always "dict" and those defensive branches are unreachable dead
code; a stored dict round-trips as a dict with list fields, and the shared
callback wrapper logs rather than silently swallows exceptions.

_get_trade_history reverts to the minimal reader keyed by the literal
"history", matching the other custom-data row keys in this file. The
take-profit JSON fix for #134 is preserved: the _TakeProfitHistoryEntry
alias keeps the list arm and safe_append_trade_take_profit_price still
accepts a two-element tuple or list with a non-bool int stage and non-bool
numeric price.

8 weeks agofix(quickadapter): correct and harmonize builtin caching (#165)
Jérôme Benoit [Tue, 28 Jul 2026 14:44:40 +0000 (16:44 +0200)] 
fix(quickadapter): correct and harmonize builtin caching (#165)

Remove @lru_cache from the instance method
QuickAdapterRegressorV3.optuna_samplers_by_namespace: decorating a bound
method kept self in the process-lifetime cache, pinning regressor
instances (models, studies, dataframes) and leaking memory across
re-instantiations. The value is trivially recomputable.

Harmonize the rest of the builtin caching:
- Centralize lru_cache sizes into _CACHE_MAXSIZE_SMALL/_LARGE constants,
  replacing scattered 8/64/128 literals (right-sizes the oversized 64 on
  _label_aux_column_name).
- Drop ineffective @lru_cache on the float-keyed static helpers
  _t_statistic/_effective_df/_t_critical: continuous per-call float keys
  give a near-zero hit rate while _effective_df pays O(n) tuple hashing.
- Return a read-only ndarray from _calculate_coeffs so callers cannot
  corrupt the shared cached kernel.
- Document the _df_signature process_only_new_candles coupling.

8 weeks agofix(quickadapter): preserve calendar PnL continuity (#142)
Jérôme Benoit [Tue, 28 Jul 2026 13:56:27 +0000 (15:56 +0200)] 
fix(quickadapter): preserve calendar PnL continuity (#142)

* fix(quickadapter): preserve calendar PnL continuity

* fix(quickadapter): drop epoch-anchored phase guard in PnL continuity

The (month-1) % n phase guard on the MS path assumed a fixed January
epoch, but pandas resample anchors calendar bins on the data origin, not
a fixed epoch, so it wrongly reset valid non-January-anchored multi-month
series (e.g. a Feb/May/Aug 3M grid). is_on_offset already rejects a
non-boundary stored date and stored + offset is the exact next candle for
any anchor phase, so the membership test alone is correct and symmetric
for MS and YS (resolving the MS/YS asymmetry). Also complete the
_TradeHistory key-mirror comment with the legacy timeframe-minutes key.

* style(quickadapter): tighten PnL continuity phase-guard rationale comment

8 weeks agorefactor(quickadapter): polish configuration validation (#164)
Jérôme Benoit [Tue, 28 Jul 2026 13:21:22 +0000 (15:21 +0200)] 
refactor(quickadapter): polish configuration validation (#164)

8 weeks agofix(quickadapter): validate coupled/bounded config inputs at the option layer (#141)
Jérôme Benoit [Tue, 28 Jul 2026 12:11:07 +0000 (14:11 +0200)] 
fix(quickadapter): validate coupled/bounded config inputs at the option layer (#141)

* fix(quickadapter): validate smoothing modes per method

* refactor(quickadapter): consolidate smoothing cross-field validation

Route the label_smoothing method x mode check through the shared label-kind
machinery instead of a bespoke loop in the smoothing getter:

- fold the per-kind coupled-field validator into _LABEL_KIND_REGISTRY as a
  third tuple element (single source of truth), with a named
  CrossFieldValidatorFn type alias matching the existing ValidateParamsFn
- get_label_kind_config runs the registered validator on each resolved
  per-column config; all four label-kind getters are now symmetric one-liners
- derive the bot_start wrap/causal-mode check's mode-aware method set from
  SMOOTHING_METHOD_MODES, dropping the duplicate _SMOOTHING_GAUSSIAN_FILTER1D
- README: keep the per-method mode matrix in the type column, drop the
  code-behavior narration from the description

* fix(quickadapter): validate exit calibration and leverage bounds at the option layer

Two config inputs bypassed the option-layer validation their siblings use:

- exit_pricing.thresholds_calibration.decline_quantile was merged raw:
  documented float (0,1) but enforced nowhere (only a consumer-side guard
  that raised TypeError on non-numeric input). Route it through a validated
  get_exit_thresholds_calibration_config using the shared _validate_params
  machinery, with DEFAULTS_EXIT_THRESHOLDS_CALIBRATION as single source of
  truth (drops the duplicate class-var default). Invalid values warn and
  fall back to 0.5.
- leverage() applied only the upper bound; the documented lower bound 1.0
  (README: float [1.0, max_leverage]) and non-numeric guarding were missing.
  Clamp to [1.0, max_leverage], falling back to proposed_leverage on
  non-numeric input.

* fix(quickadapter): validate custom_protections config at the option layer

custom_protections was the last config section read ad-hoc with hard int()/
float() casts that crashed on non-numeric input, inconsistent with the
warn-and-fall-back contract every other section uses.

Add get_custom_protections_config on the shared _validate_params machinery
(new _BoolValidator for the enabled flags; nested cooldown/drawdown/stoploss
sub-dicts validated per section) with single-source DEFAULTS_*; the
protections property consumes the validated, typed config. Invalid or
non-numeric values now warn and fall back to their documented defaults
instead of raising.

* fix(quickadapter): validate fit_live_predictions_candles at the option layer

The last strategy-side config value read with a raw int() cast (protections
and startup_candle_count) crashed on non-numeric input. Route it through a
validated get_fit_live_predictions_candles (positive int, warn and fall back
to the default) on the shared _validate_params machinery; drop the now-unused
DEFAULT_FIT_LIVE_PREDICTIONS_CANDLES import.

* refactor(quickadapter): address PR review nits

- harmonize get_exit_thresholds_calibration_config to accept the parent
  exit_pricing dict and deref thresholds_calibration internally, removing the
  double as_dict at the call site (mirrors get_custom_protections_config)
- reject bool in _NumericValidator: bool is not a valid numeric input,
  consistent with is_finite_number / leverage() / _BoolValidator
- drop the dead commented minimal_roi block referencing the removed
  DEFAULT_FIT_LIVE_PREDICTIONS_CANDLES import
- align comment terminology on 'cross-field' (matches CrossFieldValidatorFn)

* refactor(quickadapter): address second-round review nits

- warn instead of silently resetting when a config section is present but not
  a mapping: new as_config_section helper applied to custom_protections
  (+ cooldown/drawdown/stoploss) and exit_pricing.thresholds_calibration
- re-add default_exit_thresholds_calibration ClassVar as a compat alias to the
  canonical DEFAULTS_EXIT_THRESHOLDS_CALIBRATION (public API stability)
- move the minimal_roi rationale note directly above its assignment

* refactor(quickadapter): address third-round review nits

- Warn on non-mapping config sections by routing the section getters
  (label kinds, exit_pricing, reversal_confirmation, fit_live) through
  as_config_section, matching the custom_protections pattern (N-1).
- Honor the default_exit_thresholds_calibration override via an optional
  overrides argument merged over the canonical defaults; user config
  still wins in _validate_params (N-2).
- Resolve fit_live_predictions_candles once through the canonical
  validator in the regressor so an explicit 0 floors to 100, fixing the
  .iloc[-0:] whole-frame slice (N-3).

* fix(quickadapter): warn on invalid leverage before fallback

Route the configured leverage through a cached validator that logs a
harmonized warning when the value is non-numeric or a boolean before
falling back to proposed_leverage, instead of silently discarding the
user setting. The warning fires once (cached) to avoid per-call spam.

* fix(quickadapter): warn on sub-minimum leverage; cache protections

- Warn once (via the _configured_leverage cached_property) when a numeric
  leverage is below the 1.0 floor before the leverage() hook clamps it;
  the per-pair max_leverage ceiling is only known at entry time, so
  above-ceiling values stay clamped silently.
- Promote protections to a cached_property, aligning it with the sibling
  config-derived accessors and collapsing duplicate warnings on a
  malformed custom_protections/freqai section to one per strategy
  instance (reload re-instantiates the strategy, so the cache is fresh).

* refactor(quickadapter): drop dead FIT_LIVE_PREDICTIONS_CANDLES_DEFAULT

Both consumers were rewired to the resolved self._fit_live_predictions_candles,
leaving the ClassVar and its DEFAULT_FIT_LIVE_PREDICTIONS_CANDLES import unused.
Remove both (not a public-library API surface).

* style(quickadapter): drop redundant comments in exit-calibration getter

The as_dict coercion and defaults merge are self-explanatory; keep
comments only where the code is not clear on its own.

* fix(quickadapter): guard leverage finiteness; warn-once on fit-live warmup

- Reject non-finite leverage (NaN/Inf) via the shared is_finite_number
  guard before falling back to proposed_leverage, instead of letting it
  reach the clamp silently.
- Back startup_candle_count and protections with a cached
  _fit_live_predictions_candles helper so an invalid fit_live_predictions_candles
  warns once instead of on every access. startup_candle_count stays a plain
  property so the StrategyResolver keeps protecting it from config override
  (cached_property is not a property subclass).

* style(quickadapter): drop redundant _LABEL_KIND_REGISTRY comment

The tuple type (CrossFieldValidatorFn | None) and the named unpacking
(cross_field_validator) already document the third element.

* fix(quickadapter): validate exit-calibration override before use

Route the override (e.g. a subclass default_exit_thresholds_calibration)
through _validate_params against the canonical defaults so an invalid
subclass value falls back to the canonical default instead of being
trusted blindly (previously it could be returned as-is with a misleading
warning, or crash on output_type coercion). User config still wins over
the override, which still wins over the canonical default. Also restore a
concise note on the intentional silent parent coercion.

* style(quickadapter): tighten exit-calibration rationale comments

8 weeks agofix(quickadapter): enforce reversal NATR fraction bounds (#140)
Jérôme Benoit [Mon, 27 Jul 2026 18:54:23 +0000 (20:54 +0200)] 
fix(quickadapter): enforce reversal NATR fraction bounds (#140)

Enforce the documented [0, 1] upper bound for both reversal NATR multiplier
fractions at the configuration boundary:

- reject config values above 1 (warn and fall back per component)
- fail fast with ValueError when canonical defaults exceed max_value
- canonicalize the out-of-range component warning to the single
  "<= {max_value}" form, dropping the wrong "[-inf, x]" notation
- clarify the strict min < max ordering in the README tunables table with
  symmetric "(< upper bound)" / "(> lower bound)" hints

Fixes #137

8 weeks agochore(deps): lock file maintenance (#159)
renovate[bot] [Mon, 27 Jul 2026 15:22:06 +0000 (17:22 +0200)] 
chore(deps): lock file maintenance (#159)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8 weeks agorefactor(quickadapter): group related constants (#161)
Jérôme Benoit [Mon, 27 Jul 2026 15:20:08 +0000 (17:20 +0200)] 
refactor(quickadapter): group related constants (#161)

Keep canonical sources contiguous with their aliases, sets, maps, and related defaults across the strategy and regressor constant blocks. Preserve every value and consumer while removing semantic-family interleaving.

Refs #160

8 weeks agofix(quickadapter): guard filtfilt padding length (#143)
Jérôme Benoit [Mon, 27 Jul 2026 13:37:49 +0000 (15:37 +0200)] 
fix(quickadapter): guard filtfilt padding length (#143)

Derive the SciPy default pad length from the FIR coefficient lengths and use the same no-op boundary for smoothing and label lookahead.

8 weeks agofix(quickadapter): invalidate reversal caches together (#155)
Jérôme Benoit [Mon, 27 Jul 2026 13:30:03 +0000 (15:30 +0200)] 
fix(quickadapter): invalidate reversal caches together (#155)

Purge threshold and deviation entries for a pair before recording its new dataframe signature, and invalidate both caches when live pair-local label period or NATR multiplier state changes.

8 weeks agorefactor(quickadapter)!: rename mislabeled log-return feature columns (#153)
Jérôme Benoit [Sun, 26 Jul 2026 21:49:09 +0000 (23:49 +0200)] 
refactor(quickadapter)!: rename mislabeled log-return feature columns (#153)

Rename FreqAI feature columns for name/computation coherence.

Mislabeled log returns:
- %-tcp-period       -> %-top_log_return-period
- %-bcp-period       -> %-bottom_log_return-period
- %-close_pct_change -> %-close_log_return (QuickAdapterV3 and ReforceXY RLAgentStrategy)

Namespace harmonization with the %-dist_to_<x> convention:
- %-distzlema50 -> %-dist_to_zlema_50
- %-distzlema12 -> %-dist_to_zlema_12
- %-distzlema26 -> %-dist_to_zlema_26

BREAKING CHANGE: FreqAI feature column names changed; existing trained models
must be retrained since the feature identifiers differ.

8 weeks agofeat(quickadapter): make HPO trial seed variation configurable (#119)
Jérôme Benoit [Sun, 26 Jul 2026 19:58:41 +0000 (21:58 +0200)] 
feat(quickadapter): make HPO trial seed variation configurable (#119)

Add freqai.optuna_hyperopt.vary_model_seed_by_trial (default true) to make the
per-trial model-seed variation explicit.

- true (default) preserves the historical behavior: each HPO trial adds
  trial.number to the regressor model seed. false uses the same model seed for
  every trial and the final fit.
- Independent from freqai.optuna_hyperopt.seed (Optuna samplers + label-candle
  shuffling); wired through the canonical defaults, config template, startup
  logging, and README.
- Seed NGBoost's base DecisionTreeRegressor as well: NGBoost.random_state only
  seeds subsampling/validation split, not the cloned base learner, so the base
  tree needs its own random_state for reproducibility.

BREAKING CHANGE: the optuna_hyperopt boolean options (enabled, continuous,
warm_start, space_reduction, vary_model_seed_by_trial) are now validated at the
option layer and raise ValueError on non-boolean values instead of relying on
Python truthiness; a config passing a non-boolean (e.g. 1 or "false") for these
keys must use real booleans.

8 weeks agofix(quickadapter): preserve valid partial-exit remainder (#118)
Jérôme Benoit [Sun, 26 Jul 2026 19:05:39 +0000 (21:05 +0200)] 
fix(quickadapter): preserve valid partial-exit remainder (#118)

Trigger the take-profit partial exit at freqtrade's executable current_exit_rate
instead of the entry-side current_rate, and size the partial-exit remainder so it
survives freqtrade's minimum-stake guard.

- Evaluate the take-profit trigger and the remaining position at current_exit_rate
  (the rate the exit actually fills at); the previous current_rate is the entry rate
  in this callback and could fire a long exit below its bid target (symmetric short).
- freqtrade passes min_entry_stake as min_stake but rejects the partial exit when the
  remaining value is below the larger min_exit_stake. Size the remainder against a
  proven upper bound min_exit_stake <= min_stake * max(current_exit_rate/current_entry_rate,
  1/(1-|stoploss|)) (holds for both the cost- and amount-driven minimum), with a small
  rounding margin, so the shrunk remainder clears the guard. Leverage cancels; amount
  precision and final validation are left to freqtrade.

8 weeks agofix(quickadapter): sample PnL momentum per candle (#117)
Jérôme Benoit [Sun, 26 Jul 2026 16:38:29 +0000 (18:38 +0200)] 
fix(quickadapter): sample PnL momentum per candle (#117)

Sample the take-profit declining-PnL momentum gate once per candle instead of
per callback throttle, so velocity/acceleration are computed on a
candle-uniform series.

- Window sizing: ceil(30/tf)+1 samples (>=30 min velocity span on any
  timeframe), floored to 4 so both velocity and acceleration t-statistics are
  computable; warn when the nominal window is floored.
- Warm-up: fail open (never block a profitable take-profit exit) until a full
  momentum window is available.
- Persist a candle-date and timeframe marker with the PnL history; reset the
  series on legacy-history migration, timeframe change, or a candle
  discontinuity (forward gap or backward/non-monotonic date), persisting the
  reset before the append re-read.
- Fail open when the last candle lacks a valid date rather than gating on a
  stale series.
- Dedupe the take-profit order tag into a helper; type the trade history via a
  TypedDict.

2 months agorefactor(quickadapter): harmonize patterns and consolidate common logic (#126)
Jérôme Benoit [Sat, 25 Jul 2026 18:12:24 +0000 (20:12 +0200)] 
refactor(quickadapter): harmonize patterns and consolidate common logic (#126)

* refactor(quickadapter): harmonize patterns and consolidate common logic

Behavior-preserving refactor of the QuickAdapter codebase (Utils.py,
QuickAdapterV3.py, QuickAdapterRegressorV3.py). Every change verified in the
project Docker image (freqtrade + optuna/ngboost/catboost) via import smoke and
per-change golden/behavior tests; py_compile clean, ruff E,F,W baseline unchanged
or reduced, ruff format clean.

Consolidation:
- C1: single _get_validation_size() replaces 3 identical test_size None-coerce blocks (None-vs-0 distinction preserved).
- C2: _resolve_optuna_store() registry replaces 6 near-duplicate get/set_optuna_* dispatchers; hp-only/label-only asymmetry and per-accessor defaults preserved.
- C4: as_dict() helper replaces 7 dict-guard blocks across both classes.
- C5: _distance_to_reference() extracts the 4-family distance dispatch shared by _compromise_programming_scores and _topsis_scores (apply_abs flag preserves the TOPSIS-only np.abs asymmetry); golden differential over all 17 metrics is bit-identical.
- C6: _trade_natr_window() extracts the shared NATR preamble of the weighted/quantile trade-NATR methods (single-candle sentinel preserved).
- C7: _invalidate_pair_cache() replaces 2 identical per-pair cache-invalidation blocks (rebind-on-change / same-object-on-noop preserved).
- C8: _validate_step_args() shares the guard preamble of round/ceil/floor_to_step.
- C9: _pop_early_stopping_rounds() and _apply_verbosity_alias() replace repeated fit_regressor boilerplate (verbose precedence preserved).
- C10: drop dead default 2nd-args on _optuna_config[...] lookups (merge is exhaustive; constant_liar path kept).

Harmonization:
- H2/C3: fold 3 near-identical scalar validators into one _validate_scalar(predicate, constraint); exact messages preserved (subsumes H1 predicate sharing).
- H4: single canonical source for label defaults in Utils; RV3 ClassVars reference it (fixes value duplication).
- H6: enum_error_message() builder; identical-text regressor/aggregation raises routed through it.
- H7: make the intended mode="raise" explicit for label_method and scaler validation (no behavior change).

Notes:
- H5 (DEFAULT_X vs X_DEFAULT naming): the duplicated default values were removed by H4; the remaining prefix/suffix difference is a consistent module-scalar vs class-ClassVar convention, left as-is.
- H3 (magic-index TUPLE[n] -> named members) remains as a follow-up; behavior-neutral cosmetic cleanup.

* refactor(quickadapter): replace magic-index dispatch with named constants (H3)

Every fragile TUPLE[n] dispatch/comparison site (~64 across the three files) now
reads a named Final constant defined next to its tuple, extending the existing
_OPTUNA_NAMESPACES / _REGRESSOR_SPECS idiom. Reordering a literals tuple can no
longer silently break dispatch, and the trailing `# "name"` comments are removed
as redundant.

Covered tuples: _DISTANCE_METRICS, _DISTANCE_METHODS, _CLUSTER_METHODS,
_SELECTION_METHODS, _DENSITY_METHODS, _DENSITY_AGGREGATIONS, _SCALER_TYPES,
_DATA_SPLIT_METHODS, _OPTUNA_STORAGE_BACKENDS (RegressorV3); _TRADE_DIRECTIONS,
_ORDER_TYPES, _INTERPOLATION_DIRECTIONS, _TRADING_MODES, and the imported
SMOOTHING_METHODS / FILL_METHODS / WEIGHT_STRATEGIES (QuickAdapterV3).

The canonical named-derivation definitions (_UNSUPPORTED_WEIGHTS_METRICS and the
LABEL_*_DEFAULT / *_DEFAULT ClassVars) keep their index form: they are the single
source that maps index -> name. Behaviour preserved by construction (each named
constant is bound to the same tuple element); verified in the project Docker image
by asserting every constant equals its tuple value and that CP/TOPSIS scoring is
unchanged.

* refactor(quickadapter): address initial-review findings

- M1: hoist the scipy cdist kwargs in _topsis_scores and pass them into both
  _distance_to_reference calls, so _prepare_distance_kwargs (and its warn-mode
  validators) run once instead of twice for scipy metrics — restores the
  original single-warning behavior on an invalid label_distance_p / unsupported
  weights. Distance scores stay bit-identical (kwargs are deterministic).
- N1: make _validate_scalar's predicate/constraint optional; _validate_power_mean_p
  now calls it with no predicate (finite-only), dropping the dead always-true
  lambda + unreachable constraint string.
- N2: type _invalidate_pair_cache with a bound TypeVar so each caller's cache
  type flows through instead of a bare dict.
- N3: drop 3 redundant `# "name"` comments left next to named constants.

Verified in the project Docker image: CP/TOPSIS golden differential vs the prior
commit is bit-identical across all 17 metrics; TOPSIS invalid-p warnings 2 -> 1;
validator messages unchanged; py_compile clean, ruff E,F,W <= main baseline,
ruff format clean.

* refactor(quickadapter): address re-review nits (TypeVar soundness, comment cleanup)

- NEW-1: rebuild the invalidated cache via type(cache)(...) in _invalidate_pair_cache
  so the _PairCacheT TypeVar return is sound. pyright: prior plain-dict-comprehension
  reassignment reported reportAssignmentType (1 error); type(cache)(...) reports 0.
  Runtime identical (type(cache) is dict for the two concrete caches).
- NEW-2: drop the 32 remaining tautological `# "name"` comments sitting next to a
  named constant (e.g. `== _TRADE_LONG  # "long"`), consistent with the H3 cleanup.
  Index-resolving comments on the canonical definitions (e.g. `_DISTANCE_METRICS[6]
  # "mahalanobis"`) are kept — they document the literal an index maps to.

Verified in the project Docker image: CP/TOPSIS bit-identical vs prior commit across
all metrics; _invalidate_pair_cache runtime unchanged; py_compile clean, ruff E,F,W
<= main baseline, ruff format clean.

* refactor(quickadapter): strip residual tautological named-constant comments

Complete NEW-2 cleanup: remove 14 tautological `# "name"` comments on
multi-line comparison/closing-paren lines that the single-line strip
missed (e.g. `): # "power_mean"`, `) # "short"`), fixing the
long-removed/short-kept asymmetry. Removing the pinning comments lets
ruff format collapse the parenthesized `if (...)` forms to single lines.

Keeps index-resolving comments (`_ARRAY[n] # "x"`) and the
LABEL_METHOD_DEFAULT value-documenting comment. Comment-only + format
change: AST verified identical to prior commit; ruff E,F,W within
baseline; container import smoke passes.

2 months agorefactor(quickadapter): centralize exit_pricing and reversal_confirmation validation...
Jérôme Benoit [Sat, 25 Jul 2026 14:10:01 +0000 (16:10 +0200)] 
refactor(quickadapter): centralize exit_pricing and reversal_confirmation validation (#125)

Fold trade_price_target_method and reversal_confirmation inline validation onto the declarative spec-table pattern (get_exit_pricing_config, get_reversal_confirmation_config); coupled min/max NATR pair kept on validate_range. Remove the vestigial default_reversal_confirmation ClassVar. Guard np.isfinite via a shared _is_finite_value helper so every numeric validation path is total (no raise on ints >= 2**64 or non-scalar input). Cache config-derived validation properties (cached_property) in the strategy and regressor. Behavior preserved for all realistic inputs.

2 months agofix(quickadapter): refit iteration aliases, test_size None docs, live holdout_rmse...
Jérôme Benoit [Sat, 25 Jul 2026 00:08:57 +0000 (02:08 +0200)] 
fix(quickadapter): refit iteration aliases, test_size None docs, live holdout_rmse (#124)

* fix(quickadapter): clear regressor iteration aliases before refit

The refit set the canonical iteration parameter on a deep copy of the user
model_training_parameters without removing configured synonyms. CatBoost aborts
on duplicate iteration aliases, so a selection fit that used n_estimators,
num_boost_round, or num_trees crashed at the post-holdout refit. Purge all known
iteration aliases per regressor before setting the canonical count.

* docs(quickadapter): correct test_size None applicability

None reaches sklearn dynamic sizing only through timeseries_split; the
train_test_split path rejects any non-int/float value. Document that None
applies to timeseries_split and that train_test_split requires a float or int.

* fix(quickadapter): restore holdout_rmse on live model reload

A live or dry-run restart that reuses a cached model without invoking fit left
_holdout_rmse at the constructor inf placeholder, so fit_live_predictions
published holdout_rmse=inf until the next retraining window. Track pairs fitted
in the current session and, when none ran, recover the last finite holdout_rmse
from historic_predictions.

* fix(quickadapter): complete lightgbm iteration aliases and guard alias-map coverage

The lightgbm alias set omitted max_iter, num_round, and num_tree. LightGBM does
not reject duplicate iteration synonyms; it silently lets a leftover alias win
over n_estimators, so a config using one of those names would train the refit at
its original capacity instead of the selected count. Complete the set to every
num_iterations synonym and add an import-time guard that the alias map covers
REGRESSORS.

* fix(quickadapter): coerce holdout_rmse to numeric before finite filter

Guard the live holdout_rmse restore against a non-float historic column: coerce
with pd.to_numeric(errors=coerce) so np.isfinite cannot raise on an object dtype,
matching the defensive handling of the non-live replay branch.

* refactor(quickadapter): consolidate per-regressor metadata into RegressorSpec

Replace the hand-maintained REGRESSORS tuple, the _REFIT_ITERATION_ALIASES map
and the scattered magic-index dispatch with a single RegressorSpec source: the
_REGRESSOR_SPECS NamedTuple singleton (mirroring _OPTUNA_NAMESPACES) carries each
regressor's canonical iteration parameter, its iteration aliases and its RNG seed
parameter. REGRESSORS, DEFAULT_REGRESSOR and the by-name lookup derive from it,
and import-time guards enforce coverage of the Regressor literal and that each
canonical iteration parameter is one of its own aliases.

get_refit, fit_regressor and get_optuna_study_model_parameters now dispatch on
named specs (regressor == _REGRESSOR_SPECS.xgboost.name) instead of REGRESSORS[i];
fit_regressor's identical per-branch seed setdefault + trial increment is hoisted
into one spec-driven prelude. Behavior preserved: same REGRESSORS values/order,
same seed handling (verified identical across all five regressors), introspection
and library-specific fit logic left in their branches.

QuickAdapterRegressorV3 uses DEFAULT_REGRESSOR instead of REGRESSORS[0].

* fix(quickadapter): recover last published holdout_rmse (incl. inf) on live reload

Restoring the last FINITE historic holdout_rmse discarded an intentional inf from
a model trained with test_size=0 and could resurrect a stale finite score from an
earlier holdout-enabled configuration. Take the last published (non-null) value
instead, so the recovered metric faithfully reflects the cached model.

* docs(quickadapter): restore inner-validation 0.1 fallback note for test_size

The test_size row documented only the outer None behavior after the applicability
fix; restore that the inner validation split falls back to 0.1, and tighten the
refit clause to keep the row within the table column width.

2 months agofix(quickadapter): isolate holdout and refit causal window (#115)
Jérôme Benoit [Fri, 24 Jul 2026 21:11:44 +0000 (23:11 +0200)] 
fix(quickadapter): isolate holdout and refit causal window (#115)

* fix(quickadapter): isolate holdout and refit causal window

* fix(quickadapter): align holdout selection and reporting

* fix(quickadapter): harden holdout/refit robustness and dedup imports (review fixes F1-F5)

* fix(quickadapter): migrate hp_rmse key and recompose inner-split weights (RC1,RC3)

* chore(quickadapter): drop hp_rmse config migration (assumed breaking change)

* docs(quickadapter): drop duplicated shuffle constraint from test_size row

* docs(quickadapter): correct test_size default to 0.1 (freqtrade-aligned, not template value)

* refactor(quickadapter): consolidate holdout_rmse init to a single site in fit()

* fix(quickadapter): guard infeasible inner validation split

Raise a contextual DependencyException when an integer test_size is not
smaller than the training rows left after the outer holdout, and when the
feature pipeline (SVM/DBSCAN outlier removal) empties the validation set
after transform, instead of surfacing a low-level sklearn/predict error.

* refactor(quickadapter): normalize None test_size in _add_refit_data

Match _add_validation_split: coerce a None test_size to _TEST_SIZE before
the ==0 short-circuit, so the refit gate stays consistent with the
validation gate regardless of the _TEST_SIZE value.

2 months agofix(quickadapter): track exact label availability (#113)
Jérôme Benoit [Fri, 24 Jul 2026 14:00:43 +0000 (16:00 +0200)] 
fix(quickadapter): track exact label availability (#113)

Record the exact row where each Zigzag label becomes final, include TA-Lib NATR warmup/backfill in label provenance, leave unresolved trailing labels unavailable, compose centered smoothing availability over the complete kernel support, keep the incomplete right smoothing edge unavailable, reject circular wrap smoothing in causal mode, and preserve the public nine-element zigzag() tuple API.

Adds a per-row known_at_lookahead availability channel for labels and weights (weight availability lags the label by one pivot), with an optional local Gaussian fill band; the causal purge folds label and weight availability row-wise. Default path (strategy=none, fill=zero) stays byte-identical; the knn/neighbors>=2 residual is bounded, non-default, and documented.

2 months agorefactor(quickadapter): extract is_finite_number to Utils and reuse it (#122)
Jérôme Benoit [Thu, 23 Jul 2026 19:25:54 +0000 (21:25 +0200)] 
refactor(quickadapter): extract is_finite_number to Utils and reuse it (#122)

* refactor(quickadapter): extract is_finite_number to Utils and reuse it

- move the numeric/finite/non-bool scalar guard from a QuickAdapterV3
  static method to a shared Utils.is_finite_number helper
- reuse it across the strategy label getters/setters and the shared
  label_natr_multiplier validation guard
- leave the strict positive-int label_period_candles/label_horizon_candles
  paths unchanged (distinct predicate)

* refactor(quickadapter): harden label-param fallbacks and per-row NATR guard

Address the #112 re-review findings (landed in this PR):
- coerce the per-row label_period_candles series with pandas to_numeric
  before np.isfinite, matching the guarded scalar path (object/str dtype no
  longer raises)
- re-validate the persisted _label_params fallback in the label getters via
  is_finite_number before use, otherwise fall back to config
- generalize the is_trade_runmode comment (it gates both persisted-param
  reuse and per-candle param setting)
- README: drop the inaccurate 'simulated' wording and the runmode-specific
  framing on the label_period_candles/label_natr_multiplier rows

* docs(quickadapter): drop per-candle label-param behavior notes from tunables

The label_period_candles/label_natr_multiplier rows describe labeling
tunables; the per-candle strategy-NATR/exit consumption is an internal
behavior detail (already documented inline in the code), not needed to
configure the tunable. Revert both rows to their base description.

* style(quickadapter): wrap set_label_natr_multiplier guard per ruff format

The committed one-liner exceeded the 88-char line length; apply ruff
format wrapping (behavior unchanged).

* docs(quickadapter): align warm_start tunable row padding

Trim one over-pad space so the warm_start row matches the surrounding table column width (whitespace only).

2 months agofix(quickadapter): preserve label params per candle (#112)
Jérôme Benoit [Thu, 23 Jul 2026 18:31:00 +0000 (20:31 +0200)] 
fix(quickadapter): preserve label params per candle (#112)

* fix(quickadapter): preserve label params per candle

* fix(quickadapter): reject invalid dynamic label params

* refactor(quickadapter): harden label-param validation and dedupe runmode gate

- add _is_finite_number guard rejecting bool and non-numeric before
  np.isfinite (which raised on str/object) across the label getters/setters
- factor the duplicated runmode-in-TRADE_MODES predicate into the
  is_trade_runmode cached_property
- document the per-candle NATR construction and drop the redundant fillna
  after where() in the per-row NATR path
- note that backtest and hyperopt (not only backtest) retain the per-candle
  label period in the README

2 months agofix(quickadapter): make HPO state causal in backtests (#111)
Jérôme Benoit [Thu, 23 Jul 2026 17:39:11 +0000 (19:39 +0200)] 
fix(quickadapter): make HPO state causal in backtests (#111)

* fix(quickadapter): make label HPO causal in backtests

* fix(quickadapter): harden causal label HPO bounds, docs and harmonization

- guard empty prediction history and use NaT/order-safe max()/min() when
  bounding label HPO OHLCV to the current FreqAI prediction time
- gate strategy label-param loading on freqtrade TRADE_MODES to mirror the
  regressor self.live gate
- document the point-in-time study reset (supersedes explicit continuous=false)
  and causal warm-start seeding
- make the optuna_hyperopt.enabled README entry terse and add a dedicated
  causal label HPO note; unify terminology on current FreqAI prediction time

* docs(quickadapter): drop causal label HPO note from README

* docs(quickadapter): note label-namespace continuous override and refine HPO bound docstring

* fix(quickadapter): isolate HPO state in non-live runs

* fix(quickadapter): isolate non-live Optuna storage

* fix(quickadapter): silence benign Optuna study deletion on fresh storage

- treat a missing study on delete as a debug no-op: non-live runs use a
  fresh InMemoryStorage and the first live/dry-run optimization per pair
  has no persisted study yet, so optuna.delete_study raises KeyError; keep
  warning+traceback for genuine deletion failures
- drop redundant point-in-time frame copies: DataProvider.get_pair_dataframe
  already returns a caller-owned frame and it is only read downstream
- lower the non-live 'Label HPO skipped' bounds logs to debug (expected
  backtest warmup states, consistent with the throttle debug log)
- refine the point-in-time docstring (dk.full_df is the full feature frame)
  and note that self.live is unset at __init__

* docs(quickadapter): tighten HPO comments and de-parenthesize README

- make the __init__ trade-mode, non-live point-in-time HPO, and delete_study
  KeyError comments more precise and concise without dropping semantics
- reword the delete_study comment to point at the warning branch instead of
  the inaccurate 'real failures raise otherwise'
- rephrase the optuna_hyperopt.continuous README entry without a parenthetical
  precision, consistent with the surrounding prose

* chore(serena): migrate project config to the language_servers schema

Serena renamed the deprecated `languages` key to `language_servers` and
refreshed the accompanying comments; regenerate the tracked project config
to match.

2 months agoperf(quickadapter): shorten training period to 21 days
Jérôme Benoit [Tue, 21 Jul 2026 11:34:46 +0000 (13:34 +0200)] 
perf(quickadapter): shorten training period to 21 days

2 months agoMerge pull request #109 from jerome-benoit/fix/quickadapter-take-profit-stage-overshoot
Jérôme Benoit [Mon, 20 Jul 2026 21:34:10 +0000 (23:34 +0200)] 
Merge pull request #109 from jerome-benoit/fix/quickadapter-take-profit-stage-overshoot

fix(quickadapter): bound take-profit stage to filled take-profit exits

2 months agoMerge branch 'main' into fix/quickadapter-take-profit-stage-overshoot
Jérôme Benoit [Mon, 20 Jul 2026 21:33:46 +0000 (23:33 +0200)] 
Merge branch 'main' into fix/quickadapter-take-profit-stage-overshoot

2 months agoMerge pull request #110 from jerome-benoit/chore/quickadapter-fill-sigma-candles...
Jérôme Benoit [Mon, 20 Jul 2026 21:33:35 +0000 (23:33 +0200)] 
Merge pull request #110 from jerome-benoit/chore/quickadapter-fill-sigma-candles-default

chore(quickadapter): raise default fill_sigma_candles to 25.0

2 months agochore(quickadapter): raise default fill_sigma_candles to 25.0
Jérôme Benoit [Mon, 20 Jul 2026 21:31:57 +0000 (23:31 +0200)] 
chore(quickadapter): raise default fill_sigma_candles to 25.0

Update the canonical DEFAULTS_LABEL_WEIGHTING value, the config template,
and the tunables documentation in lockstep. The default is inert for the
default fill_method ("zero"); it only affects the per-pivot Gaussian
bandwidth (and its knn clip upper bound) when fill_method is "gaussian"
or "epsilon_gaussian".

2 months agochore(quickadapter): bump model and strategy version to 3.12.4
Jérôme Benoit [Mon, 20 Jul 2026 20:11:43 +0000 (22:11 +0200)] 
chore(quickadapter): bump model and strategy version to 3.12.4

2 months agorefactor(quickadapter): disambiguate and centralize final exit stage constants
Jérôme Benoit [Mon, 20 Jul 2026 20:11:15 +0000 (22:11 +0200)] 
refactor(quickadapter): disambiguate and centralize final exit stage constants

Rename the _FINAL_EXIT_STAGE tuple to _FINAL_EXIT_STAGE_PARAMS and add the
class-level constant _FINAL_EXIT_STAGE_INDEX (max(partial_exit_stages) + 1),
referenced from the configuration logging, the get_trade_exit_stage clamp,
and the plot config. This removes the triplicated stage-index expression and
distinguishes the final stage parameters from its index. Qualify the plot
config partial_exit_stages and stage-index accesses with QuickAdapterV3 to
match the surrounding constant access.

2 months agofix(quickadapter): bound take-profit stage to filled take-profit exits
Jérôme Benoit [Mon, 20 Jul 2026 19:44:08 +0000 (21:44 +0200)] 
fix(quickadapter): bound take-profit stage to filled take-profit exits

Derive the take-profit stage from filled take-profit-tagged exit orders
clamped to the final full-exit stage, instead of nr_of_successful_exits
plus in-flight exit orders. This stops the stage index from overshooting
its maximum (e.g. take_profit_*_4 while the final stage is 3) when a
final exit order is in flight or a non-take-profit exit fills.

Guard custom_exit against re-issuing the final take-profit exit while an
order is open by returning None after the model-expiry and reversal
safety exits and before the take-profit stage computation, so safety
exits still fire while in-flight orders no longer trigger a duplicate
take-profit exit.

2 months agorefactor: qualify static method calls
Jérôme Benoit [Mon, 13 Jul 2026 00:08:53 +0000 (02:08 +0200)] 
refactor: qualify static method calls

2 months agoMerge pull request #107 from jerome-benoit/renovate/lock-file-maintenance
Jérôme Benoit [Fri, 10 Jul 2026 20:53:04 +0000 (22:53 +0200)] 
Merge pull request #107 from jerome-benoit/renovate/lock-file-maintenance

chore(deps): lock file maintenance

2 months agostyle(reforcexy): format journal recovery helper
Jérôme Benoit [Sat, 4 Jul 2026 00:07:02 +0000 (02:07 +0200)] 
style(reforcexy): format journal recovery helper

2 months agofix(reforcexy): make optuna journal recovery spawn-safe
Jérôme Benoit [Sat, 4 Jul 2026 00:04:52 +0000 (02:04 +0200)] 
fix(reforcexy): make optuna journal recovery spawn-safe

2 months agochore(quickadapter): bump model and strategy version to 3.12.3
Jérôme Benoit [Fri, 3 Jul 2026 22:50:38 +0000 (00:50 +0200)] 
chore(quickadapter): bump model and strategy version to 3.12.3

2 months agochore(serena): update project configuration
Jérôme Benoit [Fri, 3 Jul 2026 22:20:52 +0000 (00:20 +0200)] 
chore(serena): update project configuration

2 months agoMerge pull request #106 from jerome-benoit/feat/reforcexy-quickadapter-port
Jérôme Benoit [Fri, 3 Jul 2026 22:16:21 +0000 (00:16 +0200)] 
Merge pull request #106 from jerome-benoit/feat/reforcexy-quickadapter-port

fix(reforcexy): recover corrupt optuna journals

2 months agorefactor(reforcexy): align sampler value naming
Jérôme Benoit [Fri, 3 Jul 2026 22:07:39 +0000 (00:07 +0200)] 
refactor(reforcexy): align sampler value naming

2 months agorefactor(reforcexy): centralize sampler dispatch values
Jérôme Benoit [Fri, 3 Jul 2026 22:00:39 +0000 (00:00 +0200)] 
refactor(reforcexy): centralize sampler dispatch values

2 months agofix(quickadapter): harden optuna journal validation
Jérôme Benoit [Fri, 3 Jul 2026 22:00:28 +0000 (00:00 +0200)] 
fix(quickadapter): harden optuna journal validation

2 months agofix(reforcexy): harden optuna journal validation
Jérôme Benoit [Fri, 3 Jul 2026 21:59:59 +0000 (23:59 +0200)] 
fix(reforcexy): harden optuna journal validation

2 months agofix(optuna): recover non-record journal replay errors
Jérôme Benoit [Fri, 3 Jul 2026 17:01:58 +0000 (19:01 +0200)] 
fix(optuna): recover non-record journal replay errors

2 months agofix(optuna): reject non-record journal tails
Jérôme Benoit [Fri, 3 Jul 2026 15:59:45 +0000 (17:59 +0200)] 
fix(optuna): reject non-record journal tails

2 months agofix(reforcexy): use resolver-compatible journal helper import
Jérôme Benoit [Fri, 3 Jul 2026 15:59:20 +0000 (17:59 +0200)] 
fix(reforcexy): use resolver-compatible journal helper import

2 months agorefactor(quickadapter): import optuna journal storage classes directly
Jérôme Benoit [Fri, 3 Jul 2026 14:53:13 +0000 (16:53 +0200)] 
refactor(quickadapter): import optuna journal storage classes directly

2 months agodocs(reforcexy): remove optuna journal recovery note
Jérôme Benoit [Fri, 3 Jul 2026 14:52:15 +0000 (16:52 +0200)] 
docs(reforcexy): remove optuna journal recovery note

2 months agochore(reforcexy): remove local validation scripts from PR
Jérôme Benoit [Fri, 3 Jul 2026 13:08:55 +0000 (15:08 +0200)] 
chore(reforcexy): remove local validation scripts from PR

2 months agodocs(reforcexy): document optuna journal recovery
Jérôme Benoit [Thu, 2 Jul 2026 23:50:19 +0000 (01:50 +0200)] 
docs(reforcexy): document optuna journal recovery

2 months agorefactor(reforcexy): cache supported modes as immutable constants
Jérôme Benoit [Thu, 2 Jul 2026 23:50:09 +0000 (01:50 +0200)] 
refactor(reforcexy): cache supported modes as immutable constants

2 months agorefactor(reforcexy): make optuna sampler dispatch exhaustive
Jérôme Benoit [Thu, 2 Jul 2026 23:49:44 +0000 (01:49 +0200)] 
refactor(reforcexy): make optuna sampler dispatch exhaustive

2 months agofix(reforcexy): recover corrupt optuna journals
Jérôme Benoit [Thu, 2 Jul 2026 23:48:41 +0000 (01:48 +0200)] 
fix(reforcexy): recover corrupt optuna journals

2 months agochore(deps): lock file maintenance (#105)
renovate[bot] [Mon, 29 Jun 2026 18:10:34 +0000 (20:10 +0200)] 
chore(deps): lock file maintenance (#105)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months agofix(quickadapter): disable broken kmedoids dependency
Jérôme Benoit [Mon, 29 Jun 2026 15:06:59 +0000 (17:06 +0200)] 
fix(quickadapter): disable broken kmedoids dependency

2 months agochore(deps): update dependency ngboost to v0.5.11 (#104)
renovate[bot] [Sat, 27 Jun 2026 12:17:20 +0000 (14:17 +0200)] 
chore(deps): update dependency ngboost to v0.5.11 (#104)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months agochore(quickadapter): bump model and strategy version to 3.12.2
Jérôme Benoit [Mon, 22 Jun 2026 13:12:56 +0000 (15:12 +0200)] 
chore(quickadapter): bump model and strategy version to 3.12.2

3 months agostyle(quickadapter): emit label_horizon_candles per pair in HPO=on log block
Jérôme Benoit [Mon, 22 Jun 2026 13:07:40 +0000 (15:07 +0200)] 
style(quickadapter): emit label_horizon_candles per pair in HPO=on log block

Two post-implementation review findings (Oracle A + Oracle B) resolved
together by a single design change.

Oracle A flagged a latent value-mismatch in the HPO=on branch: the
global `self._label_horizon_candles()` call (no `pair` argument) reads
from `ft_params` only, so when `label_horizon_candles` is unset and
the helper falls back to `label_period_candles`, the logged value
reflects the global initial seed rather than the per-pair effective
value used at fit time (where HPO-tuned `label_period_candles` per
pair drives the fallback). When the user sets `label_horizon_candles`
explicitly, the global call is correct.

Oracle B flagged a cross-branch ordering inconsistency: HPO=on emitted
`label_horizon_candles` first (before per-pair lines), while HPO=off
emitted it last (after period + multiplier). Maintainers comparing
logs across HPO toggles see the same logical scalar in different
positions.

Move the per-pair effective `label_horizon_candles` into the per-pair
line in the HPO=on branch (third field after `label_period_candles`
and `label_natr_multiplier`). This closes the per-pair accuracy gap
(Oracle A) and yields the same `period → multiplier → horizon` order
in both branches (Oracle B). No behavioral change; logging only.

3 months agorefactor(quickadapter): disambiguate label_period_candles vs label_horizon_candles
Jérôme Benoit [Mon, 22 Jun 2026 13:00:18 +0000 (15:00 +0200)] 
refactor(quickadapter): disambiguate label_period_candles vs label_horizon_candles

Three distinct issues addressed together for terminology coherence
and operator visibility.

(1) README: replace "NATR horizon" by "NATR period" in the four rows
documenting `label_period_candles` / `min_label_period_candles` /
`max_label_period_candles` / `label_candles_step`. The noun
"horizon" was also the noun in `label_horizon_candles`, which has
the opposite temporal direction (lookahead causal-split guard vs
lookback NATR period). "NATR period" matches both the tunable name
(`label_period_candles`) and the underlying API
(`ta.NATR(..., timeperiod=label_period_candles)`).

(2) `QuickAdapterV3.set_freqai_targets`: rename the local
`label_period` (a `datetime.timedelta` spanning
`len(dataframe) * timeframe_minutes`) to `series_duration`, and
update the two log labels (`label_period: 3 days, 12:00:00` →
`series_duration: 3 days, 12:00:00`). The previous name collided
with `label_period_candles` (an int candle count) at the operator
log level; `series_duration` matches the sibling `series_length`
(an int) declared two lines below.

(3) `QuickAdapterRegressorV3.__init__` startup dump: add
`label_horizon_candles` to the "Label Parameters:" section (both
HPO-enabled and HPO-disabled branches). The parameter is a
training-time data-split lookahead (causal split guards in
`_make_train_test_split_datasets` / `_make_timeseries_split_datasets`),
NOT an HPO hyperparameter — it is neither sampled
(`trial.suggest_int`) nor bounded (`min/max_*`) nor an HPO input to
`label_objective`. Placing it in "Label Parameters:" (runtime
values) rather than "Label Hyperparameters:" (HPO config) matches
its actual semantics. In the HPO-on branch the global value is
emitted before the per-pair loop to visually distinguish it; in the
HPO-off branch it trails the existing global lines so the natural
order is `label_period_candles` → `label_natr_multiplier` →
`label_horizon_candles` (the latter falls back to the former when
unset).

3 months agochore(deps): lock file maintenance (#91)
renovate[bot] [Mon, 22 Jun 2026 12:37:23 +0000 (14:37 +0200)] 
chore(deps): lock file maintenance (#91)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months agostyle(quickadapter): tighten tail probe docstring clause (c)
Jérôme Benoit [Mon, 22 Jun 2026 12:36:33 +0000 (14:36 +0200)] 
style(quickadapter): tighten tail probe docstring clause (c)

The (c) clause previously named both `ValueError` and
`UnicodeDecodeError` plus their subclass relationship. The exception
classes are visible at the `except ValueError` line below — keep the
behavioral description in the docstring ("not parseable by
json.loads (malformed JSON or invalid UTF-8)") and let the code
document the catch surface.

3 months agofix(quickadapter): catch UnicodeDecodeError in journal tail probe
Jérôme Benoit [Mon, 22 Jun 2026 12:35:27 +0000 (14:35 +0200)] 
fix(quickadapter): catch UnicodeDecodeError in journal tail probe

Codex inline review (P2) on PR #102 flagged that
`_optuna_journal_has_corrupt_tail` catches only
`json.JSONDecodeError` from `json.loads(last_line)`, but
`json.loads` raises `UnicodeDecodeError` (subclass of
`ValueError`, NOT of `JSONDecodeError`) when the trailing record
contains invalid UTF-8 bytes — a common crash pattern when
`fsync` is interrupted mid-multibyte. The exception escapes the
helper, propagates out of `optuna_create_storage` (the helper
runs BEFORE the recoverable try/except), reaches
`optuna_create_study`'s broad outer handler, and reproduces the
silent-HPO-skip symptom under a different corruption class.

Empirically reproduced:

  >>> json.loads(b'\\xc3\\x28')
  UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc3 in
  position 0: invalid continuation byte

Broaden the except clause from `json.JSONDecodeError` to
`ValueError` — the common parent of both `JSONDecodeError` and
`UnicodeDecodeError` — so the helper treats any
`json.loads`-unparseable trailing record as corruption and routes
it through the same quarantine path. Other `ValueError` subclasses
are not plausibly raised by `json.loads` on bytes input.

Reproducer at `/tmp/quickadapter-tests/test_optuna_journal_quarantine.py`
extended from 17 to 19 scenarios (Class C4 detection + end-to-end
quarantine on invalid UTF-8). All pass.

3 months agofix(quickadapter): quarantine corrupt optuna journal log and recover (#102)
Jérôme Benoit [Mon, 22 Jun 2026 12:31:01 +0000 (14:31 +0200)] 
fix(quickadapter): quarantine corrupt optuna journal log and recover (#102)

When `JournalFileBackend`'s replay encounters a corrupt journal record, optuna raises during `JournalStorage.__init__` (Class A/B: immediate `KeyError`/`json.JSONDecodeError`) or defers the raise to the next `_sync_with_backend` (Class C1/C2/C3: truncated tail / malformed last line / bare trailing newline at EOF). Both paths previously caused HPO for the affected pair to be silently skipped on every fit cycle.

Wrap `JournalStorage` construction in a narrow `try / except (KeyError, ValueError, json.JSONDecodeError)`, atomically rename the corrupt log aside as `optuna-<COIN>.log.corrupt-<UTC_µs>`, log a `WARNING`, and retry once on a fresh path. A bounded O(1) tail probe runs BEFORE construction to detect deferred-raise EOF corruption. `OSError` is intentionally excluded so filesystem failures stay operator-actionable.

The recovery follows RocksDB's documented WAL recovery philosophy (quarantine + restart), preserves forensic evidence (rename, no `unlink`), and keeps the live-journal glob `optuna-*.log` from matching quarantined artefacts.

3 months agorefactor(quickadapter): dispatch optuna_create_sampler via match + assert_never ...
Jérôme Benoit [Mon, 22 Jun 2026 10:09:19 +0000 (12:09 +0200)] 
refactor(quickadapter): dispatch optuna_create_sampler via match + assert_never (#103)

Follow-up to #101. Converts the `optuna_create_sampler` if/elif/else dispatch chain to a `match`/`case` statement using value patterns (`QuickAdapterRegressorV3._OPTUNA_SAMPLERS.<name>`) -- the per-field singleton `Literal[...]` typing introduced by #101 unlocks pyright/mypy exhaustiveness narrowing, so the final `case _: assert_never(sampler)` type-checks as `Never` and catches any future extension of `OptunaSampler` that forgets to add a corresponding match branch.

The `case None:` branch is structurally required (not stylistic): without it, after the four `Literal[...]` value patterns, pyright/mypy would narrow `sampler` to `None`, not `Never`, and `assert_never(sampler)` would fail to type-check. Its presence is what makes the "5th sampler addition -> type error at assert_never" claim effective. The error message inside `case None:` is preserved verbatim from the prior `else: raise ValueError(...)` for user-facing wire compatibility.

Behavior delta is confined to non-`Literal` string inputs (e.g. dynamically-injected `"garbage"`): prior code raised `ValueError` with the supported-values list; new code raises `AssertionError` with the standard `typing.assert_never` message. In practice this path is unreachable from every current in-repo call site -- the sole caller `optuna_create_study` already validates `sampler not in samplers` and raises `ValueError` with the supported-values list before dispatching, so misconfigured `_optuna_config["sampler"]` values surface the old error shape from the upstream gate. The new `AssertionError` would only manifest via a hypothetical future caller that bypasses `optuna_create_study`, which does not exist today.

Pattern parity: the same `assert_never` exhaustiveness idiom is already used in this file for `support_policy` dispatch; value-pattern syntax matches the 8 call sites migrated by #101.

Closes the deferred follow-up from PR #101 (issue #88).

3 months agorefactor(quickadapter): consolidate Optuna sampler tuples to NamedTuple (#101)
Jérôme Benoit [Mon, 22 Jun 2026 09:32:44 +0000 (11:32 +0200)] 
refactor(quickadapter): consolidate Optuna sampler tuples to NamedTuple (#101)

PR #81 consolidated `_OPTUNA_NAMESPACES` to `Utils._OptunaNamespaces` (a `NamedTuple` with per-field singleton `Literal` types). Propagate the same pattern to the three sibling sampler tuples in `QuickAdapterRegressorV3.py`:

- `_OPTUNA_SAMPLERS` (tpe, auto, nsgaii, nsgaiii)
- `_OPTUNA_HPO_SAMPLERS` (tpe, auto)
- `_OPTUNA_LABEL_SAMPLERS` (auto, tpe, nsgaii, nsgaiii)

Each new `_Optuna*Samplers` NamedTuple class lives at module level immediately before `class QuickAdapterRegressorV3`, matching the `_OptunaNamespaces` adjacency in `Utils.py`. Per-field types are singleton `Literal["..."]` (not the `OptunaSampler` union) to unlock pyright/mypy narrowing for a future `assert_never` migration. The class-private `_OPTUNA_*_SAMPLERS` instance constants remain class-attributes of `QuickAdapterRegressorV3` to preserve every consumer's `QuickAdapterRegressorV3._OPTUNA_*` access pattern.

Each `_Optuna*Samplers` redeclares its field defaults (per-class `Literal["tpe"] = "tpe"` etc.), replacing the prior tuple-slice derivation `_OPTUNA_HPO_SAMPLERS = _OPTUNA_SAMPLERS[:2]` and the 6-line custom reordering of `_OPTUNA_LABEL_SAMPLERS`. Trade-off: minor string-literal duplication across 3 classes is accepted for harmonization with the `_OptunaNamespaces` template; the single source of truth for the 4 valid tokens stays the `OptunaSampler = Literal[...]` alias unchanged at module level.

Migrates 8 positional-indexing call sites (`_OPTUNA_*_SAMPLERS[N]  # "name"`) to attribute access (`.<name>`); drops the 6 surviving inline annotations at the call sites (2 sites carry no annotation pre-refactor; the multi-line site collapses 3 source lines into 1) plus the 4 analogous annotations inside the deleted 6-line `_OPTUNA_LABEL_SAMPLERS` custom-ordering block. The `_OPTUNA_HPO_SAMPLERS_SET` and `_OPTUNA_LABEL_SAMPLERS_SET` frozenset companions are kept unchanged (still used in O(1) membership testing at `optuna_samplers_by_namespace`); their type annotation `Final[frozenset[OptunaSampler]]` is preserved.

Non-migration sites confirmed unchanged: the frozenset companion construction, the `', '.join` error-message iteration over `_OPTUNA_SAMPLERS`, and the `_SET` membership references. All iterate over the NamedTuple instance and produce byte-identical output.

Add `NamedTuple` to the existing `from typing import (...)` block (alphabetical, between `Literal` and `Optional`). `assert_never` already imported, kept in anticipation of the deferred follow-up.

Per-field singleton `Literal[...]` unlocks `assert_never` exhaustiveness on the `optuna_create_sampler` dispatch chain -- left to a follow-up PR since the migration (`else: raise ValueError(...)` becoming `assert_never(sampler)`) changes the user-facing error contract on the unreachable branch.

The AGENTS.md *Canonical defaults* principle and the README documented enum order are encoded in the field-declaration order of each `_Optuna*Samplers`. NamedTuple remains a `tuple` subclass, so `[0]` indexing, `len(...)`, `frozenset(...)`, `', '.join(...)`, and iteration all keep their existing semantics. No behavior change.

Reviewed by two pre-implementation design passes (3-oracle on v1; Metis + Momus + meta-Oracle on v2) and a 3-oracle review on the live PR, each citing upstream evidence from `freqtrade/freqai/` confirming no external consumer.

Follow-up from PR #81 review (Oracle harmonization dimension).

Closes #88.

3 months agorefactor(quickadapter): state-form prose and harmonization follow-up (#100)
Jérôme Benoit [Mon, 22 Jun 2026 08:36:14 +0000 (10:36 +0200)] 
refactor(quickadapter): state-form prose and harmonization follow-up (#100)

Consolidates content + harmonization + Python-idiom follow-up findings
from the 4-axis review of `add1fb7..7c8197b` (PRs #90, #94, #95, #96,
#97, #98, #99 + 4 style commits + 1 chore version-constant update).

Prose state-form (`Utils.py`, `QuickAdapterRegressorV3.py`):
- `_normalize_label_column_name` docstring: ``Raises ValueError when
  the result contains `&` or `%` after sigil strip``.
- Deprecated-config-key warning aligns with the sibling pattern at
  the adjacent branch: ``f"{old_path!r} is deprecated, use
  {new_path!r} instead"``.
- `sanitize_and_renormalize` docstring states ``mean(out) == 1`` as the
  rescale invariant.
- Optuna-label throttle log reads ``callback throttled,
  {N} candles until next emission``.
- Fit-live-predictions warmup log reads ``Fit live predictions not
  warmed up: {N} candles until warmup completion``.

Docstrings on validator/composer helpers (3 functions lacking a
docstring at HEAD):
- `_apply_support_policy`: documents the ``policy='raise'`` /
  ``policy='fallback'`` dispatch contract.
- `_compose_train_weights_with_support`: documents the support-gating
  flow (None-label-weights branch routes through
  ``_apply_support_policy`` when ``strategy != 'none'``; main branch
  composes and validates the summary against three thresholds).
- `_validate_optuna_label_best_params`: enumerates the rejection
  paths and the optional ``expected_selection_metadata`` drift gate.

Harmonization (post-merge carry-over):
- `LABEL_WEIGHT_SUFFIX` renamed to `_LABEL_WEIGHT_SUFFIX`
  (no external consumer; symmetric with
  `_LABEL_KNOWN_AT_LOOKAHEAD_SUFFIX`).
- `safe_distribution_fit` call-site contexts harmonized with the
  PR #97 / PR #99 ``[<pair>] <event>`` convention:
  `f"[{pair}] di_values_weibull_fit"` and
  `f"[{pair}] label_norm_fit:{label_col}"`.

Python idioms:
- `_adapt_label_generator` rejects any 3rd positional parameter
  whose name is not ``logger``, regardless of whether the parameter
  is required or has a default. A defaulted non-``logger`` 3rd
  positional raises ``ValueError`` at registration. The 3-arg
  pass-through is reached only when ``positional[2].name == "logger"``.
- `_build_sample_weight_inputs` switches the two `logger.debug`
  calls to lazy ``%s`` formatting so the f-string body is not
  materialized when the debug level is disabled.

3 months agochore(quickadapter): bump model and strategy version to 3.12.1
Jérôme Benoit [Mon, 22 Jun 2026 02:17:30 +0000 (04:17 +0200)] 
chore(quickadapter): bump model and strategy version to 3.12.1

3 months agorefactor(quickadapter): post-merge harmonization follow-up (#99)
Jérôme Benoit [Mon, 22 Jun 2026 02:12:50 +0000 (04:12 +0200)] 
refactor(quickadapter): post-merge harmonization follow-up (#99)

Consolidates SAFE + DRY + COSMETIC findings from the 4-axis review of
`add1fb7..d6a718f` (PRs #90, #94, #95, #96, #97 + 2 style + d6a718f).

Migration (PR #94 carry-over to `QuickAdapterV3.py`):
- `_TRADE_DIRECTIONS_SET` and `_ORDER_TYPES_SET` are
  `Final[frozenset[T]]` constants adjacent to the canonical
  `_TRADE_DIRECTIONS` / `_ORDER_TYPES` tuples; the strategy reads
  them directly at all 3 call sites. The
  `@staticmethod @lru_cache(maxsize=None)` set-views are absent at
  HEAD.

Context-prefix harmonization (PR #97 carry-over):
- The 2 `sanitize_and_renormalize` calls in
  `QuickAdapterRegressorV3._apply_pipelines` carry the `[<pair>]`
  prefix (`f"[{pair}] post_feature_pipeline:train"` / `:test`),
  matching the PR #97 convention at the 4 `compose_sample_weights`
  call sites.

Alias removal (PR #96 carry-over):
- `Utils.label_known_at_column_name` is absent at HEAD. The
  underlying column has zero external callers in the repo, and
  PR #96 shifted semantics (absolute index -> per-row offset);
  no back-compat alias is warranted.

DRY (`d6a718f` carry-over):
- The 8-tuple swap blocks in `_make_train_test_split_datasets`
  and `_make_timeseries_split_datasets` use pythonic parallel
  pair-swap (`a, b = b, a` per slot pair); 4 lines per slot pair
  instead of the 18-line 8-tuple parallel assignment.

Dead constant removal:
- `QuickAdapterRegressorV3._AGGREGATE_DISTANCE_METRICS_SET` is
  absent at HEAD; the constant has no caller. The
  `_CLUSTER_DENSITY_DISTANCE_METRICS_SET` block comment lists the
  7 aggregate metrics inline for reference (`harmonic_mean`,
  `geometric_mean`, `arithmetic_mean`, `quadratic_mean`,
  `cubic_mean`, `power_mean`, `weighted_sum`).

Cosmetic:
- `_LABEL_KNOWN_AT_LOOKAHEAD_SUFFIX` placement is adjacent to
  `LABEL_WEIGHT_SUFFIX` (both are label-aux column suffixes).
- `_known_at_lookahead` returns `int64` on both single-series and
  multi-series paths (symmetric dtype contract).
- Schema-version reset log: `f"v{existing_schema_version!r}"`
  renders unambiguously for non-int corrupt values (booleans,
  strings).

3 months agostyle(quickadapter): backtick best_params symbol reference in strategy comment
Jérôme Benoit [Mon, 22 Jun 2026 01:59:05 +0000 (03:59 +0200)] 
style(quickadapter): backtick best_params symbol reference in strategy comment

3 months agostyle(quickadapter): expand reverse-swap tuples to one identifier per line
Jérôme Benoit [Mon, 22 Jun 2026 01:58:00 +0000 (03:58 +0200)] 
style(quickadapter): expand reverse-swap tuples to one identifier per line

3 months agofix(quickadapter): route reversed train weights through support_policy (#98)
Jérôme Benoit [Mon, 22 Jun 2026 01:54:59 +0000 (03:54 +0200)] 
fix(quickadapter): route reversed train weights through support_policy (#98)

PR #85 added `_compose_train_weights_with_support` (gates training-set weights through `support_policy`) and `_compose_eval_weights` (eval-side, deliberately bypasses `support_policy`). The `reverse_train_test_order` path in `_make_train_test_split_datasets` and `_make_timeseries_split_datasets` swapped slices AT THE FINAL `build_data_dictionary` call -- AFTER weight composition -- so the actual-train slot received weights composed by `_compose_eval_weights` (silent bypass), and the actual-test slot received weights composed by `_compose_train_weights_with_support` (wrong direction, typically a no-op under `support_policy='fallback'` default).

Reachable only under `causal_mode=false` (deprecated; acausal baselines only) since `causal_mode=true` rejects `reverse_train_test_order=true` upfront.

Fix: perform the train/test slice swap BEFORE weight composition so the `train_*` and `test_*` identifiers map to their actual training roles throughout. Both call sites converge to a single `dk.build_data_dictionary` return; context strings in `support_policy` log/raise messages now reflect the true train/test role.

Add an upfront `ValueError` in `_make_train_test_split_datasets` when `test_size=0` AND `reverse_train_test_order=True`, mirroring the existing `causal_mode`/reverse rejection pattern. The `timeseries_split` path already rejects `test_size < 1` upstream of the swap.

Behavior change in the deprecated path: `support_policy='raise'` now correctly raises on actual-train insufficient support; `support_policy='fallback'` now correctly warns.

Reviewed by three parallel Oracle passes (math + algorithmics + scope/reachability; Python state-of-the-art + harmonization + implementation elegance; documentation + terminology + completeness) at design stage and again post-implementation, each citing upstream evidence from `freqtrade/freqai/`.

Follow-up from PR #80 review, deferred during PR #90.

Closes #92.

3 months agostyle(quickadapter): collapse short expression lines
Jérôme Benoit [Mon, 22 Jun 2026 01:40:50 +0000 (03:40 +0200)] 
style(quickadapter): collapse short expression lines

3 months agostyle(quickadapter): realign README configuration tunables table
Jérôme Benoit [Mon, 22 Jun 2026 01:39:01 +0000 (03:39 +0200)] 
style(quickadapter): realign README configuration tunables table

3 months agofix(quickadapter): prefix `compose_sample_weights` and `sanitize_and_renormalize...
Jérôme Benoit [Mon, 22 Jun 2026 01:34:16 +0000 (03:34 +0200)] 
fix(quickadapter): prefix `compose_sample_weights` and `sanitize_and_renormalize` log entries with caller context (#97)

`compose_sample_weights` and `sanitize_and_renormalize` accept a
required keyword-only `context: str` parameter; every warning, error,
and inner `sanitize_and_renormalize` call uses `context` as its sole
prefix.

- `compose_sample_weights(..., *, logger, context, on_collapse=...)`:
  `context: str` keyword-only, required. The 4 internal
  warnings/errors (shape-mismatch `ValueError`, all-dropped
  `LabelWeightSupportError`, sparse-mass warning, collapse-on-
  survivors `LabelWeightSupportError` and fallback warning) prefix
  with `{context}:`. Internal `sanitize_and_renormalize` calls
  compose sub-contexts: `{context}:base_only`,
  `{context}:label_weighted`, `{context}:base_fallback`. The sparse-
  mass message reads `sparse weighting mass`, accurate for both
  train and eval paths.
- `sanitize_and_renormalize(..., *, logger=None, context: str)`:
  `context` keyword-only, required. The 5 warnings/errors
  (`drop_mask` shape `ValueError`, `drop_mask` dtype `ValueError`,
  rescale-overflow warning, weights-collapsed warning,
  mask-covers-all warning) prefix with `{context}:`. The redundant
  `(context=%s, ...)` subfield is dropped.
- 5 `compose_sample_weights` call sites in
  `QuickAdapterRegressorV3.py` pass `context=context`. The 2
  external `sanitize_and_renormalize` call sites
  (`post_feature_pipeline:train`, `post_feature_pipeline:test`)
  already pass `context=`.

Log format goes from
`compose_sample_weights: sparse training mass (59/2603 rows ...)`
to
`[ETH/USDT] train_test_split:train: sparse weighting mass (59/2603 rows ...)`.
The pair, split method, and train/eval side are traceable in the
log line.

3 months agorefactor(quickadapter): migrate _*_set() lru_cache family to Final[frozenset] (#94)
Jérôme Benoit [Mon, 22 Jun 2026 01:21:28 +0000 (03:21 +0200)] 
refactor(quickadapter): migrate _*_set() lru_cache family to Final[frozenset] (#94)

Each `@staticmethod @lru_cache(maxsize=None) _X_set()` family member
maps to a class-level `_X_SET: Final[frozenset[...]]` adjacent to its
backing tuple. Derived members express their content through set
algebra over the new `_*_SET` constants.

Pairs (deleted method -> added constant):

- `_extrema_selection_methods_set()` -> `_EXTREMA_SELECTION_METHODS_SET`
- `_custom_threshold_methods_set()` -> `_CUSTOM_THRESHOLD_METHODS_SET`
- `_skimage_threshold_methods_set()` -> `_SKIMAGE_THRESHOLD_METHODS_SET`
- `_threshold_methods_set()` -> `_THRESHOLD_METHODS_SET`
- `_optuna_namespaces_set()` -> `_OPTUNA_NAMESPACES_SET`
- `_optuna_hpo_samplers_set()` -> `_OPTUNA_HPO_SAMPLERS_SET`
- `_optuna_label_samplers_set()` -> `_OPTUNA_LABEL_SAMPLERS_SET`
- `_scaler_types_set()` -> `_SCALER_TYPES_SET`
- `_scipy_metrics_set()` -> `_SCIPY_METRICS_SET`
- `_unsupported_weights_metrics_set()` -> `_UNSUPPORTED_WEIGHTS_METRICS_SET`
- `_probability_distance_metrics_set()` -> `_PROBABILITY_DISTANCE_METRICS_SET`
- `_label_selection_distance_metrics_set()` -> `_LABEL_SELECTION_DISTANCE_METRICS_SET`
  (= `_DISTANCE_METRICS_SET - _PROBABILITY_DISTANCE_METRICS_SET`)
- `_distance_methods_set()` -> `_DISTANCE_METHODS_SET`
- `_selection_methods_set()` -> `_SELECTION_METHODS_SET`
- `_distance_metrics_set()` -> `_DISTANCE_METRICS_SET`
- `_density_aggregations_set()` -> `_DENSITY_AGGREGATIONS_SET`
- `_power_mean_metrics_set()` -> `_POWER_MEAN_METRICS_SET`
- `_aggregate_distance_metrics_set()` -> `_AGGREGATE_DISTANCE_METRICS_SET`
  (= `_DISTANCE_METRICS_SET - _SCIPY_METRICS_SET - _PROBABILITY_DISTANCE_METRICS_SET`)
- `_cluster_density_distance_metrics_set()` -> `_CLUSTER_DENSITY_DISTANCE_METRICS_SET`
  (= `_SCIPY_METRICS_SET - _PROBABILITY_DISTANCE_METRICS_SET`)

Call sites at `(cls|QuickAdapterRegressorV3)._X_set()` resolve to the
matching `_X_SET` reference. `optuna_samplers_by_namespace` returns
`tuple[frozenset[OptunaSampler], OptunaSampler]` to match the new
constant types. The surviving `@lru_cache(maxsize=8)` on
`optuna_samplers_by_namespace` falls outside the `_*_set` family and
stays in place.

Closes #89.

3 months agorefactor(quickadapter): rename known_at_index to known_at_lookahead (#96)
Jérôme Benoit [Mon, 22 Jun 2026 01:20:11 +0000 (03:20 +0200)] 
refactor(quickadapter): rename known_at_index to known_at_lookahead (#96)

PR #95 retained the historical column name `<label>_known_at_index` for what is now a per-row label lookahead in candles, to keep that hotfix strictly minimal. This PR converges the column suffix, the helper, the dataclass field, the static method, and the per-call-site locals onto `_known_at_lookahead`, with a retro-compat alias on the only externally-named public helper (`label_known_at_column_name = label_known_at_lookahead_column_name`).

The auxiliary `<label>_known_at_*` column is regenerated on every training run inside `set_freqai_targets`; FreqAI persists only the fitted model and `extra_returns_per_train`, never auxiliary dataframe columns -- the rename invalidates no on-disk artifact.

Reviewed by three parallel Oracle passes (math + claims-coherence; Python state-of-the-art + harmonization; documentation + terminology + PR-description coherence), each citing upstream evidence from `freqtrade/freqai/freqai_interface.py`, `data_kitchen.py`, and `data_drawer.py`. Consensus fixes were applied: README `causal_mode` formula symbol bound to the column token (`row-wise max(<label>_known_at_lookahead)`) to colocate definition with usage.

The two causal-guard local variable pairs were also harmonized to the local `train_<noun>` family (`train_known_at_lookahead`, `train_known_at_position`) used by the surrounding `_make_*_datasets` methods.

3 months agofix(quickadapter): use slice-invariant lookahead for causal guard (#95)
Jérôme Benoit [Mon, 22 Jun 2026 01:02:56 +0000 (03:02 +0200)] 
fix(quickadapter): use slice-invariant lookahead for causal guard (#95)

* fix(quickadapter): use slice-invariant lookahead for causal guard

PR #78 stored '<label>_known_at_index' as 'arange(len) + horizon +
kernel_half_width' -- absolute positions in the dataframe passed to
'set_freqai_targets'. freqtrade's 'dk.slice_dataframe' (a '.loc' filter)
runs AFTER 'set_freqai_targets' and drops warmup rows but preserves
column values, so those pre-slice positions survived into the post-slice
'unfiltered_df'. The causal guard then compared them against
'first_test_position' derived from 'np.arange(len(unfiltered_df))' --
local post-slice positions in a different coordinate system. The unit
mismatch wiped out most or all training rows on every pair.

Production crash on 2026-06-22 (XRP/USD): "removed 2621
causal-unsafe train rows" followed by "causal guard removed all
train rows, skipping".

Fix: the column now stores a per-row label lookahead (in candles),
invariant under 'dk.slice_dataframe'. Consumers combine the row's
local position with the lookahead to recover the local known-at
position before comparing to 'first_test_position'. Column name
'<label>_known_at_index' is retained for this hotfix; a rename to
'<label>_known_at_lookahead' (with rétro-compatible alias) is left
to a follow-up PR per AGENTS.md 'small, verifiable changes'.

Touches:
- Utils.py: producer rewritten to store a constant per-row lookahead;
  'LabelData' and 'label_known_at_column_name' docstrings document the
  new contract; '_LABEL_KNOWN_AT_SUFFIX' carries an inline disambiguation.
- QuickAdapterV3.py: smoothing-lookahead advance comment harmonized to
  the canonical 'per-row label lookahead (in candles)' phrasing.
- QuickAdapterRegressorV3.py: '_known_at_index' docstring rewritten;
  'train_test_split' and 'timeseries_split' causal-mode branches add
  'train_positions + delta' before the '< first_test_position' check;
  'timeseries_split' hoists 'train_positions' for symmetry with
  'train_test_split'.
- README.md: 'causal_mode' tunable description reflects the new
  comparison semantic.

Reviewed by three parallel Oracle passes (math/algo/scope,
Python state-of-the-art / harmonization, documentation /
terminology / concision) with cross-validation; one false alarm
on a missing position-only fallback in 'timeseries_split' was
resolved by confirming 'TimeSeriesSplit.gap' enforces the
chronological purge at the sklearn layer.

* docs(quickadapter): shrink _known_at_index docstring to LabelData pointer

Per multi-oracle PR #95 review (Oracle 3 §8.1): paragraph 1 of
_known_at_index duplicated the slice-invariance rationale already
canonical on LabelData.known_at_index. Replace with a thin pointer per
AGENTS.md *No duplication: maintain single authoritative documentation
source; reference other sources rather than copying.*

3 months agofix(quickadapter): address PR #78-#81 review-comment findings (#90)
Jérôme Benoit [Mon, 22 Jun 2026 00:07:11 +0000 (02:07 +0200)] 
fix(quickadapter): address PR #78-#81 review-comment findings (#90)

Consolidates P1/P2 findings from `chatgpt-codex-connector` review
comments on PRs #78, #79, #80, #81, and PR #90.

Utils.py + label generation:
- `_generate_extrema_label` accepts `logger: Logger | None`; the
  `LabelGenerator` type signature, `generate_label_data` dispatcher,
  and `QuickAdapterV3.set_freqai_targets` caller propagate the logger.
  `_generate_extrema_label` has no `F821 logger` undefined-name path.
- `register_label_generator` routes the input through
  `_adapt_label_generator`. The adapter detects the canonical
  `(dataframe, params, logger)` shape by a positional parameter named
  `logger` at index 2 (with or without a default); other generators
  with 2 required positional parameters are wrapped via
  `functools.wraps` (preserves `__name__`/`__doc__`/`__wrapped__`) to
  drop the logger argument at dispatch, with defaulted positionals
  after index 1 left at their defaults. `ValueError` is raised at
  registration for `*args`, `**kwargs`, keyword-only `logger`, fewer
  than 2 required positionals, more than 3 required positionals, and 3
  required positionals whose third name is not `logger`.
- `safe_divide` denominator zero-check uses exact-zero
  (`denominator_arr != 0.0`); subnormal and satoshi-scale denominators
  pass the gate, and non-finite division outputs coerce to `fallback`
  via the post-division finite-mask.

Causal label split lookahead:
- `QuickAdapterV3.get_label_horizon_candles` recomputes the horizon
  from the current `label_period_candles` (via
  `get_label_period_candles`); init omits `label_horizon_candles`, so
  HPO period updates propagate to the horizon. The regressor's
  `_optuna_label_params` init likewise omits `label_horizon_candles`.
- `QuickAdapterV3.set_freqai_targets` advances `<label>_known_at_index`
  by the smoothing kernel half-width after smoothing. The
  `Utils.get_smoothing_kernel_half_width(config, *, series_length)`
  helper reuses `get_odd_window`/`get_even_window`/`get_savgol_params`
  shared with `smooth()` and dispatches on kernel routing:
  - `filtfilt`-routed zero-phase kernels (members of
    `SMOOTHING_KERNELS`: `gaussian`, `kaiser`, `kaiser_bessel_derived`,
    `triang`): half-width `effective_window - 1` (forward+backward
    pass extends dependency to the full filter length on both sides).
  - Single-pass centered windows (`smm`, `sma`, `savgol`): half-width
    `effective_window // 2`.
  - `gaussian_filter1d`: `int(4.0 * sigma + 0.5)` matching
    `scipy.ndimage` default truncation.
  The helper returns 0 when `smooth()` itself short-circuits:
  `series_length < max(window_candles, 3)` (top-level no-op) and, for
  the filtfilt/savgol routes, `series_length < effective_window`
  (downstream short-series no-op in `zero_phase_filter` /
  `savgol_filter`). The `series_length` parameter is keyword-only and
  required.

Label-weighting support policy:
- `QuickAdapterRegressorV3._compose_train_weights_with_support` routes
  the zero-pivot case (label-weighting strategy configured but no
  label weights available) through `_apply_support_policy`; the
  support policy governs the contract: `raise` raises, `fallback`
  warns.

Label Optuna selection hardening:
- `_OPTUNA_LABEL_SELECTION_SCHEMA_VERSION` is `2`, co-located with
  `_OPTUNA_LABEL_BEST_PARAMS_SCHEMA_VERSION` in `Utils`. The two
  constants are independent: wire format and selection algorithm
  carry separate version axes.
- `_optuna_label_selection_metadata` rejects non-finite `label_weights`
  / `label_p_order` with `ValueError`; downstream dict equality on the
  selection_metadata is NaN-safe.
- `_validate_optuna_label_best_params` accepts an
  `expected_selection_metadata` keyword. It rejects files that are
  not a `{schema_version, params, selection_metadata}` dict, files
  with mismatched `schema_version`, files missing or with an invalid
  `selection_metadata.schema_version`, and -- when
  `expected_selection_metadata` is provided -- files whose stored
  `selection_metadata` differs from the caller's current view. Legacy
  unversioned best-params files (no `schema_version`) are rejected
  outright. `QuickAdapterRegressorV3.optuna_load_best_params` passes
  its `_optuna_label_selection_metadata()` view for the label
  namespace; `QuickAdapterV3.optuna_load_best_params` omits the
  keyword, since the strategy reads only `label_period_candles`,
  `label_horizon_candles`, and `label_natr_multiplier` from the
  best-params file.
- The study schema-migration branch resets the Optuna label study
  whenever `existing_schema_version` is not an `int` (rejects `bool`
  and other types) or differs from `target_version`; an unversioned
  study (legacy pre-metadata) and a version-mismatched study are
  treated identically. Trials selected under a different metric
  whitelist or weighting scheme cannot be reused.
- `_optuna_label_selection_metadata` includes `label_weights` and
  `label_p_order`; `_calculate_distances` consumes both, so the
  idempotent `set_user_attr` write detects drift on those tunables.
- `_calculate_distances` validates `label_weights` length against the
  original objective count (raises on mismatch) only on the slicing
  path (when `objective_indices is not None` and the count differs).
  The sliced vector falls back to uniform weights only when
  `np.all(sliced == 0.0)` (the user's positive weights all on
  dropped objectives); slices containing negative or non-finite
  values flow through to `_validate_label_weights`.
- `_validate_label_selection_metric` accepts an `aggregate_allowed`
  parameter; cluster/density category callers pass `False`,
  restricting the valid set via the cached
  `_cluster_density_distance_metrics_set()` classmethod
  (SciPy-compatible non-probability metrics). The aggregate metrics
  (`harmonic_mean`, `geometric_mean`, `arithmetic_mean`,
  `quadratic_mean`, `cubic_mean`, `power_mean`, `weighted_sum`) live
  in the cached `_aggregate_distance_metrics_set()` classmethod
  derived by set-algebra from
  `_distance_metrics_set() - _scipy_metrics_set() -
  _probability_distance_metrics_set()`. README cluster/density metric
  rows list the SciPy-compatible set. `compromise_programming`/
  `topsis` accepts the full aggregate set via
  `_calculate_trial_distance_to_ideal`.

Out of scope:
- Two PR #80 findings: `reverse_train_test_order` support-policy
  routing; post-`feature_pipeline.fit_transform` support recheck.

References issues #87, #88, #89.

Closes #86.

3 months agofix(quickadapter): stabilize label optuna selection (#81)
Jérôme Benoit [Sun, 21 Jun 2026 21:45:02 +0000 (23:45 +0200)] 
fix(quickadapter): stabilize label optuna selection (#81)

Multi-objective `label` Pareto best-trial selection in the
QuickAdapter regressor.

- Probability-style metrics (`jensenshannon`, `hellinger`,
  `shellinger`) rejected for `label_distance_metric` /
  `label_cluster_metric` / `label_density_metric` — Pareto objective
  matrices are unbounded floats, not probability vectors. Invalid
  values fall back to `euclidean` with a warning.
- Constant objective dimensions dropped before trial-distance
  computation: a constant objective dimension is non-informative and
  would bias the geometry. Tolerance at
  `_NON_CONSTANT_OBJECTIVE_ATOL: Final[float] = 1e-8`.
- User-supplied `label_weights` matching the original objective count
  slice to align with the non-constant subset; mismatched sizes flow
  to `_validate_label_weights(mode="raise")`.
- Deterministic best-trial tie-break by `(distance, trial.number)`,
  independent of `study.best_trials` ordering.
- All-constant Pareto front falls back to the lowest `trial.number`.
- Persisted Optuna `label` study user-attr `selection_metadata` nests
  `schema_version`
  (`_OPTUNA_LABEL_SELECTION_SCHEMA_VERSION: Final[int] = 1`) and
  `method_config`. Studies without a recorded `schema_version` are
  tagged at the current version on next `optuna_create_study` (trials
  preserved); studies recording a different version are reset. The
  `selection_metadata` write is idempotent: skipped when unchanged,
  warned on diff.
- `OptunaNamespace` Literal and `_OPTUNA_NAMESPACES` (a `NamedTuple`
  of `hp` and `label` with per-field literal types) live in `Utils`,
  with `.hp` / `.label` accessors at all call sites
  (`QuickAdapterRegressorV3`, `QuickAdapterV3`). No per-class tuple
  alias, no inline `# "hp"` / `# "label"` annotations.

`config-template.json` sets `label_distance_metric: "euclidean"`.

3 months agofix(quickadapter): restore _format_collection ordering before register decorators
Jérôme Benoit [Sun, 21 Jun 2026 20:37:10 +0000 (22:37 +0200)] 
fix(quickadapter): restore _format_collection ordering before register decorators

Rebase of the label-weight-support-policy PR onto main (atop the style line-wrap commit) reapplied the _format_collection hunk at the wrong location, placing the helper AFTER its @_format_value.register(list|tuple|set|dict|np.ndarray) decorators. Move the definition back to its canonical position above the dispatchers.

3 months agostyle(quickadapter): wrap long expression lines
Jérôme Benoit [Sun, 21 Jun 2026 20:29:23 +0000 (22:29 +0200)] 
style(quickadapter): wrap long expression lines

3 months agofeat(quickadapter): add label weight support policy (#85)
Jérôme Benoit [Sun, 21 Jun 2026 20:26:45 +0000 (22:26 +0200)] 
feat(quickadapter): add label weight support policy (#85)

Per-row sample-weight composition decoupled from the final per-split
compose. Causal-guard filtering operates on raw base/label weights.
Configurable support thresholds gate the train-weight compose.

- `SampleWeightInputs` dataclass carries `(base, label,
  label_weighting_config)` from `_build_sample_weight_inputs`;
  `compose_sample_weights(base, label)` runs AFTER
  `train_test_split`/`TimeSeriesSplit` AND AFTER causal guards.
  `__post_init__` validates 1-D shape, base/label shape parity,
  required `label_weighting_config` keys, and `support_policy` enum
  membership.
- Thresholds in `freqai.label_weighting`:
  `min_pivot_equivalent_count` (default 3),
  `min_positive_label_weight_fraction` (default 0.01),
  `min_effective_sample_size` (default 3.0; Kish ESS).
- `support_policy: enum {fallback, raise}` (default `fallback`)
  drives the failure mode when any threshold trips. Eval (test/val)
  weights bypass this policy by design.
- `compose_sample_weights` takes `on_collapse: Literal["raise",
  "fallback"]` (default `"raise"`); the train path lets collapse raise
  through `LabelWeightSupportError` so `support_policy` catches it,
  the eval path passes `"fallback"` to preserve the label-derived
  drop mask.
- `_apply_support_policy` (typed `policy: LabelWeightSupportPolicy`)
  dispatches `fallback` and `raise` branches via `match`/`case` with
  `assert_never` exhaustiveness; `compose_sample_weights` applies the
  same pattern on `on_collapse`.
- `_shuffle_split_rows` 4-tuple shuffler covers label weights.
- `_filter_train_by_mask` accepts optional `train_label_weights` for
  uniform causal-guard filtering across base+label weight arrays.
- `LabelWeightSupportSummary`, `_effective_sample_size` (Kish ESS),
  and `summarize_label_weight_support` documented as operational spec.

README documents the four `label_weighting` rows;
`config-template.json` records the `fallback` default.

3 months agostyle(quickadapter): wrap long expression lines
Jérôme Benoit [Sun, 21 Jun 2026 19:38:52 +0000 (21:38 +0200)] 
style(quickadapter): wrap long expression lines

3 months agofix(quickadapter): harden numerical guard paths (#79)
Jérôme Benoit [Sun, 21 Jun 2026 19:26:38 +0000 (21:26 +0200)] 
fix(quickadapter): harden numerical guard paths (#79)

Shared finite-sample, guarded distribution-fit, safe divide/log-ratio,
and sigmoid-domain helpers. Log/division feature paths route through
the helpers; distribution fits guard empty, non-finite, and constant
samples.

- `Utils.py` helpers: `FiniteSample` dataclass with `finite_sample`;
  `safe_distribution_fit` (documented fallback-length contract);
  `safe_divide`; `safe_log_ratio`.
- `nan_average` finite/zero-weight guards; documented divergence from
  `np.nanmean` (strips +/-inf as well as NaN; bounded for current
  callers).
- `_clip_sigmoid_domain` in `LabelTransformer.py` guards
  `sp.special.logit` against values outside the open `(-1, 1)` domain
  during `sigmoid` inverse normalization.
- `feature_engineering_expand_basic` and Utils log/divide sites
  (`top_log_return`, `bottom_log_return`, `price_retracement_percent`,
  `ewo` normalize, `zigzag` log prices, KC/BB/VWAP widths) route
  through the safe helpers.
- DI Weibull and label `norm` fits in `fit_live_predictions` use
  `safe_distribution_fit`; DI cutoff fallback at
  `_DI_CUTOFF_DEFAULT: Final[float] = 2.0`.

3 months agorefactor(quickadapter): unhyphenate 'best params' in label log strings and docstring
Jérôme Benoit [Sun, 21 Jun 2026 19:04:38 +0000 (21:04 +0200)] 
refactor(quickadapter): unhyphenate 'best params' in label log strings and docstring

3 months agostyle(quickadapter): wrap long lines
Jérôme Benoit [Sun, 21 Jun 2026 18:04:17 +0000 (20:04 +0200)] 
style(quickadapter): wrap long lines

3 months agofeat(quickadapter)!: add causal label split foundation (#78)
Jérôme Benoit [Sun, 21 Jun 2026 18:01:23 +0000 (20:01 +0200)] 
feat(quickadapter)!: add causal label split foundation (#78)

Causal split guards on QuickAdapter training. Default causal mode
rejects `data_split_parameters.shuffle=true`,
`feature_parameters.shuffle_after_split=true`, and
`feature_parameters.reverse_train_test_order=true`.

- `feature_parameters.causal_mode` (default `true`): guard toggle.
  `false` is deprecated.
- `feature_parameters.label_horizon_candles` (default
  `label_period_candles`): candles after a label row before its label
  is considered known by causal split guards. Fallback chain
  `label_horizon_candles` -> `label_period_candles` -> `1`.
- `<label>_known_at_index` columns expose `LabelData.known_at_index`
  per-row; multi-label boundary via element-wise max across present
  columns.
- `timeseries_split` `gap` auto-set from `label_horizon_candles` under
  causal mode; explicit `gap < label_horizon_candles` rejected.
- Persisted Optuna `label` best-params JSON has shape
  `{schema_version, params}`
  (`_OPTUNA_LABEL_BEST_PARAMS_SCHEMA_VERSION = 2`). Unversioned files
  identified by shape; version-mismatched files emit distinct
  "missing" vs "incompatible" warnings.
- `_label_aux_column_name` shared sigil-stripping helper backs
  `label_weight_column_name` and `label_known_at_column_name`;
  uniform collision guard against `&`/`%` and empty stem.
- `QuickAdapterRegressorV3.version = 3.12.0`.

BREAKING CHANGE: `feature_parameters.causal_mode` defaults to `true`.
Configs with `data_split_parameters.shuffle=true`,
`feature_parameters.shuffle_after_split=true`, or
`feature_parameters.reverse_train_test_order=true` raise at training
time.

3 months agofeat(label_weighting): add epsilon_gaussian fill_method, fix sub-floor pivot-row...
Jérôme Benoit [Sat, 20 Jun 2026 17:31:46 +0000 (19:31 +0200)] 
feat(label_weighting): add epsilon_gaussian fill_method, fix sub-floor pivot-row dip (#84)

Add a fourth off-pivot weighting mode that superposes the epsilon
floor and the gaussian bumps additively, and fix a related defect in
_scatter_weights that allowed pivot rows to sit *below* the off-pivot
field whenever that field at the pivot's index could legitimately
exceed the pivot's raw weight.

Math
----

Define the off-pivot field

    f(i) = phi + max_{p in P} w_p * exp(-(i - p)^2 / (2 * sigma_p^2))

with phi = eps * B(W) (B mean or median, eps in [0, 1]; phi = 0 on
empty pivots or non-finite baseline), and per-pivot sigma_p from
_compute_pivot_sigmas (fixed or k-NN). The combined formulation reuses
both existing closed forms verbatim:

    fill_method = 'zero'             -> f(i) = 0
    fill_method = 'epsilon'          -> f(i) = phi  (constant in i)
    fill_method = 'gaussian'         -> f(i) = max_p w_p * exp(...)
    fill_method = 'epsilon_gaussian' -> f(i) = phi + max_p w_p * exp(...)

Bound: phi <= f(i) <= phi + max_p w_p. The new mode reduces to pure
gaussian when eps = 0 (bit-identical). The reduction is a per-row max
over per-pivot Gaussian bumps; phi is the epsilon floor.

Sub-floor / sub-bump pivot-row dip (bug fix)
--------------------------------------------

Before this change _scatter_weights wrote out[p] = w_p unconditionally,
so a pivot whose raw weight was below the off-pivot field at its
index appeared as a sharp dip relative to its neighbors. Two
manifestations of the same defect class:

- 'epsilon' / 'epsilon_gaussian' (sub-floor): a pivot with
  w_p < phi (e.g. W = (0.001, 1.0, 1.0) with eps = 0.5 and
  B = median, phi = 0.5) sat at 0.001 while neighbor rows sat at phi.
- 'gaussian' / 'epsilon_gaussian' (sub-bump): a weak pivot with a
  strong neighbor (e.g. W = (0.001, 1.0) at indices (0, 1), sigma = 1)
  sat at 0.001 while the off-pivot field at the pivot's own index was
  1.0 * exp(-0.5) ~= 0.6065 (the neighbor's gaussian bump).

Both cases are corrected by a single uniform change: _scatter_weights
now writes out[p] = max(w_p, fill[p]) so pivot rows are never written
below the off-pivot field. 'zero' is bit-identical (fill is always 0,
so max(w_p, 0) = w_p when w_p >= 0). 'gaussian' in the sparse-pivot
regime (the typical configuration, especially with k-NN bandwidth) is
also bit-identical because fill[p] equals w_p when no neighbor's bump
at p exceeds w_p.

Implementation
--------------

- _scatter_weights: pivot rows take np.maximum(weights, fill_weights)
  unconditionally. Off-pivot rows unchanged.
- _compute_epsilon_floor (renamed from _epsilon_floor): extracted
  helper that returns phi (mean / median / fallback). Reused by
  'epsilon' and 'epsilon_gaussian'. Parameter baseline narrowed to
  the FillEpsilonBaseline Literal type.
- _compute_gaussian_bumps (renamed from _gaussian_bumps): extracted
  adapter over _gaussian_fill_weights. Reused by 'gaussian' and
  'epsilon_gaussian'. logger is kwarg-only.
- compute_label_weights: dispatcher gains the FILL_METHODS[3] branch.
  The combined branch computes bumps once and adds phi in-place via
  np.add(out=fill_weights), keeping peak memory at the existing
  (chunk, M) buffer; phi is constant in p so the post-reduction add
  is algebraically identical to adding inside the chunk loop while
  saving O(chunk * M) writes. ValueError messages tightened to
  include 'supported values are ...' for parity with
  _compute_pivot_sigmas and _aggregate_metrics.
- LabelTransformer.py: extends FillMethod Literal and FILL_METHODS
  tuple with 'epsilon_gaussian' at index 3. No new tunables, no new
  validators (the existing _EnumValidator(FILL_METHODS) picks up the
  new value automatically; existing range / type validators on
  fill_epsilon / fill_sigma_* / fill_bandwidth_* apply unchanged).
- QuickAdapterV3.py: logging block refactored from if/elif chain to
  parallel if blocks keyed on tuple membership so epsilon and sigma
  parameter groups emit independently for each mode that uses them.

Documentation
-------------

README cells updated with set-membership 'Ignored when ...' clauses
matching the new index sets (epsilon | epsilon_gaussian for the
floor parameters, gaussian | epsilon_gaussian for the kernel
parameters). The fill_method description names the additive
composition explicitly and the pivot-row lift invariant
(out[p] = max(w_p, f(p))).

Verified manually on the host via AST extraction harness (no automated
test infrastructure exists in quickadapter/):

- zero mode: bit-exact with prior code (fill is 0, max(w_p, 0) = w_p).
- gaussian mode, sparse pivots: bit-identical to prior code (no
  neighbor's bump at p exceeds w_p, so the lift is a no-op).
- gaussian mode, neighbor-dominated regime: pivot rows lifted to the
  local field max, fixing the sub-bump dip. Verified with the
  counterexample W = (0.001, 1.0) at indices (0, 1), sigma = 1:
  legacy out[0] = 0.001, fixed out[0] = 1.0 * exp(-0.5) ~= 0.6065.
- epsilon back-compat (above-floor pivots): phi = eps * mean(W)
  reproduced; pivots above phi unchanged.
- epsilon pivot-dip fix: W = (0.001, 1.0, 1.0), eps = 0.5,
  baseline = median; legacy out[0] = 0.001, fixed out[0] = phi = 0.5.
- epsilon_gaussian with eps = 0: bit-identical to pure gaussian.
- epsilon_gaussian additive decomposition: out_eg - out_g = phi at
  every off-pivot row.
- epsilon_gaussian pivot-row lifted: W = (0.001, 1.0, 1.0) at
  well-separated indices (e.g. (0, 100, 200)), eps = 0.5,
  baseline = median, sigma = 2.0; out[0] = phi + 0.001 ~= 0.501
  (was 0.001 before the scatter fix).
- empty pivots: all four modes return all-zero.
- negative pivot weights still rejected by _gaussian_fill_weights.
- knn bandwidth + epsilon_gaussian: finite, bounded below by phi.
- ValueError messages on invalid fill_method / fill_epsilon_baseline
  include 'supported values are ...'.

3 months agodocs(quickadapter): document Kaiser-Bessel-derived smoothing
Jérôme Benoit [Fri, 19 Jun 2026 16:04:16 +0000 (18:04 +0200)] 
docs(quickadapter): document Kaiser-Bessel-derived smoothing

3 months agofeat(quickadapter): add Kaiser-Bessel-derived smoothing
Jérôme Benoit [Fri, 19 Jun 2026 16:03:56 +0000 (18:03 +0200)] 
feat(quickadapter): add Kaiser-Bessel-derived smoothing

3 months agodocs(quickadapter): update label weighting defaults
Jérôme Benoit [Fri, 19 Jun 2026 14:36:06 +0000 (16:36 +0200)] 
docs(quickadapter): update label weighting defaults