]> Piment Noir Git Repositories - freqai-strategies.git/log
freqai-strategies.git
3 weeks agofix(weights): tighten observability and edge-case handling in label pipeline
Jérôme Benoit [Mon, 25 May 2026 00:31:49 +0000 (02:31 +0200)] 
fix(weights): tighten observability and edge-case handling in label pipeline

- sanitize_and_renormalize accepts logger/context kwargs and warns on
  uniform-fallback collapse; six call sites in QuickAdapterRegressorV3
  thread their stage label (train_test_split / post_feature_pipeline /
  timeseries_split, train|test).
- Warn at startup when label_prediction.method='none' for any label, since
  populate_entry_trend would silently never trigger.
- Replace .notna() with np.isfinite() in the smoothed-weight clip so +Inf
  produced by smoothing kernels is also zeroed instead of relying on the
  downstream drop_mask in compose_sample_weights.
- _impute_weights tracks boundary NaN separately so injected zeros do not
  bias the interior median; finite endpoints are now preserved.

3 weeks agorefactor(weights): collapse compose_sample_weights to single-target API
Jérôme Benoit [Mon, 25 May 2026 00:04:41 +0000 (02:04 +0200)] 
refactor(weights): collapse compose_sample_weights to single-target API

LABEL_COLUMNS is single-target by design, so the dict-shaped per-label
map and row-wise aggregation in compose_sample_weights were dead
plumbing. Flatten the signature to a single label_weights vector and
read LABEL_COLUMNS[0] directly in _compose_per_row_weights. Drop the
duplicate-column guard (unreachable under single-target). Align caller
naming on base_weights to match the callee parameter. Add a defensive
check that LABEL_COLUMNS[0] is in dk.label_list to fail loudly if the
project label constant ever diverges from freqtrade's runtime view.

3 weeks agorefactor(quickadapter): drop unused sample_weighting tunables
Jérôme Benoit [Sun, 24 May 2026 23:44:44 +0000 (01:44 +0200)] 
refactor(quickadapter): drop unused sample_weighting tunables

The sample_weighting.{aggregation,softmax_temperature} options were inert:
LABEL_COLUMNS is single-label by design and compose_sample_weights only
ever sees one per-label vector, making row-wise aggregation an identity
operation regardless of the configured mode.

Removes the config block, validation specs, getter, and README entry;
compose_sample_weights keeps its kwargs with safe defaults (arithmetic_mean,
T=1.0) so the call site stays trivial.

3 weeks agodocs(quickadapter): document sample_weighting tunables
Jérôme Benoit [Sun, 24 May 2026 23:26:21 +0000 (01:26 +0200)] 
docs(quickadapter): document sample_weighting tunables

3 weeks agostyle: remove stray blank line in QuickAdapterRegressorV3
Jérôme Benoit [Sun, 24 May 2026 23:20:11 +0000 (01:20 +0200)] 
style: remove stray blank line in QuickAdapterRegressorV3

