功能细节优化
This commit is contained in:
@@ -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]:
|
||||
"""涨停敢死队排行
|
||||
|
||||
|
||||
Reference in New Issue
Block a user