|
@@ -1,6 +1,7 @@
|
|
|
from __future__ import annotations
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import time
|
|
import time
|
|
|
|
|
+from datetime import datetime, timezone
|
|
|
|
|
|
|
|
from src.trader_mcp.strategy_sdk import Strategy
|
|
from src.trader_mcp.strategy_sdk import Strategy
|
|
|
|
|
|
|
@@ -45,6 +46,8 @@ class Strategy(Strategy):
|
|
|
"base_available": {"type": "float", "default": 0.0},
|
|
"base_available": {"type": "float", "default": 0.0},
|
|
|
"counter_available": {"type": "float", "default": 0.0},
|
|
"counter_available": {"type": "float", "default": 0.0},
|
|
|
"trend_guard_active": {"type": "bool", "default": False},
|
|
"trend_guard_active": {"type": "bool", "default": False},
|
|
|
|
|
+ "regimes_updated_at": {"type": "string", "default": ""},
|
|
|
|
|
+ "account_snapshot_updated_at": {"type": "string", "default": ""},
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
def init(self):
|
|
def init(self):
|
|
@@ -60,6 +63,8 @@ class Strategy(Strategy):
|
|
|
"base_available": 0.0,
|
|
"base_available": 0.0,
|
|
|
"counter_available": 0.0,
|
|
"counter_available": 0.0,
|
|
|
"trend_guard_active": False,
|
|
"trend_guard_active": False,
|
|
|
|
|
+ "regimes_updated_at": "",
|
|
|
|
|
+ "account_snapshot_updated_at": "",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
def _log(self, message: str) -> None:
|
|
def _log(self, message: str) -> None:
|
|
@@ -92,6 +97,10 @@ class Strategy(Strategy):
|
|
|
snapshot[tf] = {"error": str(exc)}
|
|
snapshot[tf] = {"error": str(exc)}
|
|
|
return snapshot
|
|
return snapshot
|
|
|
|
|
|
|
|
|
|
+ def _refresh_regimes(self) -> None:
|
|
|
|
|
+ self.state["regimes"] = self._regime_snapshot()
|
|
|
|
|
+ self.state["regimes_updated_at"] = datetime.now(timezone.utc).isoformat()
|
|
|
|
|
+
|
|
|
def _trend_guard_status(self) -> tuple[bool, str]:
|
|
def _trend_guard_status(self) -> tuple[bool, str]:
|
|
|
if not bool(self.config.get("enable_trend_guard", True)):
|
|
if not bool(self.config.get("enable_trend_guard", True)):
|
|
|
return False, "disabled"
|
|
return False, "disabled"
|
|
@@ -159,6 +168,33 @@ class Strategy(Strategy):
|
|
|
return 0.0
|
|
return 0.0
|
|
|
return 0.0
|
|
return 0.0
|
|
|
|
|
|
|
|
|
|
+ def _refresh_balance_snapshot(self) -> None:
|
|
|
|
|
+ try:
|
|
|
|
|
+ info = self.context.get_account_info()
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ self._log(f"balance refresh failed: {exc}")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ balances = info.get("balances") if isinstance(info, dict) else []
|
|
|
|
|
+ if not isinstance(balances, list):
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ base = self._base_symbol()
|
|
|
|
|
+ quote = self.context.counter_currency or "USD"
|
|
|
|
|
+ for balance in balances:
|
|
|
|
|
+ if not isinstance(balance, dict):
|
|
|
|
|
+ continue
|
|
|
|
|
+ asset = str(balance.get("asset_code") or "").upper()
|
|
|
|
|
+ try:
|
|
|
|
|
+ available = float(balance.get("available") if balance.get("available") is not None else balance.get("total") or 0.0)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ continue
|
|
|
|
|
+ if asset == base:
|
|
|
|
|
+ self.state["base_available"] = available
|
|
|
|
|
+ if asset == str(quote).upper():
|
|
|
|
|
+ self.state["counter_available"] = available
|
|
|
|
|
+ self.state["account_snapshot_updated_at"] = datetime.now(timezone.utc).isoformat()
|
|
|
|
|
+
|
|
|
def _supported_levels(self, side: str, price: float, min_notional: float) -> int:
|
|
def _supported_levels(self, side: str, price: float, min_notional: float) -> int:
|
|
|
if min_notional <= 0 or price <= 0:
|
|
if min_notional <= 0 or price <= 0:
|
|
|
return 0
|
|
return 0
|
|
@@ -183,6 +219,14 @@ class Strategy(Strategy):
|
|
|
return True
|
|
return True
|
|
|
return selected == side
|
|
return selected == side
|
|
|
|
|
|
|
|
|
|
+ def _desired_sides(self) -> set[str]:
|
|
|
|
|
+ selected = str(self.config.get("trade_sides", "both") or "both").strip().lower()
|
|
|
|
|
+ if selected == "both":
|
|
|
|
|
+ return {"buy", "sell"}
|
|
|
|
|
+ if selected in {"buy", "sell"}:
|
|
|
|
|
+ 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) -> float:
|
|
|
if levels <= 0 or price <= 0:
|
|
if levels <= 0 or price <= 0:
|
|
|
return 0.0
|
|
return 0.0
|
|
@@ -190,6 +234,7 @@ class Strategy(Strategy):
|
|
|
fee_rate = float(self.config.get("fee_rate", 0.0025) or 0.0)
|
|
fee_rate = float(self.config.get("fee_rate", 0.0025) or 0.0)
|
|
|
max_notional = float(self.config.get("max_notional_per_order", 0.0) or 0.0)
|
|
max_notional = float(self.config.get("max_notional_per_order", 0.0) or 0.0)
|
|
|
manual = float(self.config.get("order_size", 0.0) or 0.0)
|
|
manual = float(self.config.get("order_size", 0.0) or 0.0)
|
|
|
|
|
+ min_amount = (min_notional / price) if min_notional > 0 else 0.0
|
|
|
if side == "buy":
|
|
if side == "buy":
|
|
|
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)
|
|
@@ -203,12 +248,16 @@ class Strategy(Strategy):
|
|
|
spendable_base = (base_available * safety) / (1 + fee_rate)
|
|
spendable_base = (base_available * safety) / (1 + fee_rate)
|
|
|
amount = spendable_base / max(levels, 1)
|
|
amount = spendable_base / max(levels, 1)
|
|
|
|
|
|
|
|
- min_size = (min_notional / price) if price > 0 else 0.0
|
|
|
|
|
- amount = max(amount, min_size * 1.05)
|
|
|
|
|
|
|
+ amount = max(amount, min_amount * 1.05)
|
|
|
if max_notional > 0 and price > 0:
|
|
if max_notional > 0 and price > 0:
|
|
|
amount = min(amount, max_notional / (price * (1 + fee_rate)))
|
|
amount = min(amount, max_notional / (price * (1 + fee_rate)))
|
|
|
if manual > 0:
|
|
if manual > 0:
|
|
|
- amount = min(amount, manual)
|
|
|
|
|
|
|
+ if manual >= min_amount:
|
|
|
|
|
+ amount = min(amount, manual)
|
|
|
|
|
+ else:
|
|
|
|
|
+ self._log(
|
|
|
|
|
+ f"manual order_size below minimum: order_size={manual:.6g} min_amount={min_amount:.6g} price={price} min_notional={min_notional}"
|
|
|
|
|
+ )
|
|
|
return max(amount, 0.0)
|
|
return max(amount, 0.0)
|
|
|
|
|
|
|
|
def _place_grid(self, center: float) -> None:
|
|
def _place_grid(self, center: float) -> None:
|
|
@@ -273,29 +322,135 @@ class Strategy(Strategy):
|
|
|
self.state["order_ids"] = order_ids
|
|
self.state["order_ids"] = order_ids
|
|
|
self.state["last_action"] = "seeded grid"
|
|
self.state["last_action"] = "seeded grid"
|
|
|
|
|
|
|
|
|
|
+ def _place_side_grid(self, side: str, center: float) -> None:
|
|
|
|
|
+ levels = int(self.config.get("grid_levels", 6) or 6)
|
|
|
|
|
+ step = self._grid_step_pct()
|
|
|
|
|
+ min_notional = float(self.context.minimum_order_value or 0.0)
|
|
|
|
|
+ fee_rate = float(self.config.get("fee_rate", 0.0025) or 0.0)
|
|
|
|
|
+ safety = 0.995
|
|
|
|
|
+ market = self._market_symbol()
|
|
|
|
|
+ orders = list(self.state.get("orders") or [])
|
|
|
|
|
+ order_ids = list(self.state.get("order_ids") or [])
|
|
|
|
|
+
|
|
|
|
|
+ side_levels = min(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(
|
|
|
|
|
+ f"skip side buy: insufficient counter balance quote={quote_available:.6g} max_affordable_amount={max_affordable_amount:.6g} min_amount={min_amount:.6g} fee_rate={fee_rate}"
|
|
|
|
|
+ )
|
|
|
|
|
+ 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} amount={amount:.6g} min_notional={min_notional} existing_ids={order_ids}"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ for i in range(1, 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 i > side_levels or amount < min_size:
|
|
|
|
|
+ self._log(
|
|
|
|
|
+ f"skip side {side} level {i}: amount={amount:.6g} below min_size={min_size:.6g} min_notional={min_notional} price={price}"
|
|
|
|
|
+ )
|
|
|
|
|
+ continue
|
|
|
|
|
+ try:
|
|
|
|
|
+ self._log(f"place side {side} level {i}: price={price} amount={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(f"place side {side} level {i} result status={status} order_id={order_id} raw={result}")
|
|
|
|
|
+ orders.append({"side": side, "price": price, "amount": amount, "result": result})
|
|
|
|
|
+ if order_id is not None:
|
|
|
|
|
+ order_ids.append(str(order_id))
|
|
|
|
|
+ self._log(f"seed side {side} level {i}: {price} amount {amount:.6g}")
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ self.state["last_error"] = str(exc)
|
|
|
|
|
+ self._log(f"seed side {side} level {i} failed: {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(f"side {side} placement complete: tracked_ids={order_ids}")
|
|
|
|
|
+
|
|
|
|
|
+ def _cancel_obsolete_side_orders(self, open_orders: list[dict], desired_sides: set[str]) -> list[str]:
|
|
|
|
|
+ removed: list[str] = []
|
|
|
|
|
+ for order in open_orders:
|
|
|
|
|
+ if not isinstance(order, dict):
|
|
|
|
|
+ continue
|
|
|
|
|
+ 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
|
|
|
|
|
+
|
|
|
|
|
+ def _sync_open_orders_state(self) -> list[dict]:
|
|
|
|
|
+ try:
|
|
|
|
|
+ open_orders = self.context.get_open_orders()
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ self.state["last_error"] = str(exc)
|
|
|
|
|
+ self._log(f"open orders sync failed: {exc}")
|
|
|
|
|
+ return []
|
|
|
|
|
+
|
|
|
|
|
+ if not isinstance(open_orders, list):
|
|
|
|
|
+ open_orders = []
|
|
|
|
|
+
|
|
|
|
|
+ live_orders = [order for order in open_orders if isinstance(order, dict)]
|
|
|
|
|
+ live_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]
|
|
|
|
|
+ live_ids = [oid for oid in live_ids if oid]
|
|
|
|
|
+ live_sides = [str(order.get("side") or "").lower() for order in live_orders]
|
|
|
|
|
+
|
|
|
|
|
+ self.state["orders"] = live_orders
|
|
|
|
|
+ self.state["order_ids"] = live_ids
|
|
|
|
|
+ self.state["open_order_count"] = len(live_ids)
|
|
|
|
|
+ self._log(f"sync live orders: count={len(live_ids)} sides={live_sides} ids={live_ids}")
|
|
|
|
|
+ return live_orders
|
|
|
|
|
+
|
|
|
def _cancel_orders(self, order_ids) -> None:
|
|
def _cancel_orders(self, order_ids) -> None:
|
|
|
for order_id in order_ids or []:
|
|
for order_id in order_ids or []:
|
|
|
self._log(f"dropping stale order {order_id} from state")
|
|
self._log(f"dropping stale order {order_id} from state")
|
|
|
|
|
|
|
|
def on_tick(self, tick):
|
|
def on_tick(self, tick):
|
|
|
|
|
+ self._refresh_balance_snapshot()
|
|
|
price = self._price()
|
|
price = self._price()
|
|
|
self.state["last_price"] = price
|
|
self.state["last_price"] = price
|
|
|
self.state["last_error"] = ""
|
|
self.state["last_error"] = ""
|
|
|
|
|
+ self._refresh_regimes()
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
- open_orders = self.context.get_open_orders()
|
|
|
|
|
- live_ids = []
|
|
|
|
|
- if isinstance(open_orders, list):
|
|
|
|
|
- for order in open_orders:
|
|
|
|
|
- if isinstance(order, dict):
|
|
|
|
|
- live_ids.append(str(order.get("bitstamp_order_id") or order.get("order_id") or order.get("id") or order.get("client_order_id") or ""))
|
|
|
|
|
- live_ids = [oid for oid in live_ids if oid]
|
|
|
|
|
|
|
+ live_orders = self._sync_open_orders_state()
|
|
|
|
|
+ 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 (self.state.get("order_ids") or []) if oid]
|
|
|
- stale_ids = [oid for oid in live_ids if oid not in expected_ids]
|
|
|
|
|
- missing_ids = [oid for oid in expected_ids if oid not in live_ids]
|
|
|
|
|
|
|
+ stale_ids = []
|
|
|
|
|
+ missing_ids = []
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
open_order_count = -1
|
|
open_order_count = -1
|
|
|
|
|
+ live_orders = []
|
|
|
live_ids = []
|
|
live_ids = []
|
|
|
expected_ids = []
|
|
expected_ids = []
|
|
|
stale_ids = []
|
|
stale_ids = []
|
|
@@ -306,6 +461,7 @@ class Strategy(Strategy):
|
|
|
# Workaround: after a reset, trust the fresh strategy state first.
|
|
# Workaround: after a reset, trust the fresh strategy state first.
|
|
|
# This prevents stale exec-mcp records from blocking the next clean test.
|
|
# This prevents stale exec-mcp records from blocking the next clean test.
|
|
|
if not (self.state.get("order_ids") or []):
|
|
if not (self.state.get("order_ids") or []):
|
|
|
|
|
+ live_orders = []
|
|
|
live_ids = []
|
|
live_ids = []
|
|
|
open_order_count = 0
|
|
open_order_count = 0
|
|
|
expected_ids = []
|
|
expected_ids = []
|
|
@@ -313,6 +469,7 @@ class Strategy(Strategy):
|
|
|
missing_ids = []
|
|
missing_ids = []
|
|
|
|
|
|
|
|
self.state["open_order_count"] = open_order_count
|
|
self.state["open_order_count"] = open_order_count
|
|
|
|
|
+ desired_sides = self._desired_sides()
|
|
|
|
|
|
|
|
mode = self._mode()
|
|
mode = self._mode()
|
|
|
guard_active, guard_reason = self._trend_guard_status()
|
|
guard_active, guard_reason = self._trend_guard_status()
|
|
@@ -358,9 +515,39 @@ 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
|
|
|
|
|
|
|
|
|
|
+ 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)
|
|
|
|
|
+
|
|
|
|
|
+ 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)}
|
|
|
|
|
+ missing_sides = [side for side in ("buy", "sell") if side not in current_sides]
|
|
|
|
|
+ if missing_sides and self.state.get("center_price"):
|
|
|
|
|
+ for side in missing_sides:
|
|
|
|
|
+ self._log(f"adding missing {side} side after trade_sides change, live_sides={sorted(current_sides)} live_ids={live_ids}")
|
|
|
|
|
+ self._place_side_grid(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 {','.join(missing_sides)} side(s)"
|
|
|
|
|
+ return {"action": "add_side", "price": price, "side": ",".join(missing_sides)}
|
|
|
|
|
+
|
|
|
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._place_grid(price)
|
|
|
|
|
+ live_orders = self._sync_open_orders_state()
|
|
|
self.state["seeded"] = True
|
|
self.state["seeded"] = True
|
|
|
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}")
|
|
@@ -370,6 +557,7 @@ class Strategy(Strategy):
|
|
|
self._log("no open orders, reseeding grid")
|
|
self._log("no open orders, reseeding grid")
|
|
|
self.state["center_price"] = price
|
|
self.state["center_price"] = price
|
|
|
self._place_grid(price)
|
|
self._place_grid(price)
|
|
|
|
|
+ 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"
|
|
|
return {"action": "reseed" if mode == "active" else "plan", "price": price}
|
|
return {"action": "reseed" if mode == "active" else "plan", "price": price}
|
|
@@ -385,6 +573,7 @@ class Strategy(Strategy):
|
|
|
self.state["last_error"] = str(exc)
|
|
self.state["last_error"] = str(exc)
|
|
|
self.state["center_price"] = price
|
|
self.state["center_price"] = price
|
|
|
self._place_grid(price)
|
|
self._place_grid(price)
|
|
|
|
|
+ live_orders = self._sync_open_orders_state()
|
|
|
mode = self._mode()
|
|
mode = self._mode()
|
|
|
self.state["last_action"] = "recentered" if mode == "active" else f"{mode} monitor"
|
|
self.state["last_action"] = "recentered" if mode == "active" else f"{mode} monitor"
|
|
|
self._log(f"recentered grid to {price}")
|
|
self._log(f"recentered grid to {price}")
|