astock-agent/backend/app/api/stocks.py
2026-04-15 08:58:21 +08:00

236 lines
8.8 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"""
from fastapi import APIRouter
from app.data.tushare_client import tushare_client
from app.data import tencent_client
from app.analysis.technical import add_all_indicators
from app.analysis.signals import generate_signals
router = APIRouter(prefix="/api/stocks", tags=["stocks"])
@router.get("/{ts_code}/quote")
async def get_quote(ts_code: str):
"""获取个股实时行情"""
quote = await tencent_client.get_realtime_quote(ts_code)
if not quote:
return {"error": "获取行情失败"}
return quote.model_dump()
@router.get("/{ts_code}/kline")
async def get_kline(ts_code: str, days: int = 120):
"""获取个股K线数据含技术指标"""
df = tushare_client.get_stock_daily(ts_code, days=days)
if df.empty:
return []
df = df.sort_values("trade_date").reset_index(drop=True)
df = add_all_indicators(df)
# 替换 NaN 为 NoneJSON 兼容)
import math
records = df.to_dict(orient="records")
for rec in records:
for k, v in rec.items():
if isinstance(v, float) and (math.isnan(v) or math.isinf(v)):
rec[k] = None
return records
@router.get("/{ts_code}/signals")
async def get_signals(ts_code: str):
"""获取个股技术面买卖信号"""
signal = generate_signals(ts_code)
return signal.model_dump()
@router.get("/{ts_code}/capital_flow")
async def get_capital_flow(ts_code: str, days: int = 10):
"""获取个股资金流向(含大/中/小单分拆)"""
df = tushare_client.get_stock_moneyflow(ts_code, days=days)
if df.empty:
return []
df = df.sort_values("trade_date")
records = []
for _, row in df.iterrows():
main_net = (
(row.get("buy_elg_amount", 0) or 0) - (row.get("sell_elg_amount", 0) or 0) +
(row.get("buy_lg_amount", 0) or 0) - (row.get("sell_lg_amount", 0) or 0)
)
records.append({
"trade_date": row["trade_date"],
"main_net_inflow": round(main_net, 2),
"net_mf_amount": round(float(row.get("net_mf_amount", 0) or 0), 2),
"elg_net": round(
(row.get("buy_elg_amount", 0) or 0) - (row.get("sell_elg_amount", 0) or 0), 2
),
"lg_net": round(
(row.get("buy_lg_amount", 0) or 0) - (row.get("sell_lg_amount", 0) or 0), 2
),
"md_net": round(
(row.get("buy_md_amount", 0) or 0) - (row.get("sell_md_amount", 0) or 0), 2
),
"sm_net": round(
(row.get("buy_sm_amount", 0) or 0) - (row.get("sell_sm_amount", 0) or 0), 2
),
})
return records
@router.get("/search")
async def search_stock(keyword: str):
"""搜索股票"""
basic = tushare_client.get_stock_basic()
if basic.empty:
return []
matches = basic[
basic["name"].str.contains(keyword, na=False) |
basic["ts_code"].str.contains(keyword, na=False) |
basic["symbol"].str.contains(keyword, na=False)
].head(20)
return matches[["ts_code", "name", "industry"]].to_dict(orient="records")
@router.post("/{ts_code}/diagnose")
async def diagnose_stock(ts_code: str):
"""AI 诊断个股"""
from app.config import settings
if not settings.deepseek_api_key:
return {"status": "error", "message": "未配置 LLM API Key"}
from app.llm.client import get_client
# 收集数据
quote = await tencent_client.get_realtime_quote(ts_code)
signals = generate_signals(ts_code)
df_daily = tushare_client.get_stock_daily(ts_code, days=30)
df_flow = tushare_client.get_stock_moneyflow(ts_code, days=10)
# 构建数据摘要
quote_str = ""
if quote:
quote_str = (
f"当前价: {quote.price}, 涨跌幅: {quote.pct_chg}%, "
f"换手率: {quote.turnover_rate}%, 量比: {quote.volume_ratio}, "
f"PE: {quote.pe}, PB: {quote.pb}, "
f"总市值: {quote.total_mv}亿, 流通市值: {quote.circ_mv}亿"
)
signal_str = (
f"技术评分: {signals.score}/100基于7项技术信号触发计分触发少不代表一定差可能处于蓄势阶段, "
f"信号数: {signals.signal_count}/7, "
f"均线多头: {signals.ma_bullish}, "
f"放量突破: {signals.volume_breakout}, "
f"MACD金叉: {signals.macd_golden}, "
f"RSI健康: {signals.rsi_healthy}, "
f"缩量回踩: {signals.pullback_support}, "
f"放量长阳: {signals.big_yang}, "
f"布林支撑: {signals.boll_support}, "
f"支撑位: {signals.support_price}, "
f"压力位: {signals.resist_price}, "
f"止损位: {signals.stop_loss_price}"
)
position_str = (
f"位置安全评分: {signals.position_score}/100越高表示位置越低越安全96分以上表示处于相对低位, "
f"近5日涨幅: {signals.rally_pct_5d}%, "
f"近10日涨幅: {signals.rally_pct_10d}%, "
f"距60日高点: {signals.distance_from_high}%"
)
trend_str = ""
ma_info = ""
if not df_daily.empty:
df_daily = df_daily.sort_values("trade_date")
latest = df_daily.iloc[-1]
if len(df_daily) >= 5:
pct_5d = (latest["close"] - df_daily.iloc[-5]["close"]) / df_daily.iloc[-5]["close"] * 100
trend_str += f"5日涨幅: {pct_5d:.2f}%, "
if len(df_daily) >= 20:
pct_20d = (latest["close"] - df_daily.iloc[-20]["close"]) / df_daily.iloc[-20]["close"] * 100
trend_str += f"20日涨幅: {pct_20d:.2f}%, "
vol_avg_5 = df_daily.tail(5)["vol"].mean()
vol_latest = latest["vol"]
trend_str += f"量比(5日均): {vol_latest / vol_avg_5:.2f}" if vol_avg_5 > 0 else ""
# MA 信息
if "ma5" in latest and "ma20" in latest:
ma5 = latest.get("ma5", 0)
ma10 = latest.get("ma10", 0)
ma20 = latest.get("ma20", 0)
ma60 = latest.get("ma60", 0)
price = latest["close"]
ma_info = (
f"价格与均线关系: 现价{price:.2f}, "
f"MA5={ma5:.2f}, MA10={ma10:.2f}, MA20={ma20:.2f}, MA60={ma60:.2f}, "
f"{'价格在MA5上方' if price > ma5 else '价格在MA5下方'}, "
f"{'价格在MA20上方' if price > ma20 else '价格在MA20下方'}, "
f"{'均线多头排列' if ma5 > ma10 > ma20 else '均线未多头排列'}"
)
flow_str = ""
if not df_flow.empty:
df_flow = df_flow.sort_values("trade_date")
recent_3 = df_flow.tail(3)
total_main = 0
for _, r in recent_3.iterrows():
main_net = (
(r.get("buy_elg_amount", 0) or 0) - (r.get("sell_elg_amount", 0) or 0) +
(r.get("buy_lg_amount", 0) or 0) - (r.get("sell_lg_amount", 0) or 0)
)
total_main += main_net
flow_str = f"近3日主力净流入: {total_main:.0f}"
# 基本信息
basic_info = ""
basic_df = tushare_client.get_stock_basic()
if not basic_df.empty:
row = basic_df[basic_df["ts_code"] == ts_code]
if not row.empty:
r = row.iloc[0]
basic_info = f"名称: {r['name']}, 行业: {r.get('industry', '未知')}"
user_msg = f"""请对以下A股进行全面诊断分析
股票: {ts_code} ({basic_info})
{quote_str}
技术面: {signal_str}
位置安全: {position_str}
趋势: {trend_str}
{ma_info}
资金面: {flow_str}
重要提示技术评分基于7项信号触发计分分数低不代表股票差可能处于蓄势阶段。位置安全评分高(>80)表示股价处于相对低位。请综合技术评分和位置安全评分一起判断。
请从以下维度分析Markdown格式简洁专业
## 综合评级
给出1-5星评级和一句话总结综合技术面和位置安全评分
## 技术面分析
(趋势方向、均线关系、支撑压力、量价配合,注意区分"技术信号未触发""技术面恶化"
## 资金面分析
(主力资金态度、筹码集中度推测)
## 操作建议
(适合什么类型的投资者、入场时机、风险提示)"""
try:
client = get_client()
response = await client.chat.completions.create(
model=settings.deepseek_model,
messages=[
{"role": "system", "content": "你是一位专业的A股分析师擅长技术面和资金面分析。回复使用Markdown格式简洁专业客观理性。"},
{"role": "user", "content": user_msg},
],
max_tokens=1500,
temperature=0.5,
)
content = response.choices[0].message.content.strip()
return {"status": "ok", "ts_code": ts_code, "diagnosis": content}
except Exception as e:
return {"status": "error", "message": str(e)}