|
@@ -39,6 +39,7 @@ class Strategy(Strategy):
|
|
|
"volatility_multiplier": {"type": "float", "default": 0.5, "min": 0.0, "max": 10.0},
|
|
"volatility_multiplier": {"type": "float", "default": 0.5, "min": 0.0, "max": 10.0},
|
|
|
"grid_step_min_pct": {"type": "float", "default": 0.005, "min": 0.0001, "max": 0.5},
|
|
"grid_step_min_pct": {"type": "float", "default": 0.005, "min": 0.0001, "max": 0.5},
|
|
|
"grid_step_max_pct": {"type": "float", "default": 0.03, "min": 0.0001, "max": 1.0},
|
|
"grid_step_max_pct": {"type": "float", "default": 0.03, "min": 0.0001, "max": 1.0},
|
|
|
|
|
+ "inventory_rebalance_step_factor": {"type": "float", "default": 0.15, "min": 0.0, "max": 0.9},
|
|
|
"order_notional_quote": {"type": "float", "default": 0.0, "min": 0.0},
|
|
"order_notional_quote": {"type": "float", "default": 0.0, "min": 0.0},
|
|
|
"max_order_notional_quote": {"type": "float", "default": 0.0, "min": 0.0},
|
|
"max_order_notional_quote": {"type": "float", "default": 0.0, "min": 0.0},
|
|
|
"recenter_pct": {"type": "float", "default": 0.05, "min": 0.0, "max": 0.5},
|
|
"recenter_pct": {"type": "float", "default": 0.05, "min": 0.0, "max": 0.5},
|
|
@@ -61,6 +62,12 @@ class Strategy(Strategy):
|
|
|
"debug_log": {"type": "list", "default": []},
|
|
"debug_log": {"type": "list", "default": []},
|
|
|
"base_available": {"type": "float", "default": 0.0},
|
|
"base_available": {"type": "float", "default": 0.0},
|
|
|
"counter_available": {"type": "float", "default": 0.0},
|
|
"counter_available": {"type": "float", "default": 0.0},
|
|
|
|
|
+ "grid_step_pct_buy": {"type": "float", "default": 0.0},
|
|
|
|
|
+ "grid_step_pct_sell": {"type": "float", "default": 0.0},
|
|
|
|
|
+ "inventory_skew_side": {"type": "string", "default": "none"},
|
|
|
|
|
+ "inventory_skew_ratio": {"type": "float", "default": 0.5},
|
|
|
|
|
+ "inventory_skew_imbalance": {"type": "float", "default": 0.0},
|
|
|
|
|
+ "inventory_skew_reduction_pct": {"type": "float", "default": 0.0},
|
|
|
"regimes_updated_at": {"type": "string", "default": ""},
|
|
"regimes_updated_at": {"type": "string", "default": ""},
|
|
|
"account_snapshot_updated_at": {"type": "string", "default": ""},
|
|
"account_snapshot_updated_at": {"type": "string", "default": ""},
|
|
|
"last_balance_log_signature": {"type": "string", "default": ""},
|
|
"last_balance_log_signature": {"type": "string", "default": ""},
|
|
@@ -82,6 +89,12 @@ class Strategy(Strategy):
|
|
|
"debug_log": ["init cancel all orders"],
|
|
"debug_log": ["init cancel all orders"],
|
|
|
"base_available": 0.0,
|
|
"base_available": 0.0,
|
|
|
"counter_available": 0.0,
|
|
"counter_available": 0.0,
|
|
|
|
|
+ "grid_step_pct_buy": 0.0,
|
|
|
|
|
+ "grid_step_pct_sell": 0.0,
|
|
|
|
|
+ "inventory_skew_side": "none",
|
|
|
|
|
+ "inventory_skew_ratio": 0.5,
|
|
|
|
|
+ "inventory_skew_imbalance": 0.0,
|
|
|
|
|
+ "inventory_skew_reduction_pct": 0.0,
|
|
|
"regimes_updated_at": "",
|
|
"regimes_updated_at": "",
|
|
|
"account_snapshot_updated_at": "",
|
|
"account_snapshot_updated_at": "",
|
|
|
"last_balance_log_signature": "",
|
|
"last_balance_log_signature": "",
|
|
@@ -277,9 +290,56 @@ class Strategy(Strategy):
|
|
|
self.state["atr_percent"] = atr_pct
|
|
self.state["atr_percent"] = atr_pct
|
|
|
return step
|
|
return step
|
|
|
|
|
|
|
|
|
|
+ def _inventory_rebalance_profile(self, price: float) -> dict[str, float | str]:
|
|
|
|
|
+ ratio_price = price if price > 0 else float(self.state.get("last_price") or self.state.get("center_price") or 1.0)
|
|
|
|
|
+ ratio = self._inventory_ratio(ratio_price if ratio_price > 0 else 1.0)
|
|
|
|
|
+ imbalance = min(abs(ratio - 0.5) * 2.0, 1.0)
|
|
|
|
|
+ factor = float(self.config.get("inventory_rebalance_step_factor", 0.15) or 0.0)
|
|
|
|
|
+ factor = min(max(factor, 0.0), 0.9)
|
|
|
|
|
+ favored_side = "sell" if ratio > 0.5 else "buy" if ratio < 0.5 else "none"
|
|
|
|
|
+ reduction = factor * imbalance if favored_side in {"buy", "sell"} else 0.0
|
|
|
|
|
+
|
|
|
|
|
+ self.state["inventory_skew_side"] = favored_side
|
|
|
|
|
+ self.state["inventory_skew_ratio"] = ratio
|
|
|
|
|
+ self.state["inventory_skew_imbalance"] = imbalance
|
|
|
|
|
+ self.state["inventory_skew_reduction_pct"] = reduction
|
|
|
|
|
+ return {
|
|
|
|
|
+ "ratio": ratio,
|
|
|
|
|
+ "imbalance": imbalance,
|
|
|
|
|
+ "favored_side": favored_side,
|
|
|
|
|
+ "reduction": reduction,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ def _effective_grid_steps(self, price: float) -> dict[str, float | str]:
|
|
|
|
|
+ base_step = float(self.state.get("grid_step_pct") or self._grid_step_pct())
|
|
|
|
|
+ min_step = float(self.config.get("grid_step_min_pct", 0.005) or 0.0)
|
|
|
|
|
+ profile = self._inventory_rebalance_profile(price)
|
|
|
|
|
+ favored_side = str(profile.get("favored_side") or "none")
|
|
|
|
|
+ reduction = float(profile.get("reduction") or 0.0)
|
|
|
|
|
+
|
|
|
|
|
+ buy_step = base_step
|
|
|
|
|
+ sell_step = base_step
|
|
|
|
|
+ if favored_side == "buy":
|
|
|
|
|
+ buy_step = max(base_step * (1.0 - reduction), min_step)
|
|
|
|
|
+ elif favored_side == "sell":
|
|
|
|
|
+ sell_step = max(base_step * (1.0 - reduction), min_step)
|
|
|
|
|
+
|
|
|
|
|
+ self.state["grid_step_pct_buy"] = buy_step
|
|
|
|
|
+ self.state["grid_step_pct_sell"] = sell_step
|
|
|
|
|
+ return {
|
|
|
|
|
+ "base": base_step,
|
|
|
|
|
+ "buy": buy_step,
|
|
|
|
|
+ "sell": sell_step,
|
|
|
|
|
+ "favored_side": favored_side,
|
|
|
|
|
+ "reduction": reduction,
|
|
|
|
|
+ "ratio": float(profile.get("ratio") or 0.5),
|
|
|
|
|
+ "imbalance": float(profile.get("imbalance") or 0.0),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
def _config_warning(self) -> str | None:
|
|
def _config_warning(self) -> str | None:
|
|
|
recenter_pct = float(self.state.get("recenter_pct_live") or self._recenter_threshold_pct())
|
|
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())
|
|
|
|
|
|
|
+ steps = self._effective_grid_steps(float(self.state.get("last_price") or self.state.get("center_price") or 0.0))
|
|
|
|
|
+ grid_step_pct = min(float(steps.get("buy") or 0.0), float(steps.get("sell") or 0.0))
|
|
|
if grid_step_pct <= 0:
|
|
if grid_step_pct <= 0:
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
@@ -303,6 +363,7 @@ class Strategy(Strategy):
|
|
|
def _supervision(self) -> dict:
|
|
def _supervision(self) -> dict:
|
|
|
price = float(self.state.get("last_price") or 0.0)
|
|
price = float(self.state.get("last_price") or 0.0)
|
|
|
ratio = self._inventory_ratio(price if price > 0 else 1.0)
|
|
ratio = self._inventory_ratio(price if price > 0 else 1.0)
|
|
|
|
|
+ step_profile = self._effective_grid_steps(price)
|
|
|
last_error = str(self.state.get("last_error") or "")
|
|
last_error = str(self.state.get("last_error") or "")
|
|
|
config_warning = self._config_warning()
|
|
config_warning = self._config_warning()
|
|
|
regime_1h = (((self.state.get("regimes") or {}).get("1h") or {}).get("trend") or {}).get("state")
|
|
regime_1h = (((self.state.get("regimes") or {}).get("1h") or {}).get("trend") or {}).get("state")
|
|
@@ -371,6 +432,14 @@ class Strategy(Strategy):
|
|
|
"health": "degraded" if last_error or config_warning else "healthy",
|
|
"health": "degraded" if last_error or config_warning else "healthy",
|
|
|
"degraded": bool(last_error or config_warning),
|
|
"degraded": bool(last_error or config_warning),
|
|
|
"inventory_pressure": pressure,
|
|
"inventory_pressure": pressure,
|
|
|
|
|
+ "inventory_ratio": round(ratio, 4),
|
|
|
|
|
+ "inventory_rebalance_side": step_profile.get("favored_side", "none"),
|
|
|
|
|
+ "inventory_rebalance_reduction_pct": round(float(step_profile.get("reduction") or 0.0) * 100.0, 4),
|
|
|
|
|
+ "grid_step_pct": {
|
|
|
|
|
+ "base": round(float(step_profile.get("base") or 0.0), 6),
|
|
|
|
|
+ "buy": round(float(step_profile.get("buy") or 0.0), 6),
|
|
|
|
|
+ "sell": round(float(step_profile.get("sell") or 0.0), 6),
|
|
|
|
|
+ },
|
|
|
"capacity_available": pressure == "balanced",
|
|
"capacity_available": pressure == "balanced",
|
|
|
"side_capacity": side_capacity,
|
|
"side_capacity": side_capacity,
|
|
|
"market_bias": market_bias,
|
|
"market_bias": market_bias,
|
|
@@ -544,7 +613,8 @@ class Strategy(Strategy):
|
|
|
|
|
|
|
|
fee_rate = self._live_fee_rate()
|
|
fee_rate = self._live_fee_rate()
|
|
|
safety = 0.995
|
|
safety = 0.995
|
|
|
- step = self._grid_step_pct()
|
|
|
|
|
|
|
+ step_profile = self._effective_grid_steps(center)
|
|
|
|
|
+ step = float(step_profile.get(side) or step_profile.get("base") or 0.0)
|
|
|
spendable_total = balance_total * safety
|
|
spendable_total = balance_total * safety
|
|
|
|
|
|
|
|
for level_count in range(expected_levels, 0, -1):
|
|
for level_count in range(expected_levels, 0, -1):
|
|
@@ -576,7 +646,9 @@ class Strategy(Strategy):
|
|
|
center = self._maybe_refresh_center(center)
|
|
center = self._maybe_refresh_center(center)
|
|
|
mode = self._mode()
|
|
mode = self._mode()
|
|
|
levels = int(self.config.get("grid_levels", 6) or 6)
|
|
levels = int(self.config.get("grid_levels", 6) or 6)
|
|
|
- step = self._grid_step_pct()
|
|
|
|
|
|
|
+ step_profile = self._effective_grid_steps(center)
|
|
|
|
|
+ buy_step = float(step_profile.get("buy") or step_profile.get("base") or 0.0)
|
|
|
|
|
+ sell_step = float(step_profile.get("sell") or step_profile.get("base") or 0.0)
|
|
|
min_notional = float(self.context.minimum_order_value or 0.0)
|
|
min_notional = float(self.context.minimum_order_value or 0.0)
|
|
|
market = self._market_symbol()
|
|
market = self._market_symbol()
|
|
|
orders = []
|
|
orders = []
|
|
@@ -593,8 +665,8 @@ class Strategy(Strategy):
|
|
|
sell_amount = self._suggest_amount("sell", center, max(sell_levels, 1), min_notional)
|
|
sell_amount = self._suggest_amount("sell", center, max(sell_levels, 1), min_notional)
|
|
|
|
|
|
|
|
for i in range(1, levels + 1):
|
|
for i in range(1, levels + 1):
|
|
|
- buy_price = round(center * (1 - (step * i)), 8)
|
|
|
|
|
- sell_price = round(center * (1 + (step * i)), 8)
|
|
|
|
|
|
|
+ buy_price = round(center * (1 - (buy_step * i)), 8)
|
|
|
|
|
+ sell_price = round(center * (1 + (sell_step * i)), 8)
|
|
|
if mode != "active":
|
|
if mode != "active":
|
|
|
orders.append({"side": "buy", "price": buy_price, "amount": buy_amount, "result": {"simulated": True}})
|
|
orders.append({"side": "buy", "price": buy_price, "amount": buy_amount, "result": {"simulated": True}})
|
|
|
orders.append({"side": "sell", "price": sell_price, "amount": sell_amount, "result": {"simulated": True}})
|
|
orders.append({"side": "sell", "price": sell_price, "amount": sell_amount, "result": {"simulated": True}})
|
|
@@ -670,7 +742,7 @@ class Strategy(Strategy):
|
|
|
return []
|
|
return []
|
|
|
|
|
|
|
|
min_notional = float(self.context.minimum_order_value or 0.0)
|
|
min_notional = float(self.context.minimum_order_value or 0.0)
|
|
|
- step = self._grid_step_pct()
|
|
|
|
|
|
|
+ step_profile = self._effective_grid_steps(center)
|
|
|
market = self._market_symbol()
|
|
market = self._market_symbol()
|
|
|
placed: list[str] = []
|
|
placed: list[str] = []
|
|
|
|
|
|
|
@@ -690,8 +762,9 @@ class Strategy(Strategy):
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
amount = self._suggest_amount(side, center, max(target_levels, 1), min_notional)
|
|
amount = self._suggest_amount(side, center, max(target_levels, 1), min_notional)
|
|
|
|
|
+ side_step = float(step_profile.get(side) or step_profile.get("base") or 0.0)
|
|
|
for i in range(live_count + 1, target_levels + 1):
|
|
for i in range(live_count + 1, target_levels + 1):
|
|
|
- price = round(center * (1 - (step * i)) if side == "buy" else center * (1 + (step * i)), 8)
|
|
|
|
|
|
|
+ price = round(center * (1 - (side_step * i)) if side == "buy" else center * (1 + (side_step * i)), 8)
|
|
|
min_size = (min_notional / price) if price > 0 else 0.0
|
|
min_size = (min_notional / price) if price > 0 else 0.0
|
|
|
if amount < min_size:
|
|
if amount < min_size:
|
|
|
self._log_decision(
|
|
self._log_decision(
|
|
@@ -723,12 +796,21 @@ class Strategy(Strategy):
|
|
|
|
|
|
|
|
return placed
|
|
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 full grid from there."""
|
|
|
|
|
- if fill_price <= 0:
|
|
|
|
|
|
|
+ def _current_market_anchor(self, fallback: float = 0.0) -> float:
|
|
|
|
|
+ try:
|
|
|
|
|
+ live_price = float(self._price() or 0.0)
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ self._log(f"live price refresh failed during rebuild: {exc}")
|
|
|
|
|
+ live_price = 0.0
|
|
|
|
|
+ return live_price if live_price > 0 else fallback
|
|
|
|
|
+
|
|
|
|
|
+ def _recenter_and_rebuild_from_fill(self, fill_price: float, market_price: float = 0.0) -> None:
|
|
|
|
|
+ """Treat a fill as a forced re-anchor and rebuild from the latest market price."""
|
|
|
|
|
+ anchor_price = self._current_market_anchor(market_price or fill_price)
|
|
|
|
|
+ if anchor_price <= 0:
|
|
|
return
|
|
return
|
|
|
- self._recenter_and_rebuild_from_price(fill_price, "fill rebuild")
|
|
|
|
|
|
|
+ self._log(f"fill rebuild anchor resolved: fill={fill_price} market={anchor_price}")
|
|
|
|
|
+ self._recenter_and_rebuild_from_price(anchor_price, "fill rebuild")
|
|
|
|
|
|
|
|
def _recenter_and_rebuild_from_price(self, price: float, reason: str) -> None:
|
|
def _recenter_and_rebuild_from_price(self, price: float, reason: str) -> None:
|
|
|
if price <= 0:
|
|
if price <= 0:
|
|
@@ -850,8 +932,8 @@ class Strategy(Strategy):
|
|
|
if fill_price > 0:
|
|
if fill_price > 0:
|
|
|
break
|
|
break
|
|
|
if fill_price > 0:
|
|
if fill_price > 0:
|
|
|
- self._log(f"filled order {order_id} detected via exec status={status}, recentering at {fill_price}")
|
|
|
|
|
- self._recenter_and_rebuild_from_fill(fill_price)
|
|
|
|
|
|
|
+ self._log(f"filled order {order_id} detected via exec status={status}, recentering from fill={fill_price} market={price}")
|
|
|
|
|
+ self._recenter_and_rebuild_from_fill(fill_price, price)
|
|
|
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)
|
|
@@ -1078,6 +1160,12 @@ class Strategy(Strategy):
|
|
|
"last_price": self.state.get("last_price", 0.0),
|
|
"last_price": self.state.get("last_price", 0.0),
|
|
|
"last_action": self.state.get("last_action", "idle"),
|
|
"last_action": self.state.get("last_action", "idle"),
|
|
|
"open_order_count": self.state.get("open_order_count", 0),
|
|
"open_order_count": self.state.get("open_order_count", 0),
|
|
|
|
|
+ "grid_step_pct": self.state.get("grid_step_pct", 0.0),
|
|
|
|
|
+ "grid_step_pct_buy": self.state.get("grid_step_pct_buy", 0.0),
|
|
|
|
|
+ "grid_step_pct_sell": self.state.get("grid_step_pct_sell", 0.0),
|
|
|
|
|
+ "inventory_skew_side": self.state.get("inventory_skew_side", "none"),
|
|
|
|
|
+ "inventory_skew_ratio": self.state.get("inventory_skew_ratio", 0.5),
|
|
|
|
|
+ "inventory_skew_reduction_pct": self.state.get("inventory_skew_reduction_pct", 0.0),
|
|
|
"regimes_updated_at": self.state.get("regimes_updated_at", ""),
|
|
"regimes_updated_at": self.state.get("regimes_updated_at", ""),
|
|
|
},
|
|
},
|
|
|
"assessment": {
|
|
"assessment": {
|
|
@@ -1095,10 +1183,15 @@ class Strategy(Strategy):
|
|
|
# Refresh the market-derived display values on render so the dashboard
|
|
# Refresh the market-derived display values on render so the dashboard
|
|
|
# reflects the same inputs the strategy would use on the next tick.
|
|
# reflects the same inputs the strategy would use on the next tick.
|
|
|
live_step_pct = float(self.state.get("grid_step_pct") or 0.0)
|
|
live_step_pct = float(self.state.get("grid_step_pct") or 0.0)
|
|
|
|
|
+ live_buy_step_pct = float(self.state.get("grid_step_pct_buy") or 0.0)
|
|
|
|
|
+ live_sell_step_pct = float(self.state.get("grid_step_pct_sell") or 0.0)
|
|
|
live_atr_pct = float(self.state.get("atr_percent") or 0.0)
|
|
live_atr_pct = float(self.state.get("atr_percent") or 0.0)
|
|
|
try:
|
|
try:
|
|
|
self._refresh_balance_snapshot()
|
|
self._refresh_balance_snapshot()
|
|
|
live_step_pct = self._grid_step_pct()
|
|
live_step_pct = self._grid_step_pct()
|
|
|
|
|
+ step_profile = self._effective_grid_steps(float(self.state.get("last_price") or self.state.get("center_price") or 0.0))
|
|
|
|
|
+ live_buy_step_pct = float(step_profile.get("buy") or live_step_pct)
|
|
|
|
|
+ live_sell_step_pct = float(step_profile.get("sell") or live_step_pct)
|
|
|
live_atr_pct = float(self.state.get("atr_percent") or live_atr_pct)
|
|
live_atr_pct = float(self.state.get("atr_percent") or live_atr_pct)
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
self._log(f"render refresh failed: {exc}")
|
|
self._log(f"render refresh failed: {exc}")
|
|
@@ -1113,6 +1206,9 @@ class Strategy(Strategy):
|
|
|
{"type": "metric", "label": "open orders", "value": self.state.get("open_order_count", 0)},
|
|
{"type": "metric", "label": "open orders", "value": self.state.get("open_order_count", 0)},
|
|
|
{"type": "metric", "label": f"ATR({self.config.get('volatility_timeframe', '1h')}) %", "value": round(live_atr_pct, 4)},
|
|
{"type": "metric", "label": f"ATR({self.config.get('volatility_timeframe', '1h')}) %", "value": round(live_atr_pct, 4)},
|
|
|
{"type": "metric", "label": "grid step %", "value": round(live_step_pct * 100.0, 4)},
|
|
{"type": "metric", "label": "grid step %", "value": round(live_step_pct * 100.0, 4)},
|
|
|
|
|
+ {"type": "metric", "label": "buy step %", "value": round(live_buy_step_pct * 100.0, 4)},
|
|
|
|
|
+ {"type": "metric", "label": "sell step %", "value": round(live_sell_step_pct * 100.0, 4)},
|
|
|
|
|
+ {"type": "metric", "label": "rebalance bias", "value": self.state.get("inventory_skew_side", "none")},
|
|
|
{"type": "metric", "label": "1d", "value": ((self.state.get('regimes') or {}).get('1d') or {}).get('trend', {}).get('state', 'n/a')},
|
|
{"type": "metric", "label": "1d", "value": ((self.state.get('regimes') or {}).get('1d') or {}).get('trend', {}).get('state', 'n/a')},
|
|
|
{"type": "metric", "label": "4h", "value": ((self.state.get('regimes') or {}).get('4h') or {}).get('trend', {}).get('state', 'n/a')},
|
|
{"type": "metric", "label": "4h", "value": ((self.state.get('regimes') or {}).get('4h') or {}).get('trend', {}).get('state', 'n/a')},
|
|
|
{"type": "metric", "label": "1h", "value": ((self.state.get('regimes') or {}).get('1h') or {}).get('trend', {}).get('state', 'n/a')},
|
|
{"type": "metric", "label": "1h", "value": ((self.state.get('regimes') or {}).get('1h') or {}).get('trend', {}).get('state', 'n/a')},
|