92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
"""个股分析 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 为 None(JSON 兼容)
|
||
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")
|