diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 94be914..9bef686 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -7134,6 +7134,8 @@ def render_main_page(page="trade", embed_mode=None): from lib.instance.instance_embed_context_lib import ( embed_render_plan, minimal_stats_bundle, + profit_loss_ratio_from_trades, + total_funds_usdt, trade_records_summary, ) @@ -7185,14 +7187,17 @@ def render_main_page(page="trade", embed_mode=None): total = len(records) win = count_winning_trades(records) rate = round(win / total * 100, 2) if total else 0 + profit_loss_ratio = profit_loss_ratio_from_trades(records) elif plan.records_summary: summary = trade_records_summary(conn, start_bj, end_bj, tr_ts) records = summary["records"] total = summary["total"] rate = summary["rate"] + profit_loss_ratio = summary.get("profit_loss_ratio") else: records = [] total = rate = 0 + profit_loss_ratio = None active_count = len(order_list) from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors @@ -7251,6 +7256,8 @@ def render_main_page(page="trade", embed_mode=None): record=records, total=total, rate=rate, + profit_loss_ratio=profit_loss_ratio, + total_funds=total_funds_usdt(funding_usdt, current_capital), trading_day=trading_day, funding_usdt=funding_usdt, daily_start_capital=DAILY_START_CAPITAL, @@ -7403,6 +7410,8 @@ def api_account_snapshot(): extra_blocks=not risk_status.get("can_trade", True), ) available_trading_usdt = get_available_trading_usdt() + from lib.instance.instance_embed_context_lib import total_funds_usdt + unrealized_pnl = None if exchange_private_api_configured(): from lib.instance.instance_live_pnl_lib import resolve_instance_unrealized_pnl @@ -7419,6 +7428,7 @@ def api_account_snapshot(): return jsonify({ "funding_usdt": funding_usdt, "current_capital": current_capital, + "total_funds": total_funds_usdt(funding_usdt, current_capital), "available_trading_usdt": round(available_trading_usdt, FUNDS_DECIMALS) if available_trading_usdt is not None else None, "unrealized_pnl": unrealized_pnl, "recommended_capital": recommended_capital, diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 3002c91..0245ffb 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -6922,6 +6922,8 @@ def render_main_page(page="trade", embed_mode=None): from lib.instance.instance_embed_context_lib import ( embed_render_plan, minimal_stats_bundle, + profit_loss_ratio_from_trades, + total_funds_usdt, trade_records_summary, ) @@ -6982,14 +6984,17 @@ def render_main_page(page="trade", embed_mode=None): total = len(records) win = count_winning_trades(records) rate = round(win / total * 100, 2) if total else 0 + profit_loss_ratio = profit_loss_ratio_from_trades(records) elif plan.records_summary: summary = trade_records_summary(conn, start_bj, end_bj, tr_ts) records = summary["records"] total = summary["total"] rate = summary["rate"] + profit_loss_ratio = summary.get("profit_loss_ratio") else: records = [] total = rate = 0 + profit_loss_ratio = None active_count = len(order_list) from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors @@ -7044,6 +7049,8 @@ def render_main_page(page="trade", embed_mode=None): record=records, total=total, rate=rate, + profit_loss_ratio=profit_loss_ratio, + total_funds=total_funds_usdt(funding_usdt, current_capital), trading_day=trading_day, funding_usdt=funding_usdt, daily_start_capital=DAILY_START_CAPITAL, @@ -7218,6 +7225,8 @@ def api_account_snapshot(): extra_blocks=not risk_status.get("can_trade", True), ) available_trading_usdt = get_available_trading_usdt() + from lib.instance.instance_embed_context_lib import total_funds_usdt + unrealized_pnl = None if exchange_private_api_configured(): from lib.instance.instance_live_pnl_lib import resolve_instance_unrealized_pnl @@ -7237,6 +7246,7 @@ def api_account_snapshot(): return jsonify({ "funding_usdt": funding_usdt, "current_capital": current_capital, + "total_funds": total_funds_usdt(funding_usdt, current_capital), "available_trading_usdt": round(available_trading_usdt, 2) if available_trading_usdt is not None else None, "unrealized_pnl": unrealized_pnl, "recommended_capital": recommended_capital, diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 36c3f3f..fb5bf9f 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -6471,6 +6471,8 @@ def render_main_page(page="trade", embed_mode=None): from lib.instance.instance_embed_context_lib import ( embed_render_plan, minimal_stats_bundle, + profit_loss_ratio_from_trades, + total_funds_usdt, trade_records_summary, ) @@ -6530,14 +6532,17 @@ def render_main_page(page="trade", embed_mode=None): total = len(records) win = count_winning_trades(records) rate = round(win / total * 100, 2) if total else 0 + profit_loss_ratio = profit_loss_ratio_from_trades(records) elif plan.records_summary: summary = trade_records_summary(conn, start_bj, end_bj, tr_ts) records = summary["records"] total = summary["total"] rate = summary["rate"] + profit_loss_ratio = summary.get("profit_loss_ratio") else: records = [] total = rate = 0 + profit_loss_ratio = None active_count = len(order_list) from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors @@ -6594,6 +6599,8 @@ def render_main_page(page="trade", embed_mode=None): record=records, total=total, rate=rate, + profit_loss_ratio=profit_loss_ratio, + total_funds=total_funds_usdt(funding_usdt, current_capital), trading_day=trading_day, daily_start_capital=DAILY_START_CAPITAL, current_capital=current_capital, @@ -6764,6 +6771,8 @@ def api_account_snapshot(): extra_blocks=not risk_status.get("can_trade", True), ) available_trading_usdt = get_available_trading_usdt() + from lib.instance.instance_embed_context_lib import total_funds_usdt + unrealized_pnl = None if exchange_private_api_configured(): from lib.instance.instance_live_pnl_lib import resolve_instance_unrealized_pnl @@ -6783,6 +6792,7 @@ def api_account_snapshot(): return jsonify({ "funding_usdt": funding_usdt, "current_capital": current_capital, + "total_funds": total_funds_usdt(funding_usdt, current_capital), "available_trading_usdt": round(available_trading_usdt, FUNDS_DECIMALS) if available_trading_usdt is not None else None, "unrealized_pnl": unrealized_pnl, "recommended_capital": recommended_capital, diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index 2a1af3b..987813b 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -604,7 +604,7 @@ html[data-theme="light"] .theme-toggle-btn.is-active { .instance-header-stats { display: grid; - grid-template-columns: repeat(7, minmax(0, 1fr)); + grid-template-columns: repeat(9, minmax(0, 1fr)); gap: 0; margin-top: 12px; padding: 14px 0 12px; @@ -667,7 +667,7 @@ html[data-theme="light"] .theme-toggle-btn.is-active { @media (max-width: 1100px) { .instance-header-stats { - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); row-gap: 10px; } diff --git a/lib/instance/instance_embed_context_lib.py b/lib/instance/instance_embed_context_lib.py index 803e29b..9efe145 100644 --- a/lib/instance/instance_embed_context_lib.py +++ b/lib/instance/instance_embed_context_lib.py @@ -7,6 +7,8 @@ from typing import Any EMBED_STRATEGY_PAGES = frozenset({"strategy", "strategy_trend", "strategy_roll", "strategy_records"}) +_WIN_EPS = 1e-9 + @dataclass(frozen=True) class EmbedRenderPlan: @@ -50,6 +52,48 @@ def embed_render_plan(page: str, embed_mode: str | None) -> EmbedRenderPlan: ) +def profit_loss_ratio_from_averages(avg_win: float | None, avg_loss: float | None) -> float | None: + """盈亏比 = 平均盈利 / |平均亏损|。""" + if avg_win is None or avg_loss is None: + return None + try: + aw = float(avg_win) + al = float(avg_loss) + except (TypeError, ValueError): + return None + if al == 0: + return None + return round(aw / abs(al), 2) + + +def profit_loss_ratio_from_trades(trades: list[dict[str, Any]] | None) -> float | None: + wins: list[float] = [] + losses: list[float] = [] + for row in trades or []: + if not isinstance(row, dict): + continue + try: + pnl = float(row.get("effective_pnl_amount") or row.get("pnl_amount") or 0) + except (TypeError, ValueError): + continue + if pnl > _WIN_EPS: + wins.append(pnl) + elif pnl < -_WIN_EPS: + losses.append(pnl) + avg_win = sum(wins) / len(wins) if wins else None + avg_loss = sum(losses) / len(losses) if losses else None + return profit_loss_ratio_from_averages(avg_win, avg_loss) + + +def total_funds_usdt(funding_usdt: float | None, trading_usdt: float | None) -> float | None: + if funding_usdt is None: + return None + try: + return round(float(funding_usdt) + float(trading_usdt or 0), 2) + except (TypeError, ValueError): + return None + + def trade_records_summary(conn, start_bj: str, end_bj: str, tr_ts: str) -> dict[str, Any]: """顶栏统计用 COUNT,避免 embed 壳拉 1000 行交易记录。""" from lib.trade.trade_result_lib import sql_effective_pnl_expr @@ -59,7 +103,9 @@ def trade_records_summary(conn, start_bj: str, end_bj: str, tr_ts: str) -> dict[ f""" SELECT COUNT(*) AS total, - SUM(CASE WHEN {pnl_sql} > 0 THEN 1 ELSE 0 END) AS wins + SUM(CASE WHEN {pnl_sql} > 0 THEN 1 ELSE 0 END) AS wins, + AVG(CASE WHEN {pnl_sql} > 0 THEN {pnl_sql} END) AS avg_win, + AVG(CASE WHEN {pnl_sql} < 0 THEN {pnl_sql} END) AS avg_loss FROM trade_records WHERE {tr_ts} >= ? AND {tr_ts} <= ? AND COALESCE(result, '') != '错过' @@ -70,10 +116,13 @@ def trade_records_summary(conn, start_bj: str, end_bj: str, tr_ts: str) -> dict[ total = int(row["total"] or 0) if row else 0 wins = int(row["wins"] or 0) if row else 0 rate = round(wins / total * 100, 2) if total else 0 + avg_win = float(row["avg_win"]) if row and row["avg_win"] is not None else None + avg_loss = float(row["avg_loss"]) if row and row["avg_loss"] is not None else None return { "records": [], "total": total, "rate": rate, + "profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss), } diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html index fc9c1cc..34e38d7 100644 --- a/lib/instance/templates/embed_boot_scripts.html +++ b/lib/instance/templates/embed_boot_scripts.html @@ -1053,6 +1053,10 @@ function refreshAccountSnapshot(){ const el = document.getElementById("total-capital"); if(el) el.innerText = (data.funding_usdt === null || data.funding_usdt === undefined) ? "—" : `${Number(data.funding_usdt).toFixed(2)}U`; } + if (typeof data.total_funds !== "undefined") { + const el = document.getElementById("total-funds"); + if(el) el.innerText = (data.total_funds === null || data.total_funds === undefined) ? "—" : `${Number(data.total_funds).toFixed(2)}U`; + } if (typeof data.current_capital !== "undefined") { const el = document.getElementById("current-capital"); if(el) el.innerText = `${Number(data.current_capital).toFixed(2)}U`; diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index 22036db..8cfcd67 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -7,7 +7,7 @@ - +