diff --git a/lib/cash_sample_lib.py b/lib/cash_sample_lib.py index 214a4c2..c1eeabc 100644 --- a/lib/cash_sample_lib.py +++ b/lib/cash_sample_lib.py @@ -90,6 +90,50 @@ def record_cash_samples_from_overview(rows: list[dict[str, Any]]) -> int: return len(to_write) +def first_cash_by_instance(instance_ids: list[Any] | None = None) -> dict[Any, float]: + """各机最早一笔现金采样,作初始资金回退(实盘未配 LIVE_INITIAL 时).""" + conn = get_db() + try: + if instance_ids: + ids: list[int] = [] + for x in instance_ids: + try: + ids.append(int(x)) + except (TypeError, ValueError): + continue + if not ids: + return {} + placeholders = ",".join("?" * len(ids)) + rows = conn.execute( + f""" + SELECT instance_id, cash_usdt + FROM hub_cash_samples + WHERE instance_id IN ({placeholders}) + ORDER BY ts_ms ASC + """, + tuple(ids), + ).fetchall() + else: + rows = conn.execute( + """ + SELECT instance_id, cash_usdt + FROM hub_cash_samples + ORDER BY ts_ms ASC + """ + ).fetchall() + finally: + conn.close() + out: dict[Any, float] = {} + for r in rows: + iid = r["instance_id"] + if iid in out: + continue + v = _safe_float(r["cash_usdt"]) + if v is not None and v > 0: + out[iid] = round(v, 4) + return out + + def query_cash_series( *, range_key: str = "7d", diff --git a/lib/stats_agg_lib.py b/lib/stats_agg_lib.py index 8386832..d66432b 100644 --- a/lib/stats_agg_lib.py +++ b/lib/stats_agg_lib.py @@ -4,7 +4,11 @@ from __future__ import annotations from typing import Any from lib.bridge_client_lib import pull_all_enabled, pull_all_stats -from lib.cash_sample_lib import query_cash_series, record_cash_samples_from_overview +from lib.cash_sample_lib import ( + first_cash_by_instance, + query_cash_series, + record_cash_samples_from_overview, +) def _safe_float(v: Any) -> float | None: @@ -52,6 +56,10 @@ def build_stats_summary( pass stats_rows = pull_all_stats(w) stats_by_id = {r.get("id"): r for r in stats_rows} + try: + initial_fallback = first_cash_by_instance([s.get("id") for s in snaps]) + except Exception: + initial_fallback = {} machines: list[dict[str, Any]] = [] sum_cash = 0.0 @@ -92,9 +100,13 @@ def build_stats_summary( mode = ((body.get("instance") or {}) if isinstance(body.get("instance"), dict) else {}).get( "mode" ) + # 收益率 = 窗口已结算盈亏 / 初始资金(非当前余额) + initial = _safe_float(premium.get("initial_cash_usdt")) + if initial is None or initial <= 0: + initial = _safe_float(initial_fallback.get(iid)) return_pct = None - if online and stats.get("ok") and net is not None and cash is not None and cash > 0: - return_pct = round(float(net) / float(cash) * 100.0, 2) + if online and stats.get("ok") and net is not None and initial is not None and initial > 0: + return_pct = round(float(net) / float(initial) * 100.0, 2) if online: online_n += 1 if cash is not None: @@ -127,6 +139,7 @@ def build_stats_summary( "premium_usdt": paid, "ratio_pct": ratio, "unrealized_usdt": unreal, + "initial_cash_usdt": initial, "pos_count": len(brief), "total_closed": closed if online and stats.get("ok") else None, "win_count": wins if online and stats.get("ok") else None, @@ -141,6 +154,14 @@ def build_stats_summary( } ) + def _mode_rank(m: dict[str, Any]) -> tuple[int, str]: + mode_v = str(m.get("mode") or "").strip().lower() + # 上方 sim,下方 live + rank = 0 if mode_v == "sim" else 1 if mode_v == "live" else 2 + return (rank, str(m.get("name") or m.get("id") or "")) + + machines.sort(key=_mode_rank) + agg_win_rate = round(sum_wins / sum_closed * 100, 2) if sum_closed else 0.0 return { "ok": True, diff --git a/static/stats.js b/static/stats.js index 7b15a92..efe8dcb 100644 --- a/static/stats.js +++ b/static/stats.js @@ -116,10 +116,24 @@ .join(""); } + function sortMachines(machines) { + return (machines || []).slice().sort(function (a, b) { + function rank(m) { + const mode = String((m && m.mode) || "").toLowerCase(); + if (mode === "sim") return 0; + if (mode === "live") return 1; + return 2; + } + const d = rank(a) - rank(b); + if (d !== 0) return d; + return String((a && a.name) || "").localeCompare(String((b && b.name) || ""), "zh"); + }); + } + function renderTable(machines) { const body = document.getElementById("stats-table-body"); if (!body) return; - const list = machines || []; + const list = sortMachines(machines); if (!list.length) { body.innerHTML = '
实时资金状态 + 当前窗口交易 KPI。收益率 = 窗口已实现 ÷ 当前余额。
+实时资金状态 + 当前窗口交易 KPI。排序:模拟在上、实盘在下。收益率 = 窗口已结算盈亏 ÷ 初始资金。