]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
fix(quickadapter): correct and harmonize builtin caching (#165)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Tue, 28 Jul 2026 14:44:40 +0000 (16:44 +0200)
committerGitHub <noreply@github.com>
Tue, 28 Jul 2026 14:44:40 +0000 (16:44 +0200)
Remove @lru_cache from the instance method
QuickAdapterRegressorV3.optuna_samplers_by_namespace: decorating a bound
method kept self in the process-lifetime cache, pinning regressor
instances (models, studies, dataframes) and leaking memory across
re-instantiations. The value is trivially recomputable.

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

quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py
quickadapter/user_data/strategies/QuickAdapterV3.py
quickadapter/user_data/strategies/Utils.py

index 6261d87f4fbe8382430802a0eb309d4f50fdfd2c..61f0f71c1a3288c65de163dbc8af4ab6270eba16 100644 (file)
@@ -7,7 +7,7 @@ import time
 import warnings
 from dataclasses import dataclass
 from datetime import datetime, timezone
-from functools import cached_property, lru_cache
+from functools import cached_property
 from pathlib import Path
 from typing import (
     AbstractSet,
@@ -4783,7 +4783,6 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             case _:
                 assert_never(sampler)
 
-    @lru_cache(maxsize=8)
     def optuna_samplers_by_namespace(
         self, namespace: OptunaNamespace
     ) -> tuple[frozenset[OptunaSampler], OptunaSampler]:
index c3f98130d86d9aed3d62663e90131c0d45ec5c13..4ef5055e4b6e3b2758eade20785717bf9386a71b 100644 (file)
@@ -43,6 +43,7 @@ from scipy.stats import pearsonr, t
 from technical.pivots_points import pivots_points
 from Utils import (
     DEFAULTS_EXIT_THRESHOLDS_CALIBRATION,
+    _CACHE_MAXSIZE_LARGE,
     _OPTUNA_NAMESPACES,
     EXTREMA_COLUMN,
     EXTREMA_DIRECTION_COLUMN,
@@ -657,6 +658,9 @@ class QuickAdapterV3(IStrategy):
 
     @staticmethod
     def _df_signature(df: DataFrame) -> DfSignature:
+        """Candle-cache key ``(row_count, last_date)``; assumes existing rows
+        stay immutable (holds under ``process_only_new_candles = True``).
+        """
         n = len(df)
         if n == 0:
             return (0, None)
@@ -963,7 +967,7 @@ class QuickAdapterV3(IStrategy):
         return {}
 
     @staticmethod
-    @lru_cache(maxsize=128)
+    @lru_cache(maxsize=_CACHE_MAXSIZE_LARGE)
     def _td_format(
         delta: datetime.timedelta, pattern: str = "{sign}{d}:{h:02d}:{m:02d}:{s:02d}"
     ) -> str:
@@ -1206,7 +1210,7 @@ class QuickAdapterV3(IStrategy):
         return trade.open_date_utc - offset_timedelta
 
     @staticmethod
-    @lru_cache(maxsize=128)
+    @lru_cache(maxsize=_CACHE_MAXSIZE_LARGE)
     def is_trade_duration_valid(trade_duration: Optional[int | float]) -> bool:
         return isinstance(trade_duration, (int, float)) and not (
             isna(trade_duration) or trade_duration <= 0
@@ -1372,7 +1376,7 @@ class QuickAdapterV3(IStrategy):
         )
 
     @staticmethod
-    @lru_cache(maxsize=128)
+    @lru_cache(maxsize=_CACHE_MAXSIZE_LARGE)
     def get_stoploss_factor(trade_duration_candles: int) -> float:
         return 2.75 / (1.2675 + math.atan(0.25 * trade_duration_candles))
 
@@ -1405,7 +1409,7 @@ class QuickAdapterV3(IStrategy):
         )
 
     @staticmethod
-    @lru_cache(maxsize=128)
+    @lru_cache(maxsize=_CACHE_MAXSIZE_LARGE)
     def get_take_profit_factor(trade_duration_candles: int) -> float:
         return math.log10(9.75 + 0.25 * trade_duration_candles)
 
@@ -2092,7 +2096,6 @@ class QuickAdapterV3(IStrategy):
         )
 
     @staticmethod
-    @lru_cache(maxsize=128)
     def _t_statistic(mean: float, std: float, n: int) -> float:
         """Compute t-statistic for H0: mu = 0 as ``mean * sqrt(n) / std``.
 
@@ -2108,7 +2111,7 @@ class QuickAdapterV3(IStrategy):
         return mean * math.sqrt(n) / std
 
     @staticmethod
-    @lru_cache(maxsize=128)
+    @lru_cache(maxsize=_CACHE_MAXSIZE_LARGE)
     def is_isoformat(string: str) -> bool:
         if not isinstance(string, str):
             return False
@@ -2119,7 +2122,6 @@ class QuickAdapterV3(IStrategy):
         return True
 
     @staticmethod
-    @lru_cache(maxsize=128)
     def _effective_df(x: tuple[float, ...]) -> float:
         """Effective degrees of freedom with Bartlett's autocorrelation correction.
 
@@ -2156,7 +2158,6 @@ class QuickAdapterV3(IStrategy):
         return df_eff
 
     @staticmethod
-    @lru_cache(maxsize=128)
     def _t_critical(q: float, df: float, default_t: float) -> float:
         """Critical t-value from Student's t-distribution at quantile ``q``.
 
index 90a530bb69681548c0665ba1a66527399af6260e..dc04501bd144331b11c06aaa681f0c890806ebc2 100644 (file)
@@ -68,6 +68,11 @@ else:
 
 T = TypeVar("T", pd.Series, float)
 
+# lru_cache sizes: SMALL for bounded key spaces (windows, mode strings),
+# LARGE for open numeric/string keys (formatting, rounding, statistics).
+_CACHE_MAXSIZE_SMALL: Final[int] = 8
+_CACHE_MAXSIZE_LARGE: Final[int] = 128
+
 
 @dataclass(frozen=True, slots=True)
 class FiniteSample:
@@ -664,7 +669,7 @@ LABEL_COLUMNS: Final[tuple[str, ...]] = (EXTREMA_COLUMN,)
 _FREQAI_LABEL_SIGIL_PATTERN: Final[re.Pattern[str]] = re.compile(r"^&-?")
 
 
-@lru_cache(maxsize=64)
+@lru_cache(maxsize=_CACHE_MAXSIZE_SMALL)
 def _label_aux_column_name(label_col: str, suffix: str) -> str:
     """Derive a freqtrade-safe auxiliary column name from a label column.
 
@@ -1983,26 +1988,26 @@ def non_zero_diff(s1: pd.Series, s2: pd.Series) -> pd.Series:
     return diff.where(diff != 0, np.finfo(float).eps)
 
 
-@lru_cache(maxsize=8)
+@lru_cache(maxsize=_CACHE_MAXSIZE_SMALL)
 def get_odd_window(window: int) -> int:
     if window < 1:
         raise ValueError(f"Invalid window value {window!r}: must be > 0")
     return window if window % 2 == 1 else window + 1
 
 
-@lru_cache(maxsize=8)
+@lru_cache(maxsize=_CACHE_MAXSIZE_SMALL)
 def get_even_window(window: int) -> int:
     if window < 1:
         raise ValueError(f"Invalid window value {window!r}: must be > 0")
     return window if window % 2 == 0 else window + 1
 
 
-@lru_cache(maxsize=8)
+@lru_cache(maxsize=_CACHE_MAXSIZE_SMALL)
 def get_gaussian_std(window: int) -> float:
     return (window - 1) / 6.0 if window > 1 else 0.5
 
 
-@lru_cache(maxsize=8)
+@lru_cache(maxsize=_CACHE_MAXSIZE_SMALL)
 def get_savgol_params(
     window: int, polyorder: int, mode: SmoothingMode
 ) -> tuple[int, int, str]:
@@ -2012,7 +2017,7 @@ def get_savgol_params(
     return window, polyorder, mode
 
 
-@lru_cache(maxsize=8)
+@lru_cache(maxsize=_CACHE_MAXSIZE_SMALL)
 def _calculate_coeffs(
     window: int,
     win_type: SmoothingKernel,
@@ -2036,7 +2041,9 @@ def _calculate_coeffs(
             f"Invalid window type value {win_type!r}: "
             f"supported values are {', '.join(SMOOTHING_KERNELS)}"
         )
-    return coeffs / np.sum(coeffs)
+    normalized_coeffs = coeffs / np.sum(coeffs)
+    normalized_coeffs.setflags(write=False)
+    return normalized_coeffs
 
 
 def zero_phase_filter(
@@ -2761,7 +2768,7 @@ _SCIENTIFIC_THRESHOLD_HIGH = 1e12
 _SCIENTIFIC_THRESHOLD_LOW = 1e-6
 
 
-@lru_cache(maxsize=128)
+@lru_cache(maxsize=_CACHE_MAXSIZE_LARGE)
 def format_number(value: int | float, significant_digits: int = 5) -> str:
     if not isinstance(value, (int, float, np.integer, np.floating)):
         return str(value)
@@ -2953,7 +2960,7 @@ def format_dict(
     return f"{{{joined}}}" if style == "dict" else joined
 
 
-@lru_cache(maxsize=128)
+@lru_cache(maxsize=_CACHE_MAXSIZE_LARGE)
 def calculate_min_extrema(
     length: int, fit_live_predictions_candles: int, min_extrema: int = 2
 ) -> int:
@@ -3078,7 +3085,7 @@ def calculate_zero_lag(series: pd.Series, period: int) -> pd.Series:
     return 2 * series - series.shift(int(lag))
 
 
-@lru_cache(maxsize=8)
+@lru_cache(maxsize=_CACHE_MAXSIZE_SMALL)
 def get_ma_fn(
     mamode: str,
 ) -> Callable[
@@ -3102,7 +3109,7 @@ def get_ma_fn(
     return mamodes.get(mamode, mamodes["sma"])
 
 
-@lru_cache(maxsize=8)
+@lru_cache(maxsize=_CACHE_MAXSIZE_SMALL)
 def get_zl_ma_fn(
     mamode: str,
 ) -> Callable[
@@ -3219,7 +3226,7 @@ def smma(series: pd.Series, period: int, zero_lag=False, offset=0) -> pd.Series:
     return smma
 
 
-@lru_cache(maxsize=8)
+@lru_cache(maxsize=_CACHE_MAXSIZE_SMALL)
 def get_price_fn(pricemode: str) -> Callable[[pd.DataFrame], pd.Series]:
     pricemodes = {
         "average": ta.AVGPRICE,
@@ -5320,7 +5327,7 @@ def get_optuna_study_model_parameters(
         raise ValueError(enum_error_message("regressor", regressor, REGRESSORS))
 
 
-@lru_cache(maxsize=128)
+@lru_cache(maxsize=_CACHE_MAXSIZE_LARGE)
 def largest_divisor_to_step(integer: int, step: int) -> int | None:
     if not isinstance(integer, int) or integer <= 0:
         raise ValueError(
@@ -5366,7 +5373,7 @@ def soft_extremum(series: pd.Series, alpha: float) -> float:
     return nan_average(values, weights=shifted_exponentials)
 
 
-@lru_cache(maxsize=8)
+@lru_cache(maxsize=_CACHE_MAXSIZE_SMALL)
 def get_min_max_label_period_candles(
     fit_live_predictions_candles: int,
     candles_step: int,
@@ -5429,7 +5436,7 @@ def _validate_step_args(value: float | int, step: int) -> None:
         raise ValueError(f"Invalid step value {step!r}: must be a positive integer")
 
 
-@lru_cache(maxsize=128)
+@lru_cache(maxsize=_CACHE_MAXSIZE_LARGE)
 def round_to_step(value: float | int, step: int) -> int:
     """
     Round a value to the nearest multiple of a given step.
@@ -5452,7 +5459,7 @@ def round_to_step(value: float | int, step: int) -> int:
     return int(round(float(value) / step) * step)
 
 
-@lru_cache(maxsize=128)
+@lru_cache(maxsize=_CACHE_MAXSIZE_LARGE)
 def ceil_to_step(value: float | int, step: int) -> int:
     _validate_step_args(value, step)
     if isinstance(value, (int, np.integer)):
@@ -5462,7 +5469,7 @@ def ceil_to_step(value: float | int, step: int) -> int:
     return int(math.ceil(float(value) / step) * step)
 
 
-@lru_cache(maxsize=128)
+@lru_cache(maxsize=_CACHE_MAXSIZE_LARGE)
 def floor_to_step(value: float | int, step: int) -> int:
     _validate_step_args(value, step)
     if isinstance(value, (int, np.integer)):