Explorar o código

gid trader fix

Lukas Goldschmidt hai 1 semana
pai
achega
538563012c
Modificáronse 2 ficheiros con 150 adicións e 19 borrados
  1. 20 19
      .env
  2. 130 0
      strategies/hello_world.md

+ 20 - 19
.env

@@ -725,31 +725,29 @@ class Strategy(Strategy):
             base_symbol: max(float(base_total or 0.0), 0.0),
             quote_symbol: max(float(quote_total or 0.0), 0.0),
         }
-        # Ask the shared sizing layer for a venue-valid amount at the first
-        # intended level, then walk the ladder outward. That keeps the inner
-        # levels honest and lets truncation happen on the outside.
-        reference_price = round(center * (1 - step) if side == "buy" else center * (1 + step), 8)
-        amount = self._suggest_amount(
-            side,
-            reference_price,
-            max(expected_levels, 1),
-            min_notional,
-            available_balances=balances,
-        )
-        if amount <= 0:
-            return empty
 
         spendable_total = self._resource_total_for_side(side, base_total, quote_total) * 0.995
         total_cost = 0.0
         planned_orders = []
         skipped = []
-        max_index = max(expected_levels * 4, expected_levels + 8, 12)
-
-        for level_index in range(1, max_index + 1):
+        # Walk the ladder from the inside out and size each level at its own
+        # price. That lets the planner use the maximum feasible number of
+        # levels under the current funds and per-order notional constraints.
+        for level_index in range(1, expected_levels + 1):
             price = round(center * (1 - (step * level_index)) if side == "buy" else center * (1 + (step * level_index)), 8)
             if price <= 0:
                 break
 
+            amount = self._suggest_amount(
+                side,
+                price,
+                max(expected_levels, 1),
+                min_notional,
+                available_balances=balances,
+            )
+            if amount <= 0:
+                break
+
             min_size = (min_notional / price) if min_notional > 0 else 0.0
             if amount < min_size:
                 skipped.append({"level": level_index, "reason": "below minimum size", "price": price})
@@ -761,10 +759,13 @@ class Strategy(Strategy):
 
             total_cost += cost
             planned_orders.append({"side": side, "price": price, "amount": amount, "level": level_index})
-            if len(planned_orders) >= expected_levels:
-                break
 
-        return {"amount": amount, "orders": planned_orders, "skipped": skipped, "valid": True}
+        return {
+            "amount": planned_orders[0]["amount"] if planned_orders else 0.0,
+            "orders": planned_orders,
+            "skipped": skipped,
+            "valid": True,
+        }
 
     def _plan_grid(self, center: float, *, base_total: float | None = None, quote_total: float | None = None) -> dict:
         center = float(center or 0.0)

+ 130 - 0
strategies/hello_world.md

@@ -547,6 +547,49 @@ def test_grid_plan_truncates_outer_levels_without_skipping_inner_levels(monkeypa
     assert plan["sell_skipped"] == []
 
 
+def test_grid_plan_sizes_each_level_against_its_own_price(monkeypatch):
+    class FakeContext:
+        base_currency = "XRP"
+        counter_currency = "USD"
+        market_symbol = "xrpusd"
+        minimum_order_value = 10.0
+        mode = "active"
+
+        def get_fee_rates(self, market):
+            return {"maker": 0.0, "taker": 0.004}
+
+        def suggest_order_amount(self, **kwargs):
+            quote_notional = float(kwargs.get("quote_notional") or 0.0)
+            max_notional_per_order = float(kwargs.get("max_notional_per_order") or 0.0)
+            price = float(kwargs.get("price") or 0.0)
+            if price <= 0:
+                return 0.0
+            target_quote = quote_notional if quote_notional > 0 else max_notional_per_order
+            if max_notional_per_order > 0:
+                target_quote = min(target_quote, max_notional_per_order)
+            return target_quote / price
+
+    strategy = GridStrategy(
+        FakeContext(),
+        {
+            "grid_levels": 5,
+            "grid_step_pct": 0.0062,
+            "grid_step_min_pct": 0.006125,
+            "order_notional_quote": 10.25,
+            "max_order_notional_quote": 12.0,
+        },
+    )
+
+    plan = strategy._plan_grid(1.3642, base_total=100.0, quote_total=100.0)
+
+    assert plan["counts"]["buy"] == 5
+    assert plan["counts"]["sell"] == 5
+    assert [order["level"] for order in plan["buy_orders"]] == [1, 2, 3, 4, 5]
+    assert [order["level"] for order in plan["sell_orders"]] == [1, 2, 3, 4, 5]
+    assert plan["buy_orders"][0]["amount"] != plan["buy_orders"][-1]["amount"]
+    assert plan["sell_orders"][0]["amount"] != plan["sell_orders"][-1]["amount"]
+
+
 def test_grid_shape_check_reuses_canonical_plan_without_rebuild(monkeypatch):
     class FakeContext:
         base_currency = "XRP"
@@ -617,6 +660,93 @@ def test_grid_shape_check_reuses_canonical_plan_without_rebuild(monkeypatch):
     assert ctx.cancelled_all == 0
 
 
+def test_grid_shape_check_rebuilds_when_live_counts_are_below_funded_max(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
+
+        def get_fee_rates(self, market):
+            return {"maker": 0.0, "taker": 0.004}
+
+        def get_account_info(self):
+            return {
+                "balances": [
+                    {"asset_code": "USD", "available": 100.0},
+                    {"asset_code": "XRP", "available": 100.0},
+                ]
+            }
+
+        def get_price(self, symbol):
+            return {"price": 1.3642}
+
+        def get_regime(self, symbol, timeframe="1h"):
+            return {"volatility": {"atr_percent": 0.0}, "trend": {"state": "flat"}}
+
+        def suggest_order_amount(self, **kwargs):
+            quote_notional = float(kwargs.get("quote_notional") or 0.0)
+            max_notional_per_order = float(kwargs.get("max_notional_per_order") or 0.0)
+            price = float(kwargs.get("price") or 0.0)
+            if price <= 0:
+                return 0.0
+            target_quote = quote_notional if quote_notional > 0 else max_notional_per_order
+            if max_notional_per_order > 0:
+                target_quote = min(target_quote, max_notional_per_order)
+            return target_quote / price
+
+        def get_open_orders(self):
+            return [
+                {"side": "buy", "price": 1.3557, "amount": 7.6, "id": "b1"},
+                {"side": "buy", "price": 1.3469, "amount": 7.6, "id": "b2"},
+                {"side": "buy", "price": 1.3381, "amount": 7.6, "id": "b3"},
+                {"side": "sell", "price": 1.3727, "amount": 7.5, "id": "s1"},
+                {"side": "sell", "price": 1.3816, "amount": 7.5, "id": "s2"},
+                {"side": "sell", "price": 1.3904, "amount": 7.5, "id": "s3"},
+            ]
+
+    ctx = FakeContext()
+    strategy = GridStrategy(
+        ctx,
+        {
+            "grid_levels": 5,
+            "grid_step_pct": 0.0062,
+            "grid_step_min_pct": 0.006125,
+            "order_notional_quote": 10.25,
+            "max_order_notional_quote": 12.0,
+            "order_call_delay_ms": 0,
+            "fee_rate": 0.004,
+        },
+    )
+    strategy.state["center_price"] = 1.3642
+    strategy.state["seeded"] = True
+    strategy.state["orders"] = ctx.get_open_orders()
+    strategy.state["order_ids"] = ["b1", "b2", "b3", "s1", "s2", "s3"]
+
+    monkeypatch.setattr(strategy, "_grid_refresh_paused", lambda: False)
+    monkeypatch.setattr(strategy, "_recenter_threshold_pct", lambda: 0.5)
+    monkeypatch.setattr(strategy, "_refresh_balance_snapshot", lambda: True)
+    monkeypatch.setattr(strategy, "_price", lambda: 1.3642)
+    monkeypatch.setattr(strategy, "_refresh_regimes", lambda: None)
+
+    called = {"rebuild": 0}
+
+    def fake_rebuild(*_args, **_kwargs):
+        called["rebuild"] += 1
+
+    monkeypatch.setattr(strategy, "_recenter_and_rebuild_from_price", fake_rebuild)
+    monkeypatch.setattr(strategy, "_sync_open_orders_state", lambda: ctx.get_open_orders())
+
+    result = strategy.on_tick({})
+
+    assert result["action"] == "reseed"
+    assert called["rebuild"] == 1
+
+
 def test_grid_skips_rebuild_when_balance_refresh_fails(monkeypatch):
     class FakeContext:
         base_currency = "XRP"