feat: 监控区 2x2 布局与左上今日统计卡

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-04 23:10:32 +08:00
parent eb975b0133
commit 9deb58a38a
9 changed files with 362 additions and 24 deletions
+3
View File
@@ -630,6 +630,7 @@ def register_hub_routes(app):
fetch_trades_for_trading_day,
summarize_trades,
)
from lib.trade.daily_open_limit_lib import count_opens_for_trading_day
c = _ctx()
get_db = c.get("get_db")
@@ -651,6 +652,7 @@ def register_hub_routes(app):
row_to_dict_fn=c.get("row_to_dict"),
reset_hour=reset_hour,
)
opens_today = count_opens_for_trading_day(conn, trading_day)
finally:
conn.close()
stats = summarize_trades(trades)
@@ -659,6 +661,7 @@ def register_hub_routes(app):
"ok": True,
"trading_day": trading_day,
"trading_day_reset_hour": reset_hour,
"opens_today": opens_today,
"trades": trades,
"stats": stats,
}
+93
View File
@@ -0,0 +1,93 @@
"""监控区看板:三所当日统计聚合。"""
from __future__ import annotations
from typing import Any
def _coerce_float(value: Any) -> float | None:
if value is None or value == "":
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def position_unrealized_pnl(pos: dict[str, Any]) -> float:
for key in ("unrealized_pnl", "unrealizedPnl", "upnl"):
v = _coerce_float(pos.get(key))
if v is not None:
return v
return 0.0
def _open_positions(agent: dict[str, Any] | None) -> list[dict[str, Any]]:
if not isinstance(agent, dict):
return []
positions = agent.get("positions")
if not isinstance(positions, list):
return []
out: list[dict[str, Any]] = []
for p in positions:
if not isinstance(p, dict):
continue
try:
c = abs(float(p.get("contracts") or 0))
except (TypeError, ValueError):
c = 0.0
if c > 1e-12:
out.append(p)
return out
def aggregate_monitor_board_totals(
rows: list[dict[str, Any]],
*,
trading_day: str,
reset_hour: int = 8,
) -> dict[str, Any]:
"""汇总监控 board 各行 → 左上统计卡数据。"""
open_count = 0
closed_count = 0
win_count = 0
loss_count = 0
win_pnl_u = 0.0
loss_pnl_u = 0.0
open_position_count = 0
float_pnl_u = 0.0
for row in rows or []:
if not isinstance(row, dict):
continue
day_stats = row.get("day_stats") if isinstance(row.get("day_stats"), dict) else {}
if day_stats.get("ok"):
open_count += int(day_stats.get("opens_today") or 0)
st = day_stats.get("trade_stats") if isinstance(day_stats.get("trade_stats"), dict) else {}
closed_count += int(st.get("closed_count") or 0)
win_count += int(st.get("win_count") or 0)
loss_count += int(st.get("loss_count") or 0)
win_pnl_u += float(st.get("win_pnl_u") or 0)
loss_pnl_u += float(st.get("loss_pnl_u") or 0)
ag = row.get("agent") if isinstance(row.get("agent"), dict) else {}
open_pos = _open_positions(ag)
open_position_count += len(open_pos)
agent_upnl = _coerce_float(ag.get("total_unrealized_pnl"))
if agent_upnl is not None:
float_pnl_u += agent_upnl
else:
float_pnl_u += sum(position_unrealized_pnl(p) for p in open_pos)
return {
"trading_day": trading_day,
"reset_hour": int(reset_hour),
"open_count": open_count,
"closed_count": closed_count,
"win_count": win_count,
"loss_count": loss_count,
"win_pnl_u": round(win_pnl_u, 4),
"loss_pnl_u": round(loss_pnl_u, 4),
"realized_pnl_u": round(win_pnl_u + loss_pnl_u, 4),
"open_position_count": open_position_count,
"float_pnl_u": round(float_pnl_u, 4),
}
+6
View File
@@ -616,6 +616,8 @@ def fetch_trades_for_archive(
def summarize_trades(trades: list[dict]) -> dict[str, Any]:
"""单笔列表 → 笔数 / 盈亏 / 胜败统计。"""
total_pnl = 0.0
win_pnl = 0.0
loss_pnl = 0.0
win = loss = flat = 0
for t in trades or []:
try:
@@ -625,8 +627,10 @@ def summarize_trades(trades: list[dict]) -> dict[str, Any]:
total_pnl += pnl
if pnl > 1e-9:
win += 1
win_pnl += pnl
elif pnl < -1e-9:
loss += 1
loss_pnl += pnl
else:
flat += 1
return {
@@ -635,4 +639,6 @@ def summarize_trades(trades: list[dict]) -> dict[str, Any]:
"loss_count": loss,
"flat_count": flat,
"total_pnl_u": round(total_pnl, 4),
"win_pnl_u": round(win_pnl, 4),
"loss_pnl_u": round(loss_pnl, 4),
}