feat: 补齐 spec 遗漏 — themes/history 接口 + cover_count/coverCount/daysSinceLastAppear

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sakurasan
2026-08-10 21:08:39 +08:00
co-authored by Claude
parent ac985a2140
commit 258fc9184d
5 changed files with 42 additions and 3 deletions
+18 -1
View File
@@ -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()
+14
View File
@@ -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)
+4 -2
View File
@@ -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
+2
View File
@@ -4,9 +4,11 @@ import { getApiBaseUrl } from "@/lib/api-client";
export interface ActiveCoreStock {
stockCode: string;
stockName: string;
coverCount: number | null; // 覆盖题材数
dailyGains: Record<string, number | null>; // 日期 -> 当日涨幅(可空)
appearCount: number;
lastAppear: string | null;
daysSinceLastAppear: number; // 最近上榜距窗口最新交易日的自然日差
}
export interface ActiveCoreStocksResponse {
+4
View File
@@ -68,6 +68,7 @@ function CoreStocksPage() {
{d.slice(5)}
</th>
))}
<th scope="col" className="px-2 py-2 text-right font-medium" title="覆盖题材数"></th>
<th scope="col" className="px-2 py-2 text-right font-medium"></th>
</tr>
</thead>
@@ -87,6 +88,9 @@ function CoreStocksPage() {
</td>
);
})}
<td className="px-2 py-1.5 text-right tabular-nums whitespace-nowrap text-muted-foreground">
{s.coverCount ?? "·"}
</td>
<td className="px-2 py-1.5 text-right tabular-nums whitespace-nowrap">
<span className="inline-flex items-center gap-0.5 text-orange-500">
<Flame className="h-3 w-3" />