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:
@@ -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()}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user