Sfoglia il codice sorgente

Skip grid rebuilds on inconclusive balance reads

Lukas Goldschmidt 3 settimane fa
parent
commit
988e14c3a9
2 ha cambiato i file con 85 aggiunte e 4 eliminazioni
  1. 16 4
      strategies/grid_trader.py
  2. 69 0
      tests/test_strategies.py

+ 16 - 4
strategies/grid_trader.py

@@ -336,10 +336,13 @@ class Strategy(Strategy):
             info = self.context.get_account_info()
         except Exception as exc:
             self._log(f"account info failed: {exc}")
+            # A failed balance read makes this tick unsuitable for shape decisions.
+            self.state["balance_shape_inconclusive"] = True
             return 0.0
 
         balances = info.get("balances") if isinstance(info, dict) else []
         if not isinstance(balances, list):
+            self.state["balance_shape_inconclusive"] = True
             return 0.0
         wanted = str(asset_code or "").upper()
         for balance in balances:
@@ -350,7 +353,9 @@ class Strategy(Strategy):
             try:
                 return float(balance.get("available") if balance.get("available") is not None else balance.get("total") or 0.0)
             except Exception:
+                self.state["balance_shape_inconclusive"] = True
                 return 0.0
+        self.state["balance_shape_inconclusive"] = True
         return 0.0
 
     def _refresh_balance_snapshot(self) -> bool:
@@ -358,10 +363,12 @@ class Strategy(Strategy):
             info = self.context.get_account_info()
         except Exception as exc:
             self._log(f"balance refresh failed: {exc}")
+            self.state["balance_shape_inconclusive"] = True
             return False
 
         balances = info.get("balances") if isinstance(info, dict) else []
         if not isinstance(balances, list):
+            self.state["balance_shape_inconclusive"] = True
             return False
 
         base = self._base_symbol()
@@ -373,6 +380,7 @@ class Strategy(Strategy):
             try:
                 available = float(balance.get("available") if balance.get("available") is not None else balance.get("total") or 0.0)
             except Exception:
+                self.state["balance_shape_inconclusive"] = True
                 continue
             if asset == base:
                 self.state["base_available"] = available
@@ -773,6 +781,7 @@ class Strategy(Strategy):
         previous_orders = list(self.state.get("orders") or [])
         tracked_ids_before_sync = list(self.state.get("order_ids") or [])
         rebuild_done = False
+        self.state["balance_shape_inconclusive"] = False
         balance_refresh_ok = self._refresh_balance_snapshot()
         price = self._price()
         self.state["last_price"] = price
@@ -892,6 +901,7 @@ class Strategy(Strategy):
         target_buy = self._target_levels_for_side("buy", price, live_orders, total_quote, expected_levels, float(self.context.minimum_order_value or 0.0))
         target_sell = self._target_levels_for_side("sell", price, live_orders, total_base, expected_levels, float(self.context.minimum_order_value or 0.0))
         target_total = target_buy + target_sell
