功能细节优化

This commit is contained in:
2026-06-15 01:26:39 +08:00
parent e524a3589a
commit 964c17c200
33 changed files with 6990 additions and 210 deletions

View File

@@ -1,10 +1,4 @@
"""涨跌停分析 — 连板股追踪、炸板率统计、敢死队排行
功能:
1. 连板股追踪器
2. 炸板率统计
3. 涨停敢死队排行
"""
"""涨跌停分析 — 连板股追踪、炸板率统计、敢死队排行、炸板走势统计、涨停原因分类。"""
import datetime as dt
from typing import List, Dict, Any, Optional
from collections import defaultdict, Counter
@@ -12,7 +6,24 @@ import numpy as np
from sqlalchemy import select, and_, func, desc
from db import get_session
from models import DailyQuote, StockMetric
from models import DailyQuote, StockMetric, DragonTiger
try:
import akshare as ak
AK_OK = True
except Exception:
ak = None
AK_OK = False
# 涨停原因关键词分类
LIMIT_REASON_MAP = {
"题材": ["概念", "题材", "热点", "风口", "赛道"],
"业绩": ["业绩", "净利润", "营收", "盈利", "超预期", "预增", "扭亏"],
"政策": ["政策", "补贴", "利好", "支持", "规划", "国家", "工信部", "发改委"],
"技术突破": ["突破", "新高", "均线", "金叉", "放量"],
"重组并购": ["重组", "并购", "收购", "合并", "入股"],
"情绪": ["跟风", "连板", "情绪", "氛围", "涨停潮"],
}
def get_limit_stocks(date: Optional[dt.date] = None, limit_type: str = "up") -> Dict[str, Any]:
@@ -293,6 +304,214 @@ def analyze_limit_break_rate(days: int = 60) -> Dict[str, Any]:
}
def get_consecutive_calendar(days: int = 60) -> Dict[str, Any]:
"""连板日历:记录每只股票的连板历史,分析几进几出规律"""
with get_session() as s:
latest_date = s.execute(select(func.max(DailyQuote.date))).scalar()
if not latest_date:
return {"ok": False, "msg": "暂无数据"}
start_date = latest_date - dt.timedelta(days=days)
quotes = s.execute(
select(DailyQuote)
.where(DailyQuote.date >= start_date)
.order_by(DailyQuote.code, DailyQuote.date)
).scalars().all()
stock_data = defaultdict(list)
for q in quotes:
stock_data[q.code].append(q)
all_streaks = []
current_streaks = []
for code, data in stock_data.items():
data_sorted = sorted(data, key=lambda x: x.date)
name = data_sorted[-1].name
streaks = []
current = []
for q in data_sorted:
if q.open == 0:
if len(current) >= 2:
streaks.append(current)
current = []
continue
pct = (float(q.close) - float(q.open)) / float(q.open) * 100
if pct >= 9.8:
current.append(q)
else:
if len(current) >= 2:
streaks.append(current)
current = []
if len(current) >= 2:
current_streaks.append({
"code": code, "name": name,
"days": len(current),
"start_date": current[0].date.isoformat(),
"latest_date": current[-1].date.isoformat(),
"latest_close": float(current[-1].close)
})
for streak in streaks:
all_streaks.append({
"code": code, "name": name,
"days": len(streak),
"start_date": streak[0].date.isoformat(),
"end_date": streak[-1].date.isoformat()
})
distribution = defaultdict(int)
for item in all_streaks:
distribution[f"{item['days']}"] += 1
current_streaks.sort(key=lambda x: x["days"], reverse=True)
return {
"ok": True,
"date_range": f"{start_date.isoformat()} ~ {latest_date.isoformat()}",
"current_streaks": current_streaks[:30],
"streak_distribution": dict(distribution),
"total_streaks": len(all_streaks)
}
def analyze_post_break_performance(days: int = 90) -> Dict[str, Any]:
"""炸板后走势统计:炸板后 1/3/5 日表现概率分布"""
with get_session() as s:
latest_date = s.execute(select(func.max(DailyQuote.date))).scalar()
if not latest_date:
return {"ok": False, "msg": "暂无数据"}
start_date = latest_date - dt.timedelta(days=days + 10)
quotes = s.execute(
select(DailyQuote)
.where(DailyQuote.date >= start_date)
.order_by(DailyQuote.code, DailyQuote.date)
).scalars().all()
stock_data = defaultdict(dict)
for q in quotes:
stock_data[q.code][q.date] = q
# 炸板 = 当日一度涨停但收盘时未涨停(用开高低收近似判断)
# 简化:前一日涨停,次日收盘未涨停视为炸板
perf_1d, perf_3d, perf_5d = [], [], []
for code, date_data in stock_data.items():
dates = sorted(date_data.keys())
for i in range(len(dates) - 5):
today = dates[i]
q_today = date_data[today]
if q_today.open == 0:
continue
pct_today = (float(q_today.close) - float(q_today.open)) / float(q_today.open) * 100
# 判断昨日涨停今日炸板(开高但收盘低)
if i == 0:
continue
yesterday = dates[i - 1]
q_yest = date_data[yesterday]
if q_yest.open == 0:
continue
pct_yest = (float(q_yest.close) - float(q_yest.open)) / float(q_yest.open) * 100
# 昨日涨停,今日未涨停(炸板)
if pct_yest >= 9.8 and pct_today < 9.8:
base = float(q_today.close)
# 后续 1/3/5 日表现
for horizon, perf_list in [(1, perf_1d), (3, perf_3d), (5, perf_5d)]:
if i + horizon < len(dates):
future = dates[i + horizon]
q_future = date_data[future]
ret = (float(q_future.close) - base) / base * 100
perf_list.append(round(ret, 2))
def summarize(perfs):
if not perfs:
return {}
arr = np.array(perfs)
return {
"samples": len(perfs),
"avg_ret": round(float(arr.mean()), 2),
"win_rate": round(float((arr > 0).mean() * 100), 1),
"p25": round(float(np.percentile(arr, 25)), 2),
"median": round(float(np.median(arr)), 2),
"p75": round(float(np.percentile(arr, 75)), 2),
}
return {
"ok": True,
"days": days,
"after_1d": summarize(perf_1d),
"after_3d": summarize(perf_3d),
"after_5d": summarize(perf_5d),
"conclusion": (
f"炸板后样本 {len(perf_1d)} 条,"
f"次日平均收益 {summarize(perf_1d).get('avg_ret', 0)}%"
f"次日上涨概率 {summarize(perf_1d).get('win_rate', 0)}%"
) if perf_1d else "样本不足"
}
def classify_limit_reasons(date: Optional[dt.date] = None) -> Dict[str, Any]:
"""涨停原因分类:情绪、题材、业绩、技术突破等"""
with get_session() as s:
if date is None:
date = s.execute(select(func.max(DragonTiger.date))).scalar()
if not date:
return {"ok": False, "msg": "暂无龙虎榜数据,请先入库"}
lhb_rows = s.execute(
select(DragonTiger).where(DragonTiger.date == date)
).scalars().all()
# 尝试从 AkShare 获取当日涨停原因
reason_data = {}
if AK_OK:
try:
df = ak.stock_zt_pool_em(date=date.strftime("%Y%m%d"))
if df is not None and not df.empty:
for _, r in df.iterrows():
code = str(r.get("代码", ""))
reason = str(r.get("涨停原因类别", "") or r.get("上榜原因", ""))
reason_data[code] = reason
except Exception:
pass
# 合并龙虎榜原因
for row in lhb_rows:
if row.code not in reason_data:
reason_data[row.code] = row.reason
# 分类
classified = defaultdict(list)
for code, reason in reason_data.items():
matched = False
for category, keywords in LIMIT_REASON_MAP.items():
if any(kw in reason for kw in keywords):
classified[category].append({"code": code, "reason": reason})
matched = True
break
if not matched:
classified["其他"].append({"code": code, "reason": reason})
total = sum(len(v) for v in classified.values())
summary = [
{
"category": cat,
"count": len(items),
"pct": round(len(items) / total * 100, 1) if total > 0 else 0,
"stocks": items[:10]
}
for cat, items in sorted(classified.items(), key=lambda x: len(x[1]), reverse=True)
]
return {
"ok": True,
"date": date.isoformat(),
"total": total,
"categories": summary
}
def get_limit_squad_rankings(days: int = 30, min_limits: int = 5) -> Dict[str, Any]:
"""涨停敢死队排行