From 258fc9184d437fe0e7e8c15e34c1c007ed3c5a6f Mon Sep 17 00:00:00 2001 From: Sakurasan <26715255+Sakurasan@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:08:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E9=BD=90=20spec=20=E9=81=97?= =?UTF-8?q?=E6=BC=8F=20=E2=80=94=20themes/history=20=E6=8E=A5=E5=8F=A3=20+?= =?UTF-8?q?=20cover=5Fcount/coverCount/daysSinceLastAppear?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- backend/routes/core_stocks.py | 19 ++++++++++++++++++- backend/routes/themes.py | 14 ++++++++++++++ backend/services/daily_collector.py | 6 ++++-- src/lib/core-stock-api.ts | 2 ++ src/routes/core-stocks.tsx | 4 ++++ 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/backend/routes/core_stocks.py b/backend/routes/core_stocks.py index e7e01d8..a6564af 100644 --- a/backend/routes/core_stocks.py +++ b/backend/routes/core_stocks.py @@ -1,5 +1,7 @@ """核心股历史接口:活跃核心股 + 指定日核心股/题材前10""" +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 @@ -29,7 +31,7 @@ async def active_core_stocks(): # 窗口内出现过且最近一次出现距今天数 <= 10 个交易日 placeholders = ",".join("?" * len(dates)) rows = conn.execute( - f"""SELECT trade_date, stock_code, stock_name, f3 FROM daily_core_stocks + 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, @@ -42,6 +44,7 @@ async def active_core_stocks(): s = stock_days.setdefault(code, { "stockCode": code, "stockName": r["stock_name"], + "coverCount": r["cover_count"], "dailyGains": {}, "appearCount": 0, "lastAppear": None, @@ -55,6 +58,20 @@ async def active_core_stocks(): # 稳定排序:先按出现次数降序,再按最近上榜日降序 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() diff --git a/backend/routes/themes.py b/backend/routes/themes.py index a2be720..31d6cc1 100644 --- a/backend/routes/themes.py +++ b/backend/routes/themes.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, Query, HTTPException from fastapi.responses import JSONResponse +from database import get_connection, dict_from_row from services import themes router = APIRouter() @@ -38,6 +39,19 @@ async def theme_graph( return JSONResponse(result, headers=_NO_CACHE_HEADERS) +@router.get("/history", summary="指定交易日题材涨幅前10") +async def theme_history(date: str = Query(..., description="交易日 YYYY-MM-DD")): + conn = get_connection() + try: + rows = conn.execute( + "SELECT * FROM daily_top_themes WHERE trade_date = ? ORDER BY rank ASC", (date,) + ).fetchall() + items = [dict_from_row(r) for r in rows] + return JSONResponse({"date": date, "items": items}, headers=_NO_CACHE_HEADERS) + finally: + conn.close() + + @router.get("/{theme_code}/detail", summary="题材详情") async def theme_detail(theme_code: str): data = await themes.fetch_theme_detail(theme_code) diff --git a/backend/services/daily_collector.py b/backend/services/daily_collector.py index c9b6a35..455a22e 100644 --- a/backend/services/daily_collector.py +++ b/backend/services/daily_collector.py @@ -60,10 +60,12 @@ async def collect_daily(trade_date: str, dry_run: bool = False) -> dict: # 2. 核心股前100:按领涨股 f3 降序,去重(同一股票可能是多个题材领涨股) stock_map: dict[str, dict] = {} + theme_count: dict[str, int] = {} # securityCode -> 覆盖题材数 for t in themes: code = t.get("securityCode") if not code: continue + theme_count[code] = theme_count.get(code, 0) + 1 if code not in stock_map or (t.get("f3") or 0) > (stock_map[code].get("f3") or 0): stock_map[code] = { "stock_code": code, @@ -86,8 +88,8 @@ async def collect_daily(trade_date: str, dry_run: bool = False) -> dict: try: for i, s in enumerate(core_stocks, start=1): conn.execute( - "INSERT OR IGNORE INTO daily_core_stocks (trade_date, stock_code, stock_name, f3, rank) VALUES (?,?,?,?,?)", - (trade_date, s["stock_code"], s["stock_name"], s["f3"], i), + "INSERT OR IGNORE INTO daily_core_stocks (trade_date, stock_code, stock_name, f3, cover_count, rank) VALUES (?,?,?,?,?,?)", + (trade_date, s["stock_code"], s["stock_name"], s["f3"], theme_count.get(s["stock_code"], 0), i), ) # 核心股所属题材:从 themes 列表(含 securityCode/themeCode/themeName)中 # 为每个核心股收集其全部所属题材,写入 daily_core_stock_themes diff --git a/src/lib/core-stock-api.ts b/src/lib/core-stock-api.ts index 94d5384..8cbba8b 100644 --- a/src/lib/core-stock-api.ts +++ b/src/lib/core-stock-api.ts @@ -4,9 +4,11 @@ import { getApiBaseUrl } from "@/lib/api-client"; export interface ActiveCoreStock { stockCode: string; stockName: string; + coverCount: number | null; // 覆盖题材数 dailyGains: Record; // 日期 -> 当日涨幅(可空) appearCount: number; lastAppear: string | null; + daysSinceLastAppear: number; // 最近上榜距窗口最新交易日的自然日差 } export interface ActiveCoreStocksResponse { diff --git a/src/routes/core-stocks.tsx b/src/routes/core-stocks.tsx index 3364e4f..c08f172 100644 --- a/src/routes/core-stocks.tsx +++ b/src/routes/core-stocks.tsx @@ -68,6 +68,7 @@ function CoreStocksPage() { {d.slice(5)} ))} + 题材数 上榜 @@ -87,6 +88,9 @@ function CoreStocksPage() { ); })} + + {s.coverCount ?? "·"} +