]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
fix(quickadapter): bound take-profit stage to filled take-profit exits
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Mon, 20 Jul 2026 19:44:08 +0000 (21:44 +0200)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Mon, 20 Jul 2026 20:33:37 +0000 (22:33 +0200)
Derive the take-profit stage from filled take-profit-tagged exit orders
clamped to the final full-exit stage, instead of nr_of_successful_exits
plus in-flight exit orders. This stops the stage index from overshooting
its maximum (e.g. take_profit_*_4 while the final stage is 3) when a
final exit order is in flight or a non-take-profit exit fills.

Guard custom_exit against re-issuing the final take-profit exit while an
order is open by returning None after the model-expiry and reversal
safety exits and before the take-profit stage computation, so safety
exits still fire while in-flight orders no longer trigger a duplicate
take-profit exit.

quickadapter/user_data/strategies/QuickAdapterV3.py

index 33fd950d3eeedd8adc8294a99fc3f8a2870bde7a..1eb077195ad4a4275b7f10ba336bccd26827f7dc 100644 (file)
@@ -161,6 +161,8 @@ class QuickAdapterV3(IStrategy):
     # (natr_multiplier_fraction, stake_percent, color)
     _FINAL_EXIT_STAGE: Final[tuple[float, float, str]] = (1.0, 1.0, "deepskyblue")
 
+    _TAKE_PROFIT_ORDER_TAG_PREFIX: Final[str] = "take_profit_"
+
     minimal_roi = {str(timeframe_minutes * 864): -1}
 
     # FreqAI is crashing if minimal_roi is a property
@@ -1261,14 +1263,15 @@ class QuickAdapterV3(IStrategy):
 
     @staticmethod
     def get_trade_exit_stage(trade: Trade) -> int:
-        n_open_orders = 0
-        if trade.has_open_orders:
-            n_open_orders = sum(
-                1
-                for open_order in trade.open_orders
-                if open_order.side == ("buy" if trade.is_short else "sell")
+        n_filled_take_profit_exits = sum(
+            1
+            for order in trade.select_filled_orders(trade.exit_side)
+            if (order.ft_order_tag or "").startswith(
+                QuickAdapterV3._TAKE_PROFIT_ORDER_TAG_PREFIX
             )
-        return trade.nr_of_successful_exits + n_open_orders
+        )
+        final_stage = max(QuickAdapterV3.partial_exit_stages.keys(), default=-1) + 1
+        return min(n_filled_take_profit_exits, final_stage)
 
     @staticmethod
     @lru_cache(maxsize=128)
@@ -1576,7 +1579,7 @@ class QuickAdapterV3(IStrategy):
                 )
             return (
                 -trade_partial_stake_amount,
-                f"take_profit_{trade.trade_direction}_{trade_exit_stage}",
+                f"{QuickAdapterV3._TAKE_PROFIT_ORDER_TAG_PREFIX}{trade.trade_direction}_{trade_exit_stage}",
             )
 
         return None
@@ -2082,6 +2085,9 @@ class QuickAdapterV3(IStrategy):
         ):
             return "maxima_detected_long"
 
+        if trade.has_open_orders:
+            return None
+
         trade_exit_stage = QuickAdapterV3.get_trade_exit_stage(trade)
         if trade_exit_stage in QuickAdapterV3.partial_exit_stages:
             return None
@@ -2187,7 +2193,7 @@ class QuickAdapterV3(IStrategy):
             )
 
         if trade_exit:
-            return f"take_profit_{trade.trade_direction}_{trade_exit_stage}"
+            return f"{QuickAdapterV3._TAKE_PROFIT_ORDER_TAG_PREFIX}{trade.trade_direction}_{trade_exit_stage}"
 
         return None