+        balance_shape_inconclusive = bool(self.state.get("balance_shape_inconclusive"))
         grid_not_as_expected = (
             bool(live_orders)
             and (
@@ -902,7 +912,9 @@ class Strategy(Strategy):
 
         can_make_better = target_total > 0 and (current_buy != target_buy or current_sell != target_sell)
 
-        if grid_not_as_expected and can_make_better and not self._grid_refresh_paused():
+        if balance_shape_inconclusive:
+            self._log("balance info not conclusive, skipping grid shape rebuild checks this tick")
+        elif grid_not_as_expected and can_make_better and not self._grid_refresh_paused():
             if rebuild_done:
                 return {"action": "hold", "price": price}
             self._log(
@@ -916,12 +928,12 @@ class Strategy(Strategy):
             self.state["last_action"] = "reseeded" if mode == "active" else f"{mode} monitor"
             return {"action": "reseed" if mode == "active" else "plan", "price": price}
 
-        if grid_not_as_expected and not can_make_better:
+        if not balance_shape_inconclusive and grid_not_as_expected and not can_make_better:
             self._log(
                 f"grid shape left unchanged, balance cannot improve it: live_buy={current_buy} live_sell={current_sell} target_buy={target_buy} target_sell={target_sell}"
             )
 
-        if self._order_count_mismatch(tracked_ids_before_sync, live_orders):
+        if not balance_shape_inconclusive and self._order_count_mismatch(tracked_ids_before_sync, live_orders):
             if rebuild_done:
                 return {"action": "hold", "price": price}
             self._log(f"grid mismatch detected, rebuilding full grid: tracked={len(tracked_ids_before_sync)} live={len(live_orders)}")
@@ -942,7 +954,7 @@ class Strategy(Strategy):
             self._log(f"{'seeded' if mode == 'active' else 'planned'} grid at {price}")
             return {"action": "seed" if mode == "active" else "plan", "price": price}
 
-        if ((open_order_count == 0) or missing_tracked) and not self._grid_refresh_paused():
+        if not balance_shape_inconclusive and ((open_order_count == 0) or missing_tracked) and not self._grid_refresh_paused():
             if rebuild_done:
                 return {"action": "hold", "price": price}
             self._log("missing tracked order(s), rebuilding full grid")

+ 69 - 0
tests/test_strategies.py

@@ -269,6 +269,75 @@ def test_grid_skips_rebuild_when_balance_refresh_fails(monkeypatch):
     assert ctx.placed_orders == []
 
 
+def test_grid_skips_shape_rebuild_when_balance_reads_turn_inconclusive(monkeypatch):
+    class FakeContext:
+        base_currency = "XRP"
+        counter_currency = "USD"
+        market_symbol = "xrpusd"
+        minimum_order_value = 10.0
+        mode = "active"
+
+        def __init__(self):
+            self.cancelled_all = 0
+            self.placed_orders = []
+            self.calls = 0
+
+        def get_fee_rates(self, market):
+            return {"maker": 0.0, "taker": 0.004}
+
+        def get_account_info(self):
+            self.calls += 1
+            if self.calls == 1:
+                return {
+                    "balances": [
+                        {"asset_code": "USD", "available": 41.29},
+                        {"asset_code": "XRP", "available": 9.98954},
+                    ]
+                }
+            raise RuntimeError("Bitstamp auth breaker active, retry later")
+
+        def cancel_all_orders(self):
+            self.cancelled_all += 1
+            return {"ok": True}
+
+        def suggest_order_amount(self, **kwargs):
+            return 10.0
+
+        def place_order(self, **kwargs):
+            self.placed_orders.append(kwargs)
+            return {"status": "ok", "id": f"oid-{len(self.placed_orders)}"}
+
+    ctx = FakeContext()
+    strategy = GridStrategy(ctx, {"grid_levels": 2, "order_call_delay_ms": 0, "enable_trend_guard": False, "fee_rate": 0.004})
+    strategy.state["center_price"] = 1.3285
+    strategy.state["seeded"] = True
+    strategy.state["orders"] = [
+        {"side": "buy", "price": 1.3243993, "amount": 7.63, "id": "existing-buy"},
+        {"side": "sell", "price": 1.3326007, "amount": 9.0, "id": "sell-1"},
+        {"side": "sell", "price": 1.3367011, "amount": 9.0, "id": "sell-2"},
+    ]
+    strategy.state["order_ids"] = ["existing-buy", "sell-1", "sell-2"]
+
+    def fake_sync_open_orders_state():
+        live = [{"side": "sell", "price": 1.3326007, "amount": 9.0, "id": "sell-1"}]
+        strategy.state["orders"] = live
+        strategy.state["order_ids"] = ["sell-1"]
+        strategy.state["open_order_count"] = 1
+        return live
+
+    monkeypatch.setattr(strategy, "_sync_open_orders_state", fake_sync_open_orders_state)
+    monkeypatch.setattr(strategy, "_price", lambda: 1.3285)
+    monkeypatch.setattr(strategy, "_refresh_regimes", lambda: None)
+    monkeypatch.setattr(strategy, "_grid_refresh_paused", lambda: False)
+    monkeypatch.setattr(strategy, "_recenter_threshold_pct", lambda: 0.5)
+
+    result = strategy.on_tick({})
+
+    assert result["action"] == "hold"
+    assert ctx.cancelled_all == 0
+    assert ctx.placed_orders == []
+
+
 def test_grid_missing_order_triggers_full_rebuild(monkeypatch):
     class FakeContext:
         base_currency = "XRP"