Pārlūkot izejas kodu

Add grid center shift config and docs

Lukas Goldschmidt 1 mēnesi atpakaļ
vecāks
revīzija
32b0fcc351
3 mainītis faili ar 283 papildinājumiem un 11 dzēšanām
  1. 220 0
      strategies/grid_trader.md
  2. 41 11
      strategies/grid_trader.py
  3. 22 0
      tests/test_strategies.py

+ 220 - 0
strategies/grid_trader.md

@@ -0,0 +1,220 @@
+# Grid Trader Configuration
+
+This file explains every config parameter used by `strategies/grid_trader.py`.
+
+## Core grid behavior
+
+### `grid_levels`
+- **Type:** int
+- **Default:** `6`
+- **Range:** `1..20`
+- **What it does:** Number of grid orders kept per side.
+- **When to change it:**
+  - Lower it for a smaller, simpler grid.
+  - Raise it if you want a wider ladder and more incremental entries/exits.
+- **Tradeoff:** More levels means more orders, more clutter, and usually slower capital usage.
+
+### `grid_step_pct`
+- **Type:** float
+- **Default:** `0.012`
+- **Range:** `0.001..0.1`
+- **What it does:** Base spacing between grid levels as a percentage of price.
+- **When to change it:**
+  - Increase it in volatile markets.
+  - Decrease it in quiet, tight ranges.
+- **Tradeoff:** Too small can make the grid noisy, too large can make it miss moves.
+
+### `volatility_timeframe`
+- **Type:** string
+- **Default:** `"1h"`
+- **What it does:** Timeframe used to measure volatility for adaptive grid sizing.
+- **When to change it:**
+  - Use a shorter timeframe for faster adaptation.
+  - Use a longer one for smoother, slower reactions.
+- **Tradeoff:** Shorter = more reactive, longer = more stable.
+
+### `volatility_multiplier`
+- **Type:** float
+- **Default:** `0.5`
+- **Range:** `0.0..10.0`
+- **What it does:** Scales ATR-based volatility into the live grid step.
+- **When to change it:**
+  - Increase it if you want the grid to widen more aggressively in volatility.
+  - Decrease it if you want tighter spacing.
+- **Tradeoff:** Higher values make the grid less sensitive to the base step.
+
+### `grid_step_min_pct`
+- **Type:** float
+- **Default:** `0.005`
+- **Range:** `0.0001..0.5`
+- **What it does:** Lower bound for the live grid step.
+- **When to change it:**
+  - Raise it if the grid gets too tight.
+  - Lower it if you want very fine spacing.
+
+### `grid_step_max_pct`
+- **Type:** float
+- **Default:** `0.03`
+- **Range:** `0.0001..1.0`
+- **What it does:** Upper bound for the live grid step.
+- **When to change it:**
+  - Lower it to prevent the grid from becoming too wide in volatile markets.
+  - Raise it if you want the strategy to tolerate bigger swings.
+
+## Position sizing and inventory
+
+### `order_size`
+- **Type:** float
+- **Default:** `0.0`
+- **What it does:** Fixed per-order size when you want manual sizing.
+- **When to change it:**
+  - Set it when you want a static size instead of wallet-derived sizing.
+  - Leave it at `0.0` to let the strategy derive size from balance logic.
+
+### `inventory_cap_pct`
+- **Type:** float
+- **Default:** `0.7`
+- **Range:** `0.0..1.0`
+- **What it does:** Caps how much inventory the strategy should commit.
+- **When to change it:**
+  - Lower it for more conservative inventory usage.
+  - Raise it if you want the grid to keep more capital active.
+
+### `max_notional_per_order`
+- **Type:** float
+- **Default:** `0.0`
+- **What it does:** Upper limit for the notional value of a single order.
+- **When to change it:**
+  - Set it to cap order size risk.
+  - Leave it at `0.0` for no explicit per-order cap.
+- **Note:** With `dust_collect=true`, the strategy may exceed this cap to clear small leftover balances, but never more than available funds.
+
+### `dust_collect`
+- **Type:** bool
+- **Default:** `False`
+- **What it does:** Allows the sizing logic to exceed `max_notional_per_order` if needed to consume dust.
+- **When to change it:**
+  - Enable it when you want the strategy to clean up small leftover balances.
+  - Leave it off if you want strict order caps.
+
+### `use_all_available`
+- **Type:** bool
+- **Default:** `True`
+- **What it does:** Lets the strategy use the available balance more fully when sizing orders.
+- **When to change it:**
+  - Disable it for stricter, more conservative capital usage.
+  - Keep it enabled if you want the grid to self-fill as much as possible.
+
+## Re-centering
+
+### `recenter_pct`
+- **Type:** float
+- **Default:** `0.05`
+- **Range:** `0.0..0.5`
+- **What it does:** Baseline deviation threshold that can trigger a recenter.
+- **When to change it:**
+  - Lower it for faster recentering.
+  - Raise it if you want the grid to stay anchored longer.
+- **Tradeoff:** Too low can make the grid chase price.
+
+### `recenter_atr_multiplier`
+- **Type:** float
+- **Default:** `0.35`
+- **Range:** `0.0..10.0`
+- **What it does:** Adjusts the recenter threshold using volatility.
+- **When to change it:**
+  - Increase it if you want recentering to be less eager in volatile markets.
+  - Decrease it if you want it to react faster.
+
+### `recenter_min_pct`
+- **Type:** float
+- **Default:** `0.0025`
+- **Range:** `0.0..0.5`
+- **What it does:** Minimum allowed recenter threshold.
+- **When to change it:**
+  - Raise it to prevent over-reactive recentering.
+  - Lower it if you want very tight anchor control.
+
+### `recenter_max_pct`
+- **Type:** float
+- **Default:** `0.03`
+- **Range:** `0.0..0.5`
+- **What it does:** Maximum allowed recenter threshold.
+- **When to change it:**
+  - Raise it if you want the strategy to tolerate larger drifts before recentring.
+  - Lower it if you want a tighter leash.
+
+### `center_shift_factor`
+- **Type:** float
+- **Default:** `0.3333333333`
+- **Range:** `0.0..1.0`
+- **What it does:** Controls where the new center lands between the old center and the new price.
+- **Formula:** `new_center = price + (old_center - price) * center_shift_factor`
+- **Meaning:**
+  - `0.0` = snap center to current price
+  - `1.0` = keep old center
+  - `0.333...` = move the center one third of the way back from price toward the old center
+- **When to change it:**
+  - Lower it if you want the grid to follow price more aggressively.
+  - Raise it if you want the grid to retain more of its old anchor.
+
+## Fees and execution
+
+### `fee_rate`
+- **Type:** float
+- **Default:** `0.0025`
+- **Range:** `0.0..0.05`
+- **What it does:** Fallback fee assumption used when live fee lookup is unavailable.
+- **When to change it:**
+  - Set it to match your venue if fee lookup is unreliable.
+  - Leave it as a fallback safety value if live fees are available.
+
+### `order_call_delay_ms`
+- **Type:** int
+- **Default:** `250`
+- **Range:** `0..10000`
+- **What it does:** Delay between order calls.
+- **When to change it:**
+  - Increase it if the exchange is sensitive to bursts.
+  - Set it to `0` for test or fast local runs.
+
+## Trend protection
+
+### `enable_trend_guard`
+- **Type:** bool
+- **Default:** `True`
+- **What it does:** Enables the higher-timeframe trend guard.
+- **When to change it:**
+  - Disable it if you want the grid to trade regardless of regime.
+  - Keep it on if you want the strategy to stand down in strong opposing trends.
+
+### `trend_guard_reversal_max`
+- **Type:** float
+- **Default:** `0.25`
+- **Range:** `0.0..1.0`
+- **What it does:** Maximum reversal score allowed before the trend guard stops the grid.
+- **When to change it:**
+  - Lower it for stricter trend filtering.
+  - Raise it if you want the grid to remain active more often.
+
+## Visibility
+
+### `debug_orders`
+- **Type:** bool
+- **Default:** `True`
+- **What it does:** Enables verbose order/debug logging.
+- **When to change it:**
+  - Keep it on while tuning.
+  - Turn it off when you want quieter logs.
+
+## Practical starting points
+
+- **Conservative:** lower `inventory_cap_pct`, higher `recenter_pct`, higher `center_shift_factor`
+- **Reactive:** lower `recenter_pct`, lower `center_shift_factor`, smaller `grid_step_pct`
+- **Range-friendly:** moderate `grid_levels`, moderate `grid_step_pct`, `enable_trend_guard=true`
+
+## Current recommended interpretation
+
+If you want the grid to feel like it “leans” toward the fill instead of snapping, the key knob is `center_shift_factor`.
+
+For your described behavior, `0.3333333333` is the right default.

