astock-agent/backend/app/api/monitor.py
2026-04-16 14:16:02 +08:00

171 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""涨跌停/异动监控 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]}