|
@@ -23,6 +23,7 @@ class Strategy(Strategy):
|
|
|
"recenter_atr_multiplier": {"type": "float", "default": 0.35, "min": 0.0, "max": 10.0},
|
|
"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_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},
|
|
"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},
|
|
"fee_rate": {"type": "float", "default": 0.0025, "min": 0.0, "max": 0.05},
|
|
|
"trade_sides": {"type": "string", "default": "both"},
|
|
"trade_sides": {"type": "string", "default": "both"},
|
|
|
"max_notional_per_order": {"type": "float", "default": 0.0, "min": 0.0},
|
|
"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
|
|
self.state["atr_percent"] = atr_pct
|
|
|
return step
|
|
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:
|
|
def _available_balance(self, asset_code: str) -> float:
|
|
|
try:
|
|
try:
|
|
|
info = self.context.get_account_info()
|
|
info = self.context.get_account_info()
|
|
@@ -630,17 +646,34 @@ class Strategy(Strategy):
|
|
|
|
|
|
|
|
def _recenter_and_rebuild_from_fill(self, fill_price: float) -> None:
|
|
def _recenter_and_rebuild_from_fill(self, fill_price: float) -> None:
|
|
|
fill_price = self._maybe_refresh_center(fill_price)
|
|
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:
|
|
if fill_price <= 0:
|
|
|
return
|
|
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:
|
|
try:
|
|
|
self.context.cancel_all_orders()
|
|
self.context.cancel_all_orders()
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
self.state["last_error"] = str(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.state["seeded"] = True
|
|
|
- self._place_grid(fill_price)
|
|
|
|
|
|
|
+ self._place_grid(new_center)
|
|
|
self._set_grid_refresh_pause()
|
|
self._set_grid_refresh_pause()
|
|
|
|
|
|
|
|
def _maybe_refresh_center(self, price: float) -> float:
|
|
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
|
|
deviation = abs(price - center) / center if center else 0.0
|
|
|
if mode == "active" and deviation >= recenter_pct and not self._grid_refresh_paused():
|
|
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}")
|
|
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_orders = self._sync_open_orders_state()
|
|
|
live_ids = list(self.state.get("order_ids") or [])
|
|
live_ids = list(self.state.get("order_ids") or [])
|
|
|
open_order_count = len(live_ids)
|
|
open_order_count = len(live_ids)
|
|
@@ -950,6 +977,9 @@ class Strategy(Strategy):
|
|
|
{"type": "metric", "label": "trend guard active", "value": "on"},
|
|
{"type": "metric", "label": "trend guard active", "value": "on"},
|
|
|
{"type": "text", "label": "trend guard reason", "value": "higher-timeframe trend conflict"},
|
|
{"type": "text", "label": "trend guard reason", "value": "higher-timeframe trend conflict"},
|
|
|
] if self.state.get("trend_guard_active") else []),
|
|
] 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": "text", "label": "error", "value": self.state.get("last_error", "") or "none"},
|
|
|
{"type": "log", "label": "debug log", "lines": self.state.get("debug_log") or []},
|
|
{"type": "log", "label": "debug log", "lines": self.state.get("debug_log") or []},
|
|
|
]
|
|
]
|