6a1f2608b5
Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
4.5 KiB
Python
142 lines
4.5 KiB
Python
"""内照明心复盘语录 → 交易教练点评。"""
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from hub_ai.client import generate_text, model_label
|
||
from hub_ai.rolling_summary import refresh_session_rolling_summary
|
||
from hub_ai.text_util import clip_text, is_ai_error_reply
|
||
from hub_ai.config import (
|
||
CHAT_MAX_CONTINUATIONS,
|
||
CHAT_MAX_OUTPUT_TOKENS,
|
||
CHAT_TEMPERATURE,
|
||
CHAT_USER_MESSAGE_MAX_CHARS,
|
||
)
|
||
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,
|
||
delete_chat_session,
|
||
get_active_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)
|
||
max_rows = 50
|
||
if len(trades) > max_rows:
|
||
lines.append(f"(共 {len(trades)} 笔,以下展示最近 {max_rows} 笔)")
|
||
for i, t in enumerate(trades[:max_rows], 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 = clip_text(str(t.get("note") or "").strip(), 80)
|
||
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)
|
||
user_for_prompt = clip_text(text, CHAT_USER_MESSAGE_MAX_CHARS)
|
||
|
||
user_prompt = build_archive_quote_review_prompt(
|
||
quote_date=day,
|
||
archive_trades_text=archive_trades_text,
|
||
user_message=user_for_prompt,
|
||
)
|
||
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):
|
||
delete_chat_session(sid)
|
||
return {"ok": False, "msg": reply}
|
||
|
||
append_chat_message(sid, "user", text)
|
||
session = append_chat_message(sid, "assistant", reply)
|
||
refresh_session_rolling_summary(
|
||
sid,
|
||
prior_summary="",
|
||
user_text=text,
|
||
assistant_text=reply,
|
||
bot_mode=CHAT_BOT_TRADING,
|
||
)
|
||
session = get_active_session() or session
|
||
return {
|
||
"ok": True,
|
||
"trading_day": day,
|
||
"session": session,
|
||
"sessions": list_chat_sessions(),
|
||
"reply": reply,
|
||
"model": model_label(),
|
||
}
|