diff --git a/manual_trading_hub/hub_ai/context.py b/manual_trading_hub/hub_ai/context.py index 0492d28..f429b5b 100644 --- a/manual_trading_hub/hub_ai/context.py +++ b/manual_trading_hub/hub_ai/context.py @@ -271,6 +271,28 @@ def format_context_text(payload: dict) -> str: return "\n".join(lines).strip() +def format_account_remark(ac: dict) -> str: + """分户表格备注:持仓摘要或关注点。""" + positions = ac.get("positions") or [] + if not positions: + issues = ac.get("issues") or [] + if issues: + return ";".join(str(x) for x in issues[:2]) + return "无" + parts: list[str] = [] + for p in positions[:3]: + if not isinstance(p, dict): + continue + sym = p.get("symbol") or "?" + side = p.get("side") or "?" + contracts = p.get("contracts") if p.get("contracts") is not None else p.get("size") + upnl = _position_float_pnl(p) + parts.append(f"持仓: {sym} {side} 张数{contracts} 浮盈亏{upnl:.4f}U") + if len(positions) > 3: + parts.append(f"另有{len(positions) - 3}仓") + return ";".join(parts) if parts else "无" + + def format_chat_context_brief(payload: dict, max_chars: int = 2500) -> str: text = format_context_text(payload) if len(text) <= max_chars: diff --git a/manual_trading_hub/hub_ai/prompts.py b/manual_trading_hub/hub_ai/prompts.py index 818607e..6ab2d1d 100644 --- a/manual_trading_hub/hub_ai/prompts.py +++ b/manual_trading_hub/hub_ai/prompts.py @@ -19,7 +19,7 @@ SUMMARY_SYSTEM = """ - **当前持仓浮盈亏(U)**:…(仅汇总已监控且有数据的账户) **2. 分户明细** -每个账户一行:账户名 | 状态(已监控/未监控/连接异常) | 当日平仓盈亏 | 笔数 | 当前浮盈亏 | 备注 +中控页面会自动渲染分户表格,本节不要输出 pipe 分隔行或 Markdown 表格;可写一句「见下表」或直接留空。 **3. 需关注** 仅有依据时列出(如:某户当日亏损最大、浮亏偏大、Flask/Agent 异常、有持仓但无本地监控等);若无则写「无」。 diff --git a/manual_trading_hub/hub_ai/summary.py b/manual_trading_hub/hub_ai/summary.py index d2dd066..badf87e 100644 --- a/manual_trading_hub/hub_ai/summary.py +++ b/manual_trading_hub/hub_ai/summary.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from hub_ai.client import generate_text, model_label -from hub_ai.context import build_daily_context +from hub_ai.context import build_daily_context, format_account_remark from hub_ai.prompts import SUMMARY_SYSTEM, build_summary_user_prompt from hub_ai.store import append_summary, get_latest_summary, list_summaries @@ -38,11 +38,13 @@ def generate_daily_summary( "totals": ctx.get("totals"), "by_account": { str(ac.get("key") or ac.get("id")): { + "key": ac.get("key"), "name": ac.get("name"), "status": ac.get("status"), "pnl_u": (ac.get("trade_stats") or {}).get("total_pnl_u"), "closed_count": (ac.get("trade_stats") or {}).get("closed_count"), "float_pnl_u": ac.get("float_pnl_u"), + "remark": format_account_remark(ac), "issues": ac.get("issues") or [], } for ac in ctx.get("accounts") or [] diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index d3f3caa..9e73648 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -3575,6 +3575,75 @@ body.hub-page-ai #page-ai { .ai-bubble-assistant.ai-result-md ol { margin: 4px 0 6px 1.15em; } +.ai-ac-table-wrap { + margin: 8px 0 12px; + overflow-x: auto; + border: 1px solid var(--border-soft); + border-radius: 8px; + background: color-mix(in srgb, var(--inset-surface) 88%, transparent); +} +.ai-ac-table { + width: 100%; + border-collapse: collapse; + font-size: 0.78rem; + line-height: 1.45; +} +.ai-ac-table th, +.ai-ac-table td { + padding: 8px 10px; + text-align: left; + vertical-align: top; + border-bottom: 1px solid var(--border-soft); +} +.ai-ac-table th { + font-size: 0.72rem; + font-weight: 600; + color: var(--muted); + background: color-mix(in srgb, var(--inset-surface) 60%, transparent); + white-space: nowrap; +} +.ai-ac-table tbody tr:last-child td { + border-bottom: none; +} +.ai-ac-table tbody tr:hover td { + background: color-mix(in srgb, var(--accent-dim) 35%, transparent); +} +.ai-ac-name { + display: flex; + align-items: flex-start; + gap: 8px; + min-width: 9rem; + font-weight: 500; +} +.ai-ac-name > span:last-child { + flex: 1; + min-width: 0; +} +.ai-ex-icon { + display: inline-flex; + flex-shrink: 0; + line-height: 0; + border-radius: 5px; + overflow: hidden; +} +.ai-ac-remark { + color: var(--muted); + font-size: 0.74rem; + max-width: 16rem; +} +.ai-ac-unmon { + color: var(--muted); +} +.ai-ac-err { + color: var(--red); +} +.ai-ac-warn { + color: var(--amber, #d4a017); +} +.ai-ac-table .pos, +.ai-ac-table .neg { + font-weight: 600; +} .ai-placeholder { color: var(--muted); margin: 0; diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index afbc21a..b8565e9 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -2972,10 +2972,105 @@ return renderHubMarkdown(text); } - function setAiSummaryMarkdown(body, contentMd) { + const AI_EX_SVG = { + binance: + '', + okx: + '', + gate: + '', + gate_bot: + '', + other: + '', + }; + + function resolveExchangeIconId(key, name) { + const k = String(key || "").toLowerCase(); + const n = String(name || "").toLowerCase(); + if (k === "gate_bot" || k.includes("gate_bot") || n.includes("趋势") || n.includes("gate_bot")) + return "gate_bot"; + if (k === "binance" || k.includes("binance") || n.includes("币安")) return "binance"; + if (k === "okx" || k.includes("okx")) return "okx"; + if (k === "gate" || k.includes("gate") || n.includes("gate")) return "gate"; + return "other"; + } + + function hubExchangeIconHtml(key, name) { + const id = resolveExchangeIconId(key, name); + const title = + id === "binance" + ? "Binance" + : id === "okx" + ? "OKX" + : id === "gate_bot" + ? "Gate Bot" + : id === "gate" + ? "Gate" + : ""; + return `${AI_EX_SVG[id] || AI_EX_SVG.other}`; + } + + function aiAccountStatusClass(status) { + const s = String(status || ""); + if (s === "未监控") return "ai-ac-unmon"; + if (s.includes("异常")) return "ai-ac-err"; + if (s.includes("需关注")) return "ai-ac-warn"; + return ""; + } + + function renderAiAccountTable(snapshot) { + const accounts = snapshot && snapshot.by_account; + if (!accounts || typeof accounts !== "object") return ""; + const rows = Object.values(accounts); + if (!rows.length) return ""; + const head = + "" + + "账户状态平仓盈亏笔数浮盈亏备注" + + ""; + const body = rows + .map((ac) => { + const closedPnl = Number(ac.pnl_u); + const floatPnl = Number(ac.float_pnl_u); + const remark = + ac.remark || + (Array.isArray(ac.issues) && ac.issues.length ? ac.issues.join(";") : "无"); + const statusCls = aiAccountStatusClass(ac.status); + return ( + "" + + `${hubExchangeIconHtml(ac.key, ac.name)}${esc(ac.name || "—")}` + + `${esc(ac.status || "—")}` + + `${aiPnlSigned(closedPnl, 2)}` + + `${Number(ac.closed_count) || 0}` + + `${aiPnlSigned(floatPnl, 2)}` + + `${esc(remark)}` + + "" + ); + }) + .join(""); + return `
${head}${body}
`; + } + + function renderAiSummaryBody(contentMd, snapshot) { + const md = String(contentMd || ""); + const sec2 = /\*\*2\.\s*分户明细\*\*/; + const sec3 = /\*\*3\.\s*需关注\*\*/; + const i2 = md.search(sec2); + const i3 = md.search(sec3); + const tableHtml = renderAiAccountTable(snapshot); + if (i2 >= 0 && i3 > i2 && tableHtml) { + const headEnd = i2 + md.slice(i2).match(sec2)[0].length; + const part1 = md.slice(0, headEnd); + const part2 = md.slice(i3); + return renderHubMarkdown(part1) + tableHtml + renderHubMarkdown(part2); + } + return renderHubMarkdown(md) + (tableHtml ? tableHtml : ""); + } + + function setAiSummaryMarkdown(body, contentMd, snapshot) { if (!body) return; body.classList.add("ai-result-md"); - body.innerHTML = renderHubMarkdown(contentMd); + body.innerHTML = renderAiSummaryBody(contentMd, snapshot); } function setAiSummaryPlaceholder(body, html) { @@ -3075,7 +3170,7 @@ const j = await r.json(); const latest = j.latest; if (latest && latest.content_md) { - if (body) setAiSummaryMarkdown(body, latest.content_md); + if (body) setAiSummaryMarkdown(body, latest.content_md, latest.stats_snapshot); renderAiSummaryStats(latest.stats_snapshot); const sm = document.getElementById("ai-summary-meta"); if (sm && latest.generated_at) { @@ -3117,7 +3212,7 @@ if (!j.ok && j.detail) throw new Error(j.detail); const sum = j.summary; if (sum && sum.content_md && body) { - setAiSummaryMarkdown(body, sum.content_md); + setAiSummaryMarkdown(body, sum.content_md, sum.stats_snapshot); renderAiSummaryStats(sum.stats_snapshot); } showToast(j.cached ? "已是最新上下文,返回缓存总结" : "今日总结已生成"); diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 7181c91..79ce68b 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -288,6 +288,6 @@ - +