"""核心股历史接口:活跃核心股 + 指定日核心股/题材前20""" from datetime import date as date_cls from fastapi import APIRouter, Query from fastapi.responses import JSONResponse from database import get_connection, dict_from_row router = APIRouter() _NO_CACHE_HEADERS = {"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"} def _recent_trade_dates(conn, n: int = 10) -> list[str]: """最近 n 个有数据的交易日(升序)""" rows = conn.execute( "SELECT DISTINCT trade_date FROM daily_core_stocks ORDER BY trade_date DESC LIMIT ?", (n,), ).fetchall() return [r["trade_date"] for r in reversed(rows)] @router.get("/active", summary="活跃核心股 + 最近10日涨幅矩阵") async def active_core_stocks(): conn = get_connection() try: dates = _recent_trade_dates(conn, 10) if not dates: return JSONResponse({"dates": [], "stocks": []}, headers=_NO_CACHE_HEADERS) # 窗口内出现过且最近一次出现距今天数 <= 10 个交易日 placeholders = ",".join("?" * len(dates)) rows = conn.execute( f"""SELECT trade_date, stock_code, stock_name, f3, cover_count FROM daily_core_stocks WHERE trade_date IN ({placeholders}) ORDER BY trade_date DESC, rank ASC""", dates, ).fetchall() # 组装 per-stock:每日涨幅 + 出现次数 + 最近上榜 stock_days: dict[str, dict] = {} for r in rows: code = r["stock_code"] s = stock_days.setdefault(code, { "stockCode": code, "stockName": r["stock_name"], "coverCount": r["cover_count"], "dailyGains": {}, "appearCount": 0, "lastAppear": None, }) s["dailyGains"][r["trade_date"]] = r["f3"] s["appearCount"] += 1 if s["lastAppear"] is None or r["trade_date"] > s["lastAppear"]: s["lastAppear"] = r["trade_date"] stocks = list(stock_days.values()) # 稳定排序:先按出现次数降序,再按最近上榜日降序 stocks.sort(key=lambda x: x.get("lastAppear") or "", reverse=True) stocks.sort(key=lambda x: -x["appearCount"]) # daysSinceLastAppear:最近上榜距窗口最新交易日的自然日差(简单口径) latest = dates[-1] if dates else None for s in stocks: if s.get("lastAppear") and latest: try: d1 = date_cls.fromisoformat(latest) d2 = date_cls.fromisoformat(s["lastAppear"]) s["daysSinceLastAppear"] = (d1 - d2).days except ValueError: s["daysSinceLastAppear"] = 0 else: s["daysSinceLastAppear"] = 0 return JSONResponse({"dates": dates, "stocks": stocks}, headers=_NO_CACHE_HEADERS) finally: conn.close() @router.get("/history", summary="指定交易日核心股(含所属题材)") async def core_stock_history(date: str = Query(..., description="交易日 YYYY-MM-DD")): conn = get_connection() try: rows = conn.execute( "SELECT * FROM daily_core_stocks WHERE trade_date = ? ORDER BY rank ASC", (date,) ).fetchall() # 一次性取该日全部题材关联,按 stock_code 分组,避免逐股 N+1 查询 themes_rows = conn.execute( "SELECT stock_code, theme_code, theme_name FROM daily_core_stock_themes WHERE trade_date = ?", (date,), ).fetchall() themes_by_stock: dict[str, list] = {} for t in themes_rows: themes_by_stock.setdefault(t["stock_code"], []).append( {"theme_code": t["theme_code"], "theme_name": t["theme_name"]} ) items = [] for r in rows: d = dict_from_row(r) d["themes"] = themes_by_stock.get(d["stock_code"], []) items.append(d) return JSONResponse({"date": date, "items": items}, headers=_NO_CACHE_HEADERS) finally: conn.close()