]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
fix(reforcexy): deduplicate FreqaiDataDrawer date_pred predictions
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sat, 15 Aug 2026 12:25:02 +0000 (14:25 +0200)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Sat, 15 Aug 2026 12:48:48 +0000 (14:48 +0200)
Ports QuickAdapter's _install_date_pred_dedup_patch verbatim to ReforceXY. The duplicate-date_pred / validate="m:1" MergeError originates in the shared FreqaiDataDrawer (freqtrade 2026.7) and affects every FreqAI model; ReforceXY runs in its own process and never imports QuickAdapter, so it needs its own identical copy.

ReforceXY/user_data/freqaimodels/ReforceXY.py

index 4d52aeb36f41a19ffe765ffcf20ab686db52821b..c1414ae05599cb92a353ca40b014604b4b08b0ee 100644 (file)
@@ -36,7 +36,9 @@ import matplotlib.pyplot as plt
 import matplotlib.transforms as mtransforms
 import numpy as np
 import optunahub
+import pandas as pd
 import torch as th
+from freqtrade.freqai.data_drawer import FreqaiDataDrawer
 from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
 from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
 from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment
@@ -81,6 +83,107 @@ from stable_baselines3.common.vec_env import (
     VecMonitor,
 )
 
+_DATE_PRED_DEDUP_SENTINEL = "_quickadapter_date_pred_dedup_patched"
+
+
+def _dedupe_historic_predictions_on_date_pred(frame: pd.DataFrame) -> pd.DataFrame:
+    """Return ``frame`` with a unique, chronologically ordered ``date_pred``,
+    keeping the most informative row per timestamp (a real prediction outranks a
+    zero/NaN placeholder). ``NaT`` ``date_pred`` rows are dropped: they match no
+    candle, and two or more of them break the ``validate="m:1"`` merge in
+    ``attach_return_values_to_return_dataframe`` (data_drawer.py:429-431) since
+    pandas treats repeated null keys as non-unique.
+    """
+    date_pred = pd.to_datetime(frame["date_pred"], utc=True, errors="coerce")
+    valid = date_pred.notna()
+    if valid.all() and not date_pred[valid].duplicated().any():
+        return frame
+    work = frame.reset_index(drop=True)
+    content = [column for column in work.columns if column not in ("date_pred", "date")]
+    block = work[content]
+    numeric = block.apply(pd.to_numeric, errors="coerce")
+    is_numeric = numeric.notna()
+    informative = (block.notna() & is_numeric & numeric.ne(0)) | (
+        block.notna() & ~is_numeric
+    )
+    work = work.assign(
+        _dp=date_pred.to_numpy(),
+        _score=informative.sum(axis=1).to_numpy(),
+        _nonnull=block.notna().sum(axis=1).to_numpy(),
+        _order=work.index.to_numpy(),
+    )
+    contested = work[valid.to_numpy()].sort_values(
+        ["_dp", "_score", "_nonnull", "_order"], kind="stable"
+    )
+    kept = contested.drop_duplicates("_dp", keep="last")
+    return kept.drop(columns=["_dp", "_score", "_nonnull", "_order"]).reset_index(
+        drop=True
+    )
+
+
+def _install_date_pred_dedup_patch() -> None:
+    """Keep ``FreqaiDataDrawer``'s per-pair prediction store free of duplicate
+    ``date_pred`` rows, which freqtrade 2026.7 does not deduplicate and its
+    ``validate="m:1"`` merge (data_drawer.py:429-431) then rejects with a
+    ``MergeError``. Duplicates persist across a crash or are re-created by the
+    positional trim in ``set_initial_return_values`` (data_drawer.py:319-321).
+
+    Re-verify the three wrapped signatures against ``data_drawer.py`` on every
+    freqtrade bump.
+    """
+    if getattr(FreqaiDataDrawer, _DATE_PRED_DEDUP_SENTINEL, False):
+        return
+    original_set_initial = FreqaiDataDrawer.set_initial_return_values
+    original_append = FreqaiDataDrawer.append_model_predictions
+    original_attach = FreqaiDataDrawer.attach_return_values_to_return_dataframe
+
+    def set_initial_return_values(
+        self, pair: str, pred_df: pd.DataFrame, dataframe: pd.DataFrame
+    ) -> None:
+        original_set_initial(self, pair, pred_df, dataframe)
+        frame = _dedupe_historic_predictions_on_date_pred(
+            self.historic_predictions[pair]
+        )
+        self.historic_predictions[pair] = frame
+        self.model_return_values[pair] = frame.tail(len(dataframe.index)).reset_index(
+            drop=True
+        )
+
+    def append_model_predictions(
+        self,
+        pair: str,
+        predictions: pd.DataFrame,
+        do_preds: NDArray[np.int_],
+        dk: FreqaiDataKitchen,
+        strat_df: pd.DataFrame,
+    ) -> None:
+        original_append(self, pair, predictions, do_preds, dk, strat_df)
+        frame = _dedupe_historic_predictions_on_date_pred(
+            self.historic_predictions[pair]
+        )
+        self.historic_predictions[pair] = frame
+        self.model_return_values[pair] = frame.tail(len(strat_df.index)).reset_index(
+            drop=True
+        )
+
+    def attach_return_values_to_return_dataframe(
+        self, pair: str, dataframe: pd.DataFrame
+    ) -> pd.DataFrame:
+        self.model_return_values[pair] = _dedupe_historic_predictions_on_date_pred(
+            self.model_return_values[pair]
+        )
+        return original_attach(self, pair, dataframe)
+
+    FreqaiDataDrawer.set_initial_return_values = set_initial_return_values
+    FreqaiDataDrawer.append_model_predictions = append_model_predictions
+    FreqaiDataDrawer.attach_return_values_to_return_dataframe = (
+        attach_return_values_to_return_dataframe
+    )
+    setattr(FreqaiDataDrawer, _DATE_PRED_DEDUP_SENTINEL, True)
+
+
+_install_date_pred_dedup_patch()
+
 ModelType = Literal["PPO", "RecurrentPPO", "MaskablePPO", "DQN", "QRDQN"]
 ScheduleTypeKnown = Literal["linear", "constant"]
 ScheduleType = Union[ScheduleTypeKnown, Literal["unknown"]]