"""Live trading account, risk and execution audit helpers.""" from __future__ import annotations import json from datetime import datetime from app.config.system_config import live_trading_config from app.db.schema import get_conn def _now() -> str: return datetime.now().isoformat() def _loads(value, fallback=None): try: if isinstance(value, str) and value.strip(): return json.loads(value) if isinstance(value, (dict, list)): return value except Exception: pass return fallback if fallback is not None else {} def _dumps(value) -> str: return json.dumps(value if value is not None else {}, ensure_ascii=False, sort_keys=True, default=str) def _safe_float(value, default: float = 0.0) -> float: try: if value is None or value == "": return default return float(value) except Exception: return default def _safe_int(value, default: int = 0) -> int: try: return int(value or 0) except Exception: return default def _normalize_symbol(symbol: str) -> str: value = str(symbol or "").strip().upper() if value and "/" not in value and value.endswith("USDT"): value = value[:-4] + "/USDT" return value def _deep_merge(base: dict, override: dict) -> dict: merged = dict(base or {}) for key, value in (override or {}).items(): if isinstance(value, dict) and isinstance(merged.get(key), dict): merged[key] = _deep_merge(merged[key], value) else: merged[key] = value return merged def _row(row) -> dict: if not row: return {} item = dict(row) for key in ("permissions_json", "risk_config_json", "risk_check_json", "request_json", "response_json", "payload_json"): if key in item: item[key.replace("_json", "")] = _loads(item.pop(key), {}) for key in ("testnet", "reduce_only"): if key in item: item[key] = bool(item[key]) return item def get_effective_live_trading_config() -> dict: return live_trading_config() def upsert_live_account( account_code: str = "", *, exchange: str = "", market_type: str = "", testnet: bool | None = None, status: str = "", api_key_env: str = "", api_secret_env: str = "", permissions: dict | None = None, risk_config: dict | None = None, ) -> dict: cfg = get_effective_live_trading_config() now = _now() account_code = account_code or str(cfg.get("account_code") or "binance_um_futures") exchange = exchange or str(cfg.get("exchange") or "binance") market_type = market_type or str(cfg.get("market_type") or "um_futures") if testnet is None: testnet = bool(cfg.get("testnet", True)) status = status or ("enabled" if bool(cfg.get("enabled")) else "disabled") api_key_env = api_key_env or str(cfg.get("api_key_env") or "ALPHAX_BINANCE_API_KEY") api_secret_env = api_secret_env or str(cfg.get("api_secret_env") or "ALPHAX_BINANCE_API_SECRET") permissions = permissions if isinstance(permissions, dict) else {"trade": False, "read": True} risk_config = risk_config if isinstance(risk_config, dict) else cfg.get("risk", {}) conn = get_conn() try: row = conn.execute( """ INSERT INTO live_trade_accounts ( account_code, exchange, market_type, testnet, status, api_key_env, api_secret_env, permissions_json, risk_config_json, created_at, updated_at ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) ON CONFLICT(account_code) DO UPDATE SET exchange=excluded.exchange, market_type=excluded.market_type, testnet=excluded.testnet, status=excluded.status, api_key_env=excluded.api_key_env, api_secret_env=excluded.api_secret_env, permissions_json=excluded.permissions_json, risk_config_json=excluded.risk_config_json, updated_at=excluded.updated_at RETURNING * """, ( account_code, exchange, market_type, int(bool(testnet)), status, api_key_env, api_secret_env, _dumps(permissions), _dumps(risk_config), now, now, ), ).fetchone() conn.commit() finally: conn.close() return _row(row) def update_live_account( account_id: int, *, account_code: str = "", exchange: str = "", market_type: str = "", testnet: bool | None = None, status: str = "", api_key_env: str = "", api_secret_env: str = "", permissions: dict | None = None, risk_config: dict | None = None, ) -> dict: account_id = _safe_int(account_id) if account_id <= 0: return {"ok": False, "reason": "invalid_account_id"} current = get_live_account(account_id) if not current: return {"ok": False, "reason": "account_not_found"} now = _now() account_code = account_code or str(current.get("account_code") or "") exchange = exchange or str(current.get("exchange") or "binance") market_type = market_type or str(current.get("market_type") or "um_futures") if testnet is None: testnet = bool(current.get("testnet", True)) status = status or str(current.get("status") or "disabled") api_key_env = api_key_env or str(current.get("api_key_env") or "") api_secret_env = api_secret_env or str(current.get("api_secret_env") or "") permissions = permissions if isinstance(permissions, dict) else current.get("permissions", {}) risk_config = risk_config if isinstance(risk_config, dict) else current.get("risk_config", {}) conn = get_conn() try: row = conn.execute( """ UPDATE live_trade_accounts SET account_code=%s, exchange=%s, market_type=%s, testnet=%s, status=%s, api_key_env=%s, api_secret_env=%s, permissions_json=%s, risk_config_json=%s, updated_at=%s WHERE id=%s RETURNING * """, ( account_code, exchange, market_type, int(bool(testnet)), status, api_key_env, api_secret_env, _dumps(permissions), _dumps(risk_config), now, account_id, ), ).fetchone() conn.commit() except Exception: conn.rollback() raise finally: conn.close() if not row: return {"ok": False, "reason": "account_not_found"} item = _row(row) item["ok"] = True return item def list_live_accounts() -> dict: conn = get_conn() try: rows = conn.execute("SELECT * FROM live_trade_accounts ORDER BY updated_at DESC, id DESC").fetchall() finally: conn.close() return {"items": [_row(r) for r in rows], "total": len(rows)} def get_live_account(account_id: int) -> dict: account_id = _safe_int(account_id) if account_id <= 0: return {} conn = get_conn() try: row = conn.execute("SELECT * FROM live_trade_accounts WHERE id=%s", (account_id,)).fetchone() finally: conn.close() return _row(row) def delete_live_account(account_id: int) -> dict: account_id = _safe_int(account_id) if account_id <= 0: return {"ok": False, "reason": "invalid_account_id"} conn = get_conn() try: row = conn.execute("DELETE FROM live_trade_accounts WHERE id=%s RETURNING *", (account_id,)).fetchone() conn.commit() finally: conn.close() if not row: return {"ok": False, "reason": "account_not_found"} return {"ok": True, "account": _row(row)} def list_enabled_live_accounts() -> list[dict]: conn = get_conn() try: rows = conn.execute( "SELECT * FROM live_trade_accounts WHERE status='enabled' ORDER BY id" ).fetchall() finally: conn.close() return [_row(r) for r in rows] def _config_for_account(account: dict | None = None) -> dict: cfg = get_effective_live_trading_config() if account: account_risk = account.get("risk_config") if isinstance(account.get("risk_config"), dict) else {} cfg = _deep_merge(cfg, { "exchange": account.get("exchange") or cfg.get("exchange"), "market_type": account.get("market_type") or cfg.get("market_type"), "testnet": account.get("testnet", cfg.get("testnet")), "sandbox_mode": account_risk.get("sandbox_mode") or cfg.get("sandbox_mode"), "risk": _deep_merge(cfg.get("risk") or {}, account_risk), }) return cfg def _risk_settings(cfg: dict) -> dict: risk = cfg.get("risk") if isinstance(cfg.get("risk"), dict) else {} max_symbol_leverage = _safe_float(risk.get("max_symbol_leverage"), _safe_float(cfg.get("max_symbol_leverage"), 1)) max_order_margin = _safe_float(risk.get("max_order_margin_usdt"), _safe_float(cfg.get("max_order_margin_usdt"), 0)) max_order_notional = _safe_float(risk.get("max_order_notional_usdt"), _safe_float(cfg.get("max_order_notional_usdt"), 0)) if max_order_notional <= 0 and max_order_margin > 0: max_order_notional = max_order_margin * max(1.0, max_symbol_leverage) return { "max_order_margin_usdt": max_order_margin, "max_order_notional_usdt": max_order_notional, "max_symbol_leverage": max_symbol_leverage, "max_cumulative_leverage": _safe_float(risk.get("max_cumulative_leverage"), _safe_float(cfg.get("max_cumulative_leverage"), 1)), "max_daily_order_count": _safe_int(risk.get("max_daily_order_count"), _safe_int(cfg.get("max_daily_order_count"), 0)), "allowed_symbols": [str(x).upper() for x in (risk.get("allowed_symbols") or cfg.get("allowed_symbols") or []) if str(x).strip()], } def _risk_check(payload: dict, cfg: dict, account: dict | None = None) -> tuple[str, str, dict]: symbol = _normalize_symbol(payload.get("symbol")) notional = _safe_float(payload.get("notional_usdt")) leverage = _safe_float(payload.get("leverage"), _safe_float(cfg.get("default_leverage"), 1)) risk = _risk_settings(cfg) allowed_symbols = risk["allowed_symbols"] max_notional = risk["max_order_notional_usdt"] max_margin = risk["max_order_margin_usdt"] max_leverage = risk["max_symbol_leverage"] margin = notional / leverage if leverage > 0 else notional checks = { "enabled": bool(cfg.get("enabled")), "execution_mode": cfg.get("execution_mode", "exchange_api"), "require_human_approval": bool(cfg.get("require_human_approval", True)), "account_id": _safe_int((account or {}).get("id")), "account_code": (account or {}).get("account_code", ""), "symbol": symbol, "notional_usdt": notional, "margin_usdt": margin, "max_order_margin_usdt": max_margin, "max_order_notional_usdt": max_notional, "leverage": leverage, "max_symbol_leverage": max_leverage, "max_cumulative_leverage": risk["max_cumulative_leverage"], "allowed_symbols": allowed_symbols, } if not symbol: return "blocked", "missing_symbol", checks if not bool(cfg.get("enabled")): return "blocked", "live_trading_disabled", checks if account and account.get("status") != "enabled": return "blocked", "account_disabled", checks if allowed_symbols and symbol not in allowed_symbols: return "blocked", "symbol_not_allowed", checks if max_margin > 0 and margin > max_margin: return "blocked", "margin_exceeds_limit", checks if max_notional > 0 and notional > max_notional: return "blocked", "notional_exceeds_limit", checks if max_leverage > 0 and leverage > max_leverage: return "blocked", "leverage_exceeds_limit", checks if bool(cfg.get("require_human_approval", True)): return "pending_approval", "waiting_human_approval", checks mode = str(cfg.get("execution_mode") or "exchange_api").strip().lower() if mode in ("exchange_api", "demo"): return "prepared", "exchange_ready_for_executor", checks return "prepared", "ready_for_executor", checks def create_live_order_intent(payload: dict, *, source_type: str = "manual", source_id: int = 0) -> dict: account = get_live_account(_safe_int(payload.get("account_id"))) cfg = _config_for_account(account) now = _now() symbol = _normalize_symbol(payload.get("symbol")) side = str(payload.get("side") or "long").strip().lower() if side not in ("long", "short"): side = "long" status, reason, risk = _risk_check({**payload, "symbol": symbol, "side": side}, cfg, account) conn = get_conn() try: row = conn.execute( """ INSERT INTO live_order_intents ( source_type, source_id, recommendation_id, paper_trade_id, paper_order_id, account_id, exchange, market_type, symbol, side, position_side, order_type, status, reason, quantity, price, stop_loss, take_profit, notional_usdt, leverage, reduce_only, client_order_id, risk_check_json, request_json, created_at, updated_at ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) RETURNING * """, ( source_type, _safe_int(source_id), _safe_int(payload.get("recommendation_id")), _safe_int(payload.get("paper_trade_id")), _safe_int(payload.get("paper_order_id")), _safe_int(payload.get("account_id")), str(account.get("exchange") or cfg.get("exchange") or payload.get("exchange") or "binance"), str(account.get("market_type") or cfg.get("market_type") or payload.get("market_type") or "um_futures"), symbol, side, side, str(payload.get("order_type") or "market").lower(), status, reason, _safe_float(payload.get("quantity")), _safe_float(payload.get("price")), _safe_float(payload.get("stop_loss")), _safe_float(payload.get("take_profit")), _safe_float(payload.get("notional_usdt")), _safe_float(payload.get("leverage"), _safe_float(cfg.get("default_leverage"), 1)), int(bool(payload.get("reduce_only"))), str(payload.get("client_order_id") or ""), _dumps(risk), _dumps(payload), now, now, ), ).fetchone() conn.execute( """ INSERT INTO live_order_events (intent_id, event_type, status, message, payload_json, event_time) VALUES (%s,%s,%s,%s,%s,%s) """, (row["id"], "intent_created", status, reason, _dumps(risk), now), ) conn.commit() finally: conn.close() return _row(row) def create_live_order_intents_for_accounts(payload: dict, account_ids: list[int] | None = None, *, source_type: str = "manual", source_id: int = 0) -> dict: accounts = list_enabled_live_accounts() selected = {_safe_int(x) for x in (account_ids or []) if _safe_int(x) > 0} if selected: accounts = [a for a in accounts if _safe_int(a.get("id")) in selected] if not accounts: return {"ok": False, "reason": "no_enabled_accounts", "items": []} items = [] for account in accounts: items.append(create_live_order_intent({**payload, "account_id": account["id"]}, source_type=source_type, source_id=source_id)) return {"ok": True, "items": items, "total": len(items)} def list_live_order_intents(limit: int = 50, offset: int = 0, status: str = "", account_id: int = 0) -> dict: limit = max(1, min(_safe_int(limit, 50), 200)) offset = max(0, _safe_int(offset)) params: list = [] clauses: list[str] = [] if status: clauses.append("status=%s") params.append(status) if _safe_int(account_id) > 0: clauses.append("account_id=%s") params.append(_safe_int(account_id)) where = f"WHERE {' AND '.join(clauses)}" if clauses else "" conn = get_conn() try: total = conn.execute(f"SELECT COUNT(*) FROM live_order_intents {where}", tuple(params)).fetchone()[0] rows = conn.execute( f"SELECT * FROM live_order_intents {where} ORDER BY updated_at DESC, id DESC LIMIT %s OFFSET %s", tuple(params + [limit, offset]), ).fetchall() finally: conn.close() return {"items": [_row(r) for r in rows], "total": total, "limit": limit, "offset": offset} def get_live_order_intent(intent_id: int) -> dict: intent_id = _safe_int(intent_id) if intent_id <= 0: return {} conn = get_conn() try: row = conn.execute("SELECT * FROM live_order_intents WHERE id=%s", (intent_id,)).fetchone() finally: conn.close() return _row(row) def update_live_order_intent(intent_id: int, **fields) -> dict: intent_id = _safe_int(intent_id) allowed = { "status", "reason", "quantity", "price", "exchange_order_id", "response_json", "submitted_at", "finished_at", "updated_at", } updates = [] params = [] for key, value in fields.items(): if key not in allowed: continue column_value = _dumps(value) if key == "response_json" else value updates.append(f"{key}=%s") params.append(column_value) if not updates or intent_id <= 0: return get_live_order_intent(intent_id) params.append(intent_id) conn = get_conn() try: row = conn.execute( f"UPDATE live_order_intents SET {', '.join(updates)} WHERE id=%s RETURNING *", tuple(params), ).fetchone() conn.commit() finally: conn.close() return _row(row) def record_live_order_event(intent_id: int, event_type: str, status: str, message: str = "", payload=None) -> dict: conn = get_conn() try: row = conn.execute( """ INSERT INTO live_order_events (intent_id, event_type, status, message, payload_json, event_time) VALUES (%s,%s,%s,%s,%s,%s) RETURNING * """, (_safe_int(intent_id), event_type, status, message, _dumps(payload or {}), _now()), ).fetchone() conn.commit() finally: conn.close() return _row(row) def list_live_order_events(limit: int = 80, offset: int = 0, intent_id: int = 0) -> dict: limit = max(1, min(_safe_int(limit, 80), 200)) offset = max(0, _safe_int(offset)) params: list = [] where = "" if _safe_int(intent_id) > 0: where = "WHERE intent_id=%s" params.append(_safe_int(intent_id)) conn = get_conn() try: total = conn.execute(f"SELECT COUNT(*) FROM live_order_events {where}", tuple(params)).fetchone()[0] rows = conn.execute( f"SELECT * FROM live_order_events {where} ORDER BY event_time DESC, id DESC LIMIT %s OFFSET %s", tuple(params + [limit, offset]), ).fetchall() finally: conn.close() return {"items": [_row(r) for r in rows], "total": total, "limit": limit, "offset": offset} def prepare_intent_from_paper_trade(paper_trade_id: int, account_ids: list[int] | None = None) -> dict: conn = get_conn() try: trade = conn.execute("SELECT * FROM paper_trades WHERE id=%s", (_safe_int(paper_trade_id),)).fetchone() finally: conn.close() if not trade: return {"ok": False, "reason": "paper_trade_not_found"} payload = { "symbol": trade["symbol"], "side": trade.get("side") or "long", "order_type": "market", "price": _safe_float(trade.get("entry_price")), "stop_loss": _safe_float(trade.get("stop_loss")), "take_profit": _safe_float(trade.get("tp1")), "notional_usdt": _safe_float(trade.get("notional_usdt")), "leverage": _safe_float(trade.get("leverage"), 1), "recommendation_id": _safe_int(trade.get("recommendation_id")), "paper_trade_id": _safe_int(trade.get("id")), } result = create_live_order_intents_for_accounts(payload, account_ids=account_ids, source_type="paper_trade", source_id=_safe_int(trade.get("id"))) return result if result.get("ok") else {"ok": False, "reason": result.get("reason", "intent_create_failed"), "items": result.get("items", [])} def get_live_trading_summary() -> dict: cfg = get_effective_live_trading_config() risk = _risk_settings(cfg) conn = get_conn() try: status_rows = conn.execute( "SELECT status, COUNT(*) AS count FROM live_order_intents GROUP BY status ORDER BY status" ).fetchall() latest_rows = conn.execute( "SELECT * FROM live_order_intents ORDER BY updated_at DESC, id DESC LIMIT 8" ).fetchall() account_count = conn.execute("SELECT COUNT(*) FROM live_trade_accounts").fetchone()[0] finally: conn.close() return { "enabled": bool(cfg.get("enabled")), "execution_mode": cfg.get("execution_mode", "exchange_api"), "exchange": cfg.get("exchange", "binance"), "market_type": cfg.get("market_type", "um_futures"), "testnet": bool(cfg.get("testnet", True)), "require_human_approval": bool(cfg.get("require_human_approval", True)), "max_order_margin_usdt": risk["max_order_margin_usdt"], "max_order_notional_usdt": risk["max_order_notional_usdt"], "max_symbol_leverage": risk["max_symbol_leverage"], "max_cumulative_leverage": risk["max_cumulative_leverage"], "max_daily_order_count": risk["max_daily_order_count"], "account_count": account_count, "intent_status": {r["status"]: r["count"] for r in status_rows}, "latest_intents": [_row(r) for r in latest_rows], }