Explorar o código

Clarify and stabilize grid reconciliation

Lukas Goldschmidt hai 1 mes
pai
achega
4ea20c29d6
Modificáronse 1 ficheiros con 60 adicións e 29 borrados
  1. 60 29
      strategies/grid_trader.py

+ 60 - 29
strategies/grid_trader.py

@@ -239,6 +239,13 @@ class Strategy(Strategy):
         return {"buy", "sell"}
 
     def _suggest_amount(self, side: str, price: float, levels: int, min_notional: float) -> float:
+        """Derive a per-order amount from the currently available balance.
+
+        This helper is used when the grid seeds, tops up, or replaces an order.
+        It folds in the live available balance, fee cushion, per-order caps, and
+        the exchange minimum notional. If the wallet cannot support a valid order,
+        it returns 0.0 instead of forcing an impossible minimum size.
+        """
         if levels <= 0 or price <= 0:
             return 0.0
         safety = 0.995
@@ -251,13 +258,19 @@ class Strategy(Strategy):
             quote_available = self._available_balance(quote)
             self.state["counter_available"] = quote_available
             spendable_quote = quote_available * safety
-            amount = spendable_quote / (max(levels, 1) * price * (1 + fee_rate))
+            max_affordable = spendable_quote / (price * (1 + fee_rate))
+            if max_affordable < min_amount:
+                return 0.0
+            amount = min(spendable_quote / (max(levels, 1) * price * (1 + fee_rate)), max_affordable)
         else:
             base = self._base_symbol()
             base_available = self._available_balance(base)
             self.state["base_available"] = base_available
             spendable_base = (base_available * safety) / (1 + fee_rate)
-            amount = spendable_base / max(levels, 1)
+            max_affordable = spendable_base
+            if max_affordable < min_amount:
+                return 0.0
+            amount = min(spendable_base / max(levels, 1), max_affordable)
 
         amount = max(amount, min_amount * 1.05)
         if max_notional > 0 and price > 0:
@@ -543,6 +556,50 @@ class Strategy(Strategy):
         for order_id in order_ids or []:
             self._log(f"dropping stale order {order_id} from state")
 
+    def _reconcile_after_sync(self, previous_orders: list[dict], live_orders: list[dict], desired_sides: set[str], price: float) -> tuple[list[dict], list[str], int]:
+        live_ids = list(self.state.get("order_ids") or [])
+        open_order_count = len(live_ids)
+
+        if self._mode() != "active":
+            return live_orders, live_ids, open_order_count
+
+        previous_ids = {
+            str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "")
+            for order in previous_orders
+            if isinstance(order, dict)
+        }
+        current_ids = {
+            str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "")
+            for order in live_orders
+            if isinstance(order, dict)
+        }
+        vanished_orders = [
+            order
+            for order in previous_orders
+            if isinstance(order, dict)
+            and str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "") in (previous_ids - current_ids)
+        ]
+        if vanished_orders and not self._grid_refresh_paused():
+            replaced_ids = self._place_replacement_orders(vanished_orders, price)
+            if replaced_ids:
+                live_orders = self._sync_open_orders_state()
+                live_ids = list(self.state.get("order_ids") or [])
+                open_order_count = len(live_ids)
+
+        surplus_cancelled = self._cancel_surplus_side_orders(live_orders, int(self.config.get("grid_levels", 6) or 6))
+        duplicate_cancelled = self._cancel_duplicate_level_orders(live_orders)
+        if surplus_cancelled or duplicate_cancelled:
+            live_orders = self._sync_open_orders_state()
+            live_ids = list(self.state.get("order_ids") or [])
+            open_order_count = len(live_ids)
+
+        if desired_sides != {"buy", "sell"}:
+            live_orders = self._sync_open_orders_state()
+            live_ids = list(self.state.get("order_ids") or [])
+            open_order_count = len(live_ids)
+
+        return live_orders, live_ids, open_order_count
+
     def on_tick(self, tick):
         previous_orders = list(self.state.get("orders") or [])
         self._refresh_balance_snapshot()
@@ -615,33 +672,7 @@ class Strategy(Strategy):
             self._log(f"missing tracked orders: {missing_ids}")
             self.state["order_ids"] = live_ids
 
-        cancelled_obsolete = self._cancel_obsolete_side_orders(live_orders, desired_sides)
-        if cancelled_obsolete:
-            live_orders = self._sync_open_orders_state()
-            live_ids = list(self.state.get("order_ids") or [])
-            open_order_count = len(live_ids)
-
-        previous_ids = {str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "") for order in previous_orders if isinstance(order, dict)}
-        current_ids = {str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "") for order in live_orders if isinstance(order, dict)}
-        vanished_orders = [order for order in previous_orders if isinstance(order, dict) and str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "") in (previous_ids - current_ids)]
-        if vanished_orders and self._mode() == "active" and not self._grid_refresh_paused():
-            replaced_ids = self._place_replacement_orders(vanished_orders, price)
-            if replaced_ids:
-                live_orders = self._sync_open_orders_state()
-                live_ids = list(self.state.get("order_ids") or [])
-                open_order_count = len(live_ids)
-
-        surplus_cancelled = self._cancel_surplus_side_orders(live_orders, int(self.config.get("grid_levels", 6) or 6))
-        if surplus_cancelled:
-            live_orders = self._sync_open_orders_state()
-            live_ids = list(self.state.get("order_ids") or [])
-            open_order_count = len(live_ids)
-
-        duplicate_cancelled = self._cancel_duplicate_level_orders(live_orders)
-        if duplicate_cancelled:
-            live_orders = self._sync_open_orders_state()
-            live_ids = list(self.state.get("order_ids") or [])
-            open_order_count = len(live_ids)
+        live_orders, live_ids, open_order_count = self._reconcile_after_sync(previous_orders, live_orders, desired_sides, price)
 
         if desired_sides != {"buy", "sell"}:
             current_sides = {str(order.get("side") or "").lower() for order in live_orders if isinstance(order, dict)}