From b23f4f35c623c995251e9eda0f0caa9aed8fcfab Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 26 Dec 2025 14:03:41 +0100 Subject: [PATCH] fix(quickadapter): unbiased quantile calculation with percentileofscore MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- .../user_data/strategies/QuickAdapterV3.py | 24 ++++++++++++++----- quickadapter/user_data/strategies/Utils.py | 17 ++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index b5290c5..1a18924 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -1892,8 +1892,8 @@ class QuickAdapterV3(IStrategy): self, df: DataFrame, pair: str, - side: str, - order: Literal["entry", "exit"], + side: TradeDirection, + order: OrderType, rate: float, lookback_period: int, decay_ratio: float, @@ -1949,8 +1949,9 @@ class QuickAdapterV3(IStrategy): Rejection Conditions -------------------- - Empty dataframe, invalid side/order, negative lookback, decay_ratio outside (0,1], - failure to break current threshold, or failed historical step comparison. + Empty dataframe, invalid side/order, non-finite rate, negative lookback, + decay_ratio outside (0,1], invalid min/max ordering, failure to break current + threshold, or failed historical step comparison. Complexity ---------- @@ -1963,8 +1964,7 @@ class QuickAdapterV3(IStrategy): Limitations ----------- - No validation of min/max ordering beyond usage; no strict mode; partial data may - still confirm. Rate finiteness not explicitly validated. + No strict mode; partial data may still confirm. """ if df.empty: return False @@ -1972,6 +1972,18 @@ class QuickAdapterV3(IStrategy): return False if order not in QuickAdapterV3._order_types_set(): return False + if not isinstance(rate, (int, float)) or not np.isfinite(rate): + return False + if ( + not isinstance(min_natr_ratio_percent, (int, float)) + or not isinstance(max_natr_ratio_percent, (int, float)) + or not np.isfinite(min_natr_ratio_percent) + or not np.isfinite(max_natr_ratio_percent) + or min_natr_ratio_percent < 0 + or max_natr_ratio_percent < 0 + or min_natr_ratio_percent > max_natr_ratio_percent + ): + return False trade_direction = side diff --git a/quickadapter/user_data/strategies/Utils.py b/quickadapter/user_data/strategies/Utils.py index 7827e46..c0efae2 100644 --- a/quickadapter/user_data/strategies/Utils.py +++ b/quickadapter/user_data/strategies/Utils.py @@ -14,7 +14,7 @@ import scipy as sp import talib.abstract as ta from numpy.typing import NDArray from scipy.ndimage import gaussian_filter1d -from scipy.stats import gmean +from scipy.stats import gmean, percentileofscore from technical import qtpylib T = TypeVar("T", pd.Series, float) @@ -1418,18 +1418,15 @@ def find_fractals(df: pd.DataFrame, period: int = 2) -> tuple[list[int], list[in def calculate_quantile(values: NDArray[np.floating], value: float) -> float: + """Return the quantile (0-1) of value within values. + + Uses percentileofscore(kind='mean') for unbiased estimation. + Returns np.nan if values is empty. NaN values are ignored. + """ if values.size == 0: return np.nan - first_value = values[0] - if np.allclose(values, first_value): - return ( - 0.5 - if np.isclose(value, first_value) - else (0.0 if value < first_value else 1.0) - ) - - return np.sum(values <= value) / values.size + return percentileofscore(values, value, kind="mean", nan_policy="omit") / 100.0 class TrendDirection(IntEnum): -- 2.43.0