+ 41 - 11
strategies/grid_trader.py

@@ -23,6 +23,7 @@ class Strategy(Strategy):
         "recenter_atr_multiplier": {"type": "float", "default": 0.35, "min": 0.0, "max": 10.0},
         "recenter_min_pct": {"type": "float", "default": 0.0025, "min": 0.0, "max": 0.5},
         "recenter_max_pct": {"type": "float", "default": 0.03, "min": 0.0, "max": 0.5},
+        "center_shift_factor": {"type": "float", "default": 0.3333333333, "min": 0.0, "max": 1.0},
         "fee_rate": {"type": "float", "default": 0.0025, "min": 0.0, "max": 0.05},
         "trade_sides": {"type": "string", "default": "both"},
         "max_notional_per_order": {"type": "float", "default": 0.0, "min": 0.0},
@@ -249,6 +250,21 @@ class Strategy(Strategy):
         self.state["atr_percent"] = atr_pct
         return step
 
+    def _config_warning(self) -> str | None:
+        recenter_pct = float(self.state.get("recenter_pct_live") or self._recenter_threshold_pct())
+        grid_step_pct = float(self.state.get("grid_step_pct") or self._grid_step_pct())
+        if grid_step_pct <= 0:
+            return None
+
+        ratio = recenter_pct / grid_step_pct
+        # If the recenter threshold is too close to the first step, the grid
+        # can keep rebuilding before it has a fair chance to trade.
+        if ratio <= 1.0:
+            return f"warning: recenter threshold ({recenter_pct:.4f}) is <= grid step ({grid_step_pct:.4f}), it may recenter before trading"
+        if ratio < 1.5:
+            return f"warning: recenter threshold ({recenter_pct:.4f}) is only {ratio:.2f}x the grid step ({grid_step_pct:.4f}), consider widening it"
+        return None
+
     def _available_balance(self, asset_code: str) -> float:
         try:
             info = self.context.get_account_info()