3 weeks agofeat(weights): per-label sample weights propagated to model.fit(sample_weight=.....
Jérôme Benoit [Sun, 24 May 2026 23:09:27 +0000 (01:09 +0200)] 
feat(weights): per-label sample weights propagated to model.fit(sample_weight=...) (#72)

* chore(quickadapter): bump strategy and regressor version 3.11.8 → 3.11.9

* feat(weights): add compose_sample_weights helper with mean=1 multiplicative composition

AFML §4.10 / mlfinpy canonical: per-label mean=1 normalization, multiplicative composition with temporal decay, geometric-mean aggregation for multi-label, NaN/inf handling, all-zero degenerate fallback. Validated locally with pytest (evidence: .omo/evidence/task-5-{red,green}.txt).

* fix(weights): persist label weights into <label>_weight column instead of rescaling target

Removes statistically incorrect target rescaling (label = direction × weight). Persists raw direction labels and a separate <label>_weight column for downstream sample_weight composition. Validated locally with pytest (evidence: .omo/evidence/task-6-{red,green}.txt).

* feat(weights): add _strip_label_weight_columns helper for find_labels collision avoidance

* feat(weights): compose per-label weights with temporal decay before model.fit

* feat(weights): integrate sample_weight composition into both train() data split paths

Add _train_default() mirroring BaseRegressionModel.train() with _compose_train_weights inserted between make_train_test_datasets and _apply_pipelines. Routes train_test_split path through _train_default instead of super().train(). Inserts _compose_train_weights before _apply_pipelines in timeseries_split path. Calls _strip_label_weight_columns(dk) at top of train() for both branches. Validated locally with pytest + structural AST checks (evidence: .omo/evidence/task-9-{pytest,structural}.txt).

* refactor(weights): align _train with BaseRegressionModel.train

Rename _train_default to _train to match the upstream method name (with
underscore prefix to mark it as the internal mirror, since the public
train() method routes between data split paths).

Mirror BaseRegressionModel.train line-for-line with _compose_train_weights
as the single intentional insertion between make_train_test_datasets and
the pipeline application:

- Drop ensure_datetime_series wrapper around unfiltered_df['date']:
  upstream calls .iloc[].strftime() directly.
- Drop **kwargs from self.fit(dd, dk) to match upstream signature.
- Use dk.data_dictionary['train_features'].columns for feature count log,
  matching upstream source of truth.
- Apply the same cosmetic alignment to the timeseries_split path for
  consistency between both train code paths.
- Add docstring documenting the mirror relationship and the single
  functional difference.

* refactor(weights): drop dead apply_label_weighting wrapper

Now that QuickAdapterV3.set_freqai_targets persists raw label direction
into the label column and weights into <label>_weight (consumed by
sample_weight downstream), the apply_label_weighting wrapper that
multiplied label values by their weights is no longer used.

- Drop Utils.apply_label_weighting (returned (weighted_label, weights)).
- Drop Utils._apply_label_weights (the values × weights helper).
- Switch QuickAdapterV3.set_freqai_targets to call compute_label_weights
  directly (already used internally by the removed wrapper).

* refactor(weights): simplify MAXIMA/MINIMA plot columns for binary direction

Now that the label column holds raw direction in {-1, 0, +1}, the MAXIMA
and MINIMA plot columns reduce to a where(direction>0, 0.0) /
where(direction<0, 0.0) projection.

The previous magnitude-aware logic (plot_eps padding, mask of zero values
on positive direction, etc.) was tailored to the weighted label
amplitudes and is now dead code:

- extrema.abs().where(extrema.ne(0.0)).min() always evaluates to 1.0,
  so plot_eps is always max(0.5, _PLOT_EXTREMA_MIN_EPS) = 0.5.
- direction.gt(0) & extrema.eq(0.0) is always False because direction>0
  implies extrema = +1, never 0. The .mask() branches never trigger.

Drop the now-unused _PLOT_EXTREMA_MIN_EPS class constant.

* refactor(weights): rename smooth_label to smooth for genericity

The function applies generic smoothing kernels (gaussian, kaiser, triang,
smm, sma, savgol, gaussian_filter1d) to any pd.Series. The 'label'
suffix narrowed it to label-specific use, but it now also smooths the
<label>_weight column (next commit). Drop the suffix; the smoothing
config dict is still named label_smoothing because the per-column
config map remains label-keyed.

* feat(weights): smooth <label>_weight column with the same kernel as label

Per-label weights are pointwise: only pivot indices carry the
metric-derived weight, while non-pivot indices are filled with the
median weight (see compute_label_weights / _build_weights_array).

The label column is smoothed with smooth() to spread pivot signals
over neighbouring candles. Without smoothing the weight column, the
smoothed label values around a pivot keep the constant median weight,
so the model treats high-amplitude pivot neighbours and the pivot
itself as equally important during training.

Apply smooth() to the <label>_weight column with the exact same
per-column smoothing config as the label, so the weight profile
follows the label profile candle-for-candle.

The smooth() positional argument list was redundant with the
col_smoothing_config dict keys; collapse both calls to **kwargs
unpacking. _SMOOTHING_SPECS keys exactly match smooth() parameter
names, so the unpack is type-safe.

compose_sample_weights already replaces non-finite or non-positive
values with 1.0, which absorbs any sign overshoot from kernels like
savgol at series edges.

* feat(weights): add direction and weight subplots showing raw + smoothed signals

Replace the MAXIMA/MINIMA bar visualization with two new subplots that
show both the raw and the smoothed direction/weight curves:

- Subplot 'direction' overlays raw direction (extrema_direction) and
  smoothed direction (smoothed_extrema).
- Subplot 'weight' overlays raw weight (extrema_weight) and smoothed
  weight (smoothed_extrema_weight).

Each visualization column is captured at the right point in
set_freqai_targets:
- Raw columns (EXTREMA_DIRECTION_COLUMN, EXTREMA_WEIGHT_COLUMN) are
  written before the smooth() call.
- Smoothed columns (SMOOTHED_EXTREMA_COLUMN, SMOOTHED_EXTREMA_WEIGHT_COLUMN)
  are written after the smooth() call.

Drop MAXIMA_COLUMN and MINIMA_COLUMN constants — they were only used by
the old min_max bar subplot. The new subplots convey the same direction
information plus the per-pivot weight magnitude that the legacy
weighted_label visualization showed (before the sample-weight refactor).

All four visualization column names lack the '&' prefix, so FreqAI's
find_labels auto-detection ignores them; they cannot leak into model
targets.

* refactor(weights): align visualization column names on extrema_<axis>[_smoothed]

Three-agent audit (explore + librarian + oracle) found the previous viz
column names suffered from three inconsistencies:
- 'extrema_direction' (raw) carries the axis word, but 'smoothed_extrema'
  drops it, breaking the 2x2 grid (axis × stage).
- Stage qualifier appears as PREFIX ('smoothed_extrema') for one column
  and as SUFFIX ('smoothed_extrema_weight') for another.
- 'smoothed_extrema_weight' mixes prefix stage word with suffix axis word.

Production codebases (statsmodels Kalman, bukosabino/ta MACD, mlflow
NPMI, FreqAI's own _mean/_std) overwhelmingly use suffix-decorated
processed forms with the raw form as the plain base. FreqAI's internal
pattern is suffix (&s-extrema_weight, &s-extrema_mean, &s-extrema_std);
align with it.

Rename:
- SMOOTHED_EXTREMA_COLUMN ('smoothed_extrema')
    -> EXTREMA_DIRECTION_SMOOTHED_COLUMN ('extrema_direction_smoothed')
- SMOOTHED_EXTREMA_WEIGHT_COLUMN ('smoothed_extrema_weight')
    -> EXTREMA_WEIGHT_SMOOTHED_COLUMN ('extrema_weight_smoothed')

Result: every viz column follows extrema_<axis>[_smoothed]. The 2x2
grid is uniform, sort-order groups raw and smoothed pairs together,
and the pattern is internally consistent with FreqAI's existing
suffix-based derivations.

* fix(weights): address PR #72 review comments

Three-agent cross-validation (explore + librarian + oracle) of Copilot
review comments produced these verdicts:

C1 (Utils.py:compose_sample_weights) — REAL BUG. Replacing 0-valued
weights with 1.0 silently undoes sklearn / AFML §4.10's canonical 'drop
this sample' semantic. sklearn's _check_sample_weight_equivalence,
DecisionTree _splitter.pyx, HistGBM docs, LightGBM #5553/#905, XGBoost
#3787 and mlfinlab time-decay all converge on the same contract:
sample_weight=0 means 'this sample contributes nothing'. Preserve zeros
via a drop_mask that is OR'd across labels (any label saying 'drop'
wins), then re-applied after the geometric-mean composition. Non-zero
non-finite or negative values still collapse to 1.0 (geometric mean's
neutral element) since they represent undefined weights, not exclusions.

C3/C4 (QuickAdapterRegressorV3.py:_train, timeseries_split) — REAL
REGRESSION. Commit 9953f0c removed ensure_datetime_series with the
rationale 'mirror BaseRegressionModel.train exactly'. But
ensure_datetime_series was introduced in commit ce843f9 specifically
as a workaround for freqtrade issue #13107 (int64 epoch-ms date
columns from feather/parquet handlers). Mirror the algorithm, retain
project-specific safety patches. Restore ensure_datetime_series in
both train paths.

C2/C7 (_label_weight_column_name unused) — DRY violation. Both call
sites in _compose_train_weights now use the helper instead of inline
f-strings.

C8 (train() docstring) — Inaccurate. The default path was claimed to
'Delegate to BaseRegressionModel.train()' but actually routes to
self._train() (a mirror with weight composition). Fix docstring to
reflect actual control flow.

C5/C6 (**kwargs forwarding) — FALSE POSITIVE. freqai_interface never
passes kwargs to model.train(); upstream BaseRegressionModel.train
also calls self.fit(dd, dk) without kwargs. The current code matches
upstream and the call chain is dead in practice.

* refactor(weights): factor train paths and relocate methods for structural coherence

Three-agent structural audit (explore + librarian + oracle) identified
five issues; fixes that don't fight existing conventions:

1. _train and the timeseries_split inline branch in train() shared
   ~30 lines of identical scaffolding (filter_features, dates logging,
   fit_labels guard, weight composition, pipeline application, fit,
   timing logs). Extract _train_common(unfiltered_df, pair, dk, split_fn)
   that owns the full mirror; _train_default and _train_timeseries_split
   become 4-line dispatchers passing the split callback. train() routing
   collapses to a clean two-line if/elif.

2. _label_weight_column_name, _strip_label_weight_columns and
   _compose_train_weights were inserted into the middle of the class
   constants block (between _TEST_SIZE and _SQRT_2), interrupting the
   constant-block coherence. Move them to the private instance method
   zone, immediately after _apply_pipelines (their natural neighbour).

3. _compose_train_weights duplicated the train/test weight extraction
   loop verbatim. Factor into a static _extract_split_weights helper
   that takes a split index and returns the per-label weight map; both
   train and test call sites become single expressions.

4. The four visualization column constants (EXTREMA_DIRECTION_COLUMN,
   EXTREMA_DIRECTION_SMOOTHED_COLUMN, EXTREMA_WEIGHT_COLUMN,
   EXTREMA_WEIGHT_SMOOTHED_COLUMN) were 75 lines below EXTREMA_COLUMN /
   LABEL_COLUMNS, separated by the LabelData dataclass and label
   generator registry. Move them next to EXTREMA_COLUMN where they
   logically belong.

5. Revert the train() docstring bullet to its upstream form. The
   modification introduced RST cross-reference syntax inconsistent with
   the surrounding plain-text docstring style.

* refactor(weights): centralize LABEL_WEIGHT_SUFFIX in Utils

The "_weight" suffix was duplicated as a class constant in
QuickAdapterRegressorV3 and as 5 hardcoded f-strings in QuickAdapterV3.
Three-agent audit (explore + librarian + oracle) converged on moving
this column-naming convention to Utils.py:

- PEP 8 default for constants is module-level; class-level is the
  exception for class-private semantics.
- Both consumers already import column-naming constants from Utils.py
  (LABEL_COLUMNS, EXTREMA_COLUMN, EXTREMA_WEIGHT_COLUMN, etc.). The
  suffix belongs with them.
- Production precedents (sklearn UNUSED/WARN/UNCHANGED, mlflow
  _SAMPLE_WEIGHT/_TRAINING_PREFIX, lightgbm _DatasetNames, pandas
  LOCAL_TAG) all place cross-module string tokens at module level.
- The constant describes a dataframe schema contract (column names),
  not model behaviour. Schema concerns belong in the schema module.

Add LABEL_WEIGHT_SUFFIX to Utils.py next to LABEL_COLUMNS. Remove the
class-level _LABEL_WEIGHT_SUFFIX in QuickAdapterRegressorV3 and import
the module-level constant. Replace 5 hardcoded f-strings in
QuickAdapterV3 with a local label_weight_col binding using the
imported constant.

The private _label_weight_column_name helper is kept in the regressor
since it is used twice (in _strip_label_weight_columns and
_compose_train_weights) and still adds a thin DRY layer over the
suffix synthesis.

* style: apply ruff formatting

* feat(weights): add label_weight_column helper with regex prefix strip and collision assertion

* fix(weights): preserve drop_mask and prevent NaN in compose_sample_weights fallback

* refactor(weights): adopt label_weight_column helper for canonical training column

* feat(weights): add _build_per_row_weights helper for pre-split weight composition

* feat(weights): add _make_default_split_datasets mirror with sklearn-key whitelist

* refactor(weights): make timeseries split helper accept external weights parameter

* refactor(weights): refactor _train_common chain and delete obsolete weight helpers

* refactor(weights): replace train() if/elif with dispatch dict and add weight-column uniqueness check

* fix(weights): harden compose_sample_weights for degenerate inputs

Address audit findings A0-1, A0-2, A0-3, A0-4, P2 #6, P2 #9, P2 #11
on branch feat/per-label-sample-weights.

- Extract _sanitize_and_renormalize private helper with four-guard chain
  (positive sum, finite sum, finite ratio, finite scaled) and uniform
  fallback. Used at empty-map fast path and at final fallback site.
- Empty label_weights_map now sanitizes raw temporal (A0-3).
- Subnormal temporal no longer overflows: ratio + scaled both checked
  for finiteness before returning (A0-2).
- Drop predicate unified as 'arr <= 0 or non-finite' instead of exact
  zero, eliminating the discontinuity at zero from smoothing artifacts
  (A0-4); negatives now drop, no longer rescued to 1.0 (subsumes A1-10).
- Surviving positive values floored at np.finfo(float).tiny to prevent
  subnormal arithmetic in the geo-mean log step.
- drop_mask covering all rows now raises ValueError instead of silently
  returning all-zero weights that crash XGBoost / sklearn HGBR (A0-1).
- Up-front per-label shape validation raises a precise error instead of
  letting numpy broadcasting fail mid-computation (P2 #11).

* docs(weights): document compose_sample_weights contract

Address audit findings A1-2, P2 #12, A1-12 on branch
feat/per-label-sample-weights.

Add 11-line docstring covering: output invariant (mean=1), per-label
sanitization predicate, aggregation operator, drop semantics, error
conditions, and the bounded full-series-median leakage in
compute_label_weights.

* refactor(weights): inline data-split dispatch with match/case

Address audit findings A1-1, A1-7, A1-6 (obsolete), P2 #14 on branch
feat/per-label-sample-weights. Conflict C1: A1-7 wins over A1-3
(YAGNI on subclass extension; LSP traceability preserved).

- Delete _DATA_SPLIT_DISPATCH class attribute (4 LOC).
- Delete _data_split_methods_set lru_cached helper (4 LOC dead code).
- Delete _train_default and _train_timeseries_split wrappers (~30 LOC
  pure boilerplate dispatching to _train_common).
- Inline match/case on method name in train(); pick the right
  _make_*_split_datasets via a local split_builder; nested split_fn
  closes over dk. Net -37 LOC; full LSP traceability.
- Add SplitFn module-level type alias used in _train_common signature.

* fix(weights): reject bool config values in numeric validators

Address audit findings A1-4, P2 #5, P2 #7 on branch
feat/per-label-sample-weights.

Python's `bool` is an `int` subclass, so `isinstance(True, int)` is
true and config values of `true`/`false` silently passed through as
`1`/`0` in the validators for n_splits, gap, max_train_size,
test_size and weight_factor. This commit closes that footgun:

- Add static helpers _coerce_int (always returns int, raises on bool
  or non-int) and _coerce_optional_int (returns Optional[int]) to
  centralize the validation; both echo the offending raw value via
  `{value!r}` so the diagnostic shows True/False rather than 1/0.
- Apply _coerce_int to n_splits and gap, _coerce_optional_int to
  max_train_size in _make_timeseries_split_datasets.
- Add explicit bool guard for test_size in both default-split and
  timeseries-split paths; previously test_size=true would slip past
  isinstance(_, int) and silently train on 1 sample.
- Add explicit bool guard for weight_factor before the >0 comparison.

* fix(weights): preserve mean=1 invariant across pipeline stages

Address audit finding A1-5 on branch feat/per-label-sample-weights.

compose_sample_weights guarantees sum(w)==N (mean(w)==1) over the full
training series, but this invariant breaks twice downstream: (1) the
train/test split partitions weights into disjoint subsets whose means
no longer equal 1, and (2) feature_pipeline.fit_transform may drop
rows via SVM/DBSCAN, drifting the means further. XGBoost
min_child_weight, LightGBM min_sum_hessian_in_leaf and L2
regularization are all sensitive to absolute weight scale.

- Add static helper _renormalize_to_unit_mean with the same four-guard
  chain as _sanitize_and_renormalize (positive sum, finite sum, finite
  ratio, finite scaled, uniform fallback).
- Apply at four sites: before dk.build_data_dictionary in both
  _make_default_split_datasets and _make_timeseries_split_datasets,
  and after feature_pipeline.fit_transform / .transform in
  _apply_pipelines (train and test sides).

* feat(weights): make label-weights aggregation configurable

Address audit finding A1-11 on branch feat/per-label-sample-weights.
Conflict C3: switch default to arithmetic_mean (matching
_compute_combined_label_weights), expose all 6 aggregations via
existing _aggregate_metrics infrastructure.

The hardcoded geometric mean over per-label normalized arrays was
mathematically conservative (one weak label dominates) and inconsistent
with the project's _compute_combined_label_weights default
(arithmetic_mean). For PR #44's correlated multi-target labels
(amplitude, time_to_pivot, efficiency, natr all derived from zigzag),
geomean over-counts redundant evidence and silently degrades to ~0
when any single factor is small. AFML \xc2\xa74.4 recommends arithmetic-mean
equivalents for correlated meta-labels.

- Add aggregation parameter to compose_sample_weights with default
  COMBINED_AGGREGATIONS[0] ("arithmetic_mean"); also expose
  softmax_temperature.
- Delegate the row-wise aggregation step to _aggregate_metrics, reusing
  the existing 6-operator infrastructure with uniform unit coefficients.
- Read both knobs from feature_parameters in _build_per_row_weights:
  label_weights_aggregation and label_weights_softmax_temperature.

* refactor(weights): align naming on compose/sigil/base_weights

Address audit findings A1-9, A1-14, A1-15, A1-16 on branch
feat/per-label-sample-weights.

- Rename _LABEL_WEIGHT_PREFIX_PATTERN to _FREQAI_LABEL_SIGIL_PATTERN:
  the regex strips the freqtrade-native '&' sigil, not a label-weight
  prefix; the new name describes what is matched (Utils.py).
- Rename compose_sample_weights parameter `temporal` to `base_weights`:
  the parameter accepts any base vector (recency weights or uniform
  ones), not exclusively temporal data (Utils.py).
- Rename _build_per_row_weights to _compose_per_row_weights:
  standardize on the 'compose' verb to mirror compose_sample_weights;
  this helper is the orchestrator that calls the kernel
  (QuickAdapterRegressorV3.py).
- Rename _build_weights_array to _scatter_weights: the function
  scatters sparse pivot weights into a dense default-filled array,
  not a generic 'build' (Utils.py).
- Rename eval_set_and_weights to make_test_set_and_weights: aligns
  with FreqAI's 'test_*' data_dictionary vocabulary while avoiding
  the 'test_' prefix that pytest auto-discovers (the verb 'make_'
  also clarifies it as a constructor, not a test) (Utils.py + caller).

* refactor(weights): extract _shuffle_in_unison helper

Address audit findings A1-8 and A1-13 on branch
feat/per-label-sample-weights.

- Extract the train/test shuffle pattern into a static
  _shuffle_in_unison helper. Each call shuffles features, labels and
  weights with the same random seed in lockstep. The shuffle block in
  _make_default_split_datasets shrinks from ~28 lines (two duplicated
  5-line idioms x train+test) to two helper invocations.
- Fix the dk.data_dictionary vs dd inconsistency at the feature-count
  log line: read from local dd (the pipeline's return value) rather
  than dk.data_dictionary (a side-effect set by _apply_pipelines).

* chore(weights): polish naming, validators, caching

Address audit P2 polish items #2, #4, #8, #13, #15 on branch
feat/per-label-sample-weights. P2 #21 and #22 (logger telemetry)
deliberately skipped per the no-new-comments/no-new-infrastructure
constraint; the existing fail-fast ValueError on degenerate inputs
already surfaces the most critical failure modes loudly.

- Counter-based duplicate-label diagnostic now names the offending
  weight columns instead of merely raising on a length mismatch
  (P2 #2).
- Widen shuffle seed space from random.randint(0, 100) (101 distinct
  seeds, birthday collisions at sqrt(101) ~ 10) to randint(0, 2**31-1)
  at both _shuffle_in_unison call sites (P2 #4).
- dsp = dict(self.config['freqai']['data_split_parameters']) replaced
  with self.data_split_parameters (the safe pre-populated FreqAI
  attribute used everywhere else in the file) (P2 #8).
- Cache label_weight_column with @lru_cache(maxsize=16): the helper
  is pure on its single string argument and called in tight loops at
  training; matches the file's existing convention for similar helpers
  (P2 #13).
- Rename loop variable w to label_values in compose_sample_weights;
  the outer scope spans ~20 lines and prior single-letter w obscured
  the role (P2 #15).

* docs(weights): align train() and helper docstrings with current behavior

- Rewrite train() docstring to describe match-based dispatch and the per-row
  weight composition flow through _train_common; remove stale delegation claim.
- Sync _compose_per_row_weights docstring: aggregation default is
  arithmetic_mean, not geometric_mean.
- Fix AFML citation in compose_sample_weights from section 7.4 to chapter 4.
- Document _aggregate_metrics softmax branch as a per-column convex
  combination with explicit T->0 and T->+inf limits.

* refactor(weights): consolidate sample weight renormalization helper

- Promote Utils._sanitize_and_renormalize to public sanitize_and_renormalize.
- Drop QuickAdapterRegressorV3._renormalize_to_unit_mean (cross-file
  duplication of the mean=1 invariant); replace 6 call sites with the
  unified helper. Sites that previously skipped per-element sanitization
  now also reject non-finite or non-positive entries.
- Collapse triple-guard ladder in sanitize_and_renormalize to a single
  finite-positive total check; surviving 'safe' is provably finite-nonneg
  so intermediate isfinite checks were dead code.
- Tighten _shuffle_in_unison signature from Any to concrete pd.DataFrame
  and NDArray types.
- Drop empty-fold sentinel in _make_timeseries_split_datasets and raise
  ValueError on degenerate generator output instead of silently producing
  empty index arrays.

* feat(weights): validate label_weights tunables via label_weighting block

- _compose_per_row_weights now consumes get_label_weighting_config (which
  validates aggregation against COMBINED_AGGREGATIONS and enforces
  softmax_temperature > 0 via _WEIGHTING_SPECS) instead of reading raw
  feature_parameters.label_weights_*.
- Add CONFIG_MIGRATIONS entries auto-migrating
  freqai.feature_parameters.label_weights_aggregation and
  freqai.feature_parameters.label_weights_softmax_temperature to the
  freqai.label_weighting block; users get one warning per key.
- Add module-level _logger to Utils.py and warn on
  compose_sample_weights silent fallback so collapsed-aggregation paths
  are observable.
- _make_timeseries_split_datasets honors reverse_train_test_order for
  parity with _make_default_split_datasets and raises ValueError on
  shuffle_after_split=True (chronological + shuffle is incoherent and
  would leak future data into training).

* fix(weights): seed shuffle deterministically from data_split_parameters.random_state

Replace global random.randint() with a random.Random instance derived from
data_split_parameters.random_state. When the user provides a random_state
(whitelisted in _SKLEARN_TRAIN_TEST_SPLIT_KEYS), train and test shuffles
become reproducible end-to-end; when absent, behavior remains
non-deterministic. The single parent RNG draws two independent sub-seeds
so train and test shuffles stay decorrelated.

* refactor(weights): rename for naming coherence

- Rename _make_default_split_datasets to _make_train_test_split_datasets
  to restore the case-key/method-name grep-line in train()'s match
  dispatch.
- Rename label_weight_column to label_weight_column_name across Utils.py,
  QuickAdapterV3.py and QuickAdapterRegressorV3.py: the helper returns
  a column-name string, not a column accessor; the new name matches the
  *_COLUMN constant convention used elsewhere in Utils.py.
- Drop redundant 'dk.data_dictionary = dd' in _apply_pipelines:
  build_data_dictionary already self-assigns the dict on dk and dd is
  the same object reference.

* style(weights): group EXTREMA_* constants and separate LABEL_* declarations

* docs(weights): rewrite _make_train_test_split_datasets docstring without history narration

Replace the deviation list and PR-history reference with a concise
description of what the function IS (sklearn-key whitelist, honored
tunables, weight propagation contract).

* fix(weights): resolve KeyError in label_weighting config consumption

_compose_per_row_weights passed self.config (root) to
get_label_weighting_config and accessed weighting_config['aggregation']
directly; the helper expects freqai.label_weighting and returns
{default, columns}. Fix consumes self.freqai_info['label_weighting']
and reads ['default']['aggregation'] / ['default']['softmax_temperature'].

* fix(weights): honor drop_mask in sanitize_and_renormalize fallback

When total <= 0 or non-finite, the helper returned np.ones_like(arr)
ignoring drop_mask, resurrecting dropped rows with weight=1. Fallback
now zeros drop_mask rows before returning.

* fix(config): rename reverse_test_train_order to reverse_train_test_order

Match the canonical key name from upstream freqtrade and from the code
in QuickAdapterRegressorV3._make_train_test_split_datasets and
_make_timeseries_split_datasets. The previous template key was silently
ignored.

* fix(strategy): clip smoothed weight column to non-negative finite

Some smoothing methods (savgol, filtfilt) can ring negative on positive
input. Clip the smoothed weight series to >= 0 and replace non-finite
with 0 before assigning to the dataframe so compose_sample_weights does
not silently drop rows that were positive before smoothing.

* fix(weights): apply project _TEST_SIZE default when data_split_parameters omits test_size

The whitelist comprehension that builds sklearn_kwargs only preserved keys
present in data_split_parameters; the local test_size variable computed via
dsp.get(..., _TEST_SIZE) was never injected back. Configs without an explicit
test_size silently fell through to sklearn's stock 0.25 default instead of
the project's 0.1.

Replace bare 'shuffle' insertion with setdefault for both shuffle and
test_size so sklearn_kwargs always carries the project defaults.
Update _make_train_test_split_datasets docstring to reflect the actual
default behavior.

* refactor(strategy): rename smoothed_weights to smoothed_label_weights

Aligns naming with surrounding label_weights variable, EXTREMA_WEIGHT_SMOOTHED_COLUMN
constant and compute_label_weights helper.

* feat(weights): split per-row aggregation into freqai.sample_weighting block

Cross-metric (per-pivot) and cross-label (per-row) compositions are
distinct distributions. Decouple their tunables:

- freqai.label_weighting.{aggregation,softmax_temperature} stay for
  cross-metric aggregation in compute_label_weights (combined strategy).
- New freqai.sample_weighting.{aggregation,softmax_temperature} for
  cross-label composition in
  QuickAdapterRegressorV3._compose_per_row_weights.

Add _SAMPLE_WEIGHTING_SPECS, DEFAULTS_SAMPLE_WEIGHTING and
get_sample_weighting_config helper routed through _get_label_config so
the returned shape ({default, columns}) matches the get_label_*_config
family.

* refactor(weights): pass logger as parameter and log per-label weight column status

Drop the module-level _logger introduced in Utils.py; compose_sample_weights
now takes a keyword-only logger argument, matching the caller-passes-logger
pattern used by every other helper in the file.

In _compose_per_row_weights, log per-label weight column resolution at
debug when columns are present (static across retrains) and at warning
when none are found (unexpected configuration; falls back to temporal
weights only).

* fix(weights): use shape-consistent empty containers for test_size=0 sentinels

Replace np.zeros(2)/pd.DataFrame() sentinels with iloc[:0] / weights[:0]
slices so test_features, test_labels and test_weights all have 0 rows
with preserved column names and dtype. Behavior unchanged because
_apply_pipelines skips test-side processing when test_size == 0, but
shape-consistent containers respect the declared types and avoid
surprising downstream consumers.

4 weeks agochore(deps): bump devcontainer features (node, docker-in-docker)
Jérôme Benoit [Sat, 23 May 2026 22:55:21 +0000 (00:55 +0200)] 
chore(deps): bump devcontainer features (node, docker-in-docker)

4 weeks agofix(zigzag): default normalize to False to prevent label magnitude leak (#71)
Jérôme Benoit [Sat, 23 May 2026 22:23:35 +0000 (00:23 +0200)] 
fix(zigzag): default normalize to False to prevent label magnitude leak (#71)

When set_freqai_targets is invoked by FreqAI's backtesting loop, the dataframe
passed to _generate_extrema_label spans the full historical window
(right-truncated to the current train-window stop), not just train_period_days.
With normalize=True, zigzag applies a global minmax scaling across all detected
pivots in that wider window to amplitudes, amplitude_threshold_ratios,
volume_rates and speeds. The resulting label magnitudes therefore depend on the
global pivot distribution, including pivots outside the current training slice
— a magnitude leak from out-of-train data into training labels.

Switching the zigzag default to normalize=False emits raw log-amplitude values
(|log(P2/P1)|) and defers any scaling to LabelTransformer, which is fitted
strictly on the train slice and is therefore leak-free. The two existing call
sites — _generate_extrema_label (label generation) and label_objective (Optuna
hyperopt) — both want the unnormalized output, so the redundant
normalize=False kwargs are dropped at the call sites in favor of the default.

Strategy and regressor patch versions are bumped to 3.11.8.

Caveat: with apply_label_weighting strategy="combined", the metrics now sit on
heterogeneous scales (raw log-amplitudes ~[0.005, 0.5] mix with bounded ratios
in [0, 1] like efficiency_ratio). Users relying on "combined" aggregation
(power means, weighted_median, softmax) may need to introduce metric-specific
rescaling on the train slice before aggregation. Direction-only (strategy=
"none") and single-metric strategies (e.g. strategy="amplitude") are
unaffected.

4 weeks agochore: track ReforceXY devcontainer-lock.json for reproducible builds
Jérôme Benoit [Sat, 23 May 2026 00:14:09 +0000 (02:14 +0200)] 
chore: track ReforceXY devcontainer-lock.json for reproducible builds

4 weeks agochore: track devcontainer-lock.json for reproducible builds
Jérôme Benoit [Sat, 23 May 2026 00:07:19 +0000 (02:07 +0200)] 
chore: track devcontainer-lock.json for reproducible builds

4 weeks agochore: ignore .omo and .sisyphus at root and in subdirectories
Jérôme Benoit [Sat, 23 May 2026 00:02:16 +0000 (02:02 +0200)] 
chore: ignore .omo and .sisyphus at root and in subdirectories

4 weeks agochore(deps): lock file maintenance (#70)
renovate[bot] [Thu, 21 May 2026 10:41:10 +0000 (12:41 +0200)] 
chore(deps): lock file maintenance (#70)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5 weeks agochore(deps): lock file maintenance (#69)
renovate[bot] [Mon, 11 May 2026 12:42:42 +0000 (14:42 +0200)] 
chore(deps): lock file maintenance (#69)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
6 weeks agochore(deps): lock file maintenance (#68)
renovate[bot] [Wed, 6 May 2026 12:49:47 +0000 (14:49 +0200)] 
chore(deps): lock file maintenance (#68)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
6 weeks agochore(quickadapter): refine default settings in configuration template
Jérôme Benoit [Mon, 4 May 2026 21:39:04 +0000 (23:39 +0200)] 
chore(quickadapter): refine default settings in configuration template

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
7 weeks agochore: remove stop_grace_period from docker-compose files
Jérôme Benoit [Fri, 1 May 2026 19:55:04 +0000 (21:55 +0200)] 
chore: remove stop_grace_period from docker-compose files

7 weeks agofix: pin pandas>=3.0 in Dockerfile and bump version to 3.11.7
Jérôme Benoit [Fri, 1 May 2026 19:03:15 +0000 (21:03 +0200)] 
fix: pin pandas>=3.0 in Dockerfile and bump version to 3.11.7

Prevent silent pandas downgrade to 2.x during pip install, which
causes dtype mismatches with freqtrade 2026.4 date handling code.
Includes epoch-ms range validation in ensure_datetime_series.

7 weeks agofix: ensure_datetime_series raises ValueError on None instead of silent corruption
Jérôme Benoit [Fri, 1 May 2026 17:20:24 +0000 (19:20 +0200)] 
fix: ensure_datetime_series raises ValueError on None instead of silent corruption

7 weeks agochore: bump strategy and model version to 3.11.6
Jérôme Benoit [Fri, 1 May 2026 16:09:02 +0000 (18:09 +0200)] 
chore: bump strategy and model version to 3.11.6

7 weeks agochore: remove unused json imports and fix line length formatting
Jérôme Benoit [Fri, 1 May 2026 16:02:23 +0000 (18:02 +0200)] 
chore: remove unused json imports and fix line length formatting

7 weeks agorefactor: replace lru_cache with cached_property for timeframe_minutes
Jérôme Benoit [Fri, 1 May 2026 15:53:59 +0000 (17:53 +0200)] 
refactor: replace lru_cache with cached_property for timeframe_minutes

7 weeks agorefactor: rename prepared to seeded in smma() for clarity
Jérôme Benoit [Fri, 1 May 2026 15:48:20 +0000 (17:48 +0200)] 
refactor: rename prepared to seeded in smma() for clarity

7 weeks agoperf: vectorize smma() using SMA-seeded ewm (no Python loop)
Jérôme Benoit [Fri, 1 May 2026 15:42:42 +0000 (17:42 +0200)] 
perf: vectorize smma() using SMA-seeded ewm (no Python loop)

7 weeks agofix: narrow bare except to specific types in statistical helpers
Jérôme Benoit [Fri, 1 May 2026 15:29:28 +0000 (17:29 +0200)] 
fix: narrow bare except to specific types in statistical helpers

7 weeks agorefactor: extract optuna_load/save_best_params to shared Utils
Jérôme Benoit [Fri, 1 May 2026 15:25:13 +0000 (17:25 +0200)] 
refactor: extract optuna_load/save_best_params to shared Utils

7 weeks agofix: validate epoch-ms range before converting int64 date columns
Jérôme Benoit [Fri, 1 May 2026 14:03:12 +0000 (16:03 +0200)] 
fix: validate epoch-ms range before converting int64 date columns

Reject int64 values outside [2010, 2035] epoch-ms range to fail fast
on corrupted data instead of silently producing wrong dates. Catches
nanosecond/microsecond values that would pass the int64 dtype check
but produce garbage timestamps if interpreted as milliseconds.

7 weeks agofix: add 30min stop_grace_period to prevent data corruption on shutdown
Jérôme Benoit [Fri, 1 May 2026 13:46:19 +0000 (15:46 +0200)] 
fix: add 30min stop_grace_period to prevent data corruption on shutdown

FreqAI training can take minutes to hours. Docker's default 10s grace
period causes SIGKILL mid-write, corrupting feather/pickle files.
Give freqtrade up to 30 minutes to finish training and flush data
before Docker sends SIGKILL.

7 weeks agofix: align ensure_datetime_series with freqtrade data handler pattern
Jérôme Benoit [Fri, 1 May 2026 10:32:33 +0000 (12:32 +0200)] 
fix: align ensure_datetime_series with freqtrade data handler pattern

Chain .dt.as_unit("ms") to guarantee datetime64[ms, UTC] output
resolution regardless of pandas version, matching the contract
established in freqtrade commit 2c5dc72.

Ref: freqtrade/freqtrade#13107

7 weeks agorefactor: extract ensure_datetime_series helper for date dtype workaround
Jérôme Benoit [Thu, 30 Apr 2026 22:51:29 +0000 (00:51 +0200)] 
refactor: extract ensure_datetime_series helper for date dtype workaround

Centralizes the int64 epoch-ms vs datetime detection logic into a shared
helper. Handles both formats correctly: unit='ms' for int64, passthrough
for existing datetime columns.

Ref: https://github.com/freqtrade/freqtrade/issues/13107

7 weeks agofix: workaround date dtype regression in FreqAI model training log
Jérôme Benoit [Thu, 30 Apr 2026 22:34:02 +0000 (00:34 +0200)] 
fix: workaround date dtype regression in FreqAI model training log

Same freqtrade 2026.4 regression (2c5dc72): unfiltered_df["date"] may
be int64 during training, causing .strftime() to fail.

Ref: https://github.com/freqtrade/freqtrade/issues/13107

7 weeks agochore: update serena project config template comments
Jérôme Benoit [Thu, 30 Apr 2026 22:15:38 +0000 (00:15 +0200)] 
chore: update serena project config template comments

7 weeks agofix: workaround freqtrade 2026.4 date column dtype regression
Jérôme Benoit [Thu, 30 Apr 2026 22:14:55 +0000 (00:14 +0200)] 
fix: workaround freqtrade 2026.4 date column dtype regression

Freqtrade 2026.4 (commit 2c5dc72) changed feather/parquet handlers to
use .dt.as_unit("ms") instead of to_datetime(col, unit="ms", utc=True).
This breaks when data files store dates as int64 epoch-ms, causing
AttributeError in feature_engineering_standard.

Use pd.to_datetime(col, utc=True) defensively to handle both int64 and
datetime inputs.

Ref: https://github.com/freqtrade/freqtrade/issues/13107

7 weeks agochore(deps): lock file maintenance (#67)
renovate[bot] [Wed, 29 Apr 2026 15:38:57 +0000 (17:38 +0200)] 
chore(deps): lock file maintenance (#67)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8 weeks agoRevert "chore(deps): update poetry lock file"
Jérôme Benoit [Sat, 25 Apr 2026 14:46:27 +0000 (16:46 +0200)] 
Revert "chore(deps): update poetry lock file"

This reverts commit f96f0324f67438328a797346bce907efe70cf886.

8 weeks agochore: update openspec skills to v1.3.1
Jérôme Benoit [Sat, 25 Apr 2026 14:42:00 +0000 (16:42 +0200)] 
chore: update openspec skills to v1.3.1

8 weeks agochore(deps): update poetry lock file
Jérôme Benoit [Sat, 25 Apr 2026 14:41:54 +0000 (16:41 +0200)] 
chore(deps): update poetry lock file

8 weeks agodocs: neutralize Copilot-specific references in agent instructions
Jérôme Benoit [Thu, 23 Apr 2026 20:05:09 +0000 (22:05 +0200)] 
docs: neutralize Copilot-specific references in agent instructions

8 weeks agoperf(quickadapter): fine tune trade entry condition
Jérôme Benoit [Thu, 23 Apr 2026 14:21:49 +0000 (16:21 +0200)] 
perf(quickadapter): fine tune trade entry condition

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
8 weeks agochore: remove Cline references from devcontainer configurations
Jérôme Benoit [Wed, 22 Apr 2026 23:13:09 +0000 (01:13 +0200)] 
chore: remove Cline references from devcontainer configurations

8 weeks agochore: replace agent instruction indirections with symlinks to canonical source
Jérôme Benoit [Wed, 22 Apr 2026 23:09:35 +0000 (01:09 +0200)] 
chore: replace agent instruction indirections with symlinks to canonical source

2 months agochore(deps): lock file maintenance (#66)
renovate[bot] [Tue, 21 Apr 2026 11:50:34 +0000 (13:50 +0200)] 
chore(deps): lock file maintenance (#66)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months agochore(deps): lock file maintenance (#63)
renovate[bot] [Tue, 14 Apr 2026 20:06:22 +0000 (22:06 +0200)] 
chore(deps): lock file maintenance (#63)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months agobuild(deps): bump pytest in /ReforceXY/reward_space_analysis (#64)
dependabot[bot] [Tue, 14 Apr 2026 14:18:54 +0000 (16:18 +0200)] 
build(deps): bump pytest in /ReforceXY/reward_space_analysis (#64)

Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.2 to 9.0.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/9.0.2...9.0.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 months agochore(deps): lock file maintenance (#62)
renovate[bot] [Mon, 6 Apr 2026 16:14:05 +0000 (18:14 +0200)] 
chore(deps): lock file maintenance (#62)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months agochore(qav3): refine reversal confirmation parameters
Jérôme Benoit [Mon, 6 Apr 2026 15:23:06 +0000 (17:23 +0200)] 
chore(qav3): refine reversal confirmation parameters

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
2 months agochore(quickadapter): refine template config parameters for better performance
Jérôme Benoit [Wed, 1 Apr 2026 16:19:33 +0000 (18:19 +0200)] 
chore(quickadapter): refine template config parameters for better performance

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
2 months agochore(deps): lock file maintenance (#60)
renovate[bot] [Tue, 31 Mar 2026 15:39:34 +0000 (17:39 +0200)] 
chore(deps): lock file maintenance (#60)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months agobuild(deps): bump pygments in /ReforceXY/reward_space_analysis (#61)
dependabot[bot] [Tue, 31 Mar 2026 13:47:02 +0000 (15:47 +0200)] 
build(deps): bump pygments in /ReforceXY/reward_space_analysis (#61)

Bumps [pygments](https://github.com/pygments/pygments) from 2.19.2 to 2.20.0.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.19.2...2.20.0)

---
updated-dependencies:
- dependency-name: pygments
  dependency-version: 2.20.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 months agodocs: fix semantic accuracy of README configuration tunables
Jérôme Benoit [Tue, 31 Mar 2026 00:29:28 +0000 (02:29 +0200)] 
docs: fix semantic accuracy of README configuration tunables

- polyorder: correct range from int >= 1 to int >= 0 (savgol accepts degree-0)
- robust standardization: replace 'IQR' with '(Q₃-Q₁)' (quantiles are configurable)
- label_weights: broaden scope from 'distance calculations to ideal point' to 'trial selection methods'
- label_p_order: replace 'p-order parameter for distance metrics' with 'Lp exponent for parameterized metrics'
- label_density_aggregation_param: replace 'p-order' with 'Lp exponent' for consistency

2 months agochore(deps): update dependency ngboost to v0.5.10 (#59)
renovate[bot] [Wed, 25 Mar 2026 12:01:21 +0000 (13:01 +0100)] 
chore(deps): update dependency ngboost to v0.5.10 (#59)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months agochore(deps): lock file maintenance (#58)
renovate[bot] [Mon, 23 Mar 2026 12:04:14 +0000 (13:04 +0100)] 
chore(deps): lock file maintenance (#58)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months agochore(deps): lock file maintenance (#56)
renovate[bot] [Wed, 18 Mar 2026 18:12:30 +0000 (19:12 +0100)] 
chore(deps): lock file maintenance (#56)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months agochore(deps): update dependency optuna to v4.8.0 (#57)
renovate[bot] [Tue, 17 Mar 2026 18:15:01 +0000 (19:15 +0100)] 
chore(deps): update dependency optuna to v4.8.0 (#57)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months agochore: cleanup openspec opencode commands
Jérôme Benoit [Tue, 17 Mar 2026 13:02:09 +0000 (14:02 +0100)] 
chore: cleanup openspec opencode commands

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
3 months agochore: update serena project configuration with new settings
Jérôme Benoit [Sun, 15 Mar 2026 22:17:35 +0000 (23:17 +0100)] 
chore: update serena project configuration with new settings

3 months agochore(deps): lock file maintenance (#55)
renovate[bot] [Thu, 12 Mar 2026 11:46:19 +0000 (12:46 +0100)] 
chore(deps): lock file maintenance (#55)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months agochore(deps): lock file maintenance (#54)
renovate[bot] [Wed, 4 Mar 2026 12:39:22 +0000 (13:39 +0100)] 
chore(deps): lock file maintenance (#54)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months agochore(quickadapter): update config-template.json
Jérôme Benoit [Sun, 1 Mar 2026 20:14:59 +0000 (21:14 +0100)] 
chore(quickadapter): update config-template.json

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
3 months agofeat(reforcexy): log invalid actions with enum names for non-masking models
Jérôme Benoit [Sun, 1 Mar 2026 18:54:50 +0000 (19:54 +0100)] 
feat(reforcexy): log invalid actions with enum names for non-masking models

3 months agochore: update serena project configuration
Jérôme Benoit [Thu, 26 Feb 2026 21:55:31 +0000 (22:55 +0100)] 
chore: update serena project configuration

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
3 months agochore(deps): update dependency ngboost to v0.5.9 (#53)
renovate[bot] [Thu, 26 Feb 2026 13:20:15 +0000 (14:20 +0100)] 
chore(deps): update dependency ngboost to v0.5.9 (#53)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months agochore: update OpenSpec artifacts
Jérôme Benoit [Tue, 24 Feb 2026 10:58:10 +0000 (11:58 +0100)] 
chore: update OpenSpec artifacts

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
4 months agorefactor(quickadapter): harmonize eps values across the codebase
Jérôme Benoit [Thu, 19 Feb 2026 09:07:21 +0000 (10:07 +0100)] 
refactor(quickadapter): harmonize eps values across the codebase

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
4 months agochore(deps): update dependency catboost to v1.2.10 (#52)
renovate[bot] [Thu, 19 Feb 2026 09:03:43 +0000 (10:03 +0100)] 
chore(deps): update dependency catboost to v1.2.10 (#52)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months agofeat(zigzag): add normalize flag, disable in label_objective
Jérôme Benoit [Thu, 19 Feb 2026 00:51:39 +0000 (01:51 +0100)] 
feat(zigzag): add normalize flag, disable in label_objective

4 months agofix(zigzag): skip minmax normalization with single valid value
Jérôme Benoit [Thu, 19 Feb 2026 00:40:34 +0000 (01:40 +0100)] 
fix(zigzag): skip minmax normalization with single valid value

4 months agofix(normalization): use eps-based threshold for minmax numerical stability
Jérôme Benoit [Thu, 19 Feb 2026 00:15:08 +0000 (01:15 +0100)] 
fix(normalization): use eps-based threshold for minmax numerical stability

4 months agochore: bump version to 3.11.4
Jérôme Benoit [Wed, 18 Feb 2026 23:33:06 +0000 (00:33 +0100)] 
chore: bump version to 3.11.4

4 months agostyle(zigzag): harmonize minmax_scale with codebase conventions
Jérôme Benoit [Wed, 18 Feb 2026 23:21:54 +0000 (00:21 +0100)] 
style(zigzag): harmonize minmax_scale with codebase conventions

4 months agorefactor(zigzag): replace ad-hoc x/(x+y) normalization with pure ratios + MinMaxScaler
Jérôme Benoit [Wed, 18 Feb 2026 23:07:20 +0000 (00:07 +0100)] 
refactor(zigzag): replace ad-hoc x/(x+y) normalization with pure ratios + MinMaxScaler

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
4 months agochore(deps): update dependency catboost to v1.2.9 (#51)
renovate[bot] [Wed, 18 Feb 2026 15:51:33 +0000 (16:51 +0100)] 
chore(deps): update dependency catboost to v1.2.9 (#51)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months agochore(deps): lock file maintenance (#50)
renovate[bot] [Mon, 16 Feb 2026 22:21:50 +0000 (23:21 +0100)] 
chore(deps): lock file maintenance (#50)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months agodocs(quickadapter): clarify gpu_vram_gb is available VRAM, not total
Jérôme Benoit [Sun, 15 Feb 2026 15:09:31 +0000 (16:09 +0100)] 
docs(quickadapter): clarify gpu_vram_gb is available VRAM, not total

4 months agofeat(quickadapter): add VRAM-aware CatBoost GPU hyperparameter ranges
Jérôme Benoit [Sun, 15 Feb 2026 14:21:02 +0000 (15:21 +0100)] 
feat(quickadapter): add VRAM-aware CatBoost GPU hyperparameter ranges

4 months agofix(ReforceXY): exclude gpu_memory_fraction from model params
Jérôme Benoit [Sun, 15 Feb 2026 12:41:37 +0000 (13:41 +0100)] 
fix(ReforceXY): exclude gpu_memory_fraction from model params

4 months agofeat(ReforceXY): add gpu_memory_fraction tunable
Jérôme Benoit [Sun, 15 Feb 2026 12:31:58 +0000 (13:31 +0100)] 
feat(ReforceXY): add gpu_memory_fraction tunable

4 months agofix(devcontainer): disable gh config mount to avoid migration bug
Jérôme Benoit [Sat, 14 Feb 2026 23:57:39 +0000 (00:57 +0100)] 
fix(devcontainer): disable gh config mount to avoid migration bug

Prevents gh cli from failing due to multi-account migration when
keyring (dbus) is unavailable. GITHUB_TOKEN env var is used instead.

See: https://github.com/cli/cli/issues/8441

4 months agoconfig(ReforceXY): optimize RL training params
Jérôme Benoit [Fri, 13 Feb 2026 23:32:04 +0000 (00:32 +0100)] 
config(ReforceXY): optimize RL training params

- train_period_days: 120→90 (reduce training window)
- weight_factor: 0.9→0 (disable, unused by RL models)

4 months agodocs(quickadapter): sensible template defaults
Jérôme Benoit [Fri, 13 Feb 2026 15:54:41 +0000 (16:54 +0100)] 
docs(quickadapter): sensible template defaults

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
4 months agodocs(ReforceXY): document efficiency coefficient min_range guard
Jérôme Benoit [Thu, 12 Feb 2026 23:21:50 +0000 (00:21 +0100)] 
docs(ReforceXY): document efficiency coefficient min_range guard

4 months agoMerge branch 'main' of origin into main
Jérôme Benoit [Thu, 12 Feb 2026 23:16:32 +0000 (00:16 +0100)] 
Merge branch 'main' of origin into main

4 months agofix(ReforceXY): add context-aware guard for efficiency coefficient division
Jérôme Benoit [Thu, 12 Feb 2026 23:10:08 +0000 (00:10 +0100)] 
fix(ReforceXY): add context-aware guard for efficiency coefficient division

Prevent division explosion in _compute_efficiency_coefficient() when
max_unrealized_profit ≈ min_unrealized_profit by requiring a minimum
meaningful range based on pnl_target. Also adds validation warnings
for potential_gamma=0 and pnl_target<=0 edge cases.

4 months agofeat(ReforceXY): tune reward sensitivity and extend training period
Jérôme Benoit [Thu, 12 Feb 2026 14:17:10 +0000 (15:17 +0100)] 
feat(ReforceXY): tune reward sensitivity and extend training period

- Increase pnl_amplification_sensitivity from 0.5 to 2.0 for stronger
  reward signal differentiation
- Extend train_period_days from 60 to 120 for more training data

4 months agodocs: refine README formatting
Jérôme Benoit [Wed, 11 Feb 2026 23:19:49 +0000 (00:19 +0100)] 
docs: refine README formatting

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
4 months agofix(quickadapter): use Optuna params for TimeSeriesSplit gap calculation
Jérôme Benoit [Mon, 9 Feb 2026 21:04:23 +0000 (22:04 +0100)] 
fix(quickadapter): use Optuna params for TimeSeriesSplit gap calculation

Previously gap was calculated from ft_params with a hardcoded default, which could return incorrect values when Optuna optimized parameters. Also standardizes log message format to use [pair] prefix.

4 months agochore(deps): lock file maintenance (#49)
renovate[bot] [Mon, 9 Feb 2026 15:44:14 +0000 (16:44 +0100)] 
chore(deps): lock file maintenance (#49)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months agofeat(quickadapter): add TimeSeriesSplit as alternative data splitting method (#48)
Jérôme Benoit [Sun, 8 Feb 2026 23:55:14 +0000 (00:55 +0100)] 
feat(quickadapter): add TimeSeriesSplit as alternative data splitting method (#48)

* feat(model): add TimeSeriesSplit support via train() override

* docs(readme): document TimeSeriesSplit configuration options

* fix: address PR review comments

- Use test_size parameter in TimeSeriesSplit
- Remove unused dk parameter from _make_timeseries_split_datasets()
- Assign dk.data_dictionary = dd before logging
- Fix typo: train_test_test -> train_test_split in README

* docs: integrate data_split_parameters into tunables table

Remove standalone section and add parameters to existing table
with freqai. prefix for consistency.

* refactor: use FreqAI APIs for weight calculation and data dictionary

- Use dk.set_weights_higher_recent() instead of duplicating weight formula
- Use dk.build_data_dictionary() for consistent data structure
- Respects feature_parameters.weight_factor configuration
- Fix bug: was using data_kitchen_thread_count instead of weight_factor

* refactor: extract _apply_pipelines() to reduce code duplication

- Move pipeline definition and application logic to helper method
- Reduces train() override complexity while keeping same behavior
- Helper can be reused by future custom split implementations

* style: harmonize namespace and remove inline comments

- Rename DATA_SPLIT_METHODS to _DATA_SPLIT_METHODS (private tuple pattern)
- Reference DATA_SPLIT_METHOD_DEFAULT from _DATA_SPLIT_METHODS[0]
- Remove 22 inline comments to match self-documenting codebase style

* fix: align TimeSeriesSplit weight calculation with FreqAI semantics

Calculate weights on combined train+test set before splitting to maintain
temporal weight continuity, matching FreqAI's make_train_test_datasets behavior.

* feat: add gap=0 warning and improve TimeSeriesSplit validation

- Warn when gap=0 about look-ahead bias risk (reference label_period_candles)
- Add _compute_timeseries_min_samples() for accurate minimum sample calculation
- Account for gap and test_size in minimum sample validation
- Improve error message with all relevant parameters

* style: harmonize error messages with codebase conventions

- Use 'Invalid {param} value {value!r}: {constraint}' pattern
- Align with existing validation error format (lines 718, 1145)

* style: add cached set accessor for data split methods

- Add _data_split_methods_set() with @staticmethod @lru_cache
- Use QuickAdapterRegressorV3 prefix for class attribute access
- Use cached set for O(1) membership check in validation

* fix: address PR review comments for TimeSeriesSplit

- Use dd consistently in training logs instead of dk.data_dictionary
- Use self.data_split_parameters consistently in _apply_pipelines
- Add explicit type coercion for n_splits, gap, max_train_size
- Add validation for gap >= 0 and max_train_size >= 1
- Improve test_size validation: float in (0,1) as fraction, int >= 1 as count
- Fix _compute_timeseries_min_samples formula: (n_splits+1)*test_size + n_splits*gap
- Optimize tscv.split() iteration to avoid unnecessary list materialization

* fix: correct min_samples formula to match sklearn validation

sklearn validates: n_samples - gap - (test_size * n_splits) > 0
Correct formula: test_size * n_splits + gap + 1

* feat: auto-calculate TimeSeriesSplit gap from label_period_candles

When gap=0 is configured, automatically set gap to label_period_candles
to prevent look-ahead bias from overlapping label windows. This ensures
temporal separation between train and test sets without requiring manual
configuration.

* fix: remove redundant time import shadowing module

* fix: correct min_samples formula for dynamic test_size and document test_size param

* refactor: remove redundant TimeSeriesSplit min_samples validation

* docs: clarify test_size default per split method

* refactor: move DependencyException import to file header

* style: use class name for class constant access

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* docs: use Python None instead of null in README

* docs: fix train_test_split description (sequential, not random)

* fix: use explicit None check for max_train_size validation

* docs: clarify timeseries_split as chronological split, not cross-validation

* refactor(quickadapter): shorten log prefixes and tailor empty test set error by split method

* refactor(quickadapter): use index pattern for timeseries_split method constant

Replace string literals with index access pattern following existing
codebase convention for _DATA_SPLIT_METHODS.

Also renames variables for semantic clarity:
- test_size_param -> test_size
- feat_dict -> feature_parameters

* refactor(quickadapter): use _TEST_SIZE constant instead of hardcoded 0.1

* chore(quickadapter): bump version to 3.11.2

* fix(quickadapter): restore test_size parameter in TimeSeriesSplit

The test_size variable from data_split_parameters was being
immediately overwritten by a type annotation line, making it
always None regardless of user configuration.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
4 months agorefactor: add opencode cache mounts to devcontainer.json
Jérôme Benoit [Sun, 8 Feb 2026 18:08:27 +0000 (19:08 +0100)] 
refactor: add opencode cache mounts to devcontainer.json

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
4 months agorefactor: .env initialization to devcontainer setup
Jérôme Benoit [Sun, 8 Feb 2026 18:01:40 +0000 (19:01 +0100)] 
refactor: .env initialization to devcontainer setup

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
4 months agochore(deps): lock file maintenance (#47)
renovate[bot] [Mon, 2 Feb 2026 19:10:54 +0000 (20:10 +0100)] 
chore(deps): lock file maintenance (#47)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months agofix(ReforceXY): use .item() to extract scalar from action array
Jérôme Benoit [Sat, 31 Jan 2026 20:48:50 +0000 (21:48 +0100)] 
fix(ReforceXY): use .item() to extract scalar from action array

SB3 model.predict() returns 1D array when vectorized_env=True is detected, causing int() to fail on non-0D arrays.

4 months agofix: disable moby for docker-in-docker on Debian Trixie
Jérôme Benoit [Sat, 31 Jan 2026 19:58:48 +0000 (20:58 +0100)] 
fix: disable moby for docker-in-docker on Debian Trixie

4 months agochore: update openspec artifacts
Jérôme Benoit [Sat, 31 Jan 2026 18:34:59 +0000 (19:34 +0100)] 
chore: update openspec artifacts

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
4 months agochore: update .serena/project.yml
Jérôme Benoit [Fri, 30 Jan 2026 10:20:05 +0000 (11:20 +0100)] 
chore: update .serena/project.yml

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
4 months agodocs(openspec): add project context to config
Jérôme Benoit [Wed, 28 Jan 2026 15:20:54 +0000 (16:20 +0100)] 
docs(openspec): add project context to config

4 months agochore: update OpenSpec
Jérôme Benoit [Wed, 28 Jan 2026 12:00:12 +0000 (13:00 +0100)] 
chore: update OpenSpec

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
4 months agochore: mount host gh config directory instead of creating it manually
Jérôme Benoit [Wed, 28 Jan 2026 01:57:16 +0000 (02:57 +0100)] 
chore: mount host gh config directory instead of creating it manually

4 months agochore: update OpenSpec commands and documentation
Jérôme Benoit [Tue, 27 Jan 2026 12:46:29 +0000 (13:46 +0100)] 
chore: update OpenSpec commands and documentation

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
4 months agochore(quickadapter): use np.inf instead of float("inf")
Jérôme Benoit [Tue, 27 Jan 2026 11:42:53 +0000 (12:42 +0100)] 
chore(quickadapter): use np.inf instead of float("inf")

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
4 months agorefactor(quickadapter): use ndarray.size instead of len() for array length
Jérôme Benoit [Mon, 26 Jan 2026 20:50:42 +0000 (21:50 +0100)] 
refactor(quickadapter): use ndarray.size instead of len() for array length