171 lines
5.9 KiB
Python
171 lines
5.9 KiB
Python
"""涨跌停/异动监控 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]} |