|
|
@@ -353,9 +353,22 @@ class Strategy(Strategy):
|
|
|
self.state["atr_percent"] = atr_pct
|
|
|
return step
|
|
|
|
|
|
- def _inventory_rebalance_profile(self, price: float) -> dict[str, float | str]:
|
|
|
+ def _inventory_rebalance_profile(
|
|
|
+ self,
|
|
|
+ price: float,
|
|
|
+ *,
|
|
|
+ base_total: float | None = None,
|
|
|
+ quote_total: float | None = None,
|
|
|
+ ) -> 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)
|
|
|
+ if base_total is not None or quote_total is not None:
|
|
|
+ # Shape planning should see the whole wallet, not only the free slice.
|
|
|
+ base_value = max(float(base_total if base_total is not None else self.state.get("base_available") or 0.0), 0.0) * ratio_price
|
|
|
+ counter_value = max(float(quote_total if quote_total is not None else self.state.get("counter_available") or 0.0), 0.0)
|
|
|
+ total = base_value + counter_value
|
|
|
+ ratio = base_value / total if total > 0 else 0.5
|
|
|
+ else:
|
|
|
+ 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)
|
|
|
@@ -373,10 +386,16 @@ class Strategy(Strategy):
|
|
|
"reduction": reduction,
|
|
|
}
|
|
|
|
|
|
- def _effective_grid_steps(self, price: float) -> dict[str, float | str]:
|
|
|
+ def _effective_grid_steps(
|
|
|
+ self,
|
|
|
+ price: float,
|
|
|
+ *,
|
|
|
+ base_total: float | None = None,
|
|
|
+ quote_total: float | None = None,
|
|
|
+ ) -> 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)
|
|
|
+ profile = self._inventory_rebalance_profile(price, base_total=base_total, quote_total=quote_total)
|
|
|
favored_side = str(profile.get("favored_side") or "none")
|
|
|
reduction = float(profile.get("reduction") or 0.0)
|
|
|
|
|
|
@@ -597,73 +616,6 @@ class Strategy(Strategy):
|
|
|
)
|
|
|
return True
|
|
|
|
|
|
- def _max_fundable_levels(self, side: str, price: float, amount: float, min_notional: float, *, balance_total: float | None = None) -> int:
|
|
|
- if min_notional <= 0 or price <= 0 or amount <= 0:
|
|
|
- return 0
|
|
|
- safety = 0.995
|
|
|
- fee_rate = self._live_fee_rate()
|
|
|
- if side == "buy":
|
|
|
- quote = self.context.counter_currency or "USD"
|
|
|
- quote_available = self._available_balance(quote)
|
|
|
- self.state["counter_available"] = quote_available
|
|
|
- usable_notional = (quote_available if balance_total is None else balance_total) * safety
|
|
|
- max_levels = 0
|
|
|
- for level_count in range(1, 1000):
|
|
|
- needed = 0.0
|
|
|
- for i in range(1, level_count + 1):
|
|
|
- level_price = price
|
|
|
- if level_price <= 0:
|
|
|
- return max_levels
|
|
|
- min_size = min_notional / level_price if min_notional > 0 else 0.0
|
|
|
- if amount < min_size:
|
|
|
- return max_levels
|
|
|
- needed += amount * level_price * (1 + fee_rate)
|
|
|
- if needed <= usable_notional + 1e-9:
|
|
|
- max_levels = level_count
|
|
|
- else:
|
|
|
- break
|
|
|
- return max_levels
|
|
|
-
|
|
|
- base = self._base_symbol()
|
|
|
- base_available = self._available_balance(base)
|
|
|
- self.state["base_available"] = base_available
|
|
|
- usable_base = base_available if balance_total is None else balance_total
|
|
|
- spendable_base = usable_base * safety
|
|
|
- max_levels = 0
|
|
|
- for level_count in range(1, 1000):
|
|
|
- needed = amount * level_count
|
|
|
- if amount < (min_notional / price):
|
|
|
- return max_levels
|
|
|
- if needed <= spendable_base + 1e-9:
|
|
|
- max_levels = level_count
|
|
|
- else:
|
|
|
- break
|
|
|
- return max_levels
|
|
|
-
|
|
|
- def _supported_levels(self, side: str, price: float, min_notional: float, *, balance_total: float | None = None) -> int:
|
|
|
- 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)
|
|
|
-
|
|
|
- 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:
|
|
|
selected = str(self.config.get("trade_sides", "both") or "both").strip().lower()
|
|
|
if selected == "both":
|
|
|
@@ -678,7 +630,15 @@ class Strategy(Strategy):
|
|
|
return {selected}
|
|
|
return {"buy", "sell"}
|
|
|
|
|
|
- def _suggest_amount(self, side: str, price: float, levels: int, min_notional: float) -> float:
|
|
|
+ def _suggest_amount(
|
|
|
+ self,
|
|
|
+ side: str,
|
|
|
+ price: float,
|
|
|
+ levels: int,
|
|
|
+ min_notional: float,
|
|
|
+ *,
|
|
|
+ available_balances: dict[str, float] | None = None,
|
|
|
+ ) -> float:
|
|
|
return suggest_quote_sized_amount(
|
|
|
self.context,
|
|
|
side=side,
|
|
|
@@ -690,6 +650,7 @@ class Strategy(Strategy):
|
|
|
max_order_notional_quote=float(self.config.get("max_order_notional_quote") or self.config.get("max_notional_per_order") or 0.0),
|
|
|
dust_collect=bool(self.config.get("dust_collect", False)),
|
|
|
order_size=0.0,
|
|
|
+ available_balances=available_balances,
|
|
|
)
|
|
|
|
|
|
def _grid_extreme_price(self, center: float, side: str, levels: int) -> float:
|
|
|
@@ -701,76 +662,145 @@ class Strategy(Strategy):
|
|
|
return round(center * (1 - (step * levels)), 8)
|
|
|
return round(center * (1 + (step * levels)), 8)
|
|
|
|
|
|
- def _target_levels_for_side(self, side: str, center: float, live_orders: list[dict], balance_total: float, expected_levels: int, min_notional: float) -> int:
|
|
|
- if expected_levels <= 0 or center <= 0 or balance_total <= 0:
|
|
|
- return 0
|
|
|
-
|
|
|
- side_orders = [
|
|
|
- order for order in live_orders
|
|
|
- if isinstance(order, dict) and str(order.get("side") or "").lower() == side
|
|
|
- ]
|
|
|
- amount = 0.0
|
|
|
- if side_orders:
|
|
|
- amounts = []
|
|
|
- for order in side_orders:
|
|
|
- try:
|
|
|
- amounts.append(float(order.get("amount") or 0.0))
|
|
|
- except Exception:
|
|
|
- continue
|
|
|
- if amounts:
|
|
|
- amount = sum(amounts) / len(amounts)
|
|
|
+ def _resource_total_for_side(self, side: str, base_total: float, quote_total: float) -> float:
|
|
|
+ if side == "buy":
|
|
|
+ return max(float(quote_total or 0.0), 0.0)
|
|
|
+ return max(float(base_total or 0.0), 0.0)
|
|
|
|
|
|
- reference_price = self._grid_extreme_price(center, side, expected_levels)
|
|
|
- seeded_amount = self._suggest_amount(side, reference_price, max(expected_levels, 1), min_notional)
|
|
|
- amount = max(amount, seeded_amount)
|
|
|
- if amount <= 0:
|
|
|
- return 0
|
|
|
+ def _resource_cost_for_order(self, side: str, amount: float, price: float, fee_rate: float) -> float:
|
|
|
+ if side == "buy":
|
|
|
+ return max(amount, 0.0) * max(price, 0.0) * (1.0 + max(fee_rate, 0.0))
|
|
|
+ return max(amount, 0.0)
|
|
|
|
|
|
- free_balance = max(balance_total - sum(
|
|
|
+ def _inventory_totals_from_live_orders(self, live_orders: list[dict]) -> tuple[float, float]:
|
|
|
+ reserved_quote = sum(
|
|
|
float(order.get("price") or 0.0) * float(order.get("amount") or 0.0)
|
|
|
- for order in side_orders
|
|
|
- ) if side == "buy" else balance_total - sum(
|
|
|
+ for order in live_orders
|
|
|
+ if isinstance(order, dict) and str(order.get("side") or "").lower() == "buy"
|
|
|
+ )
|
|
|
+ reserved_base = sum(
|
|
|
float(order.get("amount") or 0.0)
|
|
|
- for order in side_orders
|
|
|
- ), 0.0)
|
|
|
- placeable_levels = self._placeable_levels_for_side(side, center, amount, expected_levels, min_notional)
|
|
|
- if placeable_levels <= 0:
|
|
|
- return 0
|
|
|
+ for order in live_orders
|
|
|
+ if isinstance(order, dict) and str(order.get("side") or "").lower() == "sell"
|
|
|
+ )
|
|
|
+ base_total = max(float(self.state.get("base_available") or 0.0), 0.0) + reserved_base
|
|
|
+ quote_total = max(float(self.state.get("counter_available") or 0.0), 0.0) + reserved_quote
|
|
|
+ return base_total, quote_total
|
|
|
+
|
|
|
+ def _planned_side_orders(
|
|
|
+ self,
|
|
|
+ side: str,
|
|
|
+ center: float,
|
|
|
+ expected_levels: int,
|
|
|
+ min_notional: float,
|
|
|
+ fee_rate: float,
|
|
|
+ *,
|
|
|
+ step: float,
|
|
|
+ base_total: float,
|
|
|
+ quote_total: float,
|
|
|
+ ) -> dict:
|
|
|
+ empty = {"amount": 0.0, "orders": [], "skipped": []}
|
|
|
+ if not self._side_allowed(side):
|
|
|
+ return empty
|
|
|
+ if expected_levels <= 0 or center <= 0 or step <= 0:
|
|
|
+ return empty
|
|
|
|
|
|
- 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
|
|
|
+ base_symbol = self._base_symbol()
|
|
|
+ quote_symbol = str(self.context.counter_currency or "USD").upper()
|
|
|
+ balances = {
|
|
|
+ base_symbol: max(float(base_total or 0.0), 0.0),
|
|
|
+ quote_symbol: max(float(quote_total or 0.0), 0.0),
|
|
|
+ }
|
|
|
+ # Ask the shared sizing layer for a venue-valid amount once, then
|
|
|
+ # walk the ladder outward until we either fill the target or run out.
|
|
|
+ reference_price = round(center * (1 - (step * expected_levels)) if side == "buy" else center * (1 + (step * expected_levels)), 8)
|
|
|
+ amount = self._suggest_amount(
|
|
|
+ side,
|
|
|
+ reference_price,
|
|
|
+ max(expected_levels, 1),
|
|
|
+ min_notional,
|
|
|
+ available_balances=balances,
|
|
|
+ )
|
|
|
+ if amount <= 0:
|
|
|
+ return empty
|
|
|
|
|
|
- fee_rate = self._live_fee_rate()
|
|
|
- safety = 0.995
|
|
|
- spendable_free = free_balance * safety
|
|
|
+ spendable_total = self._resource_total_for_side(side, base_total, quote_total) * 0.995
|
|
|
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:
|
|
|
+ planned_orders = []
|
|
|
+ skipped = []
|
|
|
+ max_index = max(expected_levels * 4, expected_levels + 8, 12)
|
|
|
+
|
|
|
+ for level_index in range(1, max_index + 1):
|
|
|
+ # Skip inner levels that fail min-size, but keep pushing outward.
|
|
|
+ price = round(center * (1 - (step * level_index)) if side == "buy" else center * (1 + (step * level_index)), 8)
|
|
|
+ if price <= 0:
|
|
|
break
|
|
|
- min_size = (min_notional / level_price) if min_notional > 0 else 0.0
|
|
|
+
|
|
|
+ min_size = (min_notional / price) if min_notional > 0 else 0.0
|
|
|
if amount < min_size:
|
|
|
+ skipped.append({"level": level_index, "reason": "below minimum size", "price": price})
|
|
|
+ if side == "buy":
|
|
|
+ break
|
|
|
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:
|
|
|
+
|
|
|
+ cost = self._resource_cost_for_order(side, amount, price, fee_rate)
|
|
|
+ if total_cost + cost > spendable_total + 1e-9:
|
|
|
break
|
|
|
|
|
|
- return min(expected_levels, confirmed)
|
|
|
+ total_cost += cost
|
|
|
+ planned_orders.append({"side": side, "price": price, "amount": amount, "level": level_index})
|
|
|
+ if len(planned_orders) >= expected_levels:
|
|
|
+ break
|
|
|
|
|
|
- def _place_grid(self, center: float) -> None:
|
|
|
- center = self._maybe_refresh_center(center)
|
|
|
- mode = self._mode()
|
|
|
+ return {"amount": amount, "orders": planned_orders, "skipped": skipped}
|
|
|
+
|
|
|
+ def _plan_grid(self, center: float, *, base_total: float | None = None, quote_total: float | None = None) -> dict:
|
|
|
+ center = float(center or 0.0)
|
|
|
levels = int(self.config.get("grid_levels", 6) or 6)
|
|
|
- step_profile = self._effective_grid_steps(center)
|
|
|
+ min_notional = float(self.context.minimum_order_value or 0.0)
|
|
|
+ fee_rate = self._live_fee_rate()
|
|
|
+ # One planner feeds both seeding and shape checking, so they never
|
|
|
+ # invent different notions of the "correct" grid.
|
|
|
+ step_profile = self._effective_grid_steps(center, base_total=base_total, quote_total=quote_total)
|
|
|
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)
|
|
|
+ base_total = max(float(self.state.get("base_available") if base_total is None else base_total) or 0.0, 0.0)
|
|
|
+ quote_total = max(float(self.state.get("counter_available") if quote_total is None else quote_total) or 0.0, 0.0)
|
|
|
+
|
|
|
+ buy_plan = self._planned_side_orders(
|
|
|
+ "buy",
|
|
|
+ center,
|
|
|
+ levels,
|
|
|
+ min_notional,
|
|
|
+ fee_rate,
|
|
|
+ step=buy_step,
|
|
|
+ base_total=base_total,
|
|
|
+ quote_total=quote_total,
|
|
|
+ )
|
|
|
+ sell_plan = self._planned_side_orders(
|
|
|
+ "sell",
|
|
|
+ center,
|
|
|
+ levels,
|
|
|
+ min_notional,
|
|
|
+ fee_rate,
|
|
|
+ step=sell_step,
|
|
|
+ base_total=base_total,
|
|
|
+ quote_total=quote_total,
|
|
|
+ )
|
|
|
+ orders = [*buy_plan["orders"], *sell_plan["orders"]]
|
|
|
+ return {
|
|
|
+ "center": center,
|
|
|
+ "buy_orders": buy_plan["orders"],
|
|
|
+ "sell_orders": sell_plan["orders"],
|
|
|
+ "orders": orders,
|
|
|
+ "buy_skipped": buy_plan["skipped"],
|
|
|
+ "sell_skipped": sell_plan["skipped"],
|
|
|
+ "counts": {"buy": len(buy_plan["orders"]), "sell": len(sell_plan["orders"])},
|
|
|
+ }
|
|
|
+
|
|
|
+ def _place_grid(self, center: float) -> None:
|
|
|
+ center = self._maybe_refresh_center(center)
|
|
|
+ mode = self._mode()
|
|
|
market = self._market_symbol()
|
|
|
orders = []
|
|
|
order_ids = []
|
|
|
@@ -780,41 +810,36 @@ class Strategy(Strategy):
|
|
|
return result.get("bitstamp_order_id") or result.get("order_id") or result.get("id") or result.get("client_order_id")
|
|
|
return None
|
|
|
|
|
|
- buy_amount = self._suggest_amount("buy", self._grid_extreme_price(center, "buy", levels), max(levels, 1), min_notional)
|
|
|
- sell_amount = self._suggest_amount("sell", self._grid_extreme_price(center, "sell", levels), max(levels, 1), min_notional)
|
|
|
- buy_levels = min(levels, self._supported_levels("buy", center, min_notional)) if (mode == "active" and self._side_allowed("buy")) else (levels if self._side_allowed("buy") else 0)
|
|
|
- sell_levels = min(levels, self._supported_levels("sell", center, min_notional)) if (mode == "active" and self._side_allowed("sell")) else (levels if self._side_allowed("sell") else 0)
|
|
|
-
|
|
|
- def _place_side(side: str, side_levels: int, side_amount: float, step: float) -> None:
|
|
|
- if side_levels <= 0 or side_amount <= 0:
|
|
|
- return
|
|
|
- for i in range(1, side_levels + 1):
|
|
|
- price = round(center * (1 - (step * i)) if side == "buy" else center * (1 + (step * i)), 8)
|
|
|
- min_size = (min_notional / price) if price > 0 else 0.0
|
|
|
- if side_amount < min_size:
|
|
|
- self._log(f"seed level {i} {side} skipped: below minimum size")
|
|
|
- continue
|
|
|
- if mode != "active":
|
|
|
- orders.append({"side": side, "price": price, "amount": side_amount, "result": {"simulated": True}})
|
|
|
- self._log(f"plan level {i}: {side} {price} amount {side_amount:.6g}")
|
|
|
- continue
|
|
|
- try:
|
|
|
- result = self.context.place_order(side=side, order_type="limit", amount=side_amount, price=price, market=market)
|
|
|
- orders.append({"side": side, "price": price, "amount": side_amount, "result": result})
|
|
|
- order_id = _capture_order_id(result)
|
|
|
- if order_id is not None:
|
|
|
- order_ids.append(str(order_id))
|
|
|
- self._log(f"seed level {i}: {side} {price} amount {side_amount:.6g}")
|
|
|
- delay = max(int(self.config.get("order_call_delay_ms", 250) or 0), 0) / 1000.0
|
|
|
- if delay > 0:
|
|
|
- time.sleep(delay)
|
|
|
- self._refresh_balance_snapshot()
|
|
|
- except Exception as exc: # best effort for first draft
|
|
|
- self.state["last_error"] = str(exc)
|
|
|
- self._log(f"seed level {i} {side} failed: {exc}")
|
|
|
-
|
|
|
- _place_side("buy", buy_levels, buy_amount, buy_step)
|
|
|
- _place_side("sell", sell_levels, sell_amount, sell_step)
|
|
|
+ plan = self._plan_grid(center)
|
|
|
+ for side, skipped in (("buy", plan.get("buy_skipped") or []), ("sell", plan.get("sell_skipped") or [])):
|
|
|
+ for skipped_level in skipped:
|
|
|
+ self._log(f"seed level {skipped_level.get('level')} {side} skipped: {skipped_level.get('reason')}")
|
|
|
+
|
|
|
+ for planned_order in plan.get("orders") or []:
|
|
|
+ side = str(planned_order.get("side") or "").lower()
|
|
|
+ level = int(planned_order.get("level") or 0)
|
|
|
+ price = float(planned_order.get("price") or 0.0)
|
|
|
+ amount = float(planned_order.get("amount") or 0.0)
|
|
|
+ if price <= 0 or amount <= 0:
|
|
|
+ continue
|
|
|
+ if mode != "active":
|
|
|
+ orders.append({"side": side, "price": price, "amount": amount, "result": {"simulated": True}})
|
|
|
+ self._log(f"plan level {level}: {side} {price} amount {amount:.6g}")
|
|
|
+ continue
|
|
|
+ try:
|
|
|
+ result = self.context.place_order(side=side, order_type="limit", amount=amount, price=price, market=market)
|
|
|
+ orders.append({"side": side, "price": price, "amount": amount, "result": result})
|
|
|
+ order_id = _capture_order_id(result)
|
|
|
+ if order_id is not None:
|
|
|
+ order_ids.append(str(order_id))
|
|
|
+ self._log(f"seed level {level}: {side} {price} amount {amount:.6g}")
|
|
|
+ delay = max(int(self.config.get("order_call_delay_ms", 250) or 0), 0) / 1000.0
|
|
|
+ if delay > 0:
|
|
|
+ time.sleep(delay)
|
|
|
+ self._refresh_balance_snapshot()
|
|
|
+ except Exception as exc: # best effort for first draft
|
|
|
+ self.state["last_error"] = str(exc)
|
|
|
+ self._log(f"seed level {level} {side} failed: {exc}")
|
|
|
|
|
|
self.state["orders"] = orders
|
|
|
self.state["order_ids"] = order_ids
|
|
|
@@ -1075,43 +1100,19 @@ class Strategy(Strategy):
|
|
|
if desired_sides != {"buy", "sell"}:
|
|
|
self._log("single-side mode is disabled for this strategy, forcing full-grid rebuilds only")
|
|
|
|
|
|
- current_sides = {str(order.get("side") or "").lower() for order in live_orders if isinstance(order, dict)}
|
|
|
current_buy = sum(1 for order in live_orders if isinstance(order, dict) and str(order.get("side") or "").lower() == "buy")
|
|
|
current_sell = sum(1 for order in live_orders if isinstance(order, dict) and str(order.get("side") or "").lower() == "sell")
|
|
|
- expected_levels = int(self.config.get("grid_levels", 6) or 6)
|
|
|
- quote_currency = self.context.counter_currency or "USD"
|
|
|
- quote_available = self._available_balance(quote_currency)
|
|
|
- base_symbol = self._base_symbol()
|
|
|
- base_available = self._available_balance(base_symbol)
|
|
|
- reserved_quote = sum(
|
|
|
- float(order.get("price") or 0.0) * float(order.get("amount") or 0.0)
|
|
|
- for order in live_orders
|
|
|
- if isinstance(order, dict) and str(order.get("side") or "").lower() == "buy"
|
|
|
- )
|
|
|
- reserved_base = sum(
|
|
|
- float(order.get("amount") or 0.0)
|
|
|
- for order in live_orders
|
|
|
- if isinstance(order, dict) and str(order.get("side") or "").lower() == "sell"
|
|
|
- )
|
|
|
- total_quote = quote_available + reserved_quote
|
|
|
- total_base = base_available + reserved_base
|
|
|
- target_buy = self._target_levels_for_side("buy", price, live_orders, total_quote, expected_levels, float(self.context.minimum_order_value or 0.0))
|
|
|
- target_sell = self._target_levels_for_side("sell", price, live_orders, total_base, expected_levels, float(self.context.minimum_order_value or 0.0))
|
|
|
- target_total = target_buy + target_sell
|
|
|
+ total_base, total_quote = self._inventory_totals_from_live_orders(live_orders)
|
|
|
+ planned_grid = self._plan_grid(center, base_total=total_base, quote_total=total_quote)
|
|
|
+ target_buy = int((planned_grid.get("counts") or {}).get("buy") or 0)
|
|
|
+ target_sell = int((planned_grid.get("counts") or {}).get("sell") or 0)
|
|
|
balance_shape_inconclusive = bool(self.state.get("balance_shape_inconclusive"))
|
|
|
- grid_not_as_expected = (
|
|
|
- bool(live_orders)
|
|
|
- and (
|
|
|
- current_buy != target_buy
|
|
|
- or current_sell != target_sell
|
|
|
- )
|
|
|
- )
|
|
|
-
|
|
|
- can_make_better = target_total > 0 and (current_buy != target_buy or current_sell != target_sell)
|
|
|
+ # Shape means side counts here. Exact ids are handled by the tracked-order path below.
|
|
|
+ grid_not_as_expected = current_buy != target_buy or current_sell != target_sell
|
|
|
|
|
|
if balance_shape_inconclusive:
|
|
|
self._log("balance info not conclusive, skipping grid shape rebuild checks this tick")
|
|
|
- elif grid_not_as_expected and can_make_better:
|
|
|
+ elif grid_not_as_expected:
|
|
|
if rebuild_done:
|
|
|
return {"action": "hold", "price": price}
|
|
|
self._log(
|
|
|
@@ -1125,11 +1126,6 @@ class Strategy(Strategy):
|
|
|
self.state["last_action"] = "reseeded" if mode == "active" else f"{mode} monitor"
|
|
|
return {"action": "reseed" if mode == "active" else "plan", "price": price}
|
|
|
|
|
|
- if not balance_shape_inconclusive and grid_not_as_expected and not can_make_better:
|
|
|
- self._log(
|
|
|
- f"grid shape left unchanged, balance cannot improve it: live_buy={current_buy} live_sell={current_sell} target_buy={target_buy} target_sell={target_sell}"
|
|
|
- )
|
|
|
-
|
|
|
if not balance_shape_inconclusive and self._order_count_mismatch(tracked_ids_before_sync, live_orders):
|
|
|
if rebuild_done:
|
|
|
return {"action": "hold", "price": price}
|