|
@@ -644,6 +644,26 @@ class Strategy(Strategy):
|
|
|
amount = self._suggest_amount(side, price, int(self.config.get("grid_levels", 6) or 6), min_notional)
|
|
amount = self._suggest_amount(side, price, int(self.config.get("grid_levels", 6) or 6), min_notional)
|
|
|
return self._max_fundable_levels(side, price, amount, min_notional, balance_total=balance_total)
|
|
return self._max_fundable_levels(side, price, amount, min_notional, balance_total=balance_total)
|
|
|
|
|
|
|
|
|
|
+ def _placeable_levels_for_side(self, side: str, center: float, amount: float, expected_levels: int, min_notional: float) -> int:
|
|
|
|
|
+ if expected_levels <= 0 or center <= 0 or amount <= 0:
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ step_profile = self._effective_grid_steps(center)
|
|
|
|
|
+ step = float(step_profile.get(side) or step_profile.get("base") or 0.0)
|
|
|
|
|
+ if step <= 0:
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ placeable = 0
|
|
|
|
|
+ for i in range(1, expected_levels + 1):
|
|
|
|
|
+ price = center * (1 - (step * i)) if side == "buy" else center * (1 + (step * i))
|
|
|
|
|
+ if price <= 0:
|
|
|
|
|
+ break
|
|
|
|
|
+ min_size = (min_notional / price) if min_notional > 0 else 0.0
|
|
|
|
|
+ if amount < min_size:
|
|
|
|
|
+ continue
|
|
|
|
|
+ placeable += 1
|
|
|
|
|
+ return placeable
|
|
|
|
|
+
|
|
|
def _side_allowed(self, side: str) -> bool:
|
|
def _side_allowed(self, side: str) -> bool:
|
|
|
selected = str(self.config.get("trade_sides", "both") or "both").strip().lower()
|
|
selected = str(self.config.get("trade_sides", "both") or "both").strip().lower()
|
|
|
if selected == "both":
|
|
if selected == "both":
|
|
@@ -706,11 +726,6 @@ class Strategy(Strategy):
|
|
|
if amount <= 0:
|
|
if amount <= 0:
|
|
|
return 0
|
|
return 0
|
|
|
|
|
|
|
|
- fee_rate = self._live_fee_rate()
|
|
|
|
|
- safety = 0.995
|
|
|
|
|
- step_profile = self._effective_grid_steps(center)
|
|
|
|
|
- step = float(step_profile.get(side) or step_profile.get("base") or 0.0)
|
|
|
|
|
- current_levels = len(side_orders)
|
|
|
|
|
free_balance = max(balance_total - sum(
|
|
free_balance = max(balance_total - sum(
|
|
|
float(order.get("price") or 0.0) * float(order.get("amount") or 0.0)
|
|
float(order.get("price") or 0.0) * float(order.get("amount") or 0.0)
|
|
|
for order in side_orders
|
|
for order in side_orders
|
|
@@ -718,40 +733,35 @@ class Strategy(Strategy):
|
|
|
float(order.get("amount") or 0.0)
|
|
float(order.get("amount") or 0.0)
|
|
|
for order in side_orders
|
|
for order in side_orders
|
|
|
), 0.0)
|
|
), 0.0)
|
|
|
- spendable_free = free_balance * safety
|
|
|
|
|
- additional_levels = 0
|
|
|
|
|
|
|
+ placeable_levels = self._placeable_levels_for_side(side, center, amount, expected_levels, min_notional)
|
|
|
|
|
+ if placeable_levels <= 0:
|
|
|
|
|
+ return 0
|
|
|
|
|
|
|
|
- if side == "buy":
|
|
|
|
|
- per_level_quote = 0.0
|
|
|
|
|
- for i in range(1, expected_levels + 1):
|
|
|
|
|
- level_price = center * (1 - (step * (current_levels + i)))
|
|
|
|
|
- if level_price <= 0:
|
|
|
|
|
- break
|
|
|
|
|
- min_size = (min_notional / level_price) if min_notional > 0 else 0.0
|
|
|
|
|
- if amount < min_size:
|
|
|
|
|
- break
|
|
|
|
|
- level_quote = amount * level_price * (1 + fee_rate)
|
|
|
|
|
- if per_level_quote <= 0:
|
|
|
|
|
- per_level_quote = level_quote
|
|
|
|
|
- if (additional_levels + 1) * per_level_quote <= spendable_free + 1e-9:
|
|
|
|
|
- additional_levels += 1
|
|
|
|
|
- else:
|
|
|
|
|
- break
|
|
|
|
|
- else:
|
|
|
|
|
- per_level_base = max(amount, 0.0)
|
|
|
|
|
- for i in range(1, expected_levels + 1):
|
|
|
|
|
- level_price = center * (1 + (step * (current_levels + i)))
|
|
|
|
|
- if level_price <= 0:
|
|
|
|
|
- break
|
|
|
|
|
- min_size = (min_notional / level_price) if min_notional > 0 else 0.0
|
|
|
|
|
- if amount < min_size:
|
|
|
|
|
- break
|
|
|
|
|
- if (additional_levels + 1) * per_level_base <= spendable_free + 1e-9:
|
|
|
|
|
- additional_levels += 1
|
|
|
|
|
- else:
|
|
|
|
|
- break
|
|
|
|
|
|
|
+ step_profile = self._effective_grid_steps(center)
|
|
|
|
|
+ step = float(step_profile.get(side) or step_profile.get("base") or 0.0)
|
|
|
|
|
+ if step <= 0:
|
|
|
|
|
+ return 0
|
|
|
|
|
|
|
|
- return min(expected_levels, current_levels + additional_levels)
|
|
|
|
|
|
|
+ fee_rate = self._live_fee_rate()
|
|
|
|
|
+ safety = 0.995
|
|
|
|
|
+ spendable_free = free_balance * safety
|
|
|
|
|
+ total_cost = 0.0
|
|
|
|
|
+ confirmed = 0
|
|
|
|
|
+ for i in range(1, expected_levels + 1):
|
|
|
|
|
+ level_price = center * (1 - (step * i)) if side == "buy" else center * (1 + (step * i))
|
|
|
|
|
+ if level_price <= 0:
|
|
|
|
|
+ break
|
|
|
|
|
+ min_size = (min_notional / level_price) if min_notional > 0 else 0.0
|
|
|
|
|
+ if amount < min_size:
|
|
|
|
|
+ continue
|
|
|
|
|
+ level_cost = amount * level_price * (1 + fee_rate) if side == "buy" else max(amount, 0.0)
|
|
|
|
|
+ if total_cost + level_cost <= spendable_free + 1e-9:
|
|
|
|
|
+ total_cost += level_cost
|
|
|
|
|
+ confirmed += 1
|
|
|
|
|
+ else:
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ return min(expected_levels, confirmed)
|
|
|
|
|
|
|
|
def _place_grid(self, center: float) -> None:
|
|
def _place_grid(self, center: float) -> None:
|
|
|
center = self._maybe_refresh_center(center)
|
|
center = self._maybe_refresh_center(center)
|
|
@@ -841,6 +851,9 @@ class Strategy(Strategy):
|
|
|
self.state["center_price"] = price
|
|
self.state["center_price"] = price
|
|
|
self.state["seeded"] = True
|
|
self.state["seeded"] = True
|
|
|
self._place_grid(price)
|
|
self._place_grid(price)
|
|
|
|
|
+ # Use the freshly placed live orders as the tracked snapshot so the
|
|
|
|
|
+ # next tick compares against the rebuilt grid, not the pre-rebuild set.
|
|
|
|
|
+ self._sync_open_orders_state()
|
|
|
self._refresh_balance_snapshot()
|
|
self._refresh_balance_snapshot()
|
|
|
self._set_grid_refresh_pause()
|
|
self._set_grid_refresh_pause()
|
|
|
|
|
|
|
@@ -1053,6 +1066,12 @@ class Strategy(Strategy):
|
|
|
|
|
|
|
|
live_orders, live_ids, open_order_count = self._reconcile_after_sync(previous_orders, live_orders, desired_sides, price)
|
|
live_orders, live_ids, open_order_count = self._reconcile_after_sync(previous_orders, live_orders, desired_sides, price)
|
|
|
|
|
|
|
|
|
|
+ if self._grid_refresh_paused():
|
|
|
|
|
+ mode = self._mode()
|
|
|
|
|
+ self.state["last_action"] = "hold" if mode == "active" else f"{mode} monitor"
|
|
|
|
|
+ self._log(f"grid refresh paused, holding at {price} dev {deviation:.4f}")
|
|
|
|
|
+ return {"action": "hold" if mode == "active" else "plan", "price": price, "deviation": deviation, "refresh_paused": True}
|
|
|
|
|
+
|
|
|
if desired_sides != {"buy", "sell"}:
|
|
if desired_sides != {"buy", "sell"}:
|
|
|
self._log("single-side mode is disabled for this strategy, forcing full-grid rebuilds only")
|
|
self._log("single-side mode is disabled for this strategy, forcing full-grid rebuilds only")
|
|
|
|
|
|
|
@@ -1128,6 +1147,7 @@ class Strategy(Strategy):
|
|
|
self._place_grid(price)
|
|
self._place_grid(price)
|
|
|
live_orders = self._sync_open_orders_state()
|
|
live_orders = self._sync_open_orders_state()
|
|
|
self.state["seeded"] = True
|
|
self.state["seeded"] = True
|
|
|
|
|
+ self._set_grid_refresh_pause()
|
|
|
mode = self._mode()
|
|
mode = self._mode()
|
|
|
self._log(f"{'seeded' if mode == 'active' else 'planned'} grid at {price}")
|
|
self._log(f"{'seeded' if mode == 'active' else 'planned'} grid at {price}")
|
|
|
return {"action": "seed" if mode == "active" else "plan", "price": price}
|
|
return {"action": "seed" if mode == "active" else "plan", "price": price}
|