From f5ba61e3dbba2a10df89b60c4723d62286c28242 Mon Sep 17 00:00:00 2001 From: Sakurasan <26715255+Sakurasan@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:59:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A0=B8=E5=BF=83=E8=82=A1=E5=8E=86?= =?UTF-8?q?=E5=8F=B2/=E6=B4=BB=E8=B7=83=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- backend/routes/core_stocks.py | 81 +++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 backend/routes/core_stocks.py diff --git a/backend/routes/core_stocks.py b/backend/routes/core_stocks.py new file mode 100644 index 0000000..de6a6fc --- /dev/null +++ b/backend/routes/core_stocks.py @@ -0,0 +1,81 @@ +"""核心股历史接口:活跃核心股 + 指定日核心股/题材前10""" + +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 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"], + "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"]) + 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() + items = [] + for r in rows: + d = dict_from_row(r) + themes = conn.execute( + "SELECT theme_code, theme_name FROM daily_core_stock_themes WHERE trade_date = ? AND stock_code = ?", + (date, d["stock_code"]), + ).fetchall() + d["themes"] = [dict(t) for t in themes] + items.append(d) + return JSONResponse({"date": date, "items": items}, headers=_NO_CACHE_HEADERS) + finally: + conn.close()