feat(hub): add archive quote AI coach review from inner light page

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-11 21:10:39 +08:00
parent bb8bb3ae34
commit 180aff5310
7 changed files with 276 additions and 4 deletions
+116
View File
@@ -0,0 +1,116 @@
"""内照明心复盘语录 → 交易教练点评。"""
from __future__ import annotations
from typing import Any
from hub_ai.chat import _is_ai_error_reply
from hub_ai.client import generate_text, model_label
from hub_ai.config import CHAT_MAX_CONTINUATIONS, CHAT_MAX_OUTPUT_TOKENS, CHAT_TEMPERATURE
from hub_ai.prompts import CHAT_SYSTEM, build_archive_quote_review_prompt
from hub_ai.store import CHAT_BOT_TRADING, append_chat_message, create_new_session, list_chat_sessions
from hub_symbol_archive_lib import list_daily_trades
def _tag_label(tag: str) -> str:
t = (tag or "").strip().lower()
if t == "sick":
return "犯病"
if t == "emotion":
return "情绪化"
return t or ""
def _fmt_pnl(v: Any) -> str:
try:
n = float(v or 0)
except (TypeError, ValueError):
return ""
sign = "+" if n > 0 else ""
return f"{sign}{n:.2f}U"
def format_archive_trades_for_ai(payload: dict[str, Any]) -> str:
trades = payload.get("trades") or []
stats = payload.get("stats") or {}
lines = [
(
f"统计:开仓 {int(stats.get('open_count') or 0)} 笔,"
f"犯病 {int(stats.get('sick_count') or 0)} 笔,"
f"盈亏合计 {_fmt_pnl(stats.get('pnl_total'))}"
f"剔除犯病盈亏 {_fmt_pnl(stats.get('pnl_ex_sick'))}"
)
]
if not trades:
lines.append("(该日无交易记录)")
return "\n".join(lines)
for i, t in enumerate(trades, 1):
ex = str(t.get("exchange_key") or t.get("account_exchange_key") or "")
sym = str(t.get("symbol") or "")
direction = str(t.get("direction") or "")
opened = str(t.get("opened_at") or "")
closed = str(t.get("closed_at") or "")
hold = str(t.get("hold_minutes_text") or t.get("hold_minutes") or "")
result = str(t.get("result") or "")
pnl = _fmt_pnl(t.get("pnl_amount"))
entry = str(t.get("entry_type") or t.get("entry_reason") or t.get("monitor_type") or "")
tag = _tag_label(str(t.get("behavior_tag") or ""))
note = str(t.get("note") or "").strip()
line = (
f"{i}. {ex} | {sym} | {direction} | 开仓类型 {entry} | "
f"{opened} | 平 {closed} | 持仓 {hold} | 结果 {result} | "
f"盈亏 {pnl} | 标签 {tag}"
)
if note:
line += f" | 备注 {note}"
lines.append(line)
return "\n".join(lines)
def send_archive_quote_review(
*,
quote_date: str,
content: str,
) -> dict[str, Any]:
text = (content or "").strip()
if not text:
return {"ok": False, "msg": "语录内容不能为空"}
day = (quote_date or "").strip()[:10]
if not day:
return {"ok": False, "msg": "语录日期无效"}
session = create_new_session(
trading_day=day,
title=f"复盘 {day}",
bot_mode=CHAT_BOT_TRADING,
)
sid = session["id"]
archive_payload = list_daily_trades(trading_day=day, period="today")
archive_trades_text = format_archive_trades_for_ai(archive_payload)
append_chat_message(sid, "user", text)
user_prompt = build_archive_quote_review_prompt(
quote_date=day,
archive_trades_text=archive_trades_text,
user_message=text,
)
reply = generate_text(
system=CHAT_SYSTEM,
user=user_prompt,
temperature=CHAT_TEMPERATURE,
max_tokens=CHAT_MAX_OUTPUT_TOKENS,
max_continuations=CHAT_MAX_CONTINUATIONS,
)
if _is_ai_error_reply(reply):
return {"ok": False, "msg": reply, "session_id": sid}
session = append_chat_message(sid, "assistant", reply)
return {
"ok": True,
"trading_day": day,
"session": session,
"sessions": list_chat_sessions(),
"reply": reply,
"model": model_label(),
}
+27
View File
@@ -117,3 +117,30 @@ def build_chat_user_prompt(
parts.extend(["【用户附件说明】", attachment_note.strip()])
parts.extend(["【用户现在说(优先回应这一条)】", user_message.strip()])
return "\n\n".join(parts)
ARCHIVE_QUOTE_REVIEW_INSTRUCTION = """
【任务】用户从内照明心提交了一条复盘语录,并附上该交易日的档案交易记录(仅供你分析,用户界面不展示明细)。
请结合语录与交易记录:
1) 帮他核对自述与操作事实是否一致;
2) 指出心态、纪律、执行上的偏差点(若有);
3) 给出可落地的改进建议。
语气沿用交易教练:体贴、口语、短句,不用说教式清单;不预测涨跌,不保证收益。
""".strip()
def build_archive_quote_review_prompt(
*,
quote_date: str,
archive_trades_text: str,
user_message: str,
) -> str:
parts = [
f"【复盘交易日】{quote_date}",
ARCHIVE_QUOTE_REVIEW_INSTRUCTION,
"【该日交易记录(内照明心档案,用户不可见此段)】",
(archive_trades_text or "(该日无交易记录)").strip(),
"【用户复盘语录(对话框已展示,请优先回应)】",
user_message.strip(),
]
return "\n\n".join(parts)
+17 -1
View File
@@ -3,9 +3,10 @@ from __future__ import annotations
from typing import Callable
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
from fastapi import APIRouter, Body, File, Form, HTTPException, UploadFile
from pydantic import BaseModel, Field
from hub_ai.archive_quote import send_archive_quote_review
from hub_ai.chat import (
get_chat_state,
remove_chat_session,
@@ -40,6 +41,11 @@ class ChatSwitchBody(BaseModel):
session_id: str = Field(..., min_length=1)
class ArchiveQuoteChatBody(BaseModel):
quote_date: str = ""
content: str = ""
def create_hub_ai_router(*, load_all_exchanges: Callable[[], list]) -> APIRouter:
router = APIRouter(prefix="/api/ai", tags=["hub-ai"])
@@ -118,6 +124,16 @@ def create_hub_ai_router(*, load_all_exchanges: Callable[[], list]) -> APIRouter
raise HTTPException(status_code=404, detail="会话不存在")
return result
@router.post("/chat/archive-quote")
def api_ai_chat_archive_quote(body: ArchiveQuoteChatBody = Body(...)):
result = send_archive_quote_review(
quote_date=body.quote_date,
content=body.content,
)
if not result.get("ok"):
raise HTTPException(status_code=502, detail=result.get("msg") or "发送失败")
return result
@router.post("/chat/send")
async def api_ai_chat_send(
message: str = Form(""),