|
@@ -9,7 +9,7 @@ from src.trader_mcp.logging_utils import log_event
|
|
|
|
|
|
|
|
class Strategy(Strategy):
|
|
class Strategy(Strategy):
|
|
|
LABEL = "Grid Trader"
|
|
LABEL = "Grid Trader"
|
|
|
- TICK_MINUTES = 1.0
|
|
|
|
|
|
|
+ TICK_MINUTES = 0.50
|
|
|
CONFIG_SCHEMA = {
|
|
CONFIG_SCHEMA = {
|
|
|
"grid_levels": {"type": "int", "default": 6, "min": 1, "max": 20},
|
|
"grid_levels": {"type": "int", "default": 6, "min": 1, "max": 20},
|
|
|
"grid_step_pct": {"type": "float", "default": 0.012, "min": 0.001, "max": 0.1},
|
|
"grid_step_pct": {"type": "float", "default": 0.012, "min": 0.001, "max": 0.1},
|
|
@@ -18,12 +18,10 @@ class Strategy(Strategy):
|
|
|
"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},
|
|
|
"order_size": {"type": "float", "default": 0.0, "min": 0.0},
|
|
"order_size": {"type": "float", "default": 0.0, "min": 0.0},
|
|
|
- "inventory_cap_pct": {"type": "float", "default": 0.7, "min": 0.0, "max": 1.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},
|
|
|
"recenter_atr_multiplier": {"type": "float", "default": 0.35, "min": 0.0, "max": 10.0},
|
|
"recenter_atr_multiplier": {"type": "float", "default": 0.35, "min": 0.0, "max": 10.0},
|
|
|
"recenter_min_pct": {"type": "float", "default": 0.0025, "min": 0.0, "max": 0.5},
|
|
"recenter_min_pct": {"type": "float", "default": 0.0025, "min": 0.0, "max": 0.5},
|
|
|
"recenter_max_pct": {"type": "float", "default": 0.03, "min": 0.0, "max": 0.5},
|
|
"recenter_max_pct": {"type": "float", "default": 0.03, "min": 0.0, "max": 0.5},
|
|
|
- "center_shift_factor": {"type": "float", "default": 0.3333333333, "min": 0.0, "max": 1.0},
|
|
|
|
|
"fee_rate": {"type": "float", "default": 0.0025, "min": 0.0, "max": 0.05},
|
|
"fee_rate": {"type": "float", "default": 0.0025, "min": 0.0, "max": 0.05},
|
|
|
"trade_sides": {"type": "string", "default": "both"},
|
|
"trade_sides": {"type": "string", "default": "both"},
|
|
|
"max_notional_per_order": {"type": "float", "default": 0.0, "min": 0.0},
|
|
"max_notional_per_order": {"type": "float", "default": 0.0, "min": 0.0},
|
|
@@ -32,7 +30,6 @@ class Strategy(Strategy):
|
|
|
"enable_trend_guard": {"type": "bool", "default": True},
|
|
"enable_trend_guard": {"type": "bool", "default": True},
|
|
|
"trend_guard_reversal_max": {"type": "float", "default": 0.25, "min": 0.0, "max": 1.0},
|
|
"trend_guard_reversal_max": {"type": "float", "default": 0.25, "min": 0.0, "max": 1.0},
|
|
|
"debug_orders": {"type": "bool", "default": True},
|
|
"debug_orders": {"type": "bool", "default": True},
|
|
|
- "use_all_available": {"type": "bool", "default": True},
|
|
|
|
|
}
|
|
}
|
|
|
STATE_SCHEMA = {
|
|
STATE_SCHEMA = {
|
|
|
"center_price": {"type": "float", "default": 0.0},
|
|
"center_price": {"type": "float", "default": 0.0},
|
|
@@ -338,7 +335,7 @@ class Strategy(Strategy):
|
|
|
updated_at=now_iso,
|
|
updated_at=now_iso,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- def _supported_levels(self, side: str, price: float, min_notional: float) -> int:
|
|
|
|
|
|
|
+ def _supported_levels(self, side: str, price: float, min_notional: float, *, balance_total: float | None = None) -> int:
|
|
|
if min_notional <= 0 or price <= 0:
|
|
if min_notional <= 0 or price <= 0:
|
|
|
return 0
|
|
return 0
|
|
|
safety = 0.995
|
|
safety = 0.995
|
|
@@ -347,13 +344,14 @@ class Strategy(Strategy):
|
|
|
quote = self.context.counter_currency or "USD"
|
|
quote = self.context.counter_currency or "USD"
|
|
|
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 * safety
|
|
|
|
|
|
|
+ usable_notional = (quote_available if balance_total is None else balance_total) * safety
|
|
|
return max(int(usable_notional / min_notional), 0)
|
|
return max(int(usable_notional / min_notional), 0)
|
|
|
|
|
|
|
|
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_notional = base_available * safety * price / (1 + fee_rate)
|
|
|
|
|
|
|
+ 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)
|
|
return max(int(usable_notional / min_notional), 0)
|
|
|
|
|
|
|
|
def _side_allowed(self, side: str) -> bool:
|
|
def _side_allowed(self, side: str) -> bool:
|
|
@@ -379,10 +377,63 @@ class Strategy(Strategy):
|
|
|
fee_rate=self._live_fee_rate(),
|
|
fee_rate=self._live_fee_rate(),
|
|
|
max_notional_per_order=float(self.config.get("max_notional_per_order", 0.0) or 0.0),
|
|
max_notional_per_order=float(self.config.get("max_notional_per_order", 0.0) or 0.0),
|
|
|
dust_collect=bool(self.config.get("dust_collect", False)),
|
|
dust_collect=bool(self.config.get("dust_collect", False)),
|
|
|
- inventory_cap_pct=float(self.config.get("inventory_cap_pct", 0.0) or 0.0),
|
|
|
|
|
order_size=float(self.config.get("order_size", 0.0) or 0.0),
|
|
order_size=float(self.config.get("order_size", 0.0) or 0.0),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ 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)
|
|
|
|
|
+
|
|
|
|
|
+ if amount <= 0:
|
|
|
|
|
+ amount = self._suggest_amount(side, center, max(expected_levels, 1), min_notional)
|
|
|
|
|
+ if amount <= 0:
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ fee_rate = self._live_fee_rate()
|
|
|
|
|
+ safety = 0.995
|
|
|
|
|
+ step = self._grid_step_pct()
|
|
|
|
|
+ spendable_total = balance_total * safety
|
|
|
|
|
+
|
|
|
|
|
+ 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)
|
|
|
|
|
+ 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
|
|
|
|
|
+
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
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)
|
|
|
mode = self._mode()
|
|
mode = self._mode()
|
|
@@ -441,207 +492,71 @@ class Strategy(Strategy):
|
|
|
delay = max(int(self.config.get("order_call_delay_ms", 250) or 0), 0) / 1000.0
|
|
delay = max(int(self.config.get("order_call_delay_ms", 250) or 0), 0) / 1000.0
|
|
|
if delay > 0:
|
|
if delay > 0:
|
|
|
time.sleep(delay)
|
|
time.sleep(delay)
|
|
|
|
|
+ self._refresh_balance_snapshot()
|
|
|
|
|
|
|
|
self.state["orders"] = orders
|
|
self.state["orders"] = orders
|
|
|
self.state["order_ids"] = order_ids
|
|
self.state["order_ids"] = order_ids
|
|
|
self.state["last_action"] = "seeded grid"
|
|
self.state["last_action"] = "seeded grid"
|
|
|
self._set_grid_refresh_pause()
|
|
self._set_grid_refresh_pause()
|
|
|
|
|
|
|
|
- def _place_side_grid(self, side: str, center: float, *, start_level: int = 1) -> None:
|
|
|
|
|
|
|
+ def _top_up_grid(self, center: float, live_orders: list[dict]) -> list[str]:
|
|
|
center = self._maybe_refresh_center(center)
|
|
center = self._maybe_refresh_center(center)
|
|
|
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()
|
|
|
|
|
|
|
+ if levels <= 0 or center <= 0:
|
|
|
|
|
+ return []
|
|
|
|
|
+
|
|
|
min_notional = float(self.context.minimum_order_value or 0.0)
|
|
min_notional = float(self.context.minimum_order_value or 0.0)
|
|
|
- fee_rate = self._live_fee_rate()
|
|
|
|
|
- safety = 0.995
|
|
|
|
|
|
|
+ step = self._grid_step_pct()
|
|
|
market = self._market_symbol()
|
|
market = self._market_symbol()
|
|
|
- orders = list(self.state.get("orders") or [])
|
|
|
|
|
- order_ids = list(self.state.get("order_ids") or [])
|
|
|
|
|
- placement_levels = max(levels - max(start_level, 1) + 1, 0)
|
|
|
|
|
-
|
|
|
|
|
- side_levels = min(placement_levels, self._supported_levels(side, center, min_notional))
|
|
|
|
|
- amount = self._suggest_amount(side, center, max(side_levels, 1), min_notional)
|
|
|
|
|
-
|
|
|
|
|
- if side == "buy":
|
|
|
|
|
- quote = self.context.counter_currency or "USD"
|
|
|
|
|
- quote_available = self._available_balance(quote)
|
|
|
|
|
- max_affordable_amount = (quote_available * safety) / (center * (1 + fee_rate)) if center > 0 else 0.0
|
|
|
|
|
- min_amount = (min_notional / center) if center > 0 and min_notional > 0 else 0.0
|
|
|
|
|
- if max_affordable_amount < min_amount:
|
|
|
|
|
- self._log_decision(
|
|
|
|
|
- f"skip side {side}",
|
|
|
|
|
- reason="insufficient_counter_balance",
|
|
|
|
|
- quote=f"{quote_available:.6g}",
|
|
|
|
|
- max_affordable_amount=f"{max_affordable_amount:.6g}",
|
|
|
|
|
- min_amount=f"{min_amount:.6g}",
|
|
|
|
|
- fee_rate=f"{fee_rate:.6g}",
|
|
|
|
|
- )
|
|
|
|
|
- return
|
|
|
|
|
- amount = min(amount, max_affordable_amount)
|
|
|
|
|
-
|
|
|
|
|
- if side_levels <= 0 and min_notional > 0 and center > 0:
|
|
|
|
|
- min_amount = min_notional / center
|
|
|
|
|
- if amount >= min_amount:
|
|
|
|
|
- side_levels = 1
|
|
|
|
|
- self._log(f"side {side} restored to 1 level because amount clears minimum: amount={amount:.6g} min_amount={min_amount:.6g}")
|
|
|
|
|
- self._log(
|
|
|
|
|
- f"prepare side {side}, market={market}, center={center}, levels={side_levels}, start_level={start_level}, amount={amount:.6g}, min_notional={min_notional}, existing_ids={order_ids}"
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- for i in range(start_level, 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
|
|
|
|
|
- relative_level = i - start_level + 1
|
|
|
|
|
- if relative_level > side_levels or amount < min_size:
|
|
|
|
|
- self._log_decision(
|
|
|
|
|
- f"skip side {side} level {i}",
|
|
|
|
|
- reason="below_min_size",
|
|
|
|
|
- amount=f"{amount:.6g}",
|
|
|
|
|
- min_size=f"{min_size:.6g}",
|
|
|
|
|
- min_notional=min_notional,
|
|
|
|
|
- price=price,
|
|
|
|
|
- )
|
|
|
|
|
- continue
|
|
|
|
|
- try:
|
|
|
|
|
- self._log_decision(f"place side {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)
|
|
|
|
|
- status = None
|
|
|
|
|
- order_id = None
|
|
|
|
|
- if isinstance(result, dict):
|
|
|
|
|
- status = result.get("status")
|
|
|
|
|
- order_id = result.get("bitstamp_order_id") or result.get("order_id") or result.get("id") or result.get("client_order_id")
|
|
|
|
|
- self._log_decision(f"place side {side} level {i} result", status=status, order_id=order_id)
|
|
|
|
|
- orders.append({"side": side, "price": price, "amount": amount, "result": result})
|
|
|
|
|
- if order_id is not None:
|
|
|
|
|
- order_ids.append(str(order_id))
|
|
|
|
|
- self._log_decision(f"seed side {side} level {i}", price=price, amount=f"{amount:.6g}")
|
|
|
|
|
- except Exception as exc:
|
|
|
|
|
- self.state["last_error"] = str(exc)
|
|
|
|
|
- self._log_decision(f"seed side {side} level {i} failed", error=str(exc))
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- delay = max(int(self.config.get("order_call_delay_ms", 250) or 0), 0) / 1000.0
|
|
|
|
|
- if delay > 0:
|
|
|
|
|
- time.sleep(delay)
|
|
|
|
|
-
|
|
|
|
|
- self.state["orders"] = orders
|
|
|
|
|
- self.state["order_ids"] = order_ids
|
|
|
|
|
- self._log_decision(f"side {side} placement complete", tracked_ids=order_ids)
|
|
|
|
|
- self._set_grid_refresh_pause()
|
|
|
|
|
|
|
+ placed: list[str] = []
|
|
|
|
|
|
|
|
- def _top_up_missing_levels(self, center: float, live_orders: list[dict]) -> None:
|
|
|
|
|
- center = self._maybe_refresh_center(center)
|
|
|
|
|
- target_levels = int(self.config.get("grid_levels", 6) or 6)
|
|
|
|
|
- if target_levels <= 0:
|
|
|
|
|
- return
|
|
|
|
|
- for side in ("buy", "sell"):
|
|
|
|
|
- count = 0
|
|
|
|
|
- for order in live_orders:
|
|
|
|
|
- if not isinstance(order, dict):
|
|
|
|
|
- continue
|
|
|
|
|
- if str(order.get("side") or "").lower() == side:
|
|
|
|
|
- count += 1
|
|
|
|
|
- if 0 < count < target_levels:
|
|
|
|
|
- self._log(f"top up side {side}: have {count}, want {target_levels}")
|
|
|
|
|
- self._place_side_grid(side, center, start_level=count + 1)
|
|
|
|
|
-
|
|
|
|
|
- def _cancel_obsolete_side_orders(self, open_orders: list[dict], desired_sides: set[str]) -> list[str]:
|
|
|
|
|
- removed: list[str] = []
|
|
|
|
|
- for order in open_orders:
|
|
|
|
|
|
|
+ live_by_side: dict[str, int] = {"buy": 0, "sell": 0}
|
|
|
|
|
+ for order in live_orders:
|
|
|
if not isinstance(order, dict):
|
|
if not isinstance(order, dict):
|
|
|
continue
|
|
continue
|
|
|
side = str(order.get("side") or "").lower()
|
|
side = str(order.get("side") or "").lower()
|
|
|
- order_id = str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "")
|
|
|
|
|
- if not order_id or side in desired_sides:
|
|
|
|
|
- continue
|
|
|
|
|
- try:
|
|
|
|
|
- self.context.cancel_order(order_id)
|
|
|
|
|
- removed.append(order_id)
|
|
|
|
|
- self._log(f"cancelled obsolete {side} order {order_id}")
|
|
|
|
|
- except Exception as exc:
|
|
|
|
|
- self.state["last_error"] = str(exc)
|
|
|
|
|
- self._log(f"cancel obsolete {side} order {order_id} failed: {exc}")
|
|
|
|
|
- return removed
|
|
|
|
|
|
|
+ if side in live_by_side:
|
|
|
|
|
+ live_by_side[side] += 1
|
|
|
|
|
|
|
|
- def _cancel_surplus_side_orders(self, open_orders: list[dict], target_levels: int) -> list[str]:
|
|
|
|
|
- removed: list[str] = []
|
|
|
|
|
- if target_levels <= 0:
|
|
|
|
|
- return removed
|
|
|
|
|
for side in ("buy", "sell"):
|
|
for side in ("buy", "sell"):
|
|
|
- side_orders = [order for order in open_orders if isinstance(order, dict) and str(order.get("side") or "").lower() == side]
|
|
|
|
|
- if len(side_orders) <= target_levels:
|
|
|
|
|
|
|
+ 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
|
|
continue
|
|
|
- surplus = side_orders[target_levels:]
|
|
|
|
|
- for order in surplus:
|
|
|
|
|
- order_id = str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "")
|
|
|
|
|
- if not order_id:
|
|
|
|
|
|
|
+
|
|
|
|
|
+ amount = self._suggest_amount(side, center, max(target_levels, 1), min_notional)
|
|
|
|
|
+ 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)
|
|
|
|
|
+ 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,
|
|
|
|
|
+ )
|
|
|
continue
|
|
continue
|
|
|
try:
|
|
try:
|
|
|
- self.context.cancel_order(order_id)
|
|
|
|
|
- removed.append(order_id)
|
|
|
|
|
- self._log(f"cancelled surplus {side} order {order_id}")
|
|
|
|
|
|
|
+ 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")
|
|
|
|
|
+ if order_id is not None:
|
|
|
|
|
+ placed.append(str(order_id))
|
|
|
|
|
+ live_orders.append({"side": side, "price": price, "amount": amount, "result": result})
|
|
|
|
|
+ self._refresh_balance_snapshot()
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
self.state["last_error"] = str(exc)
|
|
self.state["last_error"] = str(exc)
|
|
|
- self._log(f"cancel surplus {side} order {order_id} failed: {exc}")
|
|
|
|
|
- return removed
|
|
|
|
|
|
|
+ self._log_decision(f"top-up {side} level {i} failed", error=str(exc))
|
|
|
|
|
+ continue
|
|
|
|
|
|
|
|
- def _cancel_duplicate_level_orders(self, open_orders: list[dict]) -> list[str]:
|
|
|
|
|
- removed: list[str] = []
|
|
|
|
|
- seen: set[tuple[str, str]] = set()
|
|
|
|
|
- for order in open_orders:
|
|
|
|
|
- if not isinstance(order, dict):
|
|
|
|
|
- continue
|
|
|
|
|
- side = str(order.get("side") or "").lower()
|
|
|
|
|
- try:
|
|
|
|
|
- price_key = f"{float(order.get('price') or 0.0):.8f}"
|
|
|
|
|
- except Exception:
|
|
|
|
|
- price_key = str(order.get("price") or "")
|
|
|
|
|
- key = (side, price_key)
|
|
|
|
|
- order_id = str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or "")
|
|
|
|
|
- if not order_id:
|
|
|
|
|
- continue
|
|
|
|
|
- if key in seen:
|
|
|
|
|
- try:
|
|
|
|
|
- self.context.cancel_order(order_id)
|
|
|
|
|
- removed.append(order_id)
|
|
|
|
|
- self._log(f"cancelled duplicate {side} level order {order_id} price={price_key}")
|
|
|
|
|
- except Exception as exc:
|
|
|
|
|
- self.state["last_error"] = str(exc)
|
|
|
|
|
- self._log(f"cancel duplicate {side} order {order_id} failed: {exc}")
|
|
|
|
|
- continue
|
|
|
|
|
- seen.add(key)
|
|
|
|
|
- return removed
|
|
|
|
|
|
|
+ delay = max(int(self.config.get("order_call_delay_ms", 250) or 0), 0) / 1000.0
|
|
|
|
|
+ if delay > 0:
|
|
|
|
|
+ time.sleep(delay)
|
|
|
|
|
|
|
|
- def _place_replacement_orders(self, vanished_orders: list[dict], price_hint: float) -> list[str]:
|
|
|
|
|
- placed: list[str] = []
|
|
|
|
|
- if not vanished_orders:
|
|
|
|
|
- return placed
|
|
|
|
|
- market = self._market_symbol()
|
|
|
|
|
- for order in vanished_orders:
|
|
|
|
|
- if not isinstance(order, dict):
|
|
|
|
|
- continue
|
|
|
|
|
- side = str(order.get("side") or "").lower()
|
|
|
|
|
- opposite = "sell" if side == "buy" else "buy" if side == "sell" else ""
|
|
|
|
|
- if not opposite:
|
|
|
|
|
- continue
|
|
|
|
|
- try:
|
|
|
|
|
- amount = float(order.get("amount") or 0.0)
|
|
|
|
|
- price = float(order.get("price") or price_hint or 0.0)
|
|
|
|
|
- except Exception:
|
|
|
|
|
- continue
|
|
|
|
|
- if amount <= 0 or price <= 0:
|
|
|
|
|
- continue
|
|
|
|
|
- try:
|
|
|
|
|
- self._log(f"replace filled {side} order with {opposite}: price={price} amount={amount:.6g}")
|
|
|
|
|
- result = self.context.place_order(side=opposite, 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")
|
|
|
|
|
- if order_id is not None:
|
|
|
|
|
- placed.append(str(order_id))
|
|
|
|
|
- except Exception as exc:
|
|
|
|
|
- self.state["last_error"] = str(exc)
|
|
|
|
|
- self._log(f"replacement order failed for {side}→{opposite} at {price}: {exc}")
|
|
|
|
|
return placed
|
|
return placed
|
|
|
|
|
|
|
|
def _recenter_and_rebuild_from_fill(self, fill_price: float) -> None:
|
|
def _recenter_and_rebuild_from_fill(self, fill_price: float) -> None:
|
|
@@ -651,31 +566,37 @@ class Strategy(Strategy):
|
|
|
return
|
|
return
|
|
|
self._recenter_and_rebuild_from_price(fill_price, "fill rebuild")
|
|
self._recenter_and_rebuild_from_price(fill_price, "fill rebuild")
|
|
|
|
|
|
|
|
- def _shifted_center_price(self, current_center: float, price: float) -> float:
|
|
|
|
|
- if current_center <= 0:
|
|
|
|
|
- return price
|
|
|
|
|
- if price <= 0:
|
|
|
|
|
- return current_center
|
|
|
|
|
- factor = float(self.config.get("center_shift_factor", 1.0 / 3.0) or 0.0)
|
|
|
|
|
- factor = max(0.0, min(1.0, factor))
|
|
|
|
|
- return price + (current_center - price) * factor
|
|
|
|
|
-
|
|
|
|
|
def _recenter_and_rebuild_from_price(self, price: float, reason: str) -> None:
|
|
def _recenter_and_rebuild_from_price(self, price: float, reason: str) -> None:
|
|
|
- current = float(self.state.get("center_price") or 0.0)
|
|
|
|
|
if price <= 0:
|
|
if price <= 0:
|
|
|
return
|
|
return
|
|
|
- new_center = self._shifted_center_price(current, price)
|
|
|
|
|
- self._log(f"{reason}: shift center from {current} to {new_center} using price={price}")
|
|
|
|
|
|
|
+ current = float(self.state.get("center_price") or 0.0)
|
|
|
|
|
+ self._log(f"{reason}: recenter from {current} to {price}")
|
|
|
try:
|
|
try:
|
|
|
self.context.cancel_all_orders()
|
|
self.context.cancel_all_orders()
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
self.state["last_error"] = str(exc)
|
|
self.state["last_error"] = str(exc)
|
|
|
self._log(f"{reason} cancel-all failed: {exc}")
|
|
self._log(f"{reason} cancel-all failed: {exc}")
|
|
|
- self.state["center_price"] = new_center
|
|
|
|
|
|
|
+ # Give the exchange a moment to release balance before we rebuild.
|
|
|
|
|
+ time.sleep(3.0)
|
|
|
|
|
+ self._refresh_balance_snapshot()
|
|
|
|
|
+ self.state["center_price"] = price
|
|
|
self.state["seeded"] = True
|
|
self.state["seeded"] = True
|
|
|
- self._place_grid(new_center)
|
|
|
|
|
|
|
+ self._place_grid(price)
|
|
|
|
|
+ self._refresh_balance_snapshot()
|
|
|
self._set_grid_refresh_pause()
|
|
self._set_grid_refresh_pause()
|
|
|
|
|
|
|
|
|
|
+ def on_stop(self):
|
|
|
|
|
+ self._log("stopping: cancel all open orders")
|
|
|
|
|
+ try:
|
|
|
|
|
+ self.context.cancel_all_orders()
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ self.state["last_error"] = str(exc)
|
|
|
|
|
+ self._log(f"stop cancel-all failed: {exc}")
|
|
|
|
|
+ self.state["orders"] = []
|
|
|
|
|
+ self.state["order_ids"] = []
|
|
|
|
|
+ self.state["open_order_count"] = 0
|
|
|
|
|
+ self.state["last_action"] = "stopped"
|
|
|
|
|
+
|
|
|
def _maybe_refresh_center(self, price: float) -> float:
|
|
def _maybe_refresh_center(self, price: float) -> float:
|
|
|
if price <= 0:
|
|
if price <= 0:
|
|
|
return price
|
|
return price
|
|
@@ -776,23 +697,12 @@ class Strategy(Strategy):
|
|
|
self._log(f"vanished order {order_id} resolved as {status}")
|
|
self._log(f"vanished order {order_id} resolved as {status}")
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
- 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
|
|
return live_orders, live_ids, open_order_count
|
|
|
|
|
|
|
|
def on_tick(self, tick):
|
|
def on_tick(self, tick):
|
|
|
previous_orders = list(self.state.get("orders") or [])
|
|
previous_orders = list(self.state.get("orders") or [])
|
|
|
tracked_ids_before_sync = list(self.state.get("order_ids") or [])
|
|
tracked_ids_before_sync = list(self.state.get("order_ids") or [])
|
|
|
|
|
+ rebuild_done = False
|
|
|
self._refresh_balance_snapshot()
|
|
self._refresh_balance_snapshot()
|
|
|
price = self._price()
|
|
price = self._price()
|
|
|
self.state["last_price"] = price
|
|
self.state["last_price"] = price
|
|
@@ -803,7 +713,7 @@ class Strategy(Strategy):
|
|
|
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)
|
|
|
- expected_ids = [str(oid) for oid in (self.state.get("order_ids") or []) if oid]
|
|
|
|
|
|
|
+ expected_ids = [str(oid) for oid in tracked_ids_before_sync if oid]
|
|
|
stale_ids = []
|
|
stale_ids = []
|
|
|
missing_ids = []
|
|
missing_ids = []
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
@@ -834,21 +744,32 @@ class Strategy(Strategy):
|
|
|
return {"action": "guard", "price": price, "reason": guard_reason}
|
|
return {"action": "guard", "price": price, "reason": guard_reason}
|
|
|
|
|
|
|
|
if mode != "active":
|
|
if mode != "active":
|
|
|
|
|
+ if open_order_count > 0:
|
|
|
|
|
+ self._log("observe mode: cancel all open orders")
|
|
|
|
|
+ try:
|
|
|
|
|
+ self.context.cancel_all_orders()
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ self.state["last_error"] = str(exc)
|
|
|
|
|
+ self._log(f"observe cancel failed: {exc}")
|
|
|
|
|
+ self.state["orders"] = []
|
|
|
|
|
+ self.state["order_ids"] = []
|
|
|
|
|
+ self.state["open_order_count"] = 0
|
|
|
|
|
+
|
|
|
if not self.state.get("seeded") or not self.state.get("center_price"):
|
|
if not self.state.get("seeded") or not self.state.get("center_price"):
|
|
|
self.state["center_price"] = price
|
|
self.state["center_price"] = price
|
|
|
- self._place_grid(price)
|
|
|
|
|
self.state["seeded"] = True
|
|
self.state["seeded"] = True
|
|
|
- self._log(f"planned grid at {price}")
|
|
|
|
|
- return {"action": "plan", "price": price}
|
|
|
|
|
|
|
+ self.state["last_action"] = "observe monitor"
|
|
|
|
|
+ self._log(f"observe at {price} dev 0.0000")
|
|
|
|
|
+ return {"action": "observe", "price": price, "deviation": 0.0}
|
|
|
|
|
|
|
|
center = float(self.state.get("center_price") or price)
|
|
center = float(self.state.get("center_price") or price)
|
|
|
recenter_pct = float(self.config.get("recenter_pct", 0.05) or 0.05)
|
|
recenter_pct = float(self.config.get("recenter_pct", 0.05) or 0.05)
|
|
|
deviation = abs(price - center) / center if center else 0.0
|
|
deviation = abs(price - center) / center if center else 0.0
|
|
|
if deviation >= recenter_pct:
|
|
if deviation >= recenter_pct:
|
|
|
self.state["center_price"] = price
|
|
self.state["center_price"] = price
|
|
|
- self._place_grid(price)
|
|
|
|
|
- self._log(f"planned recenter to {price}")
|
|
|
|
|
- return {"action": "plan", "price": price, "deviation": deviation}
|
|
|
|
|
|
|
+ self.state["last_action"] = "observe monitor"
|
|
|
|
|
+ self._log(f"observe at {price} dev {deviation:.4f}")
|
|
|
|
|
+ return {"action": "observe", "price": price, "deviation": deviation}
|
|
|
|
|
|
|
|
self.state["last_action"] = "observe monitor"
|
|
self.state["last_action"] = "observe monitor"
|
|
|
self._log(f"observe at {price} dev {deviation:.4f}")
|
|
self._log(f"observe at {price} dev {deviation:.4f}")
|
|
@@ -863,20 +784,16 @@ class Strategy(Strategy):
|
|
|
self._log(f"missing tracked orders: {missing_ids}")
|
|
self._log(f"missing tracked orders: {missing_ids}")
|
|
|
self.state["order_ids"] = live_ids
|
|
self.state["order_ids"] = live_ids
|
|
|
|
|
|
|
|
- if self._order_count_mismatch(tracked_ids_before_sync, live_orders):
|
|
|
|
|
- self.state["mismatch_ticks"] = int(self.state.get("mismatch_ticks") or 0) + 1
|
|
|
|
|
- self._log(f"order count mismatch detected: tracked={len(tracked_ids_before_sync)} live={len(live_orders)} ticks={self.state['mismatch_ticks']}")
|
|
|
|
|
- if self.state["mismatch_ticks"] >= 2 and not self._recovery_paused() and self._mode() == "active":
|
|
|
|
|
- self._recover_grid(price)
|
|
|
|
|
- return {"action": "recovery", "price": price}
|
|
|
|
|
- else:
|
|
|
|
|
- self.state["mismatch_ticks"] = 0
|
|
|
|
|
|
|
+ missing_tracked = bool(set(expected_ids) - set(live_ids))
|
|
|
|
|
|
|
|
center = self._maybe_refresh_center(float(self.state.get("center_price") or price))
|
|
center = self._maybe_refresh_center(float(self.state.get("center_price") or price))
|
|
|
recenter_pct = self._recenter_threshold_pct()
|
|
recenter_pct = self._recenter_threshold_pct()
|
|
|
deviation = abs(price - center) / center if center else 0.0
|
|
deviation = abs(price - center) / center if center else 0.0
|
|
|
if mode == "active" and deviation >= recenter_pct and not self._grid_refresh_paused():
|
|
if mode == "active" and deviation >= recenter_pct and not self._grid_refresh_paused():
|
|
|
|
|
+ if rebuild_done:
|
|
|
|
|
+ return {"action": "hold", "price": price}
|
|
|
self._log(f"recenter needed at price={price} center={center} dev={deviation:.4f} threshold={recenter_pct:.4f}")
|
|
self._log(f"recenter needed at price={price} center={center} dev={deviation:.4f} threshold={recenter_pct:.4f}")
|
|
|
|
|
+ rebuild_done = True
|
|
|
self._recenter_and_rebuild_from_price(price, "recenter")
|
|
self._recenter_and_rebuild_from_price(price, "recenter")
|
|
|
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 [])
|
|
@@ -887,40 +804,71 @@ 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 desired_sides != {"buy", "sell"}:
|
|
if desired_sides != {"buy", "sell"}:
|
|
|
- current_sides = {str(order.get("side") or "").lower() for order in live_orders if isinstance(order, dict)}
|
|
|
|
|
- missing_side = next((side for side in desired_sides if side not in current_sides), None)
|
|
|
|
|
- if missing_side and self.state.get("center_price"):
|
|
|
|
|
- self._log(f"adding missing {missing_side} side after trade_sides change, live_sides={sorted(current_sides)} live_ids={live_ids}")
|
|
|
|
|
- self._place_side_grid(missing_side, float(self.state.get("center_price") or price))
|
|
|
|
|
- live_orders = self._sync_open_orders_state()
|
|
|
|
|
- self._log(f"post-add sync: open_order_count={self.state.get('open_order_count', 0)} live_ids={self.state.get('order_ids') or []}")
|
|
|
|
|
- self.state["last_action"] = f"added {missing_side} side"
|
|
|
|
|
- return {"action": "add_side", "price": price, "side": missing_side}
|
|
|
|
|
-
|
|
|
|
|
- if desired_sides == {"buy", "sell"}:
|
|
|
|
|
- current_sides = {str(order.get("side") or "").lower() for order in live_orders if isinstance(order, dict)}
|
|
|
|
|
- tracked_sides = {str(order.get("side") or "").lower() for order in previous_orders if isinstance(order, dict)}
|
|
|
|
|
- missing_sides = [side for side in ("buy", "sell") if side not in current_sides]
|
|
|
|
|
- reconciled_sides: list[str] = []
|
|
|
|
|
- has_live_grid = bool(live_orders) or bool(live_ids) or bool(tracked_sides)
|
|
|
|
|
-
|
|
|
|
|
- # If the grid is empty because both sides were skipped, do not keep
|
|
|
|
|
- # trying to "restore" a missing side every tick. Let the normal
|
|
|
|
|
- # reseed path decide when to try again.
|
|
|
|
|
- if missing_sides and has_live_grid and self.state.get("center_price") and not self._grid_refresh_paused():
|
|
|
|
|
- for side in missing_sides:
|
|
|
|
|
- if current_sides or tracked_sides:
|
|
|
|
|
- self._log(f"adding missing {side} side, live_sides={sorted(current_sides)} tracked_sides={sorted(tracked_sides)} live_ids={live_ids}")
|
|
|
|
|
- self._place_side_grid(side, float(self.state.get("center_price") or price))
|
|
|
|
|
- reconciled_sides.append(side)
|
|
|
|
|
- live_orders = self._sync_open_orders_state()
|
|
|
|
|
- self._log(f"post-add sync: open_order_count={self.state.get('open_order_count', 0)} live_ids={self.state.get('order_ids') or []}")
|
|
|
|
|
- if live_orders and self.state.get("center_price") and not self._grid_refresh_paused():
|
|
|
|
|
- self._top_up_missing_levels(float(self.state.get("center_price") or price), live_orders)
|
|
|
|
|
- live_orders = self._sync_open_orders_state()
|
|
|
|
|
- if reconciled_sides:
|
|
|
|
|
- self.state["last_action"] = f"reconciled {','.join(reconciled_sides)}"
|
|
|
|
|
- return {"action": "reconcile", "price": price, "side": ",".join(reconciled_sides)}
|
|
|
|
|
|
|
+ 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
|
|
|
|
|
+ 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)
|
|
|
|
|
+
|
|
|
|
|
+ if grid_not_as_expected and can_make_better and not self._grid_refresh_paused():
|
|
|
|
|
+ if rebuild_done:
|
|
|
|
|
+ return {"action": "hold", "price": price}
|
|
|
|
|
+ self._log(
|
|
|
|
|
+ f"grid shape mismatch, rebuilding full grid: live_buy={current_buy} live_sell={current_sell} target_buy={target_buy} target_sell={target_sell}"
|
|
|
|
|
+ )
|
|
|
|
|
+ rebuild_done = True
|
|
|
|
|
+ self.state["center_price"] = price
|
|
|
|
|
+ self._recenter_and_rebuild_from_price(price, "grid shape rebuild")
|
|
|
|
|
+ live_orders = self._sync_open_orders_state()
|
|
|
|
|
+ mode = self._mode()
|
|
|
|
|
+ self.state["last_action"] = "reseeded" if mode == "active" else f"{mode} monitor"
|
|
|
|
|
+ return {"action": "reseed" if mode == "active" else "plan", "price": price}
|
|
|
|
|
+
|
|
|
|
|
+ if 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 self._order_count_mismatch(tracked_ids_before_sync, live_orders):
|
|
|
|
|
+ if rebuild_done:
|
|
|
|
|
+ return {"action": "hold", "price": price}
|
|
|
|
|
+ self._log(f"grid mismatch detected, rebuilding full grid: tracked={len(tracked_ids_before_sync)} live={len(live_orders)}")
|
|
|
|
|
+ rebuild_done = True
|
|
|
|
|
+ self.state["center_price"] = price
|
|
|
|
|
+ self._recenter_and_rebuild_from_price(price, "grid mismatch rebuild")
|
|
|
|
|
+ live_orders = self._sync_open_orders_state()
|
|
|
|
|
+ mode = self._mode()
|
|
|
|
|
+ self.state["last_action"] = "reseeded" if mode == "active" else f"{mode} monitor"
|
|
|
|
|
+ return {"action": "reseed" if mode == "active" else "plan", "price": price}
|
|
|
|
|
|
|
|
if (not self.state.get("seeded") or not self.state.get("center_price")) and not self._grid_refresh_paused():
|
|
if (not self.state.get("seeded") or not self.state.get("center_price")) and not self._grid_refresh_paused():
|
|
|
self.state["center_price"] = price
|
|
self.state["center_price"] = price
|
|
@@ -931,10 +879,13 @@ class Strategy(Strategy):
|
|
|
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 (open_order_count == 0 or (expected_ids and not set(expected_ids).intersection(set(live_ids)))) and not self._grid_refresh_paused():
|
|
|
|
|
- self._log("no open orders, reseeding grid")
|
|
|
|
|
|
|
+ if ((open_order_count == 0) or missing_tracked) and not self._grid_refresh_paused():
|
|
|
|
|
+ if rebuild_done:
|
|
|
|
|
+ return {"action": "hold", "price": price}
|
|
|
|
|
+ self._log("missing tracked order(s), rebuilding full grid")
|
|
|
|
|
+ rebuild_done = True
|
|
|
self.state["center_price"] = price
|
|
self.state["center_price"] = price
|
|
|
- self._place_grid(price)
|
|
|
|
|
|
|
+ self._recenter_and_rebuild_from_price(price, "missing order rebuild")
|
|
|
live_orders = self._sync_open_orders_state()
|
|
live_orders = self._sync_open_orders_state()
|
|
|
mode = self._mode()
|
|
mode = self._mode()
|
|
|
self.state["last_action"] = "reseeded" if mode == "active" else f"{mode} monitor"
|
|
self.state["last_action"] = "reseeded" if mode == "active" else f"{mode} monitor"
|