From: Jérôme Benoit Date: Wed, 1 Oct 2025 13:17:11 +0000 (+0200) Subject: perf(qav3): use vectorized ops in more places X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=34ac8bb3be75ad898ad02553b66f3093d1d03de7;p=freqai-strategies.git perf(qav3): use vectorized ops in more places Signed-off-by: Jérôme Benoit --- diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index 8092428..d4b971d 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -125,7 +125,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ) if ( not isinstance(self.freqai_info.get("identifier"), str) - or self.freqai_info.get("identifier").strip() == "" + or not self.freqai_info.get("identifier").strip() ): raise ValueError( "FreqAI model requires 'identifier' defined in the freqai section configuration" @@ -1322,6 +1322,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel): direction: Optional[optuna.study.StudyDirection] = None, directions: Optional[list[optuna.study.StudyDirection]] = None, ) -> Optional[optuna.study.Study]: + if direction is not None and directions is not None: + raise ValueError( + "Cannot specify both 'direction' and 'directions'. Use one or the other" + ) is_study_single_objective = direction is not None and directions is None if ( not is_study_single_objective @@ -1458,6 +1462,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel): direction: Optional[optuna.study.StudyDirection] = None, directions: Optional[list[optuna.study.StudyDirection]] = None, ) -> Optional[optuna.study.Study]: + if direction is not None and directions is not None: + raise ValueError( + "Cannot specify both 'direction' and 'directions'. Use one or the other" + ) identifier = self.freqai_info.get("identifier") study_name = f"{identifier}-{pair}-{namespace}" try: diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index cf9097f..12fff9d 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -213,7 +213,7 @@ class QuickAdapterV3(IStrategy): ) if ( not isinstance(self.freqai_info.get("identifier"), str) - or self.freqai_info.get("identifier").strip() == "" + or not self.freqai_info.get("identifier").strip() ): raise ValueError( "FreqAI strategy requires 'identifier' defined in the freqai section configuration" @@ -487,8 +487,7 @@ class QuickAdapterV3(IStrategy): f"{pair}: no extrema to label (label_period={QuickAdapterV3.td_format(label_period)} / {label_period_candles=} / {label_natr_ratio=:.2f})" ) else: - for pivot_idx, pivot_dir in zip(pivots_indices, pivots_directions): - dataframe.at[pivot_idx, EXTREMA_COLUMN] = pivot_dir + dataframe.loc[pivots_indices, EXTREMA_COLUMN] = pivots_directions dataframe["minima"] = np.where( dataframe[EXTREMA_COLUMN] == TrendDirection.DOWN, -1, 0 ) @@ -759,6 +758,10 @@ class QuickAdapterV3(IStrategy): current_rate: float, natr_ratio_percent: float, ) -> Optional[float]: + if not (0.0 <= natr_ratio_percent <= 1.0): + raise ValueError( + f"natr_ratio_percent must be in [0, 1], got {natr_ratio_percent}" + ) trade_duration_candles = self.get_trade_duration_candles(df, trade) if not QuickAdapterV3.is_trade_duration_valid(trade_duration_candles): return None @@ -807,7 +810,7 @@ class QuickAdapterV3(IStrategy): candle_duration_secs = max(1, int(self._candle_duration_secs)) candle_start_secs = (timestamp // candle_duration_secs) * candle_duration_secs callback_hash = get_callable_sha256(callback) - key = hashlib.sha256(f"{pair}|{callback_hash}".encode()).hexdigest() + key = hashlib.sha256(f"{pair}\x00{callback_hash}".encode()).hexdigest() if candle_start_secs != self.last_candle_start_secs.get(key): self.last_candle_start_secs[key] = candle_start_secs try: @@ -817,6 +820,15 @@ class QuickAdapterV3(IStrategy): f"Error executing callback for {pair}: {repr(e)}", exc_info=True ) + threshold_secs = 10 * candle_duration_secs + keys_to_remove = [ + key + for key, ts in self.last_candle_start_secs.items() + if ts is not None and timestamp - ts > threshold_secs + ] + for key in keys_to_remove: + del self.last_candle_start_secs[key] + def custom_stoploss( self, pair: str, diff --git a/quickadapter/user_data/strategies/Utils.py b/quickadapter/user_data/strategies/Utils.py index 2a00157..9b22ec3 100644 --- a/quickadapter/user_data/strategies/Utils.py +++ b/quickadapter/user_data/strategies/Utils.py @@ -383,11 +383,15 @@ def smma(series: pd.Series, period: int, zero_lag=False, offset=0) -> pd.Series: if zero_lag: series = calculate_zero_lag(series, period=period) - smma = pd.Series(np.nan, index=series.index) - smma.iloc[period - 1] = series.iloc[:period].mean() + values = series.to_numpy() + + smma_values = np.full(n, np.nan) + smma_values[period - 1] = np.mean(values[:period]) for i in range(period, n): - smma.iloc[i] = (smma.iloc[i - 1] * (period - 1) + series.iloc[i]) / period + smma_values[i] = (smma_values[i - 1] * (period - 1) + values[i]) / period + + smma = pd.Series(smma_values, index=series.index) if offset != 0: smma = smma.shift(offset) @@ -831,6 +835,12 @@ def get_optuna_study_model_parameters( raise ValueError( f"Unsupported regressor model: {regressor} (supported: {', '.join(regressors)})" ) + if not isinstance(expansion_ratio, (int, float)) or not ( + 0.0 <= expansion_ratio <= 1.0 + ): + raise ValueError( + f"expansion_ratio must be a float between 0 and 1, got {expansion_ratio}" + ) default_ranges = { "n_estimators": (100, 2000), "learning_rate": (1e-3, 0.5),