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:
dekun
2026-06-23 19:25:01 +08:00
parent d3d366d0ee
commit bfbd6879d6
15 changed files with 1699 additions and 43 deletions
+64 -1
View File
@@ -95,6 +95,7 @@ from settings_store import (
env_force_disabled_ids,
load_settings,
normalize_display_prefs,
normalize_supervisor_settings,
save_settings,
)
from hub_web_auth import (
@@ -119,6 +120,10 @@ from urllib.parse import urlencode
from hub_board_cache import HUB_BOARD_POLL_INTERVAL, board_store
from hub_dashboard_cache import dashboard_store
from hub_dashboard import DASHBOARD_POLL_INTERVAL_SEC
from hub_supervisor_cache import supervisor_store
from hub_supervisor_lib import process_supervisor_tick, set_supervisor_notify_hook
from hub_ai.supervisor import make_supervisor_ai_reply_fn
from hub_ai.config import trading_day_reset_hour
from hub_chart_cache import (
HUB_CHART_POLL_INTERVAL,
HUB_CHART_WATCH_TTL_SEC,
@@ -301,6 +306,7 @@ async def _run_board_aggregate() -> dict:
def _schedule_board_refresh() -> None:
board_store.request_refresh()
dashboard_store.request_refresh()
supervisor_store.request_refresh()
async def _run_archive_sync_once() -> dict:
@@ -496,11 +502,28 @@ async def _archive_sync_loop() -> None:
pass
async def _run_supervisor_tick() -> dict:
dash = dashboard_store.snapshot_dict()
board = board_store.snapshot_dict()
settings = load_settings()
ai_fn = make_supervisor_ai_reply_fn(_all_exchanges_for_ai())
return await asyncio.to_thread(
process_supervisor_tick,
dash if dash.get("ok") is not False else None,
board if board.get("ok") is not False else None,
settings,
reset_hour=trading_day_reset_hour(),
ai_reply_fn=ai_fn,
)
@asynccontextmanager
async def _hub_lifespan(_app: FastAPI):
global _archive_sync_stop, _archive_sync_task, _volume_rank_stop, _volume_rank_task
set_supervisor_notify_hook(supervisor_store.bump)
await board_store.start(_run_board_aggregate)
await dashboard_store.start(_run_dashboard_aggregate)
await supervisor_store.start(_run_supervisor_tick)
await chart_poll_store.start(_run_chart_poll)
_archive_sync_stop = asyncio.Event()
_archive_sync_task = asyncio.create_task(_archive_sync_loop(), name="hub-archive-sync")
@@ -530,8 +553,10 @@ async def _hub_lifespan(_app: FastAPI):
_volume_rank_task = None
_volume_rank_stop = None
await chart_poll_store.stop()
await supervisor_store.stop()
await dashboard_store.stop()
await board_store.stop()
set_supervisor_notify_hook(None)
app = FastAPI(title="复盘系统中控", docs_url=None, redoc_url=None, lifespan=_hub_lifespan)
@@ -737,6 +762,7 @@ async def _run_dashboard_aggregate() -> dict:
def _schedule_dashboard_refresh() -> None:
dashboard_store.request_refresh()
supervisor_store.request_refresh()
@app.get("/api/dashboard/daily")
@@ -775,6 +801,27 @@ async def api_dashboard_refresh():
return {"ok": True, "dashboard_version": dashboard_store.version}
@app.get("/api/ai/supervisor/stream")
async def api_supervisor_stream():
from fastapi.responses import StreamingResponse
return StreamingResponse(
supervisor_store.iter_sse(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
@app.post("/api/ai/supervisor/refresh")
async def api_supervisor_refresh():
supervisor_store.request_refresh()
return {"ok": True, "supervisor_version": supervisor_store.version}
@app.get("/trade")
def trade_removed_redirect():
from fastapi.responses import RedirectResponse
@@ -797,9 +844,22 @@ class SettingsDisplayBody(BaseModel):
show_nav_calculator: bool = True
class SupervisorSettingsBody(BaseModel):
enabled: bool = True
wechat_webhook: str = ""
wechat_link_base: str = ""
wechat_prefix: str = "【交易监管】"
wechat_on_program_tp_sl: bool = True
manual_close_daily_warn: int = 2
interval_warn_minutes: int = 15
freq_30m_count: int = 2
reopen_after_close_minutes: int = 30
class SettingsBody(BaseModel):
exchanges: list[dict] = Field(default_factory=list)
display: SettingsDisplayBody | None = None
supervisor: SupervisorSettingsBody | None = None
@app.post("/api/settings")
@@ -817,7 +877,10 @@ def api_save_settings(body: SettingsBody):
display = normalize_display_prefs(existing.get("display"))
if body.display is not None:
display = normalize_display_prefs(body.display.model_dump())
save_settings({"version": 1, "exchanges": to_save, "display": display})
supervisor = normalize_supervisor_settings(existing.get("supervisor"))
if body.supervisor is not None:
supervisor = normalize_supervisor_settings(body.supervisor.model_dump())
save_settings({"version": 1, "exchanges": to_save, "display": display, "supervisor": supervisor})
return {"ok": True, "settings": load_settings()}