@@ -630,17 +646,34 @@ class Strategy(Strategy):
 
     def _recenter_and_rebuild_from_fill(self, fill_price: float) -> None:
         fill_price = self._maybe_refresh_center(fill_price)
-        """Treat a fill as the new market anchor and rebuild the grid from there."""
+        """Treat a fill as the new market anchor and rebuild the full grid from there."""
         if fill_price <= 0:
             return
+        self._recenter_and_rebuild_from_price(fill_price, "fill rebuild")
+
+    def _shifted_center_price(self, current_center: float, price: float) -> float:
+        if current_center <= 0:
+            return price
+        if price <= 0:
+            return current_center
+        factor = float(self.config.get("center_shift_factor", 1.0 / 3.0) or 0.0)
+        factor = max(0.0, min(1.0, factor))
+        return price + (current_center - price) * factor
+
+    def _recenter_and_rebuild_from_price(self, price: float, reason: str) -> None:
+        current = float(self.state.get("center_price") or 0.0)
+        if price <= 0:
+            return
+        new_center = self._shifted_center_price(current, price)
+        self._log(f"{reason}: shift center from {current} to {new_center} using price={price}")
         try:
             self.context.cancel_all_orders()
         except Exception as exc:
             self.state["last_error"] = str(exc)
-            self._log(f"fill rebuild cancel-all failed: {exc}")
-        self.state["center_price"] = fill_price
+            self._log(f"{reason} cancel-all failed: {exc}")
+        self.state["center_price"] = new_center
         self.state["seeded"] = True
-        self._place_grid(fill_price)
+        self._place_grid(new_center)
         self._set_grid_refresh_pause()
 
     def _maybe_refresh_center(self, price: float) -> float:
@@ -844,13 +877,7 @@ class Strategy(Strategy):
         deviation = abs(price - center) / center if center else 0.0
         if mode == "active" and deviation >= recenter_pct and not self._grid_refresh_paused():
             self._log(f"recenter needed at price={price} center={center} dev={deviation:.4f} threshold={recenter_pct:.4f}")
-            try:
-                self.context.cancel_all_orders()
-            except Exception as exc:
-                self.state["last_error"] = str(exc)
-                self._log(f"recenter cancel-all failed: {exc}")
-            self.state["center_price"] = price
-            self._place_grid(price)
+            self._recenter_and_rebuild_from_price(price, "recenter")
             live_orders = self._sync_open_orders_state()
             live_ids = list(self.state.get("order_ids") or [])
             open_order_count = len(live_ids)
@@ -950,6 +977,9 @@ class Strategy(Strategy):
                     {"type": "metric", "label": "trend guard active", "value": "on"},
                     {"type": "text", "label": "trend guard reason", "value": "higher-timeframe trend conflict"},
                 ] if self.state.get("trend_guard_active") else []),
+                *([
+                    {"type": "text", "label": "config warning", "value": warning},
+                ] if (warning := self._config_warning()) else []),
                 {"type": "text", "label": "error", "value": self.state.get("last_error", "") or "none"},
                 {"type": "log", "label": "debug log", "lines": self.state.get("debug_log") or []},
             ]

+ 22 - 0
tests/test_strategies.py

@@ -191,3 +191,25 @@ def test_grid_top_up_uses_missing_levels_budget():
     assert len(ctx.placed_orders) == 1
     assert ctx.placed_orders[0]["side"] == "buy"
     assert float(ctx.placed_orders[0]["amount"]) > 7.57
+
+
+def test_grid_center_shifts_towards_price_by_configured_fraction():
+    class FakeContext:
+        base_currency = "XRP"
+        counter_currency = "USD"
+        market_symbol = "xrpusd"
+        minimum_order_value = 10.0
+        mode = "active"
+
+        def cancel_all_orders(self):
+            return {"ok": True}
+
+        def place_order(self, **kwargs):
+            return {"status": "ok", "id": "oid-1"}
+
+    strategy = GridStrategy(FakeContext(), {"center_shift_factor": 0.25})
+    strategy.state["center_price"] = 100.0
+
+    shifted = strategy._shifted_center_price(100.0, 160.0)
+
+    assert shifted == 145.0