Spaces:
Runtime error
Runtime error
| """ | |
| server.py - custom Spectrum 2 frontend for Gradio Server mode. | |
| This is the Off-Brand entry point: instead of the default Gradio component | |
| render, a gradio.Server instance serves the hand-built React UI | |
| (ui_kits/chan-compass/) and exposes the unchanged Python backend as JSON/SSE | |
| endpoints. The HF Space uses sdk: gradio and runs app.py, which calls | |
| app.launch(). | |
| Architecture: | |
| gradio.Server app | |
| /api/... -> JSON + Server-Sent-Events endpoints calling the backend | |
| / -> the React frontend (static), Spectrum 2 design system | |
| Backend modules (signal_runner, rotation, news_watch, research_agent, | |
| automation, emailer, finetune_data, llm_local) are imported and called with no | |
| business-logic changes. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from fastapi import Request | |
| from fastapi.responses import StreamingResponse, JSONResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from gradio import Server | |
| import paths # sets up /data, sys.path | |
| import automation | |
| import signal_runner | |
| import rotation | |
| import news_watch | |
| import research_agent | |
| import emailer | |
| import finetune_data | |
| import llm_local | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| UI_DIR = os.path.join(HERE, "ui_kits", "chan-compass") | |
| app = Server(title="Chan Compass · US") | |
| # ───────────────────────── helpers ───────────────────────── | |
| def _sse(gen): | |
| """Wrap a text generator as Server-Sent Events (one 'data:' per chunk).""" | |
| def stream(): | |
| for chunk in gen: | |
| yield f"data: {json.dumps({'text': chunk})}\n\n" | |
| yield "event: done\ndata: {}\n\n" | |
| return StreamingResponse(stream(), media_type="text/event-stream") | |
| def _df_records(df): | |
| if df is None or not hasattr(df, "to_dict"): | |
| return [] | |
| return df.to_dict(orient="records") | |
| # ───────────────────────── Signals ───────────────────────── | |
| async def signals_run(req: Request): | |
| body = await req.json() | |
| tickers = [t.strip().upper() for t in | |
| (body.get("pool") or "").replace("\n", ",").split(",") if t.strip()] | |
| force = bool(body.get("force")) | |
| df, details, summary, errors = signal_runner.run_signals(tickers or None, force=force) | |
| automation.STATE["signals_df"] = df | |
| automation.STATE["signals_details"] = details | |
| automation.STATE["signals_summary"] = summary | |
| return JSONResponse({"rows": _df_records(df), "summary": summary, | |
| "tickers": sorted(details.keys())}) | |
| async def signals_raw(ticker: str): | |
| return JSONResponse({"raw": signal_runner.stock_raw_read(ticker)}) | |
| async def signals_summary(ticker: str): | |
| raw = signal_runner.stock_raw_read(ticker or "") | |
| if not raw: | |
| return _sse(iter(["Run the analysis and select a ticker first."])) | |
| chain = automation.STATE.get("signals_details", {}).get(ticker or "", "") | |
| chain_core = chain.split("日线买卖点逐项诊断")[0].strip()[:2000] if chain else "" | |
| prompt = ("You are an equity analyst. Write a SHORT plain-English summary " | |
| "(≤100 words) for a long-term holder of a US stock: the situation " | |
| "today, whether to act or wait, and the key price levels.\n" | |
| "Use the FACT LINE for the numbers, and the RULING CHAIN (a Chinese " | |
| "multi-timeframe Chan-theory decision log) for the reasoning — " | |
| "translate and synthesize it; output ENGLISH ONLY, no Chinese " | |
| "characters, do not quote the log, no disclaimers.\n\n" | |
| f"FACT LINE:\n{raw}") | |
| if chain_core: | |
| prompt += f"\n\nRULING CHAIN (translate & synthesize, don't quote):\n{chain_core}" | |
| def gen(): | |
| final = "" | |
| for acc in llm_local.chat_stream(prompt, max_tokens=240, temperature=0.2, | |
| worker="translator"): | |
| final = acc | |
| yield acc | |
| try: | |
| finetune_data.record(raw, final) | |
| except Exception: | |
| pass | |
| return _sse(gen()) | |
| # ───────────────────────── Sector Rotation ───────────────────────── | |
| async def rotation_tables(): | |
| d1, d5, d20, asof = rotation.build_rotation(force=True) | |
| automation.STATE["rotation"] = (d1, d5, d20, asof) | |
| return JSONResponse({ | |
| "asof": asof, | |
| "d1": _df_records(rotation.fmt_table(d1)), | |
| "d5": _df_records(rotation.fmt_table(d5)), | |
| "d20": _df_records(rotation.fmt_table(d20)), | |
| }) | |
| async def rotation_narrative(): | |
| rot = automation.STATE.get("rotation") | |
| if not rot or rot[0] is None: | |
| return _sse(iter(["Refresh the rotation tables first."])) | |
| d1, d5, d20, _ = rot | |
| brief = rotation.rotation_brief(d1, d5, d20) | |
| prompt = ("You are a US equity market strategist. Based only on the sector flow " | |
| "data below (SPDR ETF proxy: change% × dollar volume, plus RS vs SPY), " | |
| "write a crisp brief (<150 words): 1) where capital is rotating " | |
| "INTO/OUT OF; 2) do 1-day moves agree with the 5/20-day trend; 3) one " | |
| "watch item. No disclaimers.\n\nDATA:\n" + brief[:2200]) | |
| return _sse(llm_local.chat_stream(prompt, max_tokens=340, worker="narrator")) | |
| # ───────────────────────── Watchlist News ───────────────────────── | |
| async def news_holdings(): | |
| return JSONResponse({"holdings": news_watch.load_holdings()}) | |
| async def news_save(req: Request): | |
| body = await req.json() | |
| tickers = [t.strip().upper() for t in | |
| (body.get("holdings") or "").replace("\n", ",").split(",") if t.strip()] | |
| news_watch.save_holdings(tickers) | |
| return JSONResponse({"saved": tickers}) | |
| async def news_check(): | |
| return _sse(news_watch.check_holdings_news_stream()) | |
| # ───────────────────────── Auto Research ───────────────────────── | |
| async def research_run(ticker: str): | |
| def gen(): | |
| last_report = "" | |
| for progress, report in research_agent.run_research_stream(ticker): | |
| last_report = report | |
| yield json.dumps({"progress": progress, "report": report}) | |
| yield json.dumps({"progress": "__done__", "report": last_report, | |
| "reports": research_agent.list_reports()}) | |
| def stream(): | |
| for chunk in gen(): | |
| yield f"data: {chunk}\n\n" | |
| yield "event: done\ndata: {}\n\n" | |
| return StreamingResponse(stream(), media_type="text/event-stream") | |
| async def research_reports(): | |
| return JSONResponse({"reports": research_agent.list_reports()}) | |
| async def research_report(name: str): | |
| return JSONResponse({"markdown": research_agent.read_report(name)}) | |
| # ───────────────────────── Automation ───────────────────────── | |
| async def automation_run(): | |
| msg = automation.run_pipeline(force=True) | |
| return JSONResponse({"message": msg}) | |
| async def automation_state(): | |
| return JSONResponse({ | |
| "log": automation.STATE.get("log", [])[-40:], | |
| "schedule": automation.schedule_info(), | |
| "traces": research_agent.list_traces(), | |
| }) | |
| async def automation_publish(req: Request): | |
| body = await req.json() | |
| import trace_publish | |
| return JSONResponse({"status": trace_publish.publish_traces(body.get("repo", ""))}) | |
| # ───────────────────────── Model ───────────────────────── | |
| async def model_status(): | |
| return JSONResponse({ | |
| "status": llm_local.status(), | |
| "workers": {k: {"model": w["model"], "ready": w["llm"] is not None, | |
| "stage": w["stage"]} | |
| for k, w in llm_local.WORKERS.items()}, | |
| }) | |
| async def model_test(): | |
| return JSONResponse({"result": llm_local.quick_test()}) | |
| async def finetune_status(): | |
| return JSONResponse({"status": finetune_data.status_line(), | |
| "count": finetune_data.count()}) | |
| async def export_dataset(): | |
| path = finetune_data.export() | |
| return JSONResponse({"path": path or "", "count": finetune_data.count()}) | |
| # ───────────────────────── Email (all tabs) ───────────────────────── | |
| async def send_email(req: Request): | |
| body = await req.json() | |
| status = emailer.send_result(body.get("content", ""), body.get("to", ""), | |
| body.get("tag", "Chan Compass")) | |
| return JSONResponse({"status": status}) | |
| # ───────────────────────── static frontend ───────────────────────── | |
| # Mounted last so /api/* wins. html=True serves index.html at /. | |
| app.mount("/", StaticFiles(directory=UI_DIR, html=True), name="ui") | |
| # ───────────────────────── startup ───────────────────────── | |
| def _startup(): | |
| automation.start_scheduler() | |
| if os.environ.get("AUTO_LOAD_MODEL", "1") == "1": | |
| import threading | |
| threading.Thread(target=llm_local.auto_load_all, daemon=True).start() | |
| def launch(): | |
| import uvicorn | |
| uvicorn.run( | |
| app, | |
| host=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"), | |
| port=int(os.environ.get("GRADIO_SERVER_PORT", os.environ.get("PORT", "7860"))), | |
| ) | |
| if __name__ == "__main__": | |
| launch() | |