|
@@ -597,8 +597,8 @@ class Strategy(Strategy):
|
|
|
)
|
|
)
|
|
|
return True
|
|
return True
|
|
|
|
|
|
|
|
- def _supported_levels(self, side: str, price: float, min_notional: float, *, balance_total: float | None = None) -> int:
|
|
|
|
|
- if min_notional <= 0 or price <= 0:
|
|
|
|
|
|
|
+ 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
|
|
return 0
|
|
|
safety = 0.995
|
|
safety = 0.995
|
|
|
fee_rate = self._live_fee_rate()
|
|
fee_rate = self._live_fee_rate()
|
|
@@ -607,14 +607,62 @@ class Strategy(Strategy):
|
|
|
quote_available = self._available_balance(quote)
|
|
quote_available = self._available_balance(quote)
|
|
|
self.state["counter_available"] = quote_available
|
|
self.state["counter_available"] = quote_available
|
|
|
usable_notional = (quote_available if balance_total is None else balance_total) * safety
|
|
usable_notional = (quote_available if balance_total is None else balance_total) * safety
|
|
|
- return max(int(usable_notional / min_notional), 0)
|
|
|
|
|
|
|
+ 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 = self._base_symbol()
|
|
|
base_available = self._available_balance(base)
|
|
base_available = self._available_balance(base)
|
|
|
self.state["base_available"] = base_available
|
|
self.state["base_available"] = base_available
|
|
|
usable_base = base_available if balance_total is None else balance_total
|
|
usable_base = base_available if balance_total is None else balance_total
|
|
|
- usable_notional = usable_base * safety * price / (1 + fee_rate)
|
|
|
|
|
- return max(int(usable_notional / min_notional), 0)
|
|
|
|
|
|
|
+ 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:
|
|
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()
|
|
@@ -644,6 +692,15 @@ class Strategy(Strategy):
|
|
|
order_size=0.0,
|
|
order_size=0.0,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ def _grid_extreme_price(self, center: float, side: str, levels: int) -> float:
|
|
|
|
|
+ step_profile = self._effective_grid_steps(center)
|
|
|
|
|
+ step = float(step_profile.get(side) or step_profile.get("base") or 0.0)
|
|
|
|
|
+ if center <= 0 or levels <= 0 or step <= 0:
|
|
|
|
|
+ return center
|
|
|
|
|
+ if side == "buy":
|
|
|
|
|
+ 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:
|
|
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:
|
|
if expected_levels <= 0 or center <= 0 or balance_total <= 0:
|
|
|
return 0
|
|
return 0
|
|
@@ -663,41 +720,48 @@ class Strategy(Strategy):
|
|
|
if amounts:
|
|
if amounts:
|
|
|
amount = sum(amounts) / len(amounts)
|
|
amount = sum(amounts) / len(amounts)
|
|
|
|
|
|
|
|
- if amount <= 0:
|
|
|
|
|
- amount = self._suggest_amount(side, center, max(expected_levels, 1), min_notional)
|
|
|
|
|
|
|
+ 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:
|
|
if amount <= 0:
|
|
|
return 0
|
|
return 0
|
|
|
|
|
|
|
|
- fee_rate = self._live_fee_rate()
|
|
|
|
|
- safety = 0.995
|
|
|
|
|
|
|
+ free_balance = max(balance_total - 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(
|
|
|
|
|
+ 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
|
|
|
|
|
+
|
|
|
step_profile = self._effective_grid_steps(center)
|
|
step_profile = self._effective_grid_steps(center)
|
|
|
step = float(step_profile.get(side) or step_profile.get("base") or 0.0)
|
|
step = float(step_profile.get(side) or step_profile.get("base") or 0.0)
|
|
|
- spendable_total = balance_total * safety
|
|
|
|
|
|
|
+ if step <= 0:
|
|
|
|
|
+ return 0
|
|
|
|
|
|
|
|
- for level_count in range(expected_levels, 0, -1):
|
|
|
|
|
- feasible = True
|
|
|
|
|
- if side == "buy":
|
|
|
|
|
- needed = 0.0
|
|
|
|
|
- for i in range(1, level_count + 1):
|
|
|
|
|
- level_price = center * (1 - (step * i))
|
|
|
|
|
- min_size = (min_notional / level_price) if level_price > 0 and min_notional > 0 else 0.0
|
|
|
|
|
- if amount < min_size:
|
|
|
|
|
- feasible = False
|
|
|
|
|
- break
|
|
|
|
|
- needed += amount * level_price * (1 + fee_rate)
|
|
|
|
|
|
|
+ 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:
|
|
else:
|
|
|
- needed = amount * level_count
|
|
|
|
|
- for i in range(1, level_count + 1):
|
|
|
|
|
- level_price = center * (1 + (step * i))
|
|
|
|
|
- min_size = (min_notional / level_price) if level_price > 0 and min_notional > 0 else 0.0
|
|
|
|
|
- if amount < min_size:
|
|
|
|
|
- feasible = False
|
|
|
|
|
- break
|
|
|
|
|
-
|
|
|
|
|
- if feasible and needed <= spendable_total + 1e-9:
|
|
|
|
|
- return level_count
|
|
|
|
|
|
|
+ break
|
|
|
|
|
|
|
|
- return 0
|
|
|
|
|
|
|
+ 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)
|
|
@@ -716,142 +780,46 @@ 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 result.get("bitstamp_order_id") or result.get("order_id") or result.get("id") or result.get("client_order_id")
|
|
|
return None
|
|
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)
|
|
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)
|
|
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)
|
|
|
- buy_amount = self._suggest_amount("buy", center, max(buy_levels, 1), min_notional)
|
|
|
|
|
- sell_amount = self._suggest_amount("sell", center, max(sell_levels, 1), min_notional)
|
|
|
|
|
-
|
|
|
|
|
- for i in range(1, levels + 1):
|
|
|
|
|
- buy_price = round(center * (1 - (buy_step * i)), 8)
|
|
|
|
|
- sell_price = round(center * (1 + (sell_step * i)), 8)
|
|
|
|
|
- if mode != "active":
|
|
|
|
|
- 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}})
|
|
|
|
|
- self._log(f"plan level {i}: buy {buy_price} amount {buy_amount:.6g} / sell {sell_price} amount {sell_amount:.6g}")
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- if i > buy_levels and i > sell_levels:
|
|
|
|
|
- self._log(f"skip level {i}: no capacity on either side")
|
|
|
|
|
- continue
|
|
|
|
|
|
|
|
|
|
- min_size_buy = (min_notional / buy_price) if buy_price > 0 else 0.0
|
|
|
|
|
- min_size_sell = (min_notional / sell_price) if sell_price > 0 else 0.0
|
|
|
|
|
-
|
|
|
|
|
- buy_error = None
|
|
|
|
|
- sell_error = None
|
|
|
|
|
- placed_any = False
|
|
|
|
|
-
|
|
|
|
|
- if i <= buy_levels and buy_amount >= min_size_buy:
|
|
|
|
|
- try:
|
|
|
|
|
- buy = self.context.place_order(side="buy", order_type="limit", amount=buy_amount, price=buy_price, market=market)
|
|
|
|
|
- orders.append({"side": "buy", "price": buy_price, "amount": buy_amount, "result": buy})
|
|
|
|
|
- buy_id = _capture_order_id(buy)
|
|
|
|
|
- if buy_id is not None:
|
|
|
|
|
- order_ids.append(str(buy_id))
|
|
|
|
|
- placed_any = True
|
|
|
|
|
- except Exception as exc: # best effort for first draft
|
|
|
|
|
- buy_error = str(exc)
|
|
|
|
|
- self.state["last_error"] = buy_error
|
|
|
|
|
- self._log(f"seed level {i} buy failed: {exc}")
|
|
|
|
|
-
|
|
|
|
|
- if i <= sell_levels and sell_amount >= min_size_sell:
|
|
|
|
|
- try:
|
|
|
|
|
- sell = self.context.place_order(side="sell", order_type="limit", amount=sell_amount, price=sell_price, market=market)
|
|
|
|
|
- orders.append({"side": "sell", "price": sell_price, "amount": sell_amount, "result": sell})
|
|
|
|
|
- sell_id = _capture_order_id(sell)
|
|
|
|
|
- if sell_id is not None:
|
|
|
|
|
- order_ids.append(str(sell_id))
|
|
|
|
|
- placed_any = True
|
|
|
|
|
- except Exception as exc: # best effort for first draft
|
|
|
|
|
- sell_error = str(exc)
|
|
|
|
|
- self.state["last_error"] = sell_error
|
|
|
|
|
- self._log(f"seed level {i} sell failed: {exc}")
|
|
|
|
|
-
|
|
|
|
|
- if placed_any:
|
|
|
|
|
- if buy_error or sell_error:
|
|
|
|
|
- self._log(
|
|
|
|
|
- f"seed level {i} partial success: buy_error={buy_error or 'none'} sell_error={sell_error or 'none'}"
|
|
|
|
|
- )
|
|
|
|
|
- else:
|
|
|
|
|
- self._log(f"seed level {i}: buy {buy_price} amount {buy_amount:.6g} / sell {sell_price} amount {sell_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()
|
|
|
|
|
- else:
|
|
|
|
|
- if buy_error or sell_error:
|
|
|
|
|
- self._log(
|
|
|
|
|
- f"seed level {i} failed: buy_error={buy_error or 'none'} sell_error={sell_error or 'none'}"
|
|
|
|
|
- )
|
|
|
|
|
- else:
|
|
|
|
|
- self._log(f"seed level {i} skipped: no order placed")
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- self.state["orders"] = orders
|
|
|
|
|
- self.state["order_ids"] = order_ids
|
|
|
|
|
- self.state["last_action"] = "seeded grid"
|
|
|
|
|
- self._set_grid_refresh_pause()
|
|
|
|
|
-
|
|
|
|
|
- def _top_up_grid(self, center: float, live_orders: list[dict]) -> list[str]:
|
|
|
|
|
- center = self._maybe_refresh_center(center)
|
|
|
|
|
- levels = int(self.config.get("grid_levels", 6) or 6)
|
|
|
|
|
- if levels <= 0 or center <= 0:
|
|
|
|
|
- return []
|
|
|
|
|
-
|
|
|
|
|
- min_notional = float(self.context.minimum_order_value or 0.0)
|
|
|
|
|
- step_profile = self._effective_grid_steps(center)
|
|
|
|
|
- market = self._market_symbol()
|
|
|
|
|
- placed: list[str] = []
|
|
|
|
|
-
|
|
|
|
|
- live_by_side: dict[str, int] = {"buy": 0, "sell": 0}
|
|
|
|
|
- for order in live_orders:
|
|
|
|
|
- if not isinstance(order, dict):
|
|
|
|
|
- continue
|
|
|
|
|
- side = str(order.get("side") or "").lower()
|
|
|
|
|
- if side in live_by_side:
|
|
|
|
|
- live_by_side[side] += 1
|
|
|
|
|
-
|
|
|
|
|
- for side in ("buy", "sell"):
|
|
|
|
|
- live_count = live_by_side.get(side, 0)
|
|
|
|
|
- max_levels = self._supported_levels(side, center, min_notional)
|
|
|
|
|
- target_levels = min(levels, max_levels)
|
|
|
|
|
- if target_levels <= live_count:
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- 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):
|
|
|
|
|
- price = round(center * (1 - (side_step * i)) if side == "buy" else center * (1 + (side_step * i)), 8)
|
|
|
|
|
|
|
+ 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
|
|
min_size = (min_notional / price) if price > 0 else 0.0
|
|
|
- if amount < min_size:
|
|
|
|
|
- self._log_decision(
|
|
|
|
|
- f"skip top-up {side} level {i}",
|
|
|
|
|
- reason="below_min_size",
|
|
|
|
|
- amount=f"{amount:.6g}",
|
|
|
|
|
- min_size=f"{min_size:.6g}",
|
|
|
|
|
- price=price,
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ 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
|
|
continue
|
|
|
try:
|
|
try:
|
|
|
- self._log_decision(f"top-up {side} level {i}", price=price, amount=f"{amount:.6g}")
|
|
|
|
|
- result = self.context.place_order(side=side, order_type="limit", amount=amount, price=price, market=market)
|
|
|
|
|
- order_id = None
|
|
|
|
|
- if isinstance(result, dict):
|
|
|
|
|
- order_id = result.get("bitstamp_order_id") or result.get("order_id") or result.get("id") or result.get("client_order_id")
|
|
|
|
|
|
|
+ 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:
|
|
if order_id is not None:
|
|
|
- placed.append(str(order_id))
|
|
|
|
|
- live_orders.append({"side": side, "price": price, "amount": amount, "result": result})
|
|
|
|
|
|
|
+ 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()
|
|
self._refresh_balance_snapshot()
|
|
|
- except Exception as exc:
|
|
|
|
|
|
|
+ except Exception as exc: # best effort for first draft
|
|
|
self.state["last_error"] = str(exc)
|
|
self.state["last_error"] = str(exc)
|
|
|
- self._log_decision(f"top-up {side} level {i} failed", error=str(exc))
|
|
|
|
|
- continue
|
|
|
|
|
|
|
+ self._log(f"seed level {i} {side} failed: {exc}")
|
|
|
|
|
|
|
|
- delay = max(int(self.config.get("order_call_delay_ms", 250) or 0), 0) / 1000.0
|
|
|
|
|
- if delay > 0:
|
|
|
|
|
- time.sleep(delay)
|
|
|
|
|
|
|
+ _place_side("buy", buy_levels, buy_amount, buy_step)
|
|
|
|
|
+ _place_side("sell", sell_levels, sell_amount, sell_step)
|
|
|
|
|
|
|
|
- return placed
|
|
|
|
|
|
|
+ self.state["orders"] = orders
|
|
|
|
|
+ self.state["order_ids"] = order_ids
|
|
|
|
|
+ self.state["last_action"] = "seeded grid"
|
|
|
|
|
+ self._set_grid_refresh_pause()
|
|
|
|
|
|
|
|
def _current_market_anchor(self, fallback: float = 0.0) -> float:
|
|
def _current_market_anchor(self, fallback: float = 0.0) -> float:
|
|
|
try:
|
|
try:
|
|
@@ -883,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()
|
|
|
|
|
|
|
@@ -1095,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")
|
|
|
|
|
|
|
@@ -1134,7 +1111,7 @@ class Strategy(Strategy):
|
|
|
|
|
|
|
|
if balance_shape_inconclusive:
|
|
if balance_shape_inconclusive:
|
|
|
self._log("balance info not conclusive, skipping grid shape rebuild checks this tick")
|
|
self._log("balance info not conclusive, skipping grid shape rebuild checks this tick")
|
|
|
- elif grid_not_as_expected and can_make_better and not self._grid_refresh_paused():
|
|
|
|
|
|
|
+ elif grid_not_as_expected and can_make_better:
|
|
|
if rebuild_done:
|
|
if rebuild_done:
|
|
|
return {"action": "hold", "price": price}
|
|
return {"action": "hold", "price": price}
|
|
|
self._log(
|
|
self._log(
|
|
@@ -1170,11 +1147,12 @@ 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}
|
|
|
|
|
|
|
|
- if not balance_shape_inconclusive and ((open_order_count == 0) or missing_tracked) and not self._grid_refresh_paused():
|
|
|
|
|
|
|
+ if not balance_shape_inconclusive and ((open_order_count == 0) or missing_tracked):
|
|
|
if rebuild_done:
|
|
if rebuild_done:
|
|
|
return {"action": "hold", "price": price}
|
|
return {"action": "hold", "price": price}
|
|
|
self._log("missing tracked order(s), rebuilding full grid")
|
|
self._log("missing tracked order(s), rebuilding full grid")
|