|
|
@@ -20,6 +20,9 @@ class Strategy(Strategy):
|
|
|
"order_size": {"type": "float", "default": 0.0, "min": 0.0},
|
|
|
"inventory_cap_pct": {"type": "float", "default": 0.7, "min": 0.0, "max": 1.0},
|
|
|
"recenter_pct": {"type": "float", "default": 0.05, "min": 0.0, "max": 0.5},
|
|
|
+ "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},
|
|
|
"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},
|
|
|
@@ -166,6 +169,29 @@ class Strategy(Strategy):
|
|
|
reason = f"1d={d1_trend} 4h={h4_trend} rev={max(d1_rev, h4_rev):.3f}"
|
|
|
return active, reason
|
|
|
|
|
|
+ def _recenter_threshold_pct(self) -> float:
|
|
|
+ base_threshold = float(self.config.get("recenter_pct", 0.05) or 0.05)
|
|
|
+ atr_multiplier = float(self.config.get("recenter_atr_multiplier", 0.35) or 0.0)
|
|
|
+ min_threshold = float(self.config.get("recenter_min_pct", 0.0025) or 0.0)
|
|
|
+ max_threshold = float(self.config.get("recenter_max_pct", 0.03) or 1.0)
|
|
|
+
|
|
|
+ try:
|
|
|
+ tf = str(self.config.get("volatility_timeframe", "1h") or "1h")
|
|
|
+ regime = self.context.get_regime(self._base_symbol(), tf)
|
|
|
+ short_regime = self.context.get_regime(self._base_symbol(), "15m")
|
|
|
+ atr_pct = float((regime or {}).get("volatility", {}).get("atr_percent") or 0.0)
|
|
|
+ short_atr_pct = float((short_regime or {}).get("volatility", {}).get("atr_percent") or 0.0)
|
|
|
+ atr_pct = max(atr_pct, short_atr_pct)
|
|
|
+ except Exception:
|
|
|
+ atr_pct = 0.0
|
|
|
+
|
|
|
+ threshold = (atr_pct / 100.0) * atr_multiplier if atr_pct > 0 else base_threshold
|
|
|
+ threshold = max(threshold, min_threshold)
|
|
|
+ threshold = min(threshold, max_threshold)
|
|
|
+ self.state["recenter_pct_live"] = threshold
|
|
|
+ self.state["recenter_atr_percent"] = atr_pct
|
|
|
+ return threshold
|
|
|
+
|
|
|
def _grid_step_pct(self) -> float:
|
|
|
base_step = float(self.config.get("grid_step_pct", 0.012) or 0.012)
|
|
|
tf = str(self.config.get("volatility_timeframe", "1h") or "1h")
|
|
|
@@ -325,6 +351,7 @@ class Strategy(Strategy):
|
|
|
return max(amount, 0.0)
|
|
|
|
|
|
def _place_grid(self, center: float) -> None:
|
|
|
+ center = self._maybe_refresh_center(center)
|
|
|
mode = self._mode()
|
|
|
levels = int(self.config.get("grid_levels", 6) or 6)
|
|
|
step = self._grid_step_pct()
|
|
|
@@ -388,6 +415,7 @@ class Strategy(Strategy):
|
|
|
self._set_grid_refresh_pause()
|
|
|
|
|
|
def _place_side_grid(self, side: str, center: float, *, start_level: int = 1) -> None:
|
|
|
+ center = self._maybe_refresh_center(center)
|
|
|
levels = int(self.config.get("grid_levels", 6) or 6)
|
|
|
step = self._grid_step_pct()
|
|
|
min_notional = float(self.context.minimum_order_value or 0.0)
|
|
|
@@ -457,6 +485,7 @@ class Strategy(Strategy):
|
|
|
self._set_grid_refresh_pause()
|
|
|
|
|
|
def _top_up_missing_levels(self, center: float, live_orders: list[dict]) -> None:
|
|
|
+ center = self._maybe_refresh_center(center)
|
|
|
target_levels = int(self.config.get("grid_levels", 6) or 6)
|
|
|
if target_levels <= 0:
|
|
|
return
|
|
|
@@ -571,6 +600,7 @@ class Strategy(Strategy):
|
|
|
return placed
|
|
|
|
|
|
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."""
|
|
|
if fill_price <= 0:
|
|
|
return
|
|
|
@@ -584,6 +614,21 @@ class Strategy(Strategy):
|
|
|
self._place_grid(fill_price)
|
|
|
self._set_grid_refresh_pause()
|
|
|
|
|
|
+ def _maybe_refresh_center(self, price: float) -> float:
|
|
|
+ if price <= 0:
|
|
|
+ return price
|
|
|
+ current = float(self.state.get("center_price") or 0.0)
|
|
|
+ if current <= 0:
|
|
|
+ self.state["center_price"] = price
|
|
|
+ return price
|
|
|
+ deviation = abs(price - current) / current if current else 0.0
|
|
|
+ threshold = self._recenter_threshold_pct()
|
|
|
+ if deviation >= threshold:
|
|
|
+ self._log(f"recenter anchor from {current} to {price} dev={deviation:.4f} threshold={threshold:.4f}")
|
|
|
+ self.state["center_price"] = price
|
|
|
+ return price
|
|
|
+ return current
|
|
|
+
|
|
|
def _sync_open_orders_state(self) -> list[dict]:
|
|
|
try:
|
|
|
open_orders = self.context.get_open_orders()
|
|
|
@@ -765,11 +810,11 @@ class Strategy(Strategy):
|
|
|
else:
|
|
|
self.state["mismatch_ticks"] = 0
|
|
|
|
|
|
- center = float(self.state.get("center_price") or price)
|
|
|
- recenter_pct = float(self.config.get("recenter_pct", 0.05) or 0.05)
|
|
|
+ center = self._maybe_refresh_center(float(self.state.get("center_price") or price))
|
|
|
+ recenter_pct = self._recenter_threshold_pct()
|
|
|
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}")
|
|
|
+ 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:
|