feat: 持仓同步优化 + Binance 平仓/减仓安全增强
## 核心功能 1. **持仓自动同步与插入** - 修复策略状态查询条件 (active -> running) - 移除阻塞空持仓同步的早期返回逻辑 - 自动检测交易所存在但本地不存在的持仓并插入数据库 - 修复 INSERT 语句列匹配 (移除 created_at,添加 user_id) 2. **Binance 平仓/减仓优化** - 订单执行前强制同步持仓 (Pre-execution Sync) - Close/Reduce 订单自动数量修正:防止超额平仓 - 当请求平仓数量 > 实际持仓时,自动调整为实际持有量 - 当数据库无持仓记录时,强制设置 amount=0 阻止错误订单 3. **日志增强** - 每10秒输出实际持仓详情 (symbol, size, entry_price) - 常规同步日志降级为 debug,保留关键操作为 info - 同步失败日志升级为 ERROR 并附带 traceback
This commit is contained in:
@@ -67,6 +67,7 @@ class PendingOrderWorker:
|
|||||||
self._position_sync_enabled = os.getenv("POSITION_SYNC_ENABLED", "true").lower() == "true"
|
self._position_sync_enabled = os.getenv("POSITION_SYNC_ENABLED", "true").lower() == "true"
|
||||||
self._position_sync_interval_sec = float(os.getenv("POSITION_SYNC_INTERVAL_SEC", "10"))
|
self._position_sync_interval_sec = float(os.getenv("POSITION_SYNC_INTERVAL_SEC", "10"))
|
||||||
self._last_position_sync_ts = 0.0
|
self._last_position_sync_ts = 0.0
|
||||||
|
logger.info(f"PendingOrderWorker: sync_enabled={self._position_sync_enabled}, interval={self._position_sync_interval_sec}s")
|
||||||
|
|
||||||
def start(self) -> bool:
|
def start(self) -> bool:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@@ -95,7 +96,9 @@ class PendingOrderWorker:
|
|||||||
time.sleep(self.poll_interval_sec)
|
time.sleep(self.poll_interval_sec)
|
||||||
|
|
||||||
def _tick(self) -> None:
|
def _tick(self) -> None:
|
||||||
|
# logger.info(f"[PendingOrderWorker] _tick start. last_sync={self._last_position_sync_ts}")
|
||||||
orders = self._fetch_pending_orders(limit=self.batch_size)
|
orders = self._fetch_pending_orders(limit=self.batch_size)
|
||||||
|
# logger.info(f"[PendingOrderWorker] orders fetched: {len(orders)}")
|
||||||
if not orders:
|
if not orders:
|
||||||
self._maybe_sync_positions()
|
self._maybe_sync_positions()
|
||||||
return
|
return
|
||||||
@@ -124,13 +127,14 @@ class PendingOrderWorker:
|
|||||||
return
|
return
|
||||||
if now - float(self._last_position_sync_ts or 0.0) < float(self._position_sync_interval_sec):
|
if now - float(self._last_position_sync_ts or 0.0) < float(self._position_sync_interval_sec):
|
||||||
return
|
return
|
||||||
|
logger.debug(f"[PendingOrderWorker] Triggering sync... (now={now}, last={self._last_position_sync_ts})")
|
||||||
self._last_position_sync_ts = now
|
self._last_position_sync_ts = now
|
||||||
try:
|
try:
|
||||||
self._sync_positions_best_effort()
|
self._sync_positions_best_effort()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.info(f"position sync skipped/failed: {e}")
|
logger.debug(f"position sync skipped/failed: {e}")
|
||||||
|
|
||||||
def _sync_positions_best_effort(self) -> None:
|
def _sync_positions_best_effort(self, target_strategy_id: Optional[int] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Best-effort reconciliation:
|
Best-effort reconciliation:
|
||||||
- If exchange position is flat, delete local row from qd_strategy_positions.
|
- If exchange position is flat, delete local row from qd_strategy_positions.
|
||||||
@@ -138,15 +142,23 @@ class PendingOrderWorker:
|
|||||||
|
|
||||||
This prevents "ghost positions" when positions are closed externally on the exchange.
|
This prevents "ghost positions" when positions are closed externally on the exchange.
|
||||||
"""
|
"""
|
||||||
# 1) Load local positions
|
# 1) Load local positions (filtered if target_strategy_id provided)
|
||||||
|
logger.debug(f"[PositionSync] Entering _sync_positions_best_effort for target={target_strategy_id}")
|
||||||
with get_db_connection() as db:
|
with get_db_connection() as db:
|
||||||
cur = db.cursor()
|
cur = db.cursor()
|
||||||
cur.execute("SELECT id, strategy_id, symbol, side, size, entry_price FROM qd_strategy_positions ORDER BY updated_at DESC")
|
if target_strategy_id:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id, strategy_id, symbol, side, size, entry_price FROM qd_strategy_positions WHERE strategy_id = %s ORDER BY updated_at DESC",
|
||||||
|
(int(target_strategy_id),)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cur.execute("SELECT id, strategy_id, symbol, side, size, entry_price FROM qd_strategy_positions ORDER BY updated_at DESC")
|
||||||
rows = cur.fetchall() or []
|
rows = cur.fetchall() or []
|
||||||
cur.close()
|
cur.close()
|
||||||
|
|
||||||
if not rows:
|
# [Defect Fix] Removed early return to allow syncing active strategies even if local DB is empty.
|
||||||
return
|
# if not rows and not target_strategy_id:
|
||||||
|
# return
|
||||||
|
|
||||||
# Group by strategy_id for efficient exchange queries.
|
# Group by strategy_id for efficient exchange queries.
|
||||||
sid_to_rows: Dict[int, List[Dict[str, Any]]] = {}
|
sid_to_rows: Dict[int, List[Dict[str, Any]]] = {}
|
||||||
@@ -156,11 +168,44 @@ class PendingOrderWorker:
|
|||||||
continue
|
continue
|
||||||
sid_to_rows.setdefault(sid, []).append(r)
|
sid_to_rows.setdefault(sid, []).append(r)
|
||||||
|
|
||||||
|
# If targeted sync but no local rows found, we assume user might have opened position externally
|
||||||
|
# but DB is empty. However, without knowing *which* symbol to check, we can't easily auto-discover
|
||||||
|
# unless we fetch ALL positions from exchange for that strategy.
|
||||||
|
# But `load_strategy_configs(sid)` gives us the exchange keys.
|
||||||
|
# So if target_strategy_id is set but `sid_to_rows` is empty, we SHOULD explicitly add it to `sid_to_rows`
|
||||||
|
# so logic below enters and calls `client.get_positions()`.
|
||||||
|
if target_strategy_id and target_strategy_id not in sid_to_rows:
|
||||||
|
sid_to_rows[target_strategy_id] = []
|
||||||
|
|
||||||
|
# [Log Fix] Load all ACTIVE LIVE strategies to ensure we sync/log them even if local DB is empty.
|
||||||
|
# Otherwise, if we have no local positions, we would silently skip the exchange check.
|
||||||
|
try:
|
||||||
|
with get_db_connection() as db:
|
||||||
|
cur = db.cursor()
|
||||||
|
# Fetch all strategies configured for LIVE execution
|
||||||
|
cur.execute("SELECT id FROM qd_strategies_trading WHERE status = 'running' AND execution_mode = 'live'")
|
||||||
|
active_rows = cur.fetchall() or []
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
logger.debug(f"[PositionSync] Found {len(active_rows)} active live strategies in DB.")
|
||||||
|
for _ar in active_rows:
|
||||||
|
_sid = int(_ar.get("id") or 0)
|
||||||
|
if _sid > 0 and _sid not in sid_to_rows:
|
||||||
|
if target_strategy_id and target_strategy_id != _sid:
|
||||||
|
continue
|
||||||
|
sid_to_rows[_sid] = []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load active strategies for sync: {e}", exc_info=True)
|
||||||
|
|
||||||
# 2) Reconcile per strategy
|
# 2) Reconcile per strategy
|
||||||
for sid, plist in sid_to_rows.items():
|
for sid, plist in sid_to_rows.items():
|
||||||
|
if target_strategy_id and sid != target_strategy_id:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
sc = load_strategy_configs(int(sid))
|
sc = load_strategy_configs(int(sid))
|
||||||
if (sc.get("execution_mode") or "").strip().lower() != "live":
|
exec_mode = (sc.get("execution_mode") or "").strip().lower()
|
||||||
|
if exec_mode != "live":
|
||||||
|
logger.debug(f"[PositionSync] Strategy {sid} skipped: execution_mode='{exec_mode}' (needs 'live')")
|
||||||
continue
|
continue
|
||||||
exchange_config = resolve_exchange_config(sc.get("exchange_config") or {})
|
exchange_config = resolve_exchange_config(sc.get("exchange_config") or {})
|
||||||
safe_cfg = safe_exchange_config_for_log(exchange_config)
|
safe_cfg = safe_exchange_config_for_log(exchange_config)
|
||||||
@@ -169,20 +214,36 @@ class PendingOrderWorker:
|
|||||||
if market_type in ("futures", "future", "perp", "perpetual"):
|
if market_type in ("futures", "future", "perp", "perpetual"):
|
||||||
market_type = "swap"
|
market_type = "swap"
|
||||||
|
|
||||||
|
# Lazy import MT5 here to allow elif chain later
|
||||||
|
global MT5Client
|
||||||
|
if MT5Client is None:
|
||||||
|
try:
|
||||||
|
from app.services.mt5_trading import MT5Client as _MT5Client
|
||||||
|
MT5Client = _MT5Client
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
client = create_client(exchange_config, market_type=market_type)
|
client = create_client(exchange_config, market_type=market_type)
|
||||||
|
|
||||||
# Build an "exchange snapshot" per symbol+side
|
# Build an "exchange snapshot" per symbol+side
|
||||||
exch_size: Dict[str, Dict[str, float]] = {} # {symbol: {long: size, short: size}}
|
exch_size: Dict[str, Dict[str, float]] = {} # {symbol: {long: size, short: size}}
|
||||||
|
exch_entry_price: Dict[str, Dict[str, float]] = {} # {symbol: {long: px, short: px}}
|
||||||
|
|
||||||
if isinstance(client, BinanceFuturesClient) and market_type == "swap":
|
if isinstance(client, BinanceFuturesClient) and market_type == "swap":
|
||||||
all_pos = client.get_positions() or []
|
all_pos = client.get_positions() or []
|
||||||
|
# Handle dict response if needed (wrapper)
|
||||||
|
if isinstance(all_pos, dict) and "raw" in all_pos:
|
||||||
|
all_pos = all_pos["raw"]
|
||||||
|
|
||||||
if isinstance(all_pos, list):
|
if isinstance(all_pos, list):
|
||||||
for p in all_pos:
|
for p in all_pos:
|
||||||
sym = str(p.get("symbol") or "").strip().upper()
|
sym = str(p.get("symbol") or "").strip().upper()
|
||||||
try:
|
try:
|
||||||
amt = float(p.get("positionAmt") or 0.0)
|
amt = float(p.get("positionAmt") or 0.0)
|
||||||
|
ep = float(p.get("entryPrice") or 0.0)
|
||||||
except Exception:
|
except Exception:
|
||||||
amt = 0.0
|
amt = 0.0
|
||||||
|
ep = 0.0
|
||||||
if not sym or abs(amt) <= 0:
|
if not sym or abs(amt) <= 0:
|
||||||
continue
|
continue
|
||||||
# Map to our symbol format: BTCUSDT -> BTC/USDT (best-effort)
|
# Map to our symbol format: BTCUSDT -> BTC/USDT (best-effort)
|
||||||
@@ -191,6 +252,7 @@ class PendingOrderWorker:
|
|||||||
hb_sym = f"{hb_sym[:-4]}/USDT"
|
hb_sym = f"{hb_sym[:-4]}/USDT"
|
||||||
side = "long" if amt > 0 else "short"
|
side = "long" if amt > 0 else "short"
|
||||||
exch_size.setdefault(hb_sym, {"long": 0.0, "short": 0.0})[side] = abs(float(amt))
|
exch_size.setdefault(hb_sym, {"long": 0.0, "short": 0.0})[side] = abs(float(amt))
|
||||||
|
exch_entry_price.setdefault(hb_sym, {"long": 0.0, "short": 0.0})[side] = abs(float(ep))
|
||||||
|
|
||||||
elif isinstance(client, OkxClient) and market_type == "swap":
|
elif isinstance(client, OkxClient) and market_type == "swap":
|
||||||
resp = client.get_positions()
|
resp = client.get_positions()
|
||||||
@@ -351,15 +413,7 @@ class PendingOrderWorker:
|
|||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check for MT5 client (forex)
|
elif MT5Client is not None and isinstance(client, MT5Client):
|
||||||
global MT5Client
|
|
||||||
if MT5Client is None:
|
|
||||||
try:
|
|
||||||
from app.services.mt5_trading import MT5Client as _MT5Client
|
|
||||||
MT5Client = _MT5Client
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
if MT5Client is not None and isinstance(client, MT5Client):
|
|
||||||
# MT5 forex positions
|
# MT5 forex positions
|
||||||
positions = client.get_positions()
|
positions = client.get_positions()
|
||||||
if isinstance(positions, list):
|
if isinstance(positions, list):
|
||||||
@@ -383,6 +437,22 @@ class PendingOrderWorker:
|
|||||||
logger.debug(f"position sync: skip unsupported market/client: sid={sid}, cfg={safe_cfg}, market_type={market_type}, client={type(client)}")
|
logger.debug(f"position sync: skip unsupported market/client: sid={sid}, cfg={safe_cfg}, market_type={market_type}, client={type(client)}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# [DEBUG] Log all normalized exchange keys for inspection
|
||||||
|
logger.debug(f"[PositionSync] Strategy {sid} Exchange Keys: {list(exch_size.keys())}")
|
||||||
|
|
||||||
|
# [Log Optimization] Always log current positions every sync cycle (10s)
|
||||||
|
pos_summary_parts = []
|
||||||
|
for _sym, _sides in exch_size.items():
|
||||||
|
for _side_key, _qty in _sides.items():
|
||||||
|
if _qty > 0:
|
||||||
|
_ep = exch_entry_price.get(_sym, {}).get(_side_key, 0.0)
|
||||||
|
pos_summary_parts.append(f"{_sym} {_side_key} size={_qty} entry={_ep}")
|
||||||
|
|
||||||
|
if pos_summary_parts:
|
||||||
|
logger.info(f"[PositionSync] Strategy {sid} ({safe_cfg.get('exchange_id', 'unknown')}) positions: {'; '.join(pos_summary_parts)}")
|
||||||
|
else:
|
||||||
|
logger.info(f"[PositionSync] Strategy {sid} ({safe_cfg.get('exchange_id', 'unknown')}) has NO positions on exchange.")
|
||||||
|
|
||||||
# 3) Apply reconciliation to local rows.
|
# 3) Apply reconciliation to local rows.
|
||||||
to_delete_ids: List[int] = []
|
to_delete_ids: List[int] = []
|
||||||
to_update: List[Dict[str, Any]] = []
|
to_update: List[Dict[str, Any]] = []
|
||||||
@@ -402,15 +472,51 @@ class PendingOrderWorker:
|
|||||||
exch = exch_size.get(sym) or {}
|
exch = exch_size.get(sym) or {}
|
||||||
exch_qty = float(exch.get(side) or 0.0)
|
exch_qty = float(exch.get(side) or 0.0)
|
||||||
|
|
||||||
|
# Lookup entry price
|
||||||
|
exch_ep_map = exch_entry_price.get(sym) or {}
|
||||||
|
exch_price = float(exch_ep_map.get(side) or 0.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
local_price = float(r.get("entry_price") or 0.0)
|
||||||
|
except Exception:
|
||||||
|
local_price = 0.0
|
||||||
|
logger.debug(f"[PositionSync] Check ID={rid} {sym} {side}: local_sz={local_size} px={local_price}, exch_sz={exch_qty} px={exch_price}")
|
||||||
|
|
||||||
if exch_qty <= eps:
|
if exch_qty <= eps:
|
||||||
# Exchange is flat -> delete local position (self-heal).
|
# Exchange is flat -> delete local position (self-heal).
|
||||||
to_delete_ids.append(rid)
|
to_delete_ids.append(rid)
|
||||||
else:
|
else:
|
||||||
# Update local size if it diverged materially (best-effort).
|
# Update local size if it diverged materially (best-effort), OR if entry_price changed significantly (>0.5% diff)
|
||||||
if local_size <= 0 or abs(exch_qty - local_size) / max(1.0, local_size) > 0.01:
|
# or if local_price is 0 (first sync)
|
||||||
to_update.append({"id": rid, "size": exch_qty})
|
price_diff_ratio = 0.0
|
||||||
|
if local_price > 0:
|
||||||
|
price_diff_ratio = abs(exch_price - local_price) / local_price
|
||||||
|
else:
|
||||||
|
price_diff_ratio = 1.0 if exch_price > 0 else 0.0
|
||||||
|
|
||||||
if not to_delete_ids and not to_update:
|
if (local_size <= 0 or abs(exch_qty - local_size) / max(1.0, local_size) > 0.01) or (price_diff_ratio > 0.005):
|
||||||
|
logger.info(f"[PositionSync] -> Flagged for UPDATE: {sym} (local_sz={local_size}->{exch_qty}, px={local_price}->{exch_price})")
|
||||||
|
to_update.append({"id": rid, "size": exch_qty, "entry_price": exch_price})
|
||||||
|
|
||||||
|
# [New Feature] Detect positions that exist on exchange but not in local DB, and insert them.
|
||||||
|
to_insert: List[Dict[str, Any]] = []
|
||||||
|
local_symbols_sides = {(str(r.get("symbol") or "").strip(), str(r.get("side") or "").strip().lower()) for r in plist}
|
||||||
|
|
||||||
|
for _sym, _sides_map in exch_size.items():
|
||||||
|
for _side, _qty in _sides_map.items():
|
||||||
|
if _qty > 1e-12 and (_sym, _side) not in local_symbols_sides:
|
||||||
|
# Exchange has this position but local DB does not
|
||||||
|
_ep = exch_entry_price.get(_sym, {}).get(_side, 0.0)
|
||||||
|
to_insert.append({
|
||||||
|
"strategy_id": sid,
|
||||||
|
"symbol": _sym,
|
||||||
|
"side": _side,
|
||||||
|
"size": _qty,
|
||||||
|
"entry_price": _ep
|
||||||
|
})
|
||||||
|
logger.info(f"[PositionSync] -> Flagged for INSERT: {_sym} {_side} size={_qty} entry={_ep}")
|
||||||
|
|
||||||
|
if not to_delete_ids and not to_update and not to_insert:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
with get_db_connection() as db:
|
with get_db_connection() as db:
|
||||||
@@ -418,14 +524,27 @@ class PendingOrderWorker:
|
|||||||
for rid in to_delete_ids:
|
for rid in to_delete_ids:
|
||||||
cur.execute("DELETE FROM qd_strategy_positions WHERE id = %s", (int(rid),))
|
cur.execute("DELETE FROM qd_strategy_positions WHERE id = %s", (int(rid),))
|
||||||
for u in to_update:
|
for u in to_update:
|
||||||
cur.execute("UPDATE qd_strategy_positions SET size = %s, updated_at = NOW() WHERE id = %s", (float(u["size"]), int(u["id"])))
|
cur.execute(
|
||||||
|
"UPDATE qd_strategy_positions SET size = %s, entry_price = %s, updated_at = NOW() WHERE id = %s",
|
||||||
|
(float(u["size"]), float(u["entry_price"]), int(u["id"]))
|
||||||
|
)
|
||||||
|
for ins in to_insert:
|
||||||
|
cur.execute(
|
||||||
|
"""INSERT INTO qd_strategy_positions (user_id, strategy_id, symbol, side, size, entry_price, updated_at)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, NOW())""",
|
||||||
|
(1, int(ins["strategy_id"]), str(ins["symbol"]), str(ins["side"]), float(ins["size"]), float(ins["entry_price"]))
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
cur.close()
|
cur.close()
|
||||||
|
|
||||||
if to_delete_ids:
|
if to_delete_ids:
|
||||||
logger.info(f"position sync: removed {len(to_delete_ids)} ghost positions for strategy_id={sid}")
|
logger.debug(f"position sync: removed {len(to_delete_ids)} ghost positions for strategy_id={sid}")
|
||||||
|
if to_update:
|
||||||
|
logger.debug(f"position sync: updated {len(to_update)} positions for strategy_id={sid}")
|
||||||
|
if to_insert:
|
||||||
|
logger.debug(f"position sync: inserted {len(to_insert)} new positions for strategy_id={sid}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.info(f"position sync: strategy_id={sid} failed: {e}")
|
logger.error(f"position sync: strategy_id={sid} failed: {e}", exc_info=True)
|
||||||
|
|
||||||
def _fetch_pending_orders(self, limit: int = 50) -> List[Dict[str, Any]]:
|
def _fetch_pending_orders(self, limit: int = 50) -> List[Dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
@@ -876,6 +995,50 @@ class PendingOrderWorker:
|
|||||||
if leverage <= 0:
|
if leverage <= 0:
|
||||||
leverage = 1.0
|
leverage = 1.0
|
||||||
|
|
||||||
|
# [FEATURE] Sync positions before execution to ensure size is checking against reality
|
||||||
|
# The user requested to sync before EVERY live order to prevent mismatch.
|
||||||
|
try:
|
||||||
|
logger.info(f"[Sync] Triggering pre-execution sync for strategy {strategy_id} before order {order_id}")
|
||||||
|
self._sync_positions_best_effort(target_strategy_id=strategy_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Pre-execution sync failed: {e}")
|
||||||
|
|
||||||
|
# [FEATURE] Auto-correct amount for Close/Reduce signals if we hold less than requested
|
||||||
|
if reduce_only:
|
||||||
|
try:
|
||||||
|
with get_db_connection() as db:
|
||||||
|
cur = db.cursor()
|
||||||
|
# We need to find the specific position.
|
||||||
|
# Symbol stored in DB is normalized (e.g. BTC/USDT).
|
||||||
|
# The payload 'symbol' might be 'BTCUSDT' or 'BTC/USDT'.
|
||||||
|
# We try to match what stored in DB.
|
||||||
|
# Best effort: try exact match, then normalized.
|
||||||
|
qry_sym = str(symbol or "").strip().upper()
|
||||||
|
# Mapping logic similar to _sync:
|
||||||
|
if qry_sym.endswith("USDT") and "/" not in qry_sym:
|
||||||
|
qry_sym = f"{qry_sym[:-4]}/USDT"
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"SELECT size FROM qd_strategy_positions WHERE strategy_id = %s AND symbol = %s AND side = %s",
|
||||||
|
(strategy_id, qry_sym, pos_side)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
held_size = float(row["size"] or 0.0)
|
||||||
|
if amount > held_size:
|
||||||
|
logger.warning(f"[RiskControl] Adjusting Close amount from {amount} to {held_size} (Held) for {symbol}")
|
||||||
|
amount = held_size
|
||||||
|
else:
|
||||||
|
# No position found in DB?
|
||||||
|
# If reduce_only, and no position, maybe it's 0.
|
||||||
|
logger.warning(f"[RiskControl] Close signal for {symbol} but NO position found in DB. Setting amount=0.")
|
||||||
|
amount = 0.0
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[RiskControl] Failed to check DB position logic: {e}")
|
||||||
|
|
||||||
|
|
||||||
# Collect raw exchange interactions / intermediate states for debugging & persistence.
|
# Collect raw exchange interactions / intermediate states for debugging & persistence.
|
||||||
phases: Dict[str, Any] = {}
|
phases: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user