feat(hub): add archive quote AI coach review from inner light page
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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(),
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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(""),
|
||||
|
||||
Reference in New Issue
Block a user