akg-factor-bridge/api.py

138 lines
6.3 KiB
Python
Raw Normal View History

"""桥侧计划 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
2026-08-03 16:33:23 +08:00
统一任务调度平台XXL-JOB触发入口挂在 /api/v1/xxl/* xxl.py2026-08-03
盘前链build plan push-pool可由平台拉起并回调结案.env XXL_TRIGGER_KEY 才启用
"""
import logging
import os
import pandas as pd
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import PlainTextResponse
import config
import db
import plan
2026-08-18 16:01:40 +08:00
import plan_reconcile
import regime
2026-08-03 16:33:23 +08:00
from xxl import router as xxl_router
_access = logging.getLogger("plan.access")
app = FastAPI(title="akg-factor-bridge · 每日选股计划", version="0.1")
2026-08-03 16:33:23 +08:00
app.include_router(xxl_router)
@app.get("/health")
def health():
"""存活探针。2026-09-03 起带 pool_top 与 pool_maxPMS 的池深探针拿它与自身的计划深度
比较池深不变式台账 008库连不上时也照样返回这两项"""
depth = {"pool_top": config.POOL_TOP, "pool_max": config.POOL_MAX}
try:
d = plan._latest_date("t_factor_akg_score") # noqa: SLF001 —— 桥内自用
except Exception as e: # noqa: BLE001 —— 库连不上也要能回答"我还活着"
return {"ok": False, "error": repr(e), **depth}
return {"ok": True, "latest_plan_date": d, **depth}
@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(request: Request, date: str | None = None, format: str = "json",
top: int = 20, obs_top: int = 10, theme_cap: int = 5):
"""向下兼容承诺2026-09-02 方案 2.7main / observe 的装配、排序、裁剪与既有字段
一字不动每行只多联入判决类字段顶层只新增 generated_atplan_versionregime
card_countscandidateswatchsegments_pointedsnapshot2026-09-03 再加 market
环境段市场四项 regime 一样只从当日快照读快照缺失为空字典PMS 按字段名取值忽略未知键"""
try:
data = plan.collect(date, top, obs_top, theme_cap)
except RuntimeError as e:
raise HTTPException(status_code=404, detail=str(e))
data.pop("_full", None)
ds = data["date"]
reg = regime.read_from_snapshot(ds)
data["snapshot"] = "present" if os.path.exists(regime.snapshot_path(ds)) else "missing"
data["regime"] = reg or {"status": regime.UNKNOWN, "weak_day": None,
"source": "当日快照无环境段08:45 追加未跑或快照缺失)"}
data["market"] = regime.read_section(ds, "market") or {}
_access.info("plan client=%s date=%s regime=%s generated_at=%s version=%s "
"top=%s obs_top=%s theme_cap=%s",
request.client.host if request.client else "-", ds,
data["regime"].get("status"), data.get("generated_at"),
data.get("plan_version"), top, obs_top, theme_cap)
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}
2026-08-18 16:01:40 +08:00
@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
try:
v = plan_reconcile.verdict(k, L)
except Exception as e: # noqa: BLE001 —— 单票数据异常(如 score 缺行)不崩整批,
verdicts.append({"input": c, "code": k, # 与上面认不出形态的处理对称
"error": f"{type(e).__name__}: {e}"})
continue
2026-08-18 16:01:40 +08:00
v["input"] = c
verdicts.append(v)
return {"date": d, "stale": L.get("stale", ""),
"count": len(verdicts), "verdicts": verdicts}