]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
refactor(quickadapter): consolidate Optuna sampler tuples to NamedTuple (#101)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Mon, 22 Jun 2026 09:32:44 +0000 (11:32 +0200)
committerGitHub <noreply@github.com>
Mon, 22 Jun 2026 09:32:44 +0000 (11:32 +0200)
PR #81 consolidated `_OPTUNA_NAMESPACES` to `Utils._OptunaNamespaces` (a `NamedTuple` with per-field singleton `Literal` types). Propagate the same pattern to the three sibling sampler tuples in `QuickAdapterRegressorV3.py`:

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

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

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

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

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

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

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

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

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

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

Closes #88.

quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py

index ba715a4bc990f311ccfa3656519e347fdf8ad2cf..261fbd3553a5d1606965e0afd35927528999bfaf 100644 (file)
@@ -14,6 +14,7 @@ from typing import (
     ClassVar,
     Final,
     Literal,
+    NamedTuple,
     Optional,
     Union,
     assert_never,
@@ -167,6 +168,25 @@ class SampleWeightInputs:
             )
 
 
+class _OptunaSamplers(NamedTuple):
+    tpe: Literal["tpe"] = "tpe"
+    auto: Literal["auto"] = "auto"
+    nsgaii: Literal["nsgaii"] = "nsgaii"
+    nsgaiii: Literal["nsgaiii"] = "nsgaiii"
+
+
+class _OptunaHpoSamplers(NamedTuple):
+    tpe: Literal["tpe"] = "tpe"
+    auto: Literal["auto"] = "auto"
+
+
+class _OptunaLabelSamplers(NamedTuple):
+    auto: Literal["auto"] = "auto"
+    tpe: Literal["tpe"] = "tpe"
+    nsgaii: Literal["nsgaii"] = "nsgaii"
+    nsgaiii: Literal["nsgaiii"] = "nsgaiii"
+
+
 class QuickAdapterRegressorV3(BaseRegressionModel):
     """
     The following freqaimodel is released to sponsors of the non-profit FreqAI open-source project.
@@ -202,22 +222,12 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         optuna.study.StudyDirection.MAXIMIZE,
     ) * _OPTUNA_LABEL_N_OBJECTIVES
     _OPTUNA_STORAGE_BACKENDS: Final[tuple[str, ...]] = ("file", "sqlite")
-    _OPTUNA_SAMPLERS: Final[tuple[OptunaSampler, ...]] = (
-        "tpe",
-        "auto",
-        "nsgaii",
-        "nsgaiii",
-    )
-    _OPTUNA_HPO_SAMPLERS: Final[tuple[OptunaSampler, ...]] = _OPTUNA_SAMPLERS[:2]
+    _OPTUNA_SAMPLERS: Final[_OptunaSamplers] = _OptunaSamplers()
+    _OPTUNA_HPO_SAMPLERS: Final[_OptunaHpoSamplers] = _OptunaHpoSamplers()
     _OPTUNA_HPO_SAMPLERS_SET: Final[frozenset[OptunaSampler]] = frozenset(
         _OPTUNA_HPO_SAMPLERS
     )
-    _OPTUNA_LABEL_SAMPLERS: Final[tuple[OptunaSampler, ...]] = (
-        _OPTUNA_SAMPLERS[1],  # "auto"
-        _OPTUNA_SAMPLERS[0],  # "tpe"
-        _OPTUNA_SAMPLERS[2],  # "nsgaii"
-        _OPTUNA_SAMPLERS[3],  # "nsgaiii"
-    )
+    _OPTUNA_LABEL_SAMPLERS: Final[_OptunaLabelSamplers] = _OptunaLabelSamplers()
     _OPTUNA_LABEL_SAMPLERS_SET: Final[frozenset[OptunaSampler]] = frozenset(
         _OPTUNA_LABEL_SAMPLERS
     )
@@ -1239,16 +1249,14 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 .get("n_jobs", QuickAdapterRegressorV3.OPTUNA_N_JOBS_DEFAULT),
                 max(int(self.max_system_threads / 4), 1),
             ),
-            "sampler": QuickAdapterRegressorV3._OPTUNA_HPO_SAMPLERS[0],  # "tpe"
+            "sampler": QuickAdapterRegressorV3._OPTUNA_HPO_SAMPLERS.tpe,
             "storage": QuickAdapterRegressorV3._OPTUNA_STORAGE_BACKENDS[0],  # "file"
             "continuous": True,
             "warm_start": True,
             "n_startup_trials": QuickAdapterRegressorV3.OPTUNA_N_STARTUP_TRIALS_DEFAULT,
             "n_trials": QuickAdapterRegressorV3.OPTUNA_N_TRIALS_DEFAULT,
             "timeout": QuickAdapterRegressorV3.OPTUNA_TIMEOUT_DEFAULT,
-            "label_sampler": QuickAdapterRegressorV3._OPTUNA_LABEL_SAMPLERS[
-                0
-            ],  # "auto"
+            "label_sampler": QuickAdapterRegressorV3._OPTUNA_LABEL_SAMPLERS.auto,
             "label_candles_step": QuickAdapterRegressorV3.OPTUNA_LABEL_CANDLES_STEP_DEFAULT,
             "space_reduction": QuickAdapterRegressorV3.OPTUNA_SPACE_REDUCTION_DEFAULT,
             "space_fraction": QuickAdapterRegressorV3.OPTUNA_SPACE_FRACTION_DEFAULT,
@@ -4235,7 +4243,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             sampler = self._optuna_config.get(
                 "sampler",
             )
-        if sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS[0]:  # "tpe"
+        if sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS.tpe:
             return optuna.samplers.TPESampler(
                 n_startup_trials=self._optuna_config.get(
                     "n_startup_trials",
@@ -4251,19 +4259,19 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                     "seed", QuickAdapterRegressorV3.OPTUNA_SEED_DEFAULT
                 ),
             )
-        elif sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS[1]:  # "auto"
+        elif sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS.auto:
             return optunahub.load_module("samplers/auto_sampler").AutoSampler(
                 seed=self._optuna_config.get(
                     "seed", QuickAdapterRegressorV3.OPTUNA_SEED_DEFAULT
                 )
             )
-        elif sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS[2]:  # "nsgaii"
+        elif sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS.nsgaii:
             return optuna.samplers.NSGAIISampler(
                 seed=self._optuna_config.get(
                     "seed", QuickAdapterRegressorV3.OPTUNA_SEED_DEFAULT
                 ),
             )
-        elif sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS[3]:  # "nsgaiii"
+        elif sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS.nsgaiii:
             return optuna.samplers.NSGAIIISampler(
                 seed=self._optuna_config.get(
                     "seed", QuickAdapterRegressorV3.OPTUNA_SEED_DEFAULT
@@ -4283,14 +4291,14 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             return (
                 QuickAdapterRegressorV3._OPTUNA_HPO_SAMPLERS_SET,
                 self._optuna_config.get(
-                    "sampler", QuickAdapterRegressorV3._OPTUNA_HPO_SAMPLERS[0]
+                    "sampler", QuickAdapterRegressorV3._OPTUNA_HPO_SAMPLERS.tpe
                 ),
             )
         elif namespace == _OPTUNA_NAMESPACES.label:
             return (
                 QuickAdapterRegressorV3._OPTUNA_LABEL_SAMPLERS_SET,
                 self._optuna_config.get(
-                    "label_sampler", QuickAdapterRegressorV3._OPTUNA_LABEL_SAMPLERS[0]
+                    "label_sampler", QuickAdapterRegressorV3._OPTUNA_LABEL_SAMPLERS.auto
                 ),
             )
         else: