From 9f799b04f630a97d520b5a4a703ef8714812bcf2 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Mon, 8 Jun 2026 00:41:46 +0800 Subject: [PATCH] 1 --- app/services/live_trading_account.py | 124 +++++++++++++++++++++++++-- static/live_trading.html | 2 +- tests/test_live_trading.py | 80 +++++++++++++++++ 3 files changed, 196 insertions(+), 10 deletions(-) diff --git a/app/services/live_trading_account.py b/app/services/live_trading_account.py index 62eab37..05a7381 100644 --- a/app/services/live_trading_account.py +++ b/app/services/live_trading_account.py @@ -62,6 +62,16 @@ def _position_pnl_pct(unrealized_pnl: float, margin: float, position_value: floa return 0.0 +def _display_symbol(symbol: str) -> str: + """Normalize ccxt futures symbols like BANK/USDT:USDT for UI display.""" + value = str(symbol or "").strip().upper() + if ":" in value: + value = value.split(":", 1)[0] + if value and "/" not in value and value.endswith("USDT"): + value = value[:-4] + "/USDT" + return value + + def _compact_position(item: dict, account: dict | None = None) -> dict: info = item.get("info") if isinstance(item.get("info"), dict) else {} contracts = _safe_float(item.get("contracts") or info.get("positionAmt")) @@ -89,7 +99,7 @@ def _compact_position(item: dict, account: dict | None = None) -> dict: leverage_source = "account_config" if leverage > 0 else "missing" side = item.get("side") or ("long" if contracts > 0 else ("short" if contracts < 0 else "")) return { - "symbol": item.get("symbol") or info.get("symbol"), + "symbol": _display_symbol(item.get("symbol") or info.get("symbol")), "side": side, "side_label": _position_side_label(side), "contracts": abs(contracts), @@ -109,7 +119,7 @@ def _compact_order(item: dict) -> dict: return { "id": str(item.get("id") or info.get("orderId") or ""), "client_order_id": item.get("clientOrderId") or info.get("clientOrderId") or "", - "symbol": item.get("symbol") or info.get("symbol"), + "symbol": _display_symbol(item.get("symbol") or info.get("symbol")), "type": item.get("type") or info.get("type"), "side": item.get("side") or info.get("side"), "status": item.get("status") or info.get("status"), @@ -143,10 +153,7 @@ def _enrich_positions(positions: list[dict]) -> list[dict]: 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 + return _display_symbol(symbol) def _order_history_symbols(account: dict, overview: dict) -> list[str]: @@ -273,22 +280,121 @@ def _max_drawdown(history: list[dict]) -> tuple[float, float]: return round(max_dd_pct, 6), round(max_dd_usdt, 8) +def _order_ts(order: dict) -> str: + return str(order.get("timestamp") or "") + + +def _order_price(order: dict) -> float: + return _safe_float(order.get("average") or order.get("price")) + + +def _order_amount(order: dict) -> float: + return _safe_float(order.get("filled") or order.get("amount")) + + +def _order_direction(order: dict, *, closing: bool = False) -> str: + side = str(order.get("side") or "").strip().lower() + position_side = str(order.get("position_side") or "").strip().lower() + if position_side in {"long", "short"}: + return position_side + if closing: + if side == "sell": + return "long" + if side == "buy": + return "short" + if side == "buy": + return "long" + if side == "sell": + return "short" + return "" + + +def _realized_pnl_pct(side: str, entry_price: float, exit_price: float) -> float: + if entry_price <= 0 or exit_price <= 0: + return 0.0 + if side == "short": + return round((entry_price / exit_price - 1.0) * 100.0, 6) + return round((exit_price / entry_price - 1.0) * 100.0, 6) + + def _historical_positions_from_orders(orders: list[dict]) -> list[dict]: + """Build a position-level history from exchange order history. + + Binance futures order history is order-centric. For the console we pair + open orders with later reduce-only/realized-PnL orders so users can see + entry price, exit price and whether the completed position made money. + """ + active_lots: dict[tuple[str, str], list[dict]] = {} rows = [] + sorted_orders = sorted(orders or [], key=_order_ts) + for order in sorted_orders: + status = str(order.get("status") or "").lower() + if status not in {"closed", "filled"}: + continue + symbol = _display_symbol(order.get("symbol") or "") + if not symbol: + continue + amount = _order_amount(order) + price = _order_price(order) + pnl = _safe_float(order.get("realized_pnl")) + reduce_only = bool(order.get("reduce_only")) + is_close = reduce_only or abs(pnl) > 0 + direction = _order_direction(order, closing=is_close) + if not direction: + continue + key = (symbol, direction) + if not is_close: + active_lots.setdefault(key, []).append({ + "time": order.get("timestamp") or "", + "entry_price": price, + "amount": amount, + }) + continue + + lot = active_lots.get(key, []).pop(0) if active_lots.get(key) else {} + entry_price = _safe_float(lot.get("entry_price")) + exit_price = price + if pnl == 0 and entry_price > 0 and exit_price > 0 and amount > 0: + raw_pnl = (exit_price - entry_price) * amount if direction == "long" else (entry_price - exit_price) * amount + pnl = round(raw_pnl, 8) + pnl_pct = _realized_pnl_pct(direction, entry_price, exit_price) + result = "盈利" if pnl > 0 else "亏损" if pnl < 0 else "未知" + rows.append({ + "time": order.get("timestamp") or "", + "symbol": symbol, + "side": direction, + "side_label": _position_side_label(direction), + "entry_price": entry_price, + "exit_price": exit_price, + "price": exit_price, + "amount": amount, + "realized_pnl": pnl, + "realized_pnl_pct": pnl_pct, + "result": result, + "status": order.get("status") or "", + }) + + if rows: + return sorted(rows, key=lambda x: str(x.get("time") or ""), reverse=True)[:30] + for order in orders or []: status = str(order.get("status") or "").lower() if status not in {"closed", "filled"}: continue + symbol = _display_symbol(order.get("symbol") or "") pnl = _safe_float(order.get("realized_pnl")) result = "盈利" if pnl > 0 else "亏损" if pnl < 0 else "未知" rows.append({ "time": order.get("timestamp") or "", - "symbol": order.get("symbol") or "", + "symbol": symbol, "side": order.get("side") or "", "side_label": _position_side_label(order.get("side")), - "price": _safe_float(order.get("average") or order.get("price")), - "amount": _safe_float(order.get("filled") or order.get("amount")), + "entry_price": 0, + "exit_price": _order_price(order), + "price": _order_price(order), + "amount": _order_amount(order), "realized_pnl": pnl, + "realized_pnl_pct": 0, "result": result, "status": order.get("status") or "", }) diff --git a/static/live_trading.html b/static/live_trading.html index d7950dd..6400a99 100644 --- a/static/live_trading.html +++ b/static/live_trading.html @@ -141,7 +141,7 @@ if($('exchangeCacheNote'))$('exchangeCacheNote').textContent=cache.loaded?(cache $('accountInfo').innerHTML=[info('可用资金',fmt(b.free,2)+' USDT'),info('保证金占用',fmt(b.used||p.used_margin_usdt,2)+' USDT'),info('持仓名义价值',fmt(p.open_position_value_usdt,2)+' USDT'),info('浮动盈亏',(Number(p.unrealized_pnl_usdt||0)>0?'+':'')+fmt(p.unrealized_pnl_usdt,2)+' USDT'),info('权益峰值',fmt(p.peak_equity_usdt,2)+' USDT'),info('统计口径',p.basis||'等待首次同步')].join('')+(errors.length?'
| '+esc(h)+' | '}).join('')+'
|---|