Add AI trading supervisor with WeChat push and daily session
Proactive monitoring for manual/hub closes and new opens prevents overtrading via in-app alerts, configurable WeChat links, and supervisor chat. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -174,6 +174,59 @@ ARCHIVE_QUOTE_REVIEW_INSTRUCTION = """
|
||||
""".strip()
|
||||
|
||||
|
||||
SUPERVISOR_SYSTEM = """
|
||||
你是交易监管值班员,职责是防止过度交易与频繁手动操作。用中文、短句、克制语气。
|
||||
|
||||
规则:
|
||||
- 只依据提供的结构化事件与账户快照说话;禁止预测涨跌、保证收益。
|
||||
- **手动平仓、中控平仓、新开仓**:指出频率、间隔、是否偏急;提醒休息,不训斥。
|
||||
- **程序止盈/程序止损**:肯定按计划执行,鼓励保持纪律,提醒别立刻反手再开。
|
||||
- 不替用户做决定,不暗示绕过实例冷静期/日冻结。
|
||||
- 每次 1~3 句,必须写完整;禁止长清单和「第1点第2点」。
|
||||
- 实例已进入冷静期/日冻结时,明确说明状态,建议暂停手动开平。
|
||||
""".strip()
|
||||
|
||||
|
||||
def build_supervisor_ai_prompt(
|
||||
*,
|
||||
context_text: str,
|
||||
trading_day: str,
|
||||
event: dict,
|
||||
warnings: list[dict],
|
||||
) -> str:
|
||||
warn_lines = "\n".join(f"- {w.get('message')}" for w in (warnings or []) if w.get("message"))
|
||||
parts = [
|
||||
f"【交易日】{trading_day}",
|
||||
"【监管事件】",
|
||||
str(event or {}),
|
||||
"【当前多账户快照】",
|
||||
(context_text or "(无)").strip(),
|
||||
]
|
||||
if warn_lines.strip():
|
||||
parts.extend(["【已触发频率警告】", warn_lines.strip()])
|
||||
parts.append("请给出 1~3 句监管评语:")
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def build_supervisor_chat_prompt(
|
||||
*,
|
||||
context_text: str,
|
||||
trading_day: str,
|
||||
history_lines: str,
|
||||
user_message: str,
|
||||
) -> str:
|
||||
parts = [f"【交易日】{trading_day}"]
|
||||
if history_lines.strip():
|
||||
parts.extend(["【今日监管对话】", history_lines.strip()])
|
||||
parts.extend([
|
||||
"【当前多账户快照】",
|
||||
(context_text or "(无)").strip(),
|
||||
"【用户现在说】",
|
||||
user_message.strip(),
|
||||
])
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def build_archive_quote_review_prompt(
|
||||
*,
|
||||
quote_date: str,
|
||||
|
||||
@@ -19,8 +19,11 @@ from hub_ai.client import model_label
|
||||
from hub_ai.config import trading_day_reset_hour
|
||||
from hub_ai.context import build_daily_context
|
||||
from hub_ai.store import get_latest_summary, list_summaries
|
||||
from hub_ai.supervisor import send_supervisor_chat
|
||||
from hub_ai.supervisor_store import get_supervisor_session_state
|
||||
from hub_ai.summary import generate_daily_summary
|
||||
from hub_trades_lib import current_trading_day
|
||||
from settings_store import normalize_supervisor_settings
|
||||
|
||||
|
||||
class ChatSendBody(BaseModel):
|
||||
@@ -47,6 +50,11 @@ class ArchiveQuoteChatBody(BaseModel):
|
||||
content: str = ""
|
||||
|
||||
|
||||
class SupervisorChatBody(BaseModel):
|
||||
message: str = ""
|
||||
trading_day: str = ""
|
||||
|
||||
|
||||
def create_hub_ai_router(*, load_all_exchanges: Callable[[], list]) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/ai", tags=["hub-ai"])
|
||||
|
||||
@@ -165,4 +173,28 @@ def create_hub_ai_router(*, load_all_exchanges: Callable[[], list]) -> APIRouter
|
||||
raise HTTPException(status_code=502, detail=result.get("msg") or "发送失败")
|
||||
return result
|
||||
|
||||
@router.get("/supervisor/session")
|
||||
def api_ai_supervisor_session(trading_day: str = ""):
|
||||
day = _day(trading_day)
|
||||
return get_supervisor_session_state(day)
|
||||
|
||||
@router.get("/supervisor/rules")
|
||||
def api_ai_supervisor_rules():
|
||||
from settings_store import load_settings
|
||||
|
||||
cfg = normalize_supervisor_settings(load_settings().get("supervisor"))
|
||||
return {"ok": True, "supervisor": cfg}
|
||||
|
||||
@router.post("/supervisor/chat/send")
|
||||
def api_ai_supervisor_chat_send(body: SupervisorChatBody = SupervisorChatBody()):
|
||||
exchanges = load_all_exchanges()
|
||||
result = send_supervisor_chat(
|
||||
exchanges,
|
||||
body.message,
|
||||
trading_day=_day(body.trading_day) if body.trading_day.strip() else None,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=502, detail=result.get("msg") or "发送失败")
|
||||
return result
|
||||
|
||||
return router
|
||||
|
||||
@@ -142,7 +142,8 @@ def get_active_session() -> Optional[dict]:
|
||||
|
||||
CHAT_BOT_TRADING = "trading"
|
||||
CHAT_BOT_GENERAL = "general"
|
||||
CHAT_BOT_MODES = frozenset({CHAT_BOT_TRADING, CHAT_BOT_GENERAL})
|
||||
CHAT_BOT_SUPERVISOR = "supervisor"
|
||||
CHAT_BOT_MODES = frozenset({CHAT_BOT_TRADING, CHAT_BOT_GENERAL, CHAT_BOT_SUPERVISOR})
|
||||
|
||||
|
||||
def _normalize_bot_mode(raw: Any) -> str:
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"""交易监管:AI 评语与用户回聊。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from hub_ai.client import generate_text, model_label
|
||||
from hub_ai.config import (
|
||||
CHAT_MAX_OUTPUT_TOKENS,
|
||||
CHAT_TEMPERATURE,
|
||||
trading_day_reset_hour,
|
||||
)
|
||||
from hub_ai.context import build_chat_context, format_chat_context_for_chat
|
||||
from hub_ai.prompts import SUPERVISOR_SYSTEM, build_supervisor_ai_prompt, build_supervisor_chat_prompt
|
||||
from hub_ai.supervisor_store import (
|
||||
append_supervisor_ai_message,
|
||||
ensure_supervisor_session,
|
||||
get_supervisor_session_state,
|
||||
)
|
||||
from hub_ai.store import append_chat_message
|
||||
from hub_trades_lib import current_trading_day
|
||||
|
||||
|
||||
def generate_supervisor_ai_reply(
|
||||
*,
|
||||
event: dict,
|
||||
warnings: list[dict],
|
||||
trading_day: str,
|
||||
session_id: str,
|
||||
exchanges: list[dict],
|
||||
) -> str:
|
||||
ctx = build_chat_context(exchanges, trading_day=trading_day)
|
||||
brief = format_chat_context_for_chat(ctx, max_chars=6000)
|
||||
user_prompt = build_supervisor_ai_prompt(
|
||||
context_text=brief,
|
||||
trading_day=trading_day,
|
||||
event=event,
|
||||
warnings=warnings,
|
||||
)
|
||||
return generate_text(
|
||||
system=SUPERVISOR_SYSTEM,
|
||||
user=user_prompt,
|
||||
temperature=min(0.35, CHAT_TEMPERATURE),
|
||||
max_tokens=min(512, CHAT_MAX_OUTPUT_TOKENS),
|
||||
max_continuations=1,
|
||||
)
|
||||
|
||||
|
||||
def make_supervisor_ai_reply_fn(exchanges: list[dict]):
|
||||
def _fn(*, event: dict, warnings: list[dict], trading_day: str, session_id: str) -> str:
|
||||
return generate_supervisor_ai_reply(
|
||||
event=event,
|
||||
warnings=warnings or [],
|
||||
trading_day=trading_day,
|
||||
session_id=session_id,
|
||||
exchanges=exchanges,
|
||||
)
|
||||
|
||||
return _fn
|
||||
|
||||
|
||||
def send_supervisor_chat(
|
||||
exchanges: list[dict],
|
||||
message: str,
|
||||
*,
|
||||
trading_day: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
text = (message or "").strip()
|
||||
if not text:
|
||||
return {"ok": False, "msg": "消息不能为空"}
|
||||
day = (trading_day or "").strip()[:10] or current_trading_day(
|
||||
reset_hour=trading_day_reset_hour()
|
||||
)
|
||||
session = ensure_supervisor_session(day)
|
||||
sid = str(session.get("id") or "")
|
||||
prior = session.get("messages") or []
|
||||
ctx = build_chat_context(exchanges, trading_day=day)
|
||||
brief = format_chat_context_for_chat(ctx, max_chars=6000)
|
||||
recent = []
|
||||
for m in prior[-8:]:
|
||||
role = m.get("role")
|
||||
if role not in ("user", "assistant", "system"):
|
||||
continue
|
||||
label = {"user": "用户", "assistant": "监管", "system": "系统"}.get(role, role)
|
||||
recent.append(f"{label}:{str(m.get('content') or '').strip()}")
|
||||
user_prompt = build_supervisor_chat_prompt(
|
||||
context_text=brief,
|
||||
trading_day=day,
|
||||
history_lines="\n".join(recent),
|
||||
user_message=text,
|
||||
)
|
||||
reply = generate_text(
|
||||
system=SUPERVISOR_SYSTEM,
|
||||
user=user_prompt,
|
||||
temperature=min(0.4, CHAT_TEMPERATURE),
|
||||
max_tokens=min(768, CHAT_MAX_OUTPUT_TOKENS),
|
||||
max_continuations=1,
|
||||
)
|
||||
if not reply or reply.strip().startswith("AI "):
|
||||
return {"ok": False, "msg": reply or "AI 生成失败", "session_id": sid}
|
||||
append_chat_message(sid, "user", text)
|
||||
session = append_supervisor_ai_message(sid, reply.strip())
|
||||
state = get_supervisor_session_state(day)
|
||||
return {
|
||||
"ok": True,
|
||||
"trading_day": day,
|
||||
"session": session,
|
||||
"reply": reply.strip(),
|
||||
"model": model_label(),
|
||||
"message_count": state.get("message_count"),
|
||||
"unread_system": state.get("unread_system"),
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"""交易监管专用会话(今日长会话,bot_mode=supervisor)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from hub_ai.store import (
|
||||
CHAT_BOT_SUPERVISOR,
|
||||
append_chat_message,
|
||||
load_chat_store,
|
||||
save_chat_store,
|
||||
)
|
||||
|
||||
|
||||
def _supervisor_title(trading_day: str) -> str:
|
||||
return f"今日监管 {trading_day}"
|
||||
|
||||
|
||||
def find_supervisor_session(trading_day: str) -> Optional[dict]:
|
||||
day = (trading_day or "").strip()[:10]
|
||||
store = load_chat_store()
|
||||
for s in store.get("sessions") or []:
|
||||
if str(s.get("bot_mode") or "") != CHAT_BOT_SUPERVISOR:
|
||||
continue
|
||||
if str(s.get("trading_day") or "") == day:
|
||||
return s
|
||||
return None
|
||||
|
||||
|
||||
def ensure_supervisor_session(trading_day: str) -> dict:
|
||||
day = (trading_day or "").strip()[:10]
|
||||
existing = find_supervisor_session(day)
|
||||
if existing:
|
||||
return existing
|
||||
store = load_chat_store()
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
session = {
|
||||
"id": uuid.uuid4().hex,
|
||||
"trading_day": day,
|
||||
"title": _supervisor_title(day),
|
||||
"bot_mode": CHAT_BOT_SUPERVISOR,
|
||||
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"messages": [],
|
||||
"rolling_summary": "",
|
||||
"supervisor_locked": True,
|
||||
}
|
||||
store.setdefault("sessions", []).append(session)
|
||||
save_chat_store(store)
|
||||
return session
|
||||
|
||||
|
||||
def append_supervisor_system_message(
|
||||
session_id: str,
|
||||
content: str,
|
||||
*,
|
||||
event_type: str = "",
|
||||
level: str = "info",
|
||||
) -> dict:
|
||||
store = load_chat_store()
|
||||
target = None
|
||||
for s in store.get("sessions") or []:
|
||||
if str(s.get("id")) == str(session_id):
|
||||
target = s
|
||||
break
|
||||
if not target:
|
||||
raise KeyError("session_not_found")
|
||||
from datetime import datetime
|
||||
|
||||
msg = {
|
||||
"role": "system",
|
||||
"content": (content or "").strip(),
|
||||
"at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"event_type": event_type,
|
||||
"level": level,
|
||||
}
|
||||
target.setdefault("messages", []).append(msg)
|
||||
target["updated_at"] = msg["at"]
|
||||
save_chat_store(store)
|
||||
return target
|
||||
|
||||
|
||||
def append_supervisor_ai_message(session_id: str, content: str) -> dict:
|
||||
return append_chat_message(session_id, "assistant", content)
|
||||
|
||||
|
||||
def get_supervisor_session_state(trading_day: str) -> dict[str, Any]:
|
||||
from hub_ai.client import model_label
|
||||
|
||||
session = ensure_supervisor_session(trading_day)
|
||||
msgs = session.get("messages") or []
|
||||
unread = sum(1 for m in msgs if m.get("role") == "system" and not m.get("read"))
|
||||
return {
|
||||
"ok": True,
|
||||
"session": session,
|
||||
"trading_day": trading_day,
|
||||
"message_count": len(msgs),
|
||||
"unread_system": unread,
|
||||
"model": model_label(),
|
||||
}
|
||||
Reference in New Issue
Block a user