"""涨跌停/异动监控 API 盘后:使用 Tushare 涨跌停列表和日级数据(完整准确) 盘中:涨跌停仍用 Tushare,异动股用腾讯实时行情(量比/振幅/急涨急跌) """ from fastapi import APIRouter from app.data.tushare_client import tushare_client from app.config import is_market_session router = APIRouter(prefix="/api/monitor", tags=["monitor"]) @router.get("/limits") async def get_limits(): """获取涨跌停数据""" trade_date = tushare_client.get_latest_trade_date() is_realtime = is_market_session() limit_df = tushare_client.get_limit_list(trade_date) if limit_df.empty: return {"trade_date": trade_date, "is_realtime": False, "limit_up": [], "limit_down": []} # 拆分涨停和跌停 up_df = limit_df[limit_df["limit"] == "U"].sort_values("pct_chg", ascending=False) down_df = limit_df[limit_df["limit"] == "D"].sort_values("pct_chg", ascending=True) def _parse(df): results = [] for _, r in df.head(50).iterrows(): results.append({ "ts_code": r.get("ts_code", ""), "name": r.get("name", ""), "close": float(r.get("close", 0)), "pct_chg": float(r.get("pct_chg", 0)), "limit_times": int(r.get("limit_times", 0)), "first_time": str(r.get("first_time", "")), "last_time": str(r.get("last_time", "")), "open_times": int(r.get("open_times", 0)), "fd_amount": float(r.get("fd_amount", 0)) if r.get("fd_amount") else 0, "up_stat": str(r.get("up_stat", "")), }) return results return { "trade_date": trade_date, "is_realtime": is_realtime, "limit_up": _parse(up_df), "limit_down": _parse(down_df), } @router.get("/unusual") async def get_unusual(): """获取异动股(量比>3、振幅>8%、快速拉升) 盘中时使用腾讯实时行情补充量比和涨跌幅, 盘后使用 Tushare 日级数据。 """ trade_date = tushare_client.get_latest_trade_date() is_realtime = is_market_session() if is_realtime: # 盘中:用腾讯实时行情扫描异动 return await _get_unusual_realtime(trade_date) # 盘后:使用 Tushare 日级数据 daily = tushare_client.get_daily_all(trade_date) if daily.empty: return {"trade_date": trade_date, "is_realtime": False, "stocks": []} basic = tushare_client.get_daily_basic(trade_date) if not basic.empty: daily = daily.merge(basic[["ts_code", "turnover_rate", "volume_ratio"]], on="ts_code", how="left") stock_basic = tushare_client.get_stock_basic() name_map = {} if not stock_basic.empty: for _, r in stock_basic.iterrows(): name_map[r["ts_code"]] = r["name"] unusual = [] for _, r in daily.iterrows(): ts = r.get("ts_code", "") if not ts.endswith((".SH", ".SZ")): continue pct = r.get("pct_chg", 0) vol_ratio = r.get("volume_ratio", 0) high = r.get("high", 0) low = r.get("low", 0) pre_close = r.get("pre_close", 0) amplitude = (high - low) / pre_close * 100 if pre_close > 0 else 0 tags = [] if vol_ratio and vol_ratio > 3: tags.append("巨量") if amplitude > 8: tags.append("高振幅") if pct > 7: tags.append("急涨") elif pct < -7: tags.append("急跌") if tags: unusual.append({ "ts_code": ts, "name": name_map.get(ts, ts), "close": float(r.get("close", 0)), "pct_chg": float(pct), "amplitude": round(float(amplitude), 2), "volume_ratio": round(float(vol_ratio), 2) if vol_ratio and vol_ratio == vol_ratio else 0, "turnover_rate": round(float(r.get("turnover_rate", 0)), 2), "tags": tags, }) unusual.sort(key=lambda x: abs(x["pct_chg"]), reverse=True) return {"trade_date": trade_date, "is_realtime": False, "stocks": unusual[:50]} async def _get_unusual_realtime(trade_date: str) -> dict: """盘中:用腾讯实时行情扫描异动""" from app.data.tencent_client import get_realtime_quotes_batch stock_basic = tushare_client.get_stock_basic() if stock_basic.empty: return {"trade_date": trade_date, "is_realtime": True, "stocks": []} # 只扫描主板(非 ST) valid = stock_basic[ ~stock_basic["name"].str.contains("ST", na=False) ] codes = valid["ts_code"].tolist() # 分批获取实时行情 unusual = [] batch_size = 200 for i in range(0, len(codes), batch_size): batch = codes[i:i + batch_size] quotes = await get_realtime_quotes_batch(batch) for ts_code, q in quotes.items(): if not q.price or q.price <= 0: continue tags = [] if q.volume_ratio and q.volume_ratio > 3: tags.append("巨量") if q.amplitude and q.amplitude > 8: tags.append("高振幅") if q.pct_chg > 7: tags.append("急涨") elif q.pct_chg < -7: tags.append("急跌") if tags: unusual.append({ "ts_code": ts_code, "name": q.name or ts_code, "close": q.price, "pct_chg": q.pct_chg, "amplitude": round(q.amplitude, 2) if q.amplitude else 0, "volume_ratio": round(q.volume_ratio, 2) if q.volume_ratio else 0, "turnover_rate": round(q.turnover_rate, 2) if q.turnover_rate else 0, "tags": tags, }) unusual.sort(key=lambda x: abs(x["pct_chg"]), reverse=True) return {"trade_date": trade_date, "is_realtime": True, "stocks": unusual[:50]}