Initial commit: Stock Quote Agent - 美股实时行情查询

This commit is contained in:
zhanggangyong
2026-02-05 13:00:59 +00:00
parent 4d6a7c2dc1
commit f84a99db3c
4 changed files with 431 additions and 2 deletions
+39
View File
@@ -0,0 +1,39 @@
FROM python:3.11-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖
RUN pip install --no-cache-dir \
fastapi==0.109.0 \
uvicorn[standard]==0.27.0 \
pydantic==2.5.3 \
requests>=2.31.0 \
aiohttp>=3.9.0
# 复制 common 模块(回调工具)
COPY common/agent_callback_utils.py /app/common/
RUN touch /app/common/__init__.py
# 复制应用代码
COPY agents/stock_quote_agent/stock_quote_agent.py /app/
# 环境变量
ENV PYTHONUNBUFFERED=1
ENV SERVICE_HOST=0.0.0.0
ENV SERVICE_PORT=8080
# 回调配置
ENV AGENT_CALLBACK_URL=http://mcp-server:8002/api/v1/billing/agent-callback
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \
CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health').read()" || exit 1
EXPOSE 8080
CMD ["python3", "-u", "stock_quote_agent.py"]
+37 -2
View File
@@ -1,3 +1,38 @@
# stock-quote-agent
# Stock Quote Agent
美股实时行情查询 Agent - 获取股票价格、涨跌幅、成交量等数据
美股实时行情查询 Agent - 获取股票价格、涨跌幅、成交量等数据。
## 功能特性
- 📈 实时股票行情查询
- 📊 批量行情查询
- 🔥 热门股票行情
- 💹 52周高低价
- 📉 市值和市盈率
## API 端点
- `GET /` - 健康检查
- `GET /quote?symbol=AAPL` - 获取单个股票行情
- `POST /quote` - 获取单个股票行情
- `POST /batch` - 批量获取股票行情
- `GET /popular` - 获取热门股票行情
## 使用示例
```bash
# 获取苹果股票行情
curl "http://localhost:8080/quote?symbol=AAPL"
# 批量查询
curl -X POST "http://localhost:8080/batch" \
-H "Content-Type: application/json" \
-d '{"symbols": ["AAPL", "MSFT", "TSLA"]}'
```
## 部署
```bash
docker build -t stock-quote-agent -f Dockerfile .
docker run -p 8080:8080 stock-quote-agent
```
+5
View File
@@ -0,0 +1,5 @@
fastapi==0.109.0
uvicorn[standard]==0.27.0
pydantic==2.5.3
requests>=2.31.0
aiohttp>=3.9.0
+350
View File
@@ -0,0 +1,350 @@
"""
Stock Quote Agent - 美股实时行情查询 Agent
使用 Yahoo Finance API 获取美股实时行情数据
"""
import os
import sys
import logging
import aiohttp
from typing import Optional, List, Dict, Any
from datetime import datetime
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
import uvicorn
# 添加 common 模块路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 导入回调工具
try:
from common.agent_callback_utils import AgentCallbackHandler, CallbackContextManager
CALLBACK_ENABLED = True
except ImportError:
CALLBACK_ENABLED = False
AgentCallbackHandler = None
CallbackContextManager = None
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 环境变量
SERVICE_HOST = os.getenv("SERVICE_HOST", "0.0.0.0")
SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080"))
POD_NAME = os.getenv("POD_NAME", "stock-quote-agent")
USER_ID = os.getenv("USER_ID", "")
# Yahoo Finance API (通过 RapidAPI)
RAPIDAPI_KEY = os.getenv("RAPIDAPI_KEY", "")
YAHOO_FINANCE_HOST = "yahoo-finance15.p.rapidapi.com"
# FastAPI 应用
app = FastAPI(
title="Stock Quote Agent",
description="美股实时行情查询 - 获取股票价格、涨跌幅、成交量等数据",
version="1.0.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 回调处理器
callback_handler: Optional[AgentCallbackHandler] = None
# ==================== 请求/响应模型 ====================
class QuoteRequest(BaseModel):
"""行情查询请求"""
symbol: str = Field(..., description="股票代码,如 AAPL, TSLA, MSFT")
user_id: Optional[str] = Field(None, description="用户ID(用于计费回调)")
class QuoteResponse(BaseModel):
"""行情查询响应"""
symbol: str
name: str
price: float
change: float
change_percent: float
volume: int
market_cap: Optional[float] = None
pe_ratio: Optional[float] = None
high_52week: Optional[float] = None
low_52week: Optional[float] = None
timestamp: str
class BatchQuoteRequest(BaseModel):
"""批量行情查询请求"""
symbols: List[str] = Field(..., description="股票代码列表")
user_id: Optional[str] = Field(None, description="用户ID(用于计费回调)")
class BatchQuoteResponse(BaseModel):
"""批量行情查询响应"""
quotes: List[QuoteResponse]
success_count: int
failed_count: int
timestamp: str
class HealthResponse(BaseModel):
"""健康检查响应"""
status: str
pod_name: str
api_configured: bool
callback_enabled: bool
timestamp: str
# ==================== 生命周期 ====================
@app.on_event("startup")
async def startup_event():
"""应用启动时初始化回调处理器"""
global callback_handler
if CALLBACK_ENABLED:
callback_handler = AgentCallbackHandler(
agent_name=POD_NAME,
user_id=USER_ID
)
logger.info(f"回调处理器已初始化: callback_url={callback_handler.callback_url}")
else:
logger.warning("回调模块未加载,计费回调功能不可用")
# ==================== 辅助函数 ====================
async def fetch_stock_quote(symbol: str) -> Dict[str, Any]:
"""获取股票行情数据"""
# 使用免费的 Yahoo Finance API 替代方案
url = f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
params = {
"interval": "1d",
"range": "1d"
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers, timeout=15) as response:
if response.status == 200:
data = await response.json()
result = data.get("chart", {}).get("result", [])
if not result:
return {"success": False, "error": f"未找到股票: {symbol}"}
quote_data = result[0]
meta = quote_data.get("meta", {})
indicators = quote_data.get("indicators", {}).get("quote", [{}])[0]
current_price = meta.get("regularMarketPrice", 0)
previous_close = meta.get("previousClose", 0)
change = current_price - previous_close if previous_close else 0
change_percent = (change / previous_close * 100) if previous_close else 0
return {
"success": True,
"symbol": symbol.upper(),
"name": meta.get("shortName", symbol),
"price": current_price,
"change": round(change, 2),
"change_percent": round(change_percent, 2),
"volume": indicators.get("volume", [0])[-1] if indicators.get("volume") else 0,
"market_cap": meta.get("marketCap"),
"pe_ratio": None,
"high_52week": meta.get("fiftyTwoWeekHigh"),
"low_52week": meta.get("fiftyTwoWeekLow")
}
else:
return {"success": False, "error": f"API 请求失败: HTTP {response.status}"}
except Exception as e:
logger.error(f"获取行情失败: {symbol} - {e}")
return {"success": False, "error": str(e)}
# ==================== API 端点 ====================
@app.get("/health", response_model=HealthResponse)
@app.get("/", response_model=HealthResponse)
async def health_check():
"""健康检查"""
return HealthResponse(
status="healthy",
pod_name=POD_NAME,
api_configured=True, # 使用免费 API
callback_enabled=CALLBACK_ENABLED,
timestamp=datetime.utcnow().isoformat()
)
@app.post("/quote", response_model=QuoteResponse)
async def get_quote(request: QuoteRequest):
"""获取单个股票行情"""
# 使用回调上下文管理器
if CALLBACK_ENABLED and callback_handler and request.user_id:
with CallbackContextManager(
handler=callback_handler,
user_id=request.user_id,
request_id=f"stock-quote-{int(datetime.utcnow().timestamp())}"
) as ctx:
ctx.add_tool("stock_quote")
ctx.add_tool("yahoo_finance")
result = await fetch_stock_quote(request.symbol)
if not result["success"]:
raise HTTPException(status_code=500, detail=result.get("error"))
return QuoteResponse(
symbol=result["symbol"],
name=result["name"],
price=result["price"],
change=result["change"],
change_percent=result["change_percent"],
volume=result["volume"],
market_cap=result.get("market_cap"),
pe_ratio=result.get("pe_ratio"),
high_52week=result.get("high_52week"),
low_52week=result.get("low_52week"),
timestamp=datetime.utcnow().isoformat()
)
else:
result = await fetch_stock_quote(request.symbol)
if not result["success"]:
raise HTTPException(status_code=500, detail=result.get("error"))
return QuoteResponse(
symbol=result["symbol"],
name=result["name"],
price=result["price"],
change=result["change"],
change_percent=result["change_percent"],
volume=result["volume"],
market_cap=result.get("market_cap"),
pe_ratio=result.get("pe_ratio"),
high_52week=result.get("high_52week"),
low_52week=result.get("low_52week"),
timestamp=datetime.utcnow().isoformat()
)
@app.get("/quote")
async def get_quote_get(
symbol: str = Query(..., description="股票代码"),
user_id: Optional[str] = Query(None, description="用户ID")
):
"""GET 方式获取行情"""
request = QuoteRequest(symbol=symbol, user_id=user_id)
return await get_quote(request)
@app.post("/batch", response_model=BatchQuoteResponse)
async def batch_get_quotes(request: BatchQuoteRequest):
"""批量获取股票行情"""
if CALLBACK_ENABLED and callback_handler and request.user_id:
with CallbackContextManager(
handler=callback_handler,
user_id=request.user_id,
request_id=f"stock-batch-{int(datetime.utcnow().timestamp())}"
) as ctx:
ctx.add_tool("stock_quote")
ctx.add_tool("batch_quote")
quotes = []
success_count = 0
failed_count = 0
for symbol in request.symbols:
result = await fetch_stock_quote(symbol)
if result["success"]:
quotes.append(QuoteResponse(
symbol=result["symbol"],
name=result["name"],
price=result["price"],
change=result["change"],
change_percent=result["change_percent"],
volume=result["volume"],
market_cap=result.get("market_cap"),
pe_ratio=result.get("pe_ratio"),
high_52week=result.get("high_52week"),
low_52week=result.get("low_52week"),
timestamp=datetime.utcnow().isoformat()
))
success_count += 1
else:
failed_count += 1
return BatchQuoteResponse(
quotes=quotes,
success_count=success_count,
failed_count=failed_count,
timestamp=datetime.utcnow().isoformat()
)
else:
quotes = []
success_count = 0
failed_count = 0
for symbol in request.symbols:
result = await fetch_stock_quote(symbol)
if result["success"]:
quotes.append(QuoteResponse(
symbol=result["symbol"],
name=result["name"],
price=result["price"],
change=result["change"],
change_percent=result["change_percent"],
volume=result["volume"],
market_cap=result.get("market_cap"),
pe_ratio=result.get("pe_ratio"),
high_52week=result.get("high_52week"),
low_52week=result.get("low_52week"),
timestamp=datetime.utcnow().isoformat()
))
success_count += 1
else:
failed_count += 1
return BatchQuoteResponse(
quotes=quotes,
success_count=success_count,
failed_count=failed_count,
timestamp=datetime.utcnow().isoformat()
)
@app.get("/popular")
async def get_popular_stocks():
"""获取热门股票行情"""
popular_symbols = ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA", "NVDA", "META"]
request = BatchQuoteRequest(symbols=popular_symbols)
return await batch_get_quotes(request)
# ==================== 主入口 ====================
def main():
"""主函数"""
logger.info(f"启动 Stock Quote Agent - {POD_NAME}")
logger.info(f"回调功能: {'已启用' if CALLBACK_ENABLED else '未启用'}")
uvicorn.run(app, host=SERVICE_HOST, port=SERVICE_PORT, log_level="info")
if __name__ == "__main__":
main()