Przeglądaj źródła

Improve grid top-up sizing

Lukas Goldschmidt 1 miesiąc temu
rodzic
commit
1f49ad69f8
2 zmienionych plików z 87 dodań i 3 usunięć
  1. 5 3
      strategies/grid_trader.py
  2. 82 0
      tests/test_strategies.py

+ 5 - 3
strategies/grid_trader.py

@@ -441,8 +441,9 @@ class Strategy(Strategy):
         market = self._market_symbol()
         orders = list(self.state.get("orders") or [])
         order_ids = list(self.state.get("order_ids") or [])
+        placement_levels = max(levels - max(start_level, 1) + 1, 0)
 
-        side_levels = min(levels, self._supported_levels(side, center, min_notional))
+        side_levels = min(placement_levels, self._supported_levels(side, center, min_notional))
         amount = self._suggest_amount(side, center, max(side_levels, 1), min_notional)
 
         if side == "buy":
@@ -468,13 +469,14 @@ class Strategy(Strategy):
                 side_levels = 1
                 self._log(f"side {side} restored to 1 level because amount clears minimum: amount={amount:.6g} min_amount={min_amount:.6g}")
         self._log(
-            f"prepare side {side}, market={market}, center={center}, levels={side_levels}, amount={amount:.6g}, min_notional={min_notional}, existing_ids={order_ids}"
+            f"prepare side {side}, market={market}, center={center}, levels={side_levels}, start_level={start_level}, amount={amount:.6g}, min_notional={min_notional}, existing_ids={order_ids}"
         )
 
         for i in range(start_level, levels + 1):
             price = round(center * (1 - (step * i)) if side == "buy" else center * (1 + (step * i)), 8)
             min_size = (min_notional / price) if price > 0 else 0.0
-            if i > side_levels or amount < min_size:
+            relative_level = i - start_level + 1
+            if relative_level > side_levels or amount < min_size:
                 self._log_decision(
                     f"skip side {side} level {i}",
                     reason="below_min_size",

+ 82 - 0
tests/test_strategies.py

@@ -8,6 +8,7 @@ from fastapi.testclient import TestClient
 from src.trader_mcp import strategy_registry, strategy_store
 from src.trader_mcp.server import app
 from src.trader_mcp.strategy_context import StrategyContext
+from strategies.grid_trader import Strategy as GridStrategy
 
 
 STRATEGY_CODE = '''
@@ -109,3 +110,84 @@ def test_stop_loss_strategy_loads_with_aligned_regime_config(tmp_path):
     finally:
         strategy_store.DB_PATH = original_db
         strategy_registry.STRATEGIES_DIR = original_dir
+
+
+def test_grid_top_up_uses_missing_levels_budget():
+    class FakeContext:
+        base_currency = "XRP"
+        counter_currency = "USD"
+        market_symbol = "xrpusd"
+        minimum_order_value = 10.0
+        mode = "active"
+
+        def __init__(self):
+            self.placed_orders = []
+
+        def get_fee_rates(self, market):
+            return {"maker": 0.0, "taker": 0.004}
+
+        def get_account_info(self):
+            return {
+                "balances": [
+                    {"asset_code": "USD", "available": 13.55},
+                    {"asset_code": "XRP", "available": 22.0103},
+                ]
+            }
+
+        def suggest_order_amount(
+            self,
+            *,
+            side,
+            price,
+            levels,
+            min_notional,
+            fee_rate,
+            max_notional_per_order=0.0,
+            dust_collect=False,
+            inventory_cap_pct=0.0,
+            order_size=0.0,
+            safety=0.995,
+        ):
+            if side == "buy":
+                quote_available = 13.55
+                spendable_quote = quote_available * safety
+                quote_cap = min(spendable_quote, max_notional_per_order) if max_notional_per_order > 0 else spendable_quote
+                if quote_cap < min_notional * (1 + fee_rate):
+                    return 0.0
+                return quote_cap / (price * (1 + fee_rate))
+            return 0.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,
+            "grid_step_pct": 0.0062,
+            "grid_step_min_pct": 0.0033,
+            "grid_step_max_pct": 0.012,
+            "max_notional_per_order": 12,
+            "order_call_delay_ms": 0,
+            "trade_sides": "both",
+            "debug_orders": True,
+            "dust_collect": True,
+            "enable_trend_guard": False,
+            "fee_rate": 0.004,
+        },
+    )
+    strategy.state["center_price"] = 1.3285
+    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"]
+
+    strategy._top_up_missing_levels(strategy.state["center_price"], strategy.state["orders"])
+
+    assert len(ctx.placed_orders) == 1
+    assert ctx.placed_orders[0]["side"] == "buy"
+    assert float(ctx.placed_orders[0]["amount"]) > 7.57