189 lines
7.6 KiB
Python
189 lines
7.6 KiB
Python
"""核心股历史接口:活跃核心股 + 指定日核心股/题材前20"""
|
|
|
|
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
|
|
|
|
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="活跃核心股 + 最近N日涨幅矩阵")
|
|
async def active_core_stocks(days: int = Query(10, ge=1, le=30, description="交易日天数")):
|
|
conn = get_connection()
|
|
try:
|
|
dates = _recent_trade_dates(conn, days)
|
|
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, cover_count 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"],
|
|
"coverCount": r["cover_count"],
|
|
"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"])
|
|
|
|
# 一次性取窗口内全部题材关联,按 stock_code 分组(跨天题材去重)
|
|
themes_rows = conn.execute(
|
|
f"""SELECT stock_code, theme_code, theme_name FROM daily_core_stock_themes
|
|
WHERE trade_date IN ({placeholders})""",
|
|
dates,
|
|
).fetchall()
|
|
themes_by_stock: dict[str, dict[str, dict]] = {}
|
|
for t in themes_rows:
|
|
per = themes_by_stock.setdefault(t["stock_code"], {})
|
|
per.setdefault(t["theme_code"], {"theme_code": t["theme_code"], "theme_name": t["theme_name"]})
|
|
for s in stocks:
|
|
s["themes"] = list(themes_by_stock.get(s["stockCode"], {}).values())
|
|
|
|
# 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()
|
|
|
|
|
|
@router.get("/consecutive", summary="连续上榜的核心股")
|
|
async def consecutive_core_stocks(min_days: int = Query(3, ge=2, le=10, description="最小连续天数")):
|
|
from datetime import date as date_cls
|
|
conn = get_connection()
|
|
try:
|
|
# 最近30个有数据的交易日
|
|
rows = conn.execute(
|
|
"SELECT DISTINCT trade_date FROM daily_core_stocks ORDER BY trade_date DESC LIMIT 30"
|
|
).fetchall()
|
|
all_dates = [r["trade_date"] for r in reversed(rows)]
|
|
|
|
if not all_dates:
|
|
return JSONResponse({"stocks": []}, headers=_NO_CACHE_HEADERS)
|
|
|
|
# 查询所有上榜记录
|
|
placeholders = ",".join("?" * len(all_dates))
|
|
rows = conn.execute(
|
|
f"""SELECT trade_date, stock_code, stock_name, f3, cover_count
|
|
FROM daily_core_stocks
|
|
WHERE trade_date IN ({placeholders})
|
|
ORDER BY stock_code, trade_date""",
|
|
all_dates,
|
|
).fetchall()
|
|
|
|
# 按股票分组,记录上榜日期
|
|
stock_dates: dict[str, dict] = {}
|
|
for r in rows:
|
|
code = r["stock_code"]
|
|
s = stock_dates.setdefault(code, {
|
|
"stockCode": code,
|
|
"stockName": r["stock_name"],
|
|
"dates": [],
|
|
"dailyGains": {},
|
|
})
|
|
s["dates"].append(r["trade_date"])
|
|
s["dailyGains"][r["trade_date"]] = r["f3"]
|
|
|
|
# 计算连续上榜天数(从最新交易日往回数)
|
|
results = []
|
|
for code, s in stock_dates.items():
|
|
dates_list = sorted(s["dates"])
|
|
consecutive = 0
|
|
last_n = []
|
|
for i in range(len(dates_list) - 1, -1, -1):
|
|
if i == len(dates_list) - 1:
|
|
consecutive = 1
|
|
last_n.append(dates_list[i])
|
|
else:
|
|
prev_idx = all_dates.index(dates_list[i + 1]) if dates_list[i + 1] in all_dates else -1
|
|
curr_idx = all_dates.index(dates_list[i]) if dates_list[i] in all_dates else -1
|
|
if prev_idx - curr_idx <= 2:
|
|
consecutive += 1
|
|
last_n.append(dates_list[i])
|
|
else:
|
|
break
|
|
|
|
if consecutive >= min_days:
|
|
results.append({
|
|
"stockCode": code,
|
|
"stockName": s["stockName"],
|
|
"consecutiveDays": consecutive,
|
|
"recentDates": last_n,
|
|
"dailyGains": {d: s["dailyGains"].get(d) for d in last_n},
|
|
})
|
|
|
|
results.sort(key=lambda x: -x["consecutiveDays"])
|
|
return JSONResponse({"stocks": results}, 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()
|
|
# 一次性取该日全部题材关联,按 stock_code 分组,避免逐股 N+1 查询
|
|
themes_rows = conn.execute(
|
|
"SELECT stock_code, theme_code, theme_name FROM daily_core_stock_themes WHERE trade_date = ?",
|
|
(date,),
|
|
).fetchall()
|
|
themes_by_stock: dict[str, list] = {}
|
|
for t in themes_rows:
|
|
themes_by_stock.setdefault(t["stock_code"], []).append(
|
|
{"theme_code": t["theme_code"], "theme_name": t["theme_name"]}
|
|
)
|
|
items = []
|
|
for r in rows:
|
|
d = dict_from_row(r)
|
|
d["themes"] = themes_by_stock.get(d["stock_code"], [])
|
|
items.append(d)
|
|
return JSONResponse({"date": date, "items": items}, headers=_NO_CACHE_HEADERS)
|
|
finally:
|
|
conn.close()
|