From: Jérôme Benoit Date: Mon, 22 Jun 2026 09:32:44 +0000 (+0200) Subject: refactor(quickadapter): consolidate Optuna sampler tuples to NamedTuple (#101) X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=6bdc61f797c74162ada021afa5f9de0b9862fb47;p=freqai-strategies.git refactor(quickadapter): consolidate Optuna sampler tuples to NamedTuple (#101) PR #81 consolidated `_OPTUNA_NAMESPACES` to `Utils._OptunaNamespaces` (a `NamedTuple` with per-field singleton `Literal` types). Propagate the same pattern to the three sibling sampler tuples in `QuickAdapterRegressorV3.py`: - `_OPTUNA_SAMPLERS` (tpe, auto, nsgaii, nsgaiii) - `_OPTUNA_HPO_SAMPLERS` (tpe, auto) - `_OPTUNA_LABEL_SAMPLERS` (auto, tpe, nsgaii, nsgaiii) Each new `_Optuna*Samplers` NamedTuple class lives at module level immediately before `class QuickAdapterRegressorV3`, matching the `_OptunaNamespaces` adjacency in `Utils.py`. Per-field types are singleton `Literal["..."]` (not the `OptunaSampler` union) to unlock pyright/mypy narrowing for a future `assert_never` migration. The class-private `_OPTUNA_*_SAMPLERS` instance constants remain class-attributes of `QuickAdapterRegressorV3` to preserve every consumer's `QuickAdapterRegressorV3._OPTUNA_*` access pattern. Each `_Optuna*Samplers` redeclares its field defaults (per-class `Literal["tpe"] = "tpe"` etc.), replacing the prior tuple-slice derivation `_OPTUNA_HPO_SAMPLERS = _OPTUNA_SAMPLERS[:2]` and the 6-line custom reordering of `_OPTUNA_LABEL_SAMPLERS`. Trade-off: minor string-literal duplication across 3 classes is accepted for harmonization with the `_OptunaNamespaces` template; the single source of truth for the 4 valid tokens stays the `OptunaSampler = Literal[...]` alias unchanged at module level. Migrates 8 positional-indexing call sites (`_OPTUNA_*_SAMPLERS[N] # "name"`) to attribute access (`.`); 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. --- diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index ba715a4..261fbd3 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -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: