akg-factor-bridge/api.py

107 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""桥侧计划 API07-30 用户需求):对外提供每日选股计划。
容器常驻命令改为 uvicorn 后随容器启动docker-compose 已配端口,默认 8300
cron 的 docker exec 构建/出计划照旧互不影响。局域网内部服务v1 无鉴权。
GET /health 存活 + 最新计划日
GET /plan 最新一天的计划JSON
GET /plan?date=2026-07-30 指定日期
GET /plan?format=md Markdown 原文(浏览器直接可读)
GET /plan/dates 可用日期列表
POST /plan/refresh?date=... 重新生成该日计划文件data/plan/*.md
统一任务调度平台XXL-JOB触发入口挂在 /api/v1/xxl/*(见 xxl.py2026-08-03
盘前链build → plan → push-pool可由平台拉起并回调结案.env 配 XXL_TRIGGER_KEY 才启用。
"""
import pandas as pd
from fastapi import FastAPI, HTTPException
from fastapi.responses import PlainTextResponse
import db
import plan
import plan_reconcile
from xxl import router as xxl_router
app = FastAPI(title="akg-factor-bridge · 每日选股计划", version="0.1")
app.include_router(xxl_router)
@app.get("/health")
def health():
try:
d = plan._latest_date("t_factor_akg_score") # noqa: SLF001 —— 桥内自用
except Exception as e: # noqa: BLE001 —— 库连不上也要能回答"我还活着"
return {"ok": False, "error": repr(e)}
return {"ok": True, "latest_plan_date": d}
@app.get("/plan/dates")
def plan_dates(limit: int = 30):
df = db.read_mysql(
"factor", "SELECT DISTINCT trade_date FROM t_factor_akg_score "
"ORDER BY trade_date DESC LIMIT %s", (int(limit),))
if df.empty:
return {"dates": []}
return {"dates": [pd.Timestamp(x).date().isoformat()
for x in df["trade_date"]]}
@app.get("/plan")
def get_plan(date: str | None = None, format: str = "json",
top: int = 20, obs_top: int = 10, theme_cap: int = 5):
try:
data = plan.collect(date, top, obs_top, theme_cap)
except RuntimeError as e:
raise HTTPException(status_code=404, detail=str(e))
if format == "md":
return PlainTextResponse(plan.render_md(data),
media_type="text/markdown; charset=utf-8")
return data
@app.post("/plan/refresh")
def refresh(date: str | None = None):
try:
out = plan.generate(date)
except SystemExit as e:
raise HTTPException(status_code=404, detail=str(e))
return {"ok": True, "file": out}
@app.get("/plan/verdict")
def plan_verdict(codes: str | None = None, code: str | None = None,
date: str | None = None):
"""逐票『计划判决』(只读)——今日页 / 机会线索页对齐用。
GET /plan/verdict?codes=300750,600438,SH688041 多只(逗号分隔)
GET /plan/verdict?code=300750&date=2026-08-04 单只 + 指定档位日
每只票返回 decisionmain 主榜 / observe 观察档 / reject 不采纳 / absent 无此票)、
与命令行 plan_reconcile 完全同口径的 verdict_text页面直接展示的一行解释
以及 score/rank/tier/upside/热度/传导/赛道/图谱证据等明细。
date 缺省=当日档位日。三种代码形态都收600000.SH / SH600000 / 600000。纯 SELECT不写任何库。
"""
raw = (codes or code or "").strip()
want = [c.strip() for c in raw.split(",") if c.strip()]
if not want:
raise HTTPException(status_code=400, detail="缺少 code / codes 参数")
d = date or plan_reconcile.latest_date()
if not d:
raise HTTPException(status_code=404, detail="档位表为空——先跑当日构建")
try:
L = plan_reconcile._load(d) # noqa: SLF001 —— 桥内自用只读加载
except Exception as e: # noqa: BLE001 —— 数据层异常统一收成 500
raise HTTPException(status_code=500, detail=f"加载档位数据失败: {e!r}")
verdicts = []
for c in want:
try:
k = plan_reconcile._norm_code(c) # noqa: SLF001
except SystemExit as e: # _norm_code 认不出的形态会 raise SystemExit
verdicts.append({"input": c, "error": str(e)})
continue
v = plan_reconcile.verdict(k, L)
v["input"] = c
verdicts.append(v)
return {"date": d, "stale": L.get("stale", ""),
"count": len(verdicts), "verdicts": verdicts}