feat: 补齐 spec 遗漏 — themes/history 接口 + cover_count/coverCount/daysSinceLastAppear
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user