586 lines
22 KiB
Python
586 lines
22 KiB
Python
"""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],
|
|
}
|