]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
fix(quickadapter): make HPO state causal in backtests (#111)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Thu, 23 Jul 2026 17:39:11 +0000 (19:39 +0200)
committerGitHub <noreply@github.com>
Thu, 23 Jul 2026 17:39:11 +0000 (19:39 +0200)
* 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.

.serena/project.yml
README.md
quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py
quickadapter/user_data/strategies/QuickAdapterV3.py

index 95bc49c03f62b3f679374d07923304016d98cb53..e65f8849463298c99a7fd313a66a92d99b32c51c 100644 (file)
@@ -8,6 +8,13 @@ ignore_all_files_in_gitignore: true
 
 # list of additional paths to ignore in this project.
 # Same syntax as gitignore, so you can use * and **.
+# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases.
+# Example:
+#   ignored_paths:
+#     - "examples/**"
+#     - ".worktrees/**"
+#     - "**/bin/**"
+#     - "**/obj/**"
 # Note: global ignored_paths from serena_config.yml are also applied additively.
 ignored_paths: []
 
@@ -57,41 +64,6 @@ included_optional_tools: []
 # Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
 fixed_tools: []
 
-# list of languages for which language servers are started (LSP backend only); choose from:
-#   ada                 al                  angular             ansible             bash
-#   bsl                 clojure             cpp                 cpp_ccls            crystal
-#   csharp              csharp_omnisharp    cue                 dart                elixir
-#   elm                 erlang              fortran             fsharp              gdscript
-#   go                  groovy              haskell             haxe                hlsl
-#   html                java                json                julia               kotlin
-#   latex               lean4               lua                 luau                markdown
-#   matlab              msl                 nix                 ocaml               pascal
-#   perl                php                 php_phpactor        php_phpantom        powershell
-#   python              python_jedi         python_pyrefly      python_ty           r
-#   rego                ruby                ruby_solargraph     rust                scala
-#   scss                solidity            svelte              swift               systemverilog
-#   terraform           toml                typescript          typescript_vts      vue
-#   yaml                zig
-#   (This list may be outdated; generated with scripts/print_language_list.py;
-#   For the current list, see values of Language enum here:
-#   https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
-# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
-# Note:
-#   - For C, use cpp
-#   - For JavaScript, use typescript
-#   - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
-#   - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
-#   - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
-#   - For Free Pascal/Lazarus, use pascal
-# Special requirements:
-#   Some languages require additional setup/installations.
-#   See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
-# When using multiple languages, the first language server that supports a given file will be used for that file.
-# The first language is the default language and the respective language server will be used as a fallback.
-# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
-languages:
-- python
-
 # time budget (seconds) per tool call for the retrieval of additional symbol information
 # such as docstrings or parameter information.
 # This overrides the corresponding setting in the global configuration; see the documentation there.
@@ -168,3 +140,38 @@ ls_additional_workspace_folders: []
 #     - "./subproject2"
 ls_workspace_folders:
 - .
+
+# list of language servers to start when using the LSP backend; choose from:
+#   ada                 al                  angular             ansible             bash
+#   bsl                 clojure             cpp                 cpp_ccls            crystal
+#   csharp              csharp_omnisharp    cue                 dart                elixir
+#   elm                 erlang              fortran             fsharp              gdscript
+#   go                  groovy              haskell             haxe                hlsl
+#   html                java                json                julia               kotlin
+#   latex               lean4               lua                 luau                markdown
+#   matlab              msl                 nix                 ocaml               pascal
+#   perl                php                 php_phpactor        php_phpantom        powershell
+#   python              python_jedi         python_pyrefly      python_ty           r
+#   rego                ruby                ruby_solargraph     rust                scala
+#   scss                solidity            svelte              swift               systemverilog
+#   terraform           toml                typescript          typescript_vts      vue
+#   yaml                zig
+#   (This list may be outdated; generated with scripts/print_language_list.py;
+#   For the current list, see values of Language enum here:
+#   https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
+# For some languages, there are several alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
+# Note:
+#   - For C, use cpp
+#   - For JavaScript, use typescript
+#   - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
+#   - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
+#   - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
+#   - For Free Pascal/Lazarus, use pascal
+# Special requirements:
+#   Some language servers require additional setup/installations.
+#   See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
+# When using multiple language servers, the first language server that supports a given file will be used for that file.
+# The first language server is the default language and the respective language server will be used as a fallback.
+# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
+language_servers:
+- python
index e8a5d0a7f183f9fa27b85719c6c886eea0138215..e8c83c43d5b354c639e409ecac3c408643dfdd03 100644 (file)
--- a/README.md
+++ b/README.md
@@ -130,12 +130,12 @@ docker compose up -d --build
 | freqai.label_prediction.outlier_quantile                       | 0.999                         | float (0,1)                                                                                                                                                                                                  | Quantile threshold for predictions outlier filtering.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
 | freqai.label_prediction.keep_fraction                          | 0.0075                        | float (0,1]                                                                                                                                                                                                  | Fraction of extrema used for thresholds. 1 uses all, lower values keep only most significant. Applies to `rank_extrema` and `rank_peaks`; ignored for `partition`.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
 | _Optuna / HPO_                                                 |                               |                                                                                                                                                                                                              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
-| freqai.optuna_hyperopt.enabled                                 | false                         | bool                                                                                                                                                                                                         | Enables HPO.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
+| freqai.optuna_hyperopt.enabled                                 | false                         | bool                                                                                                                                                                                                         | Enables regressor and dynamic label HPO.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
 | freqai.optuna_hyperopt.sampler                                 | `tpe`                         | enum {`tpe`,`auto`}                                                                                                                                                                                          | HPO sampler algorithm for `hp` namespace. `tpe` uses [TPESampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.TPESampler.html) with multivariate, group, and constant_liar (when multiple workers), `auto` uses [AutoSampler](https://hub.optuna.org/samplers/auto_sampler).                                                                                                                                                                                                                                                                                      |
 | freqai.optuna_hyperopt.label_sampler                           | `auto`                        | enum {`auto`,`tpe`,`nsgaii`,`nsgaiii`}                                                                                                                                                                       | HPO sampler algorithm for multi-objective `label` namespace. `nsgaii` uses [NSGAIISampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIISampler.html), `nsgaiii` uses [NSGAIIISampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIIISampler.html).                                                                                                                                                                                                                                                            |
 | freqai.optuna_hyperopt.storage                                 | `file`                        | enum {`file`,`sqlite`}                                                                                                                                                                                       | HPO storage backend.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
-| freqai.optuna_hyperopt.continuous                              | true                          | bool                                                                                                                                                                                                         | Continuous HPO.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
-| freqai.optuna_hyperopt.warm_start                              | true                          | bool                                                                                                                                                                                                         | Warm start HPO with previous best value(s).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
+| freqai.optuna_hyperopt.continuous                              | true                          | bool                                                                                                                                                                                                         | Continuous HPO. Forced for both namespaces in backtest and hyperopt, resetting the study on each optimization.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
+| freqai.optuna_hyperopt.warm_start                              | true                          | bool                                                                                                                                                                                                         | Warm start HPO with previous best value(s). Persisted values are loaded and saved only in live and dry-run modes; non-live runs reuse only values produced earlier in the same run.                                                                                                                                                                                                                                                                                                                                                                                                                         |
 | freqai.optuna_hyperopt.n_startup_trials                        | 15                            | int >= 0                                                                                                                                                                                                     | HPO startup trials.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
 | freqai.optuna_hyperopt.n_trials                                | 50                            | int >= 1                                                                                                                                                                                                     | Maximum HPO trials.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
 | freqai.optuna_hyperopt.n_jobs                                  | CPU threads / 4               | int >= 1                                                                                                                                                                                                     | Parallel HPO workers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
index d448922276886b6aa4315df77453c3a0b2b1fd38..dfe3d08dac28583c4bdd9b2f1711d3abb824caa5 100644 (file)
@@ -32,6 +32,7 @@ import skimage
 import sklearn
 from datasieve.pipeline import Pipeline
 from datasieve.transforms import SKLearnWrapper
+from freqtrade.enums import TRADE_MODES
 from freqtrade.exceptions import DependencyException
 from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
 from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
@@ -1433,17 +1434,20 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         default_label_period_candles, default_label_natr_multiplier = (
             self._label_defaults
         )
+        # self.live is unset until IFreqaiModel.start(), so derive trade-mode
+        # from the configured runmode here.
+        trade_mode = self.config.get("runmode") in TRADE_MODES
         for pair in self.pairs:
             self._optuna_hp_value[pair] = -1
             self._optuna_label_values[pair] = [
                 -1
             ] * QuickAdapterRegressorV3._OPTUNA_LABEL_N_OBJECTIVES
             self._optuna_hp_params[pair] = (
-                self.optuna_load_best_params(pair, _OPTUNA_NAMESPACES.hp) or {}
-            )
-            self._optuna_label_params[pair] = self.optuna_load_best_params(
-                pair, _OPTUNA_NAMESPACES.label
-            ) or {
+                self.optuna_load_best_params(pair, _OPTUNA_NAMESPACES.hp)
+                if trade_mode
+                else None
+            ) or {}
+            configured_label_params = {
                 "label_period_candles": self.ft_params.get(
                     "label_period_candles",
                     default_label_period_candles,
@@ -1455,6 +1459,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                     )
                 ),
             }
+            self._optuna_label_params[pair] = (
+                self.optuna_load_best_params(pair, _OPTUNA_NAMESPACES.label)
+                if trade_mode
+                else None
+            ) or configured_label_params
             self.set_optuna_label_candle(pair)
             self._optuna_label_candles[pair] = 0
 
@@ -2578,25 +2587,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             self.optuna_throttle_callback(
                 pair=pair,
                 namespace=_OPTUNA_NAMESPACES.label,
-                callback=lambda: self.optuna_optimize(
-                    pair=pair,
-                    namespace=_OPTUNA_NAMESPACES.label,
-                    objective=lambda trial: label_objective(
-                        trial,
-                        self.data_provider.get_pair_dataframe(
-                            pair=pair, timeframe=self.config.get("timeframe")
-                        ),
-                        fit_live_predictions_candles,
-                        self._optuna_config.get(
-                            "label_candles_step",
-                            QuickAdapterRegressorV3.OPTUNA_LABEL_CANDLES_STEP_DEFAULT,
-                        ),
-                        min_label_period_candles=self._min_label_period_candles,
-                        max_label_period_candles=self._max_label_period_candles,
-                        min_label_natr_multiplier=self._min_label_natr_multiplier,
-                        max_label_natr_multiplier=self._max_label_natr_multiplier,
-                    ),
-                    directions=list(QuickAdapterRegressorV3._OPTUNA_LABEL_DIRECTIONS),
+                callback=lambda: self._optimize_labels_as_of_prediction_time(
+                    dk,
+                    pair,
+                    fit_live_predictions_candles,
                 ),
             )
 
@@ -2746,6 +2740,77 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             hp_rmse if hp_rmse is not None else np.inf
         )
 
+    def _label_hpo_dataframe_as_of_prediction_time(
+        self,
+        dk: FreqaiDataKitchen,
+        pair: str,
+    ) -> pd.DataFrame:
+        """Return pair OHLCV bounded to the current FreqAI prediction time.
+
+        In backtests, ``historic_predictions`` holds the preceding prediction
+        window and ``dk.full_df`` is the full feature frame; the current
+        prediction time is the first ``dk.full_df`` timestamp strictly after the
+        last recorded prediction. Live and dry-run modes rely on the already
+        point-in-time DataProvider frame. This explicit backtest bound is
+        required because DataProvider otherwise exposes the complete historical
+        timerange.
+        """
+        pair_dataframe = self.data_provider.get_pair_dataframe(
+            pair=pair, timeframe=self.config.get("timeframe")
+        )
+        if self.live or pair_dataframe.empty:
+            return pair_dataframe
+
+        history_dates = ensure_datetime_series(
+            self.dd.historic_predictions[pair]["date"]
+        )
+        if history_dates.empty:
+            logger.debug(
+                "[%s] Label HPO skipped: no prior predictions to bound the current FreqAI prediction time",
+                pair,
+            )
+            return pair_dataframe.iloc[:0]
+
+        pair_dates = ensure_datetime_series(pair_dataframe["date"])
+        full_dates = ensure_datetime_series(dk.full_df["date"])
+        current_dates = full_dates.loc[full_dates > history_dates.max()]
+        if current_dates.empty:
+            logger.debug(
+                "[%s] Label HPO skipped: current FreqAI prediction time is unavailable",
+                pair,
+            )
+            return pair_dataframe.iloc[:0]
+
+        return pair_dataframe.loc[pair_dates <= current_dates.min()]
+
+    def _optimize_labels_as_of_prediction_time(
+        self,
+        dk: FreqaiDataKitchen,
+        pair: str,
+        fit_live_predictions_candles: int,
+    ) -> Optional[optuna.study.Study]:
+        label_dataframe = self._label_hpo_dataframe_as_of_prediction_time(dk, pair)
+        if label_dataframe.empty:
+            return None
+        return self.optuna_optimize(
+            pair=pair,
+            namespace=_OPTUNA_NAMESPACES.label,
+            objective=lambda trial: label_objective(
+                trial,
+                label_dataframe,
+                fit_live_predictions_candles,
+                self._optuna_config.get(
+                    "label_candles_step",
+                    QuickAdapterRegressorV3.OPTUNA_LABEL_CANDLES_STEP_DEFAULT,
+                ),
+                min_label_period_candles=self._min_label_period_candles,
+                max_label_period_candles=self._max_label_period_candles,
+                min_label_natr_multiplier=self._min_label_natr_multiplier,
+                max_label_natr_multiplier=self._max_label_natr_multiplier,
+            ),
+            directions=list(QuickAdapterRegressorV3._OPTUNA_LABEL_DIRECTIONS),
+        )
+
     @staticmethod
     def optuna_validate_value(value: Any) -> Optional[float]:
         return value if isinstance(value, (int, float)) and np.isfinite(value) else None
@@ -4166,7 +4231,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             logger.warning(
                 f"[{pair}] Optuna {namespace} {objective_type} objective hyperopt best params found has invalid optimization target value(s)"
             )
-        self.optuna_save_best_params(pair, namespace)
+        if self.live:
+            self.optuna_save_best_params(pair, namespace)
         return study
 
     @staticmethod
@@ -4270,6 +4336,9 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         )
 
     def optuna_create_storage(self, pair: str) -> optuna.storages.BaseStorage:
+        if not self.live:
+            return optuna.storages.InMemoryStorage()
+
         storage_dir = self.full_path
         storage_filename = f"optuna-{pair.split('/')[0]}"
         storage_backend = self._optuna_config.get("storage")
@@ -4436,7 +4505,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             )
             return None
 
-        continuous = self._optuna_config.get("continuous")
+        # Non-live HPO is point-in-time: reset the study each optimization and
+        # never persist best params. Warm start still seeds it from the previous
+        # cutoff's in-memory best, which stays causal as it predates the current
+        # cutoff.
+        continuous = self._optuna_config.get("continuous") or not self.live
         if continuous:
             QuickAdapterRegressorV3.optuna_delete_study(
                 pair, namespace, study_name, storage
@@ -4587,6 +4660,14 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     ) -> None:
         try:
             optuna.delete_study(study_name=study_name, storage=storage)
+        except KeyError as e:
+            # A missing study is a benign no-op: non-live runs use a fresh
+            # InMemoryStorage and the first live/dry-run optimization per pair
+            # has none yet. optuna reports it as KeyError; other failures reach
+            # the warning branch below.
+            logger.debug(
+                f"[{pair}] Optuna {namespace} study {study_name} absent; nothing to delete: {e!r}"
+            )
         except Exception as e:
             logger.warning(
                 f"[{pair}] Optuna {namespace} study {study_name} deletion failed: {e!r}",
index b1e860b223f88e588a2a5d4d0156e958496e9e2e..967d53fddef68ab68fe799d5d2a3b06c02d6c06a 100644 (file)
@@ -17,6 +17,7 @@ from typing import (
 import numpy as np
 import pandas_ta as pta
 import talib.abstract as ta
+from freqtrade.enums import TRADE_MODES
 from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
 from freqtrade.persistence import Trade
 from freqtrade.strategy import AnnotationType, stoploss_from_absolute
@@ -454,9 +455,14 @@ class QuickAdapterV3(IStrategy):
             self._label_defaults
         )
         self._label_params: dict[str, dict[str, Any]] = {}
+        # Mirror the regressor's ``self.live`` gate (runmode in TRADE_MODES):
+        # persisted label params are reused only in live and dry-run.
+        load_persisted_label_params = self.config.get("runmode") in TRADE_MODES
         for pair in self.pairs:
-            label_best_params = self.optuna_load_best_params(
-                pair, _OPTUNA_NAMESPACES.label
+            label_best_params = (
+                self.optuna_load_best_params(pair, _OPTUNA_NAMESPACES.label)
+                if load_persisted_label_params
+                else None
             )
             self._label_params[pair] = (
                 label_best_params