Files

163 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""题材数据路由:题材列表、题材详情、题材相关股票、题材历史"""
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()
# 上游反代/CDN 可能按 path 缓存,显式禁止缓存
_NO_CACHE_HEADERS = {"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"}
@router.get("", summary="题材列表")
async def theme_list(
sort_field: int = Query(1, ge=1, le=5, description="排序字段:1=涨幅 3=强度 4=热度排名 5=成交额"),
asc: bool = Query(False, description="True=升序, False=降序"),
):
if sort_field not in (1, 3, 4, 5):
raise HTTPException(status_code=400, detail="排序字段仅支持 1/3/4/5")
data = await themes.fetch_theme_list(sort_field, asc)
return JSONResponse(
{"data": data, "count": len(data), "sort_field": sort_field, "asc": asc},
headers=_NO_CACHE_HEADERS,
)
@router.get("/graph", summary="热点穿透:题材-股票网状关系图")
async def theme_graph(
sort_field: int = Query(1, description="题材排序:1=涨幅 4=热度"),
top: int = Query(30, ge=1, le=200, description="题材数量"),
limit: int = Query(1000, ge=100, le=2000, description="下发的股票节点上限(按穿透度取前 N 只)"),
):
if sort_field not in (1, 4):
raise HTTPException(status_code=400, detail="排序字段仅支持 1(涨幅)/4(热度)")
result = await themes.fetch_theme_graph(sort_field, top, limit)
return JSONResponse(result, headers=_NO_CACHE_HEADERS)
@router.get("/history", summary="指定交易日题材涨幅前20")
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("/active", summary="活跃题材 + 最近10日涨幅矩阵")
async def active_themes():
conn = get_connection()
try:
# 最近 10 个有数据的交易日(升序)
rows = conn.execute(
"SELECT DISTINCT trade_date FROM daily_top_themes ORDER BY trade_date DESC LIMIT 10"
).fetchall()
dates = [r["trade_date"] for r in reversed(rows)]
if not dates:
return JSONResponse({"dates": [], "themes": []}, headers=_NO_CACHE_HEADERS)
# 窗口内全部题材上榜记录,按天分组排列(当日 rank 即排名)
placeholders = ",".join("?" * len(dates))
rows = conn.execute(
f"""SELECT trade_date, theme_code, theme_name, bf3, rank FROM daily_top_themes
WHERE trade_date IN ({placeholders})
ORDER BY trade_date DESC, rank ASC""",
dates,
).fetchall()
# 按题材聚合:每日涨幅矩阵 + 上榜次数 + 最近上榜日 + 最佳排名
theme_map: dict[str, dict] = {}
for r in rows:
code = r["theme_code"]
t = theme_map.setdefault(code, {
"themeCode": code,
"themeName": r["theme_name"],
"dailyGains": {},
"appearCount": 0,
"lastAppear": None,
"bestRank": None,
})
t["dailyGains"][r["trade_date"]] = r["bf3"]
t["appearCount"] += 1
if t["lastAppear"] is None or r["trade_date"] > t["lastAppear"]:
t["lastAppear"] = r["trade_date"]
if t["bestRank"] is None or (r["rank"] or 0) < t["bestRank"]:
t["bestRank"] = r["rank"] or 0
themes_list = list(theme_map.values())
# 稳定排序:先按上榜次数降序,再按最佳排名升序
themes_list.sort(key=lambda x: x["bestRank"] if x["bestRank"] is not None else 10**9)
themes_list.sort(key=lambda x: -x["appearCount"])
return JSONResponse({"dates": dates, "themes": themes_list}, headers=_NO_CACHE_HEADERS)
finally:
conn.close()
@router.get("/{theme_code}/news", summary="题材相关新闻(分页)")
async def theme_news(
theme_code: str,
page_num: int = Query(1, ge=1, description="页码"),
page_size: int = Query(10, ge=1, le=50, description="每页条数"),
max_eu_time: str = Query("", description="分页游标(上一页返回的 maxEuTime)"),
):
result = await themes.fetch_theme_news(theme_code, page_num, max_eu_time, page_size)
if result is None:
return JSONResponse(
{"data": None, "theme_code": theme_code},
headers=_NO_CACHE_HEADERS,
)
return JSONResponse(
{"data": result, "theme_code": theme_code},
headers=_NO_CACHE_HEADERS,
)
@router.get("/{theme_code}/quote", summary="单题材实时行情(强度/热度/涨幅)")
async def theme_quote(theme_code: str):
result = await themes.fetch_theme_quote(theme_code)
if result is None:
return JSONResponse(
{"data": None, "theme_code": theme_code},
headers=_NO_CACHE_HEADERS,
)
return JSONResponse(
{"data": result, "theme_code": theme_code},
headers=_NO_CACHE_HEADERS,
)
@router.get("/{theme_code}/detail", summary="题材详情")
async def theme_detail(theme_code: str):
data = await themes.fetch_theme_detail(theme_code)
if not data:
return JSONResponse(
{"data": None, "theme_code": theme_code},
headers=_NO_CACHE_HEADERS,
)
return JSONResponse(
{"data": data, "theme_code": theme_code},
headers=_NO_CACHE_HEADERS,
)
@router.get("/{theme_code}/stocks", summary="题材相关股票(全部)")
async def theme_stocks(theme_code: str):
result = await themes.fetch_theme_stocks(theme_code)
return JSONResponse(
{
"data": result.get("stockList", []),
"statistic": result.get("statistic", {}),
"total": result.get("total", 0),
"theme_code": theme_code,
},
headers=_NO_CACHE_HEADERS,
)