|
|
@@ -239,6 +239,13 @@ class Strategy(Strategy):
|
|
|
return {"buy", "sell"}
|
|
|
|
|
|
def _suggest_amount(self, side: str, price: float, levels: int, min_notional: float) -> float:
|
|
|
+ """Derive a per-order amount from the currently available balance.
|
|
|
+
|
|
|
+ This helper is used when the grid seeds, tops up, or replaces an order.
|
|
|
+ It folds in the live available balance, fee cushion, per-order caps, and
|
|
|
+ the exchange minimum notional. If the wallet cannot support a valid order,
|
|
|
+ it returns 0.0 instead of forcing an impossible minimum size.
|
|
|
+ """
|
|
|
if levels <= 0 or price <= 0:
|
|
|
return 0.0
|
|
|
safety = 0.995
|
|
|
@@ -251,13 +258,19 @@ class Strategy(Strategy):
|
|
|
quote_available = self._available_balance(quote)
|
|
|
self.state["counter_available"] = quote_available
|
|
|
spendable_quote = quote_available * safety
|
|
|
- amount = spendable_quote / (max(levels, 1) * price * (1 + fee_rate))
|
|
|
+ max_affordable = spendable_quote / (price * (1 + fee_rate))
|
|
|
+ if max_affordable < min_amount:
|
|
|
+ return 0.0
|
|
|
+ amount = min(spendable_quote / (max(levels, 1) * price * (1 + fee_rate)), max_affordable)
|
|
|
else:
|
|
|
base = self._base_symbol()
|
|
|
base_available = self._available_balance(base)
|
|
|
self.state["base_available"] = base_available
|
|
|
spendable_base = (base_available * safety) / (1 + fee_rate)
|
|
|
- amount = spendable_base / max(levels, 1)
|
|
|
+ max_affordable = spendable_base
|
|
|
+ if max_affordable < min_amount:
|
|
|
+ return 0.0
|
|
|
+ amount = min(spendable_base / max(levels, 1), max_affordable)
|
|
|
|
|
|
amount = max(amount, min_amount * 1.05)
|
|
|
if max_notional > 0 and price > 0:
|
|
|
@@ -543,6 +556,50 @@ class Strategy(Strategy):
|
|
|
for order_id in order_ids or []:
|
|
|
self._log(f"dropping stale order {order_id} from state")
|
|
|
|
|
|
+ def _reconcile_after_sync(self, previous_orders: list[dict], live_orders: list[dict], desired_sides: set[str], price: float) -> tuple[list[dict], list[str], int]:
|
|
|
+ live_ids = list(self.state.get("order_ids") or [])
|
|
|
+ open_order_count = len(live_ids)
|
|
|
+
|
|
|
+ if self._mode() != "active":
|
|
|
+ return live_orders, live_ids, open_order_count
|
|
|
+
|
|
|
+ previous_ids = {
|
|
|
+ str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "")
|
|
|
+ for order in previous_orders
|
|
|
+ if isinstance(order, dict)
|
|
|
+ }
|
|
|
+ current_ids = {
|
|
|
+ str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "")
|
|
|
+ for order in live_orders
|
|
|
+ if isinstance(order, dict)
|
|
|
+ }
|
|
|
+ vanished_orders = [
|
|
|
+ order
|
|
|
+ for order in previous_orders
|
|
|
+ if isinstance(order, dict)
|
|
|
+ and str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "") in (previous_ids - current_ids)
|
|
|
+ ]
|
|
|
+ if vanished_orders and not self._grid_refresh_paused():
|
|
|
+ replaced_ids = self._place_replacement_orders(vanished_orders, price)
|
|
|
+ if replaced_ids:
|
|
|
+ live_orders = self._sync_open_orders_state()
|
|
|
+ live_ids = list(self.state.get("order_ids") or [])
|
|
|
+ open_order_count = len(live_ids)
|
|
|
+
|
|
|
+ surplus_cancelled = self._cancel_surplus_side_orders(live_orders, int(self.config.get("grid_levels", 6) or 6))
|
|
|
+ duplicate_cancelled = self._cancel_duplicate_level_orders(live_orders)
|
|
|
+ if surplus_cancelled or duplicate_cancelled:
|
|
|
+ live_orders = self._sync_open_orders_state()
|
|
|
+ live_ids = list(self.state.get("order_ids") or [])
|
|
|
+ open_order_count = len(live_ids)
|
|
|
+
|
|
|
+ if desired_sides != {"buy", "sell"}:
|
|
|
+ live_orders = self._sync_open_orders_state()
|
|
|
+ live_ids = list(self.state.get("order_ids") or [])
|
|
|
+ open_order_count = len(live_ids)
|
|
|
+
|
|
|
+ return live_orders, live_ids, open_order_count
|
|
|
+
|
|
|
def on_tick(self, tick):
|
|
|
previous_orders = list(self.state.get("orders") or [])
|
|
|
self._refresh_balance_snapshot()
|
|
|
@@ -615,33 +672,7 @@ class Strategy(Strategy):
|
|
|
self._log(f"missing tracked orders: {missing_ids}")
|
|
|
self.state["order_ids"] = live_ids
|
|
|
|
|
|
- cancelled_obsolete = self._cancel_obsolete_side_orders(live_orders, desired_sides)
|
|
|
- if cancelled_obsolete:
|
|
|
- live_orders = self._sync_open_orders_state()
|
|
|
- live_ids = list(self.state.get("order_ids") or [])
|
|
|
- open_order_count = len(live_ids)
|
|
|
-
|
|
|
- previous_ids = {str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "") for order in previous_orders if isinstance(order, dict)}
|
|
|
- current_ids = {str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "") for order in live_orders if isinstance(order, dict)}
|
|
|
- vanished_orders = [order for order in previous_orders if isinstance(order, dict) and str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "") in (previous_ids - current_ids)]
|
|
|
- if vanished_orders and self._mode() == "active" and not self._grid_refresh_paused():
|
|
|
- replaced_ids = self._place_replacement_orders(vanished_orders, price)
|
|
|
- if replaced_ids:
|
|
|
- live_orders = self._sync_open_orders_state()
|
|
|
- live_ids = list(self.state.get("order_ids") or [])
|
|
|
- open_order_count = len(live_ids)
|
|
|
-
|
|
|
- surplus_cancelled = self._cancel_surplus_side_orders(live_orders, int(self.config.get("grid_levels", 6) or 6))
|
|
|
- if surplus_cancelled:
|
|
|
- live_orders = self._sync_open_orders_state()
|
|
|
- live_ids = list(self.state.get("order_ids") or [])
|
|
|
- open_order_count = len(live_ids)
|
|
|
-
|
|
|
- duplicate_cancelled = self._cancel_duplicate_level_orders(live_orders)
|
|
|
- if duplicate_cancelled:
|
|
|
- live_orders = self._sync_open_orders_state()
|
|
|
- live_ids = list(self.state.get("order_ids") or [])
|
|
|
- open_order_count = len(live_ids)
|
|
|
+ live_orders, live_ids, open_order_count = self._reconcile_after_sync(previous_orders, live_orders, desired_sides, price)
|
|
|
|
|
|
if desired_sides != {"buy", "sell"}:
|
|
|
current_sides = {str(order.get("side") or "").lower() for order in live_orders if isinstance(order, dict)}
|