]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
fix(quickadapter): unbiased quantile calculation with percentileofscore
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Fri, 26 Dec 2025 13:03:41 +0000 (14:03 +0100)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Fri, 26 Dec 2025 13:03:41 +0000 (14:03 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
quickadapter/user_data/strategies/QuickAdapterV3.py
quickadapter/user_data/strategies/Utils.py

index b5290c5595edbc69052956ce00d8f7dfee4e3bf2..1a18924bba4ff7577a63a284a6fa7c816e9a1aba 100644 (file)
@@ -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
 
index 7827e46f21191e435a8b577e1f0bf1b0b603a177..c0efae2834ae624badcfba464a137e9b12f47ef7 100644 (file)
@@ -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):