fix: feed today-only data to AI daily summary to reduce hallucination

Shrink summary context and prompts to today's trades and positions only, and tighten anti-fabrication rules.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-09 09:39:40 +08:00
parent a5c6e0c5b6
commit 2786acf884
4 changed files with 123 additions and 23 deletions
+102 -5
View File
@@ -517,6 +517,97 @@ def format_context_text(payload: dict) -> str:
return "\n".join(lines).strip() return "\n".join(lines).strip()
def format_summary_context_text(payload: dict) -> str:
"""今日总结专用:仅当日平仓/持仓/监控,不含昨日明细与资金走势。"""
lines = []
totals = payload.get("totals") or {}
day = totals.get("trading_day")
lines.append(
f"【合计·今日 {day}】平仓盈亏 {totals.get('total_pnl_u')}U | "
f"笔数 {totals.get('closed_count')}(胜{totals.get('win_count')}/负{totals.get('loss_count')}| "
f"实盘持仓 {totals.get('open_position_count', 0)} 仓 | "
f"浮盈亏 {totals.get('float_pnl_u')}U | "
f"资金账户合计 {_fmt_fund(totals.get('total_funding_usdt'))} | "
f"交易账户合计 {_fmt_fund(totals.get('total_trading_usdt'))}"
)
lines.append(
f"【说明】交易日={day}"
"「持仓」= 交易所 Agent 实盘;「趋势/关键位/监控单/加仓」= 本地计划,不等于已开仓。"
)
lines.append("")
for ac in payload.get("accounts") or []:
st = ac.get("trade_stats") or {}
lines.append(f"--- 账户:{ac.get('name')} ({ac.get('key')}) ---")
lines.append(f"状态:{ac.get('status')}")
if ac.get("status") == "未监控":
lines.append("")
continue
lines.append(
f"资金账户 {_fmt_fund(ac.get('funding_usdt'))} | "
f"交易账户 {_fmt_fund(ac.get('trading_usdt'))} | "
f"可用 {_fmt_fund(ac.get('available_trading_usdt'))}"
)
lines.append(
f"今日({day})平仓:{st.get('closed_count')} 笔,盈亏 {st.get('total_pnl_u')}U "
f"(胜{st.get('win_count')}/负{st.get('loss_count')}"
)
open_n = int(ac.get("open_position_count") or _account_open_position_count(ac))
if open_n <= 0:
lines.append("当前交易所持仓:无(空仓)")
else:
lines.append(
f"当前交易所持仓:{open_n} 仓 | 浮盈亏合计 {ac.get('float_pnl_u')}U"
)
mon = ac.get("monitor_lines") or {}
if mon.get("trends"):
lines.append("趋势回调计划(本地,非持仓):")
for row in mon["trends"][:8]:
lines.append(f" - {row}")
if mon.get("rolls"):
lines.append("顺势加仓(本地,非持仓):")
for row in mon["rolls"][:8]:
lines.append(f" - {row}")
if mon.get("keys"):
lines.append("关键位监控(本地,非持仓):")
for row in mon["keys"][:8]:
lines.append(f" - {row}")
if mon.get("orders"):
lines.append("进行中的下单监控(本地,非持仓):")
for row in mon["orders"][:8]:
lines.append(f" - {row}")
positions = ac.get("positions") or []
if positions:
lines.append("持仓明细(交易所实盘):")
for p in positions[:8]:
if not isinstance(p, dict):
continue
sym = p.get("symbol") or "?"
side = p.get("side") or "?"
contracts = p.get("contracts") or p.get("size") or "?"
upnl = _position_float_pnl(p)
lines.append(f" - {sym} {side} 张数{contracts} 浮盈亏{upnl:.4f}U")
lines.append(
f"Agent合约余额:{ac.get('balance_usdt') if ac.get('balance_usdt') is not None else '未知'} USDT"
)
trades_today = ac.get("trades") or []
if trades_today:
lines.append("今日平仓明细:")
for t in trades_today[:15]:
lines.append(f" - {_format_trade_line(t)}")
else:
lines.append("今日平仓明细:无")
issues = ac.get("issues") or []
if issues:
lines.append("关注点:" + "".join(issues))
lines.append("")
return "\n".join(lines).strip()
def summary_context_hash(payload: dict) -> str:
text = format_summary_context_text(payload)
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]
def format_account_remark(ac: dict) -> str: def format_account_remark(ac: dict) -> str:
"""分户表格备注:监控摘要 + 持仓。""" """分户表格备注:监控摘要 + 持仓。"""
parts: list[str] = [] parts: list[str] = []
@@ -548,14 +639,20 @@ def format_account_remark(ac: dict) -> str:
return "".join(parts) return "".join(parts)
def collect_closed_trades_snapshot(accounts: list[dict], *, today: str, yesterday: str) -> list[dict]: def collect_closed_trades_snapshot(
accounts: list[dict],
*,
today: str,
yesterday: str | None = None,
) -> list[dict]:
rows: list[dict] = [] rows: list[dict] = []
for ac in accounts or []: for ac in accounts or []:
name = ac.get("name") or ac.get("key") name = ac.get("name") or ac.get("key")
for t in ac.get("trades_yesterday") or []: if yesterday:
if not isinstance(t, dict): for t in ac.get("trades_yesterday") or []:
continue if not isinstance(t, dict):
rows.append({**t, "account_name": name, "trading_day": yesterday}) continue
rows.append({**t, "account_name": name, "trading_day": yesterday})
for t in ac.get("trades") or []: for t in ac.get("trades") or []:
if not isinstance(t, dict): if not isinstance(t, dict):
continue continue
+7 -8
View File
@@ -5,20 +5,19 @@ SUMMARY_SYSTEM = """
硬性规则: 硬性规则:
- 只能陈述数据中明确出现的数字与事实;禁止编造成交、止损、扛单、行情预测。 - 只能陈述数据中明确出现的数字与事实;禁止编造成交、止损、扛单、行情预测。
- 上下文含「昨日+今日」个交易日的平仓与「近15日资金快照」;须连贯引用,不得只写单日而忽略另一日 - 上下文含「今日」个交易日的平仓、持仓与监控;不得引用昨日、历史走势或数据里未出现的账户名
- 未监控的账户必须标注「未监控」,不得臆测其盈亏。 - 未监控的账户必须标注「未监控」,不得臆测其盈亏。
- 连接失败或数据缺失的账户如实写明,不要猜测。 - 连接失败或数据缺失的账户如实写明,不要猜测。
- 趋势回调计划、顺势加仓、关键位监控、进行中的下单监控:仅据数据列示,无则写「无」。 - 趋势回调计划、顺势加仓、关键位监控、进行中的下单监控:仅据数据列示,无则写「无」。
- 第1~4节保持客观台账;**第5节操作建议**可基于资金账户/交易账户余额、15日资金变化、仓位与监控单,给出简短、可执行的资金与仓位安排建议(仍禁止预测涨跌、保证收益)。 - 第1~4节保持客观台账;**第5节操作建议**可基于当日资金账户/交易账户余额、仓位与监控单,给出简短、可执行的资金与仓位安排建议(仍禁止预测涨跌、保证收益)。
- 禁止夸张词(致命、崩溃、灾难等)。 - 禁止输出 pipe 分隔的 Markdown 表格或「详细数据支持」附录;禁止夸张词(致命、崩溃、灾难等)。
输出格式(Markdown,标题必须一致): 输出格式(Markdown,标题必须一致):
**今日交易总结({trading_day}** **今日交易总结({trading_day}**
**1. 总览** **1. 总览**
- **对比说明**:昨日 vs 今日(交易日日期见数据)
- **合计盈亏(U)**:今日平仓合计 … - **合计盈亏(U)**:今日平仓合计 …
- **平仓笔数**:今日 …(胜 / 负 / 平);昨日 … - **平仓笔数**:今日 …(胜 / 负 / 平)
- **当前持仓浮盈亏(U)**:… - **当前持仓浮盈亏(U)**:…
- **资金合计**:资金账户 … / 交易账户 …(仅已监控且有数据账户) - **资金合计**:资金账户 … / 交易账户 …(仅已监控且有数据账户)
@@ -29,10 +28,10 @@ SUMMARY_SYSTEM = """
仅有依据时列出(亏损、浮亏、监控/趋势/关键位异常、资金缺口等);若无则写「无」。 仅有依据时列出(亏损、浮亏、监控/趋势/关键位异常、资金缺口等);若无则写「无」。
**4. 数据说明** **4. 数据说明**
列出数据缺口(某户未启用、接口失败、缺15日资金快照等)。 列出数据缺口(某户未启用、接口失败等)。
**5. 操作建议** **5. 操作建议**
基于各户资金账户与交易账户余额、近15日资金走势、持仓与监控单,给出 2~5 条简短建议(如:是否需要从资金账户补充交易账户、哪户风险敞口偏高等)。无依据则写「暂无」。 基于各户当日资金账户与交易账户余额、持仓与监控单,给出 2~5 条简短建议(如:是否需要从资金账户补充交易账户、哪户风险敞口偏高等)。无依据则写「暂无」。
""".strip() """.strip()
@@ -57,7 +56,7 @@ def build_summary_user_prompt(context_text: str, trading_day: str) -> str:
return f""" return f"""
交易日(今日):{trading_day} 交易日(今日):{trading_day}
以下为中控聚合的多账户数据(含昨日+今日平仓、近15日资金快照、趋势回调/顺势加仓/关键位/监控单): 以下为中控聚合的多账户数据(今日平仓、持仓、趋势回调/顺势加仓/关键位/监控单):
{context_text} {context_text}
""".strip() """.strip()
+13 -9
View File
@@ -8,6 +8,8 @@ from hub_ai.context import (
build_daily_context, build_daily_context,
collect_closed_trades_snapshot, collect_closed_trades_snapshot,
format_account_remark, format_account_remark,
format_summary_context_text,
summary_context_hash,
) )
from hub_ai.prompts import SUMMARY_SYSTEM, build_summary_user_prompt from hub_ai.prompts import SUMMARY_SYSTEM, build_summary_user_prompt
from hub_ai.store import append_summary, get_latest_summary, list_summaries from hub_ai.store import append_summary, get_latest_summary, list_summaries
@@ -15,13 +17,10 @@ from hub_ai.store import append_summary, get_latest_summary, list_summaries
def _stats_snapshot_from_ctx(ctx: dict) -> dict: def _stats_snapshot_from_ctx(ctx: dict) -> dict:
day = ctx.get("trading_day") day = ctx.get("trading_day")
prev = ctx.get("prev_trading_day")
accounts = ctx.get("accounts") or [] accounts = ctx.get("accounts") or []
return { return {
"totals": ctx.get("totals"), "totals": ctx.get("totals"),
"prev_trading_day": prev, "closed_trades": collect_closed_trades_snapshot(accounts, today=day),
"fund_history": ctx.get("fund_history"),
"closed_trades": collect_closed_trades_snapshot(accounts, today=day, yesterday=prev),
"by_account": { "by_account": {
str(ac.get("key") or ac.get("id")): { str(ac.get("key") or ac.get("id")): {
"key": ac.get("key"), "key": ac.get("key"),
@@ -31,9 +30,7 @@ def _stats_snapshot_from_ctx(ctx: dict) -> dict:
"trading_usdt": ac.get("trading_usdt"), "trading_usdt": ac.get("trading_usdt"),
"available_trading_usdt": ac.get("available_trading_usdt"), "available_trading_usdt": ac.get("available_trading_usdt"),
"pnl_u": (ac.get("trade_stats") or {}).get("total_pnl_u"), "pnl_u": (ac.get("trade_stats") or {}).get("total_pnl_u"),
"pnl_u_yesterday": (ac.get("trade_stats_yesterday") or {}).get("total_pnl_u"),
"closed_count": (ac.get("trade_stats") or {}).get("closed_count"), "closed_count": (ac.get("trade_stats") or {}).get("closed_count"),
"closed_count_yesterday": (ac.get("trade_stats_yesterday") or {}).get("closed_count"),
"float_pnl_u": ac.get("float_pnl_u"), "float_pnl_u": ac.get("float_pnl_u"),
"remark": format_account_remark(ac), "remark": format_account_remark(ac),
"monitor_lines": ac.get("monitor_lines") or {}, "monitor_lines": ac.get("monitor_lines") or {},
@@ -52,9 +49,16 @@ def generate_daily_summary(
) -> dict[str, Any]: ) -> dict[str, Any]:
ctx = build_daily_context(exchanges, trading_day=trading_day) ctx = build_daily_context(exchanges, trading_day=trading_day)
day = ctx["trading_day"] day = ctx["trading_day"]
summary_payload = {
"trading_day": day,
"totals": ctx.get("totals"),
"accounts": ctx.get("accounts"),
}
summary_text = format_summary_context_text(summary_payload)
digest = summary_context_hash(summary_payload)
if not force: if not force:
latest = get_latest_summary(day) latest = get_latest_summary(day)
if latest and latest.get("context_hash") == ctx.get("context_hash"): if latest and latest.get("context_hash") == digest:
return { return {
"ok": True, "ok": True,
"cached": True, "cached": True,
@@ -64,7 +68,7 @@ def generate_daily_summary(
} }
system = SUMMARY_SYSTEM.replace("{trading_day}", day) system = SUMMARY_SYSTEM.replace("{trading_day}", day)
user = build_summary_user_prompt(ctx["text"], day) user = build_summary_user_prompt(summary_text, day)
content = generate_text(system=system, user=user, temperature=0.15) content = generate_text(system=system, user=user, temperature=0.15)
if content.startswith("AI 调用失败"): if content.startswith("AI 调用失败"):
return {"ok": False, "msg": content, "trading_day": day} return {"ok": False, "msg": content, "trading_day": day}
@@ -74,7 +78,7 @@ def generate_daily_summary(
trading_day=day, trading_day=day,
content_md=content, content_md=content,
model=model_label(), model=model_label(),
context_hash=ctx.get("context_hash") or "", context_hash=digest,
stats_snapshot=stats_snapshot, stats_snapshot=stats_snapshot,
) )
return { return {
+1 -1
View File
@@ -3092,7 +3092,7 @@
.join(""); .join("");
return ( return (
`<div class="ai-closed-trades-wrap">` + `<div class="ai-closed-trades-wrap">` +
`<h4 class="ai-closed-trades-title">平仓明细(昨日 + 今日)</h4>` + `<h4 class="ai-closed-trades-title">平仓明细(今日)</h4>` +
`<div class="ai-ac-table-wrap"><table class="ai-ac-table ai-closed-trades-table">${head}<tbody>${body}</tbody></table></div>` + `<div class="ai-ac-table-wrap"><table class="ai-ac-table ai-closed-trades-table">${head}<tbody>${body}</tbody></table></div>` +
`</div>` `</div>`
); );