This commit is contained in:
aaron 2026-02-19 21:20:20 +08:00
parent ae743f9d39
commit b61b9a0f0b
13 changed files with 1616 additions and 2 deletions

View File

@ -145,3 +145,15 @@ PAPER_TRADING_POSITION_C=200
# 可选值: zhipu, deepseek
SMART_AGENT_MODEL=deepseek
CRYPTO_AGENT_MODEL=deepseek
STOCK_AGENT_MODEL=deepseek
# ----------------------------------------------------------------------------
# 美股智能体配置
# ----------------------------------------------------------------------------
# 监控的股票代码(逗号分隔)
# 配置策略科技龙头30% + AI/半导体成长30% + 生物医疗20% + 新能源10% + 金融10%
STOCK_SYMBOLS=AAPL,MSFT,GOOGL,META,AMZN,NVDA,AMD,AVGO,ARM,PLTR,SNOW,LLY,NVO,VRTX,TSLA,ENPH,V,MA,HD,COST
# 分析间隔(秒,美股交易时间内每小时分析一次=3600秒
STOCK_ANALYSIS_INTERVAL=3600
# 触发 LLM 分析的置信度阈值0-1
STOCK_LLM_THRESHOLD=0.60

131
backend/app/api/stocks.py Normal file
View File

@ -0,0 +1,131 @@
"""
美股相关 API 路由
"""
from fastapi import APIRouter, HTTPException
from typing import Dict, Any, List
from app.utils.logger import logger
from app.config import get_settings
router = APIRouter()
# 全局变量,用于访问智能体实例
_stock_agent_instance = None
def set_stock_agent(agent):
"""设置智能体实例(由 main.py 调用)"""
global _stock_agent_instance
_stock_agent_instance = agent
@router.get("/status")
async def get_stock_status() -> Dict[str, Any]:
"""
获取美股智能体状态
Returns:
智能体状态信息
"""
try:
if _stock_agent_instance is None:
return {
"enabled": False,
"message": "美股智能体未启用"
}
status = _stock_agent_instance.get_status()
status["enabled"] = True
return status
except Exception as e:
logger.error(f"获取美股状态失败: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/symbols")
async def get_stock_symbols() -> Dict[str, Any]:
"""
获取当前监控的股票列表
Returns:
股票列表
"""
try:
settings = get_settings()
symbols = settings.stock_symbols.split(',') if settings.stock_symbols else []
return {
"symbols": symbols,
"count": len(symbols)
}
except Exception as e:
logger.error(f"获取股票列表失败: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/analyze/{symbol}")
async def analyze_stock(symbol: str) -> Dict[str, Any]:
"""
手动触发分析指定股票
Args:
symbol: 股票代码 'AAPL'
Returns:
分析结果
"""
try:
if _stock_agent_instance is None:
raise HTTPException(status_code=400, detail="美股智能体未启用")
# 执行单次分析
result = await _stock_agent_instance.analyze_once(symbol)
if "error" in result:
raise HTTPException(status_code=400, detail=result["error"])
return {
"success": True,
"symbol": symbol,
"result": result
}
except HTTPException:
raise
except Exception as e:
logger.error(f"分析 {symbol} 失败: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/quote/{symbol}")
async def get_stock_quote(symbol: str) -> Dict[str, Any]:
"""
获取股票实时行情
Args:
symbol: 股票代码
Returns:
行情数据
"""
try:
from app.services.yfinance_service import get_yfinance_service
yf_service = get_yfinance_service()
quote = yf_service.get_ticker(symbol.upper())
if quote is None:
raise HTTPException(status_code=404, detail=f"无法获取 {symbol} 的行情")
return {
"success": True,
"symbol": symbol.upper(),
"quote": quote
}
except HTTPException:
raise
except Exception as e:
logger.error(f"获取 {symbol} 行情失败: {e}")
raise HTTPException(status_code=500, detail=str(e))

View File

@ -137,6 +137,12 @@ class Settings(BaseSettings):
# Agent 模型配置 (可选值: zhipu, deepseek)
smart_agent_model: str = "deepseek" # SmartAgent 使用的模型
crypto_agent_model: str = "deepseek" # CryptoAgent 使用的模型
stock_agent_model: str = "deepseek" # StockAgent 使用的模型
# 美股智能体配置
stock_symbols: str = "AAPL,TSLA,NVDA,MSFT,GOOGL" # 监控的股票代码,逗号分隔
stock_analysis_interval: int = 300 # 分析间隔默认5分钟
stock_llm_threshold: float = 0.70 # 触发 LLM 分析的置信度阈值
class Config:
env_file = find_env_file()

View File

@ -9,13 +9,14 @@ from fastapi.responses import FileResponse
from contextlib import asynccontextmanager
from app.config import get_settings
from app.utils.logger import logger
from app.api import chat, stock, skills, llm, auth, admin, paper_trading
from app.api import chat, stock, skills, llm, auth, admin, paper_trading, stocks
import os
# 后台任务
_price_monitor_task = None
_report_task = None
_stock_agent_task = None
async def price_monitor_loop():
@ -147,7 +148,7 @@ async def periodic_report_loop():
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期管理"""
global _price_monitor_task, _report_task
global _price_monitor_task, _report_task, _stock_agent_task
# 启动时执行
logger.info("应用启动")
@ -161,6 +162,19 @@ async def lifespan(app: FastAPI):
_report_task = asyncio.create_task(periodic_report_loop())
logger.info("定时报告任务已创建")
# 启动美股智能体
if getattr(settings, 'stock_symbols', '') and settings.stock_symbols.strip():
try:
from app.stock_agent.stock_agent import get_stock_agent
stock_agent = get_stock_agent()
_stock_agent_task = asyncio.create_task(stock_agent.start())
# 设置智能体实例到 API 模块
stocks.set_stock_agent(stock_agent)
logger.info(f"美股智能体已启动,监控: {settings.stock_symbols}")
except Exception as e:
logger.error(f"美股智能体启动失败: {e}")
logger.error(f"提示: 请确保已安装 yfinance (pip install yfinance)")
yield
# 关闭时执行
@ -180,6 +194,15 @@ async def lifespan(app: FastAPI):
pass
logger.info("定时报告任务已停止")
# 停止美股智能体
if _stock_agent_task:
_stock_agent_task.cancel()
try:
await _stock_agent_task
except asyncio.CancelledError:
pass
logger.info("美股智能体已停止")
logger.info("应用关闭")
@ -209,6 +232,7 @@ app.include_router(stock.router, prefix="/api/stock", tags=["股票数据"])
app.include_router(skills.router, prefix="/api/skills", tags=["技能管理"])
app.include_router(llm.router, tags=["LLM模型"])
app.include_router(paper_trading.router, tags=["模拟交易"])
app.include_router(stocks.router, prefix="/api/stocks", tags=["美股分析"])
# 挂载静态文件
frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend")

View File

@ -0,0 +1,246 @@
"""
YFinance 服务 - 美股数据获取
支持获取美股的实时行情和历史 K 线数据
"""
import pandas as pd
from typing import Dict, List, Optional
from datetime import datetime, timedelta
from app.utils.logger import logger
import time
class YFinanceService:
"""YFinance 服务类"""
def __init__(self):
"""初始化服务"""
try:
import yfinance as yf
self.yf = yf
self._cache = {} # 数据缓存
self._cache_time = {} # 缓存时间
self._cache_ttl = 300 # 缓存有效期(秒)
logger.info("YFinance 服务初始化成功")
except ImportError:
logger.error("yfinance 未安装,请运行: pip install yfinance")
raise
def get_ticker(self, symbol: str) -> Optional[Dict]:
"""
获取股票实时行情
Args:
symbol: 股票代码 'AAPL'
Returns:
行情数据字典
"""
try:
ticker = self.yf.Ticker(symbol)
# 使用 history 方法获取数据(更可靠,避免 429 错误)
hist = ticker.history(period="2d", interval="1h")
if hist.empty:
logger.warning(f"无法获取 {symbol} 的历史数据")
return None
latest = hist.iloc[-1]
return {
'symbol': symbol,
'lastPrice': float(latest['Close']),
'priceChange': float(latest['Close'] - latest['Open']),
'priceChangePercent': float((latest['Close'] - latest['Open']) / latest['Open'] * 100) if latest['Open'] > 0 else 0,
'volume': int(latest['Volume']),
'high': float(latest['High']),
'low': float(latest['Low']),
'open': float(latest['Open']),
'prevClose': float(latest['Close']),
'timestamp': datetime.now().isoformat()
}
except Exception as e:
error_msg = str(e)
# 过滤掉常见的 429 错误信息
if "429" in error_msg or "Too Many Requests" in error_msg:
logger.warning(f"YFinance API 限流,请稍后再试 ({symbol})")
else:
logger.error(f"获取 {symbol} 行情失败: {error_msg}")
return None
def get_multi_timeframe_data(
self,
symbol: str,
timeframes: Optional[Dict[str, tuple]] = None
) -> Dict[str, pd.DataFrame]:
"""
获取多时间周期的 K 线数据
Args:
symbol: 股票代码
timeframes: 时间周期配置 {'1d': ('1d', '3mo'), ...}
Returns:
多时间周期数据字典 {'1d': df, '1h': df, ...}
"""
if timeframes is None:
# 默认时间周期配置
timeframes = {
'1d': ('1d', '3mo'), # 日级别3个月
'1h': ('1h', '1mo'), # 小时级别1个月
}
result = {}
for tf_name, (interval, period) in timeframes.items():
try:
df = self._get_cached_data(symbol, interval, period)
if df is not None and not df.empty:
result[tf_name] = df
logger.debug(f"获取 {symbol} {tf_name} 数据成功: {len(df)}")
else:
logger.warning(f"获取 {symbol} {tf_name} 数据失败或为空")
except Exception as e:
logger.error(f"获取 {symbol} {tf_name} 数据出错: {e}")
return result
def _get_cached_data(
self,
symbol: str,
interval: str,
period: str
) -> Optional[pd.DataFrame]:
"""获取带缓存的数据"""
cache_key = f"{symbol}_{interval}_{period}"
now = datetime.now()
# 检查缓存
if cache_key in self._cache:
cache_time = self._cache_time.get(cache_key)
if cache_time and (now - cache_time).total_seconds() < self._cache_ttl:
logger.debug(f"使用缓存数据: {cache_key}")
return self._cache[cache_key]
# 获取新数据
try:
ticker = self.yf.Ticker(symbol)
df = ticker.history(period=period, interval=interval)
if df.empty:
return None
# 转换数据格式(兼容现有代码)
df = self._format_dataframe(df)
# 更新缓存
self._cache[cache_key] = df
self._cache_time[cache_key] = now
return df
except Exception as e:
logger.error(f"获取数据失败 {cache_key}: {e}")
return None
def _format_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
"""
格式化 DataFrame 以兼容现有代码
yfinance 原始格式:
- 列名大写: Open, High, Low, Close, Volume
- 索引是 Datetime
转换后格式:
- 列名小写: open, high, low, close, volume
- 重置索引time 作为一列
- 添加技术指标
"""
df = df.copy()
# 列名转为小写
df.columns = [col.lower() for col in df.columns]
# 重置索引
df = df.reset_index()
# 重命名日期列
if 'date' in df.columns:
df = df.rename(columns={'date': 'time'})
elif 'datetime' in df.columns:
df = df.rename(columns={'datetime': 'time'})
# 删除不需要的列
cols_to_keep = ['time', 'open', 'high', 'low', 'close', 'volume']
df = df[[col for col in cols_to_keep if col in df.columns]]
# 添加技术指标(与 binance_service 一致)
df = self._add_indicators(df)
return df
def _add_indicators(self, df: pd.DataFrame) -> pd.DataFrame:
"""
添加技术指标到 DataFrame
Args:
df: 原始数据
Returns:
添加了技术指标的 DataFrame
"""
df = df.copy()
# 移动平均线
df['ma5'] = df['close'].rolling(window=5).mean()
df['ma10'] = df['close'].rolling(window=10).mean()
df['ma20'] = df['close'].rolling(window=20).mean()
df['ma50'] = df['close'].rolling(window=50).mean()
# RSI
delta = df['close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
rs = gain / loss
df['rsi'] = 100 - (100 / (1 + rs))
# MACD (使用与 binance_service 相同的计算方法)
ema_fast = df['close'].ewm(span=12, adjust=False).mean()
ema_slow = df['close'].ewm(span=26, adjust=False).mean()
df['macd'] = ema_fast - ema_slow
df['macd_signal'] = df['macd'].ewm(span=9, adjust=False).mean()
df['macd_hist'] = df['macd'] - df['macd_signal']
# ATR
high_low = df['high'] - df['low']
high_close = abs(df['high'] - df['close'].shift())
low_close = abs(df['low'] - df['close'].shift())
true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
df['atr'] = true_range.rolling(window=14).mean()
# KDJ 指标
low_min = df['low'].rolling(window=9).min()
high_max = df['high'].rolling(window=9).max()
rsv = (df['close'] - low_min) / (high_max - low_min) * 100
df['k'] = rsv.ewm(com=2, adjust=False).mean()
df['d'] = df['k'].ewm(com=2, adjust=False).mean()
df['j'] = 3 * df['k'] - 2 * df['d']
return df
def clear_cache(self):
"""清空缓存"""
self._cache.clear()
self._cache_time.clear()
logger.info("YFinance 缓存已清空")
# 全局单例
_yfinance_service: Optional[YFinanceService] = None
def get_yfinance_service() -> YFinanceService:
"""获取 YFinance 服务单例"""
global _yfinance_service
if _yfinance_service is None:
_yfinance_service = YFinanceService()
return _yfinance_service

View File

@ -0,0 +1,6 @@
"""
美股交易智能体包
"""
from app.stock_agent.stock_agent import StockAgent, get_stock_agent
__all__ = ['StockAgent', 'get_stock_agent']

View File

@ -0,0 +1,356 @@
"""
美股交易智能体 - 主控制器LLM 驱动版
只进行市场分析和通知不执行模拟交易
"""
import asyncio
from typing import Dict, Any, List, Optional
from datetime import datetime, timedelta
import pandas as pd
from app.utils.logger import logger
from app.config import get_settings
from app.services.yfinance_service import get_yfinance_service
from app.services.feishu_service import get_feishu_service
from app.services.telegram_service import get_telegram_service
from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer
class StockAgent:
"""美股交易信号智能体LLM 驱动,仅分析通知)"""
def __init__(self):
"""初始化智能体"""
self.settings = get_settings()
self.yfinance = get_yfinance_service()
self.feishu = get_feishu_service()
self.telegram = get_telegram_service()
self.llm_analyzer = LLMSignalAnalyzer()
# 状态管理
self.last_signals: Dict[str, Dict[str, Any]] = {}
self.signal_cooldown: Dict[str, datetime] = {}
# 配置
self.symbols = self.settings.stock_symbols.split(',')
# 运行状态
self.running = False
self._event_loop = None
self._task = None
logger.info(f"美股智能体初始化完成,监控股票: {self.symbols}")
async def start(self):
"""启动智能体"""
if self.running:
logger.warning("美股智能体已在运行中")
return
self.running = True
self._event_loop = asyncio.get_event_loop()
logger.info("美股智能体已启动")
# 启动分析任务
self._task = asyncio.create_task(self._analysis_loop())
async def stop(self):
"""停止智能体"""
self.running = False
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
logger.info("美股智能体已停止")
async def _analysis_loop(self):
"""分析循环 - 只在美股交易时间内运行"""
while self.running:
try:
# 检查是否在美股交易时间
if not self._is_market_hours():
# 不在交易时间,等待 10 分钟后再次检查
logger.debug("非美股交易时间,等待中...")
await asyncio.sleep(600)
continue
# 在交易时间内,分析所有股票
for symbol in self.symbols:
if not self.running:
break
await self.analyze_symbol(symbol)
# 等待 1 小时后进行下次分析
logger.info("本次分析完成,等待 1 小时后进行下次分析...")
await asyncio.sleep(3600)
except Exception as e:
logger.error(f"分析循环出错: {e}")
await asyncio.sleep(60) # 出错后等待 1 分钟再重试
def _is_market_hours(self) -> bool:
"""
判断当前是否在美股交易时间
美股交易时间: 周一至周五 9:30-16:00 (EST)
北京时间:
- 冬令时 (11-3): 22:30-05:00 (次日)
- 夏令时 (3-11): 21:30-04:00 (次日)
Returns:
是否在交易时间
"""
from datetime import datetime
# 获取当前时间
now = datetime.now()
# 检查是否为周末
if now.weekday() >= 5: # 5=周六, 6=周日
return False
# 获取当前小时和分钟
hour = now.hour
minute = now.minute
current_time = hour * 100 + minute # 转换为数字,如 2130 表示 21:30
# 判断夏令时/冬令时简单判断3-11月为夏令时
is_summer = 3 <= now.month <= 11
if is_summer:
# 夏令时: 21:30-04:00 (次日)
# 即 2130-2359 或 0000-0400
if current_time >= 2130 or current_time < 400:
return True
else:
# 冬令时: 22:30-05:00 (次日)
# 即 2230-2359 或 0000-0500
if current_time >= 2230 or current_time < 500:
return True
return False
async def analyze_symbol(self, symbol: str):
"""
分析单个股票
Args:
symbol: 股票代码
"""
try:
# 1. 获取多时间周期数据
data = self.yfinance.get_multi_timeframe_data(symbol)
# 2. 验证数据完整性
if not self._validate_data(data):
logger.warning(f"{symbol} 数据不完整,跳过本次分析")
return
# 3. 获取当前价格
ticker = self.yfinance.get_ticker(symbol)
if not ticker:
logger.warning(f"无法获取 {symbol} 当前价格")
return
current_price = ticker['lastPrice']
logger.info(f"\n{'='*60}")
logger.info(f"📊 分析 {symbol} @ ${current_price:,.2f}")
logger.info(f"{'='*60}")
# 4. LLM 分析
logger.info(f"\n🤖 【LLM 分析中...】")
result = await self.llm_analyzer.analyze(
symbol, data,
symbols=self.symbols,
position_info=None # 美股不跟踪持仓
)
# 输出分析摘要
summary = result.get('analysis_summary', '')
logger.info(f" 市场状态: {summary}")
# 输出新闻情绪
news_sentiment = result.get('news_sentiment', '')
news_impact = result.get('news_impact', '')
if news_sentiment:
sentiment_icon = {'positive': '📈', 'negative': '📉', 'neutral': ''}.get(news_sentiment, '')
logger.info(f" 新闻情绪: {sentiment_icon} {news_sentiment}")
if news_impact:
logger.info(f" 消息影响: {news_impact}")
# 输出关键价位
levels = result.get('key_levels', {})
if levels.get('support') or levels.get('resistance'):
support_str = ', '.join([f"${s:,.2f}" for s in levels.get('support', [])[:2]])
resistance_str = ', '.join([f"${r:,.2f}" for r in levels.get('resistance', [])[:2]])
logger.info(f" 支撑位: {support_str or '-'}")
logger.info(f" 阻力位: {resistance_str or '-'}")
# 5. 处理信号
signals = result.get('signals', [])
if not signals:
logger.info(f"\n⏸️ 结论: 无交易信号,继续观望")
return
# 输出所有信号
logger.info(f"\n🎯 【发现 {len(signals)} 个信号】")
for sig in signals:
signal_type = sig.get('type', 'unknown')
type_map = {'short_term': '短线', 'medium_term': '中线', 'long_term': '长线'}
type_text = type_map.get(signal_type, signal_type)
action = sig.get('action', 'wait')
action_map = {'buy': '🟢 做多', 'sell': '🔴 做空'}
action_text = action_map.get(action, action)
grade = sig.get('grade', 'D')
confidence = sig.get('confidence', 0)
grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '', 'D': ''}.get(grade, '')
logger.info(f"\n {type_text} {action_text} [{grade}{grade_icon}] {confidence}%")
# 6. 过滤并通知最佳信号
best_signal = self._get_best_signal(signals)
if not best_signal:
logger.info(f"\n⏸️ 信号质量不高,不发送通知")
return
# 检查置信度阈值
threshold = self.settings.stock_llm_threshold * 100
if best_signal.get('confidence', 0) < threshold:
logger.info(f"\n⏸️ 置信度不足 ({best_signal.get('confidence', 0)}% < {threshold}%)")
return
# 检查冷却时间
if not self._should_send_signal(symbol, best_signal):
logger.info(f"\n⏸️ 信号冷却中,不发送通知")
return
# 发送通知
await self._send_signal_notification(symbol, best_signal, current_price)
# 更新状态
self.last_signals[symbol] = best_signal
self.signal_cooldown[symbol] = datetime.now()
except Exception as e:
logger.error(f"❌ 分析 {symbol} 出错: {e}")
import traceback
logger.error(traceback.format_exc())
def _get_best_signal(self, signals: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
"""获取最佳信号"""
# 过滤掉 D 级信号
valid_signals = [s for s in signals if s.get('grade', 'D') != 'D']
if not valid_signals:
return None
# 按等级和置信度排序
grade_order = {'A': 0, 'B': 1, 'C': 2}
valid_signals.sort(key=lambda x: (
grade_order.get(x.get('grade', 'C'), 3),
-x.get('confidence', 0)
))
return valid_signals[0]
def _should_send_signal(self, symbol: str, signal: Dict[str, Any]) -> bool:
"""判断是否应该发送信号"""
action = signal.get('action', 'wait')
if action == 'wait':
return False
# 检查冷却时间60分钟内不重复发送相同方向的信号
if symbol in self.signal_cooldown:
cooldown_end = self.signal_cooldown[symbol] + timedelta(minutes=60)
if datetime.now() < cooldown_end:
if symbol in self.last_signals:
if self.last_signals[symbol].get('action') == action:
logger.debug(f"{symbol} 信号冷却中,跳过")
return False
return True
async def _send_signal_notification(
self,
symbol: str,
signal: Dict[str, Any],
current_price: float
):
"""发送信号通知"""
try:
# 使用正确的方法格式化信号
card = self.llm_analyzer.format_feishu_card(signal, symbol)
title = card['title']
content = card['content']
# 发送到飞书
await self.feishu.send_markdown(title, content)
# 发送到 Telegram
await self.telegram.send_message(self.llm_analyzer.format_signal_message(signal, symbol))
logger.info(f"✅ 信号通知已发送: {title}")
except Exception as e:
logger.error(f"发送通知失败: {e}")
def _validate_data(self, data: Dict[str, pd.DataFrame]) -> bool:
"""验证数据完整性"""
required_intervals = ['1d', '1h']
for interval in required_intervals:
if interval not in data or data[interval].empty:
return False
if len(data[interval]) < 20:
return False
return True
async def analyze_once(self, symbol: str) -> Dict[str, Any]:
"""单次分析(用于测试或手动触发)"""
data = self.yfinance.get_multi_timeframe_data(symbol)
if not self._validate_data(data):
return {'error': '数据不完整'}
result = await self.llm_analyzer.analyze(
symbol, data,
symbols=self.symbols,
position_info=None
)
return result
def get_status(self) -> Dict[str, Any]:
"""获取智能体状态"""
return {
'running': self.running,
'symbols': self.symbols,
'mode': 'LLM 驱动(仅分析通知)',
'last_signals': {
symbol: {
'type': sig.get('type'),
'action': sig.get('action'),
'confidence': sig.get('confidence'),
'grade': sig.get('grade')
}
for symbol, sig in self.last_signals.items()
}
}
# 全局单例
_stock_agent: Optional[StockAgent] = None
def get_stock_agent() -> StockAgent:
"""获取美股智能体单例"""
global _stock_agent
if _stock_agent is None:
_stock_agent = StockAgent()
return _stock_agent

134
scripts/README.md Normal file
View File

@ -0,0 +1,134 @@
# 美股手动分析脚本使用说明
## 📜 脚本列表
| 脚本 | 说明 | 推荐度 |
|------|------|--------|
| `scripts/stock.sh` | 快捷分析脚本(推荐) | ⭐⭐⭐ |
| `scripts/analyze_stock_simple.py` | Python 简化版 | ⭐⭐ |
| `scripts/analyze_stock.py` | Python 完整版 | ⭐ |
## 🚀 快速开始
### 方式 1: 使用快捷脚本(推荐)
```bash
# 分析单只股票
./scripts/stock.sh AAPL
# 分析多只股票
./scripts/stock.sh AAPL TSLA NVDA
# 分析配置的所有股票
./scripts/stock.sh
```
### 方式 2: 使用 Python 脚本
```bash
# 简化版
cd backend
python3 ../scripts/analyze_stock_simple.py AAPL
python3 ../scripts/analyze_stock_simple.py AAPL TSLA NVDA
# 完整版
python3 ../scripts/analyze_stock.py AAPL
python3 ../scripts/analyze_stock.py AAPL TSLA NVDA
```
## 📋 输出示例
```
============================================================
📊 分析 AAPL
============================================================
价格: $178.50 (+1.25%)
K线: ['1d', '1h']
🤖 LLM分析中...
状态: 震荡上涨MACD 金叉形成
🎯 发现 1 个信号:
🟢 做多 [A⭐⭐⭐] 85%
入场: $178.50
止损: $172.80
止盈: $205.28
理由: 突破 MA20 阻力RSI=58 进入强势区...
✅ 完成
```
## ⚙️ 配置说明
`.env` 文件中配置默认股票列表:
```bash
# 美股智能体配置
STOCK_SYMBOLS=AAPL,TSLA,NVDA,MSFT,GOOGL
```
## 📝 完整版功能
完整版脚本 (`analyze_stock.py`) 提供更多详细信息:
- ✅ 当前行情详情
- ✅ K 线数据统计
- ✅ 新闻情绪分析
- ✅ 关键支撑/阻力位
- ✅ 详细信号列表
- ✅ 风险提示
- ✅ 通知预览
## 🔧 依赖要求
确保已安装 yfinance
```bash
cd backend
pip install yfinance
```
## 💡 提示
1. **首次使用**:建议先运行一次 `./scripts/stock.sh` 测试
2. **分析频率**:不要频繁分析,避免 API 限流
3. **最佳时间**:在美股交易时间内分析效果最佳
4. **股票代码**:使用美股代码,如 AAPL, TSLA, NVDA 等
## 📊 支持的股票
任何美股代码都可以分析,常见示例:
- 科技股: AAPL, MSFT, GOOGL, META, AMZN, NVDA, TSLA
- 金融股: JPM, BAC, GS, MS
- 医疗股: JNJ, PFE, UNH, ABT
- 消费股: NKE, KO, MCD, SBUX
## ⚠️ 注意事项
1. 数据来源YFinanceYahoo Finance
2. 数据延迟:约 15 分钟
3. API 限制:过于频繁的请求可能被限流
4. 市场时间:美股交易时间分析效果最佳
## 🆘 故障排除
### 错误ModuleNotFoundError: No module named 'yfinance'
```bash
cd backend
pip install yfinance
```
### 错误:无法获取行情
- 检查股票代码是否正确
- 检查网络连接
- 稍后重试
### 错误LLM 分析失败
- 检查 API 密钥配置
- 检查 DeepSeek/Zhipu API 是否可用

198
scripts/STOCK_USAGE.md Normal file
View File

@ -0,0 +1,198 @@
# 美股分析脚本使用说明
## 📊 当前股票池配置
### 平衡型配置20 只股票)
**配置策略:科技龙头 30% + AI/半导体 30% + 生物医疗 20% + 新能源 10% + 金融 10%**
```
# 科技龙头(核心 6 只)
AAPL - 苹果(消费电子生态)
MSFT - 微软(云计算 + AI
GOOGL - 谷歌(搜索 + 云计算)
META - 元平台(社交 + 元宇宙)
AMZN - 亚马逊(电商 + 云服务)
NVDA - 英伟达AI 芯片龙头)
# AI/半导体(成长 6 只)
AMD - 高性能计算芯片
AVGO - 博通(半导体 + 软件)
ARM - ARM 架构AI 芯片)
PLTR - PalantirAI 数据分析)
SNOW - Snowflake云端数据仓库
# 生物医药(创新 3 只)
LLY - 礼来GLP-1 减肥药)
NVO - 诺和诺德(糖尿病药物)
VRTX - 福泰制药(罕见病)
# 新能源(趋势 2 只)
TSLA - 特斯拉(电动车)
ENPH - Enphase太阳能微逆
# 金融(防御 2 只)
V - Visa支付网络
MA - 万事达卡
# 消费品牌(稳定 2 只)
HD - 家得宝
COST - 好市多
```
### 配置特点
- ✅ 覆盖 6 大行业板块
- ✅ 包含 9 只优质大盘股
- ✅ 包含 6 只高成长股
- ✅ 包含 5 只创新潜力股
- ✅ 兼顾收益与风险
- ✅ 分析间隔1 小时(美股交易时间内)
## ⚠️ 当前问题
### 1. YFinance API 限流
```
429 Client Error: Too Many Requests
```
**原因**: YFinance API 有请求频率限制
**解决方案**:
- 等待几分钟后重试
- 减少同时分析的股票数量
- 使用缓存的数据
### 2. 脚本参数解析问题
**症状**: 脚本把 `CONFIG`、`找到.ENV文件` 等当成股票代码
**原因**: Shell 脚本参数传递错误
**解决方案**: 使用新的测试脚本
## ✅ 正确的使用方式
### 方式 1: 直接使用 Python 脚本(推荐)
```bash
# 在项目根目录运行
python3 scripts/test_stock.py AAPL
python3 scripts/test_stock.py AAPL TSLA
python3 scripts/test_stock.py AAPL TSLA NVDA
```
### 方式 2: 通过 API
```bash
# 启动服务后
curl http://localhost:8000/api/stocks/quote/AAPL
# 手动触发分析
curl -X POST http://localhost:8000/api/stocks/analyze/AAPL
```
### 方式 3: 等待自动分析
启动服务后StockAgent 会在美股交易时间内自动分析:
- 夏令时: 21:30 - 04:00 (北京时间)
- 冬令时: 22:30 - 05:00 (北京时间)
## 🔧 故障排除
### 问题 1: ModuleNotFoundError: No module named 'yfinance'
```bash
cd backend
pip install yfinance
```
### 问题 2: 429 Too Many Requests
**原因**: API 限流
**解决**:
```bash
# 等待 5-10 分钟后重试
# 或者使用 API 端点(有缓存)
curl http://localhost:8000/api/stocks/quote/AAPL
```
### 问题 3: Expecting value: line 1 column 1 (char 0)
**原因**: YFinance 返回了错误页面而非数据
**解决**: 通常与 429 错误相关,等待后重试
## 📋 脚本对比
| 脚本 | 状态 | 说明 |
|------|------|------|
| `scripts/stock.sh` | ⚠️ | Shell 脚本,参数解析有问题 |
| `scripts/analyze_stock_simple.py` | ⚠️ | 路径问题 |
| `scripts/analyze_stock.py` | ⚠️ | 路径问题 |
| `scripts/test_stock.py` | ✅ | **推荐使用** |
## 🚀 推荐命令
```bash
# 分析单只股票(最简单)
python3 scripts/test_stock.py AAPL
# 分析多只股票
python3 scripts/test_stock.py AAPL TSLA NVDA
# 或者使用 API需要服务运行中
curl -X POST http://localhost:8000/api/stocks/analyze/AAPL
```
## 💡 提示
1. **避开高峰期**: 美股开盘前后 (21:30-22:30) API 请求较多
2. **减少并发**: 不要同时分析太多股票
3. **使用缓存**: 通过 API 调用可以使用缓存数据
4. **等待重试**: 遇到 429 错误等待几分钟后重试
## 🔄 工作流程
```
┌─────────────────────────────────────────────────────────────┐
│ 美股分析流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 获取行情数据 (YFinance) │
│ ↓ │
│ 2. 获取 K 线数据 (多时间周期) │
│ ↓ │
│ 3. LLM 技术分析 │
│ ↓ │
│ 4. 生成交易信号 │
│ ↓ │
│ 5. 发送通知 (飞书 + Telegram) │
│ │
└─────────────────────────────────────────────────────────────┘
```
## 📊 输出格式
```
============================================================
📊 分析 AAPL
============================================================
价格: $178.50 (+1.25%)
成交量: 52,345,600
🤖 LLM分析中...
市场状态: 震荡上涨MACD 金叉形成
🎯 发现 1 个信号:
🟢 做多 [A⭐⭐⭐] 85%
入场: $178.50
止损: $172.80
止盈: $205.28
理由: 突破 MA20 阻力RSI=58 进入强势区...
✅ 分析完成
```

213
scripts/analyze_stock.py Executable file
View File

@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""
美股手动分析脚本
用法:
python3 scripts/analyze_stock.py AAPL # 分析单只股票
python3 scripts/analyze_stock.py AAPL TSLA # 分析多只股票
python3 scripts/analyze_stock.py # 分析配置的所有股票
环境:
需要在 backend 目录下运行或设置 PYTHONPATH
"""
import sys
import os
import asyncio
from datetime import datetime
# 添加项目路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'backend'))
from app.services.yfinance_service import get_yfinance_service
from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer
from app.config import get_settings
from app.utils.logger import logger
async def analyze_stock(symbol: str):
"""
分析单只股票
Args:
symbol: 股票代码
"""
print("\n" + "=" * 80)
print(f"📊 分析 {symbol}")
print("=" * 80)
try:
# 1. 获取服务
yf_service = get_yfinance_service()
llm_analyzer = LLMSignalAnalyzer()
settings = get_settings()
# 2. 获取当前行情
print(f"\n📈 获取当前行情...")
ticker = yf_service.get_ticker(symbol)
if not ticker:
print(f"❌ 无法获取 {symbol} 的行情数据")
return
current_price = ticker['lastPrice']
price_change = ticker['priceChange']
price_change_percent = ticker['priceChangePercent']
print(f" 最新价格: ${current_price:,.2f}")
print(f" 涨跌额: ${price_change:+,.2f}")
print(f" 涨跌幅: {price_change_percent:+.2f}%")
print(f" 成交量: {ticker['volume']:,}")
# 3. 获取 K 线数据
print(f"\n📊 获取 K 线数据...")
data = yf_service.get_multi_timeframe_data(symbol)
if not data:
print(f"❌ 无法获取 {symbol} 的 K 线数据")
return
for tf, df in data.items():
print(f" {tf}: {len(df)} 条数据")
# 4. LLM 分析
print(f"\n🤖 LLM 分析中...")
print("-" * 80)
result = await llm_analyzer.analyze(
symbol,
data,
symbols=[symbol],
position_info=None
)
# 5. 输出分析结果
print("\n📋 分析结果:")
print("-" * 80)
# 市场状态
summary = result.get('analysis_summary', '')
print(f"市场状态: {summary}")
# 新闻情绪
news_sentiment = result.get('news_sentiment', '')
if news_sentiment:
sentiment_map = {'positive': '📈 积极', 'negative': '📉 消极', 'neutral': ' 中性'}
print(f"新闻情绪: {sentiment_map.get(news_sentiment, news_sentiment)}")
news_impact = result.get('news_impact', '')
if news_impact:
print(f"消息影响: {news_impact}")
# 关键价位
levels = result.get('key_levels', {})
if levels.get('support') or levels.get('resistance'):
print(f"\n关键价位:")
if levels.get('support'):
support_str = ', '.join([f"${s:,.2f}" for s in levels.get('support', [])[:3]])
print(f" 支撑位: {support_str}")
if levels.get('resistance'):
resistance_str = ', '.join([f"${r:,.2f}" for r in levels.get('resistance', [])[:3]])
print(f" 阻力位: {resistance_str}")
# 信号
signals = result.get('signals', [])
if not signals:
print(f"\n⏸️ 结论: 无交易信号,继续观望")
else:
print(f"\n🎯 发现 {len(signals)} 个信号:")
print("-" * 80)
for sig in signals:
signal_type = sig.get('type', 'unknown')
type_map = {'short_term': '短线', 'medium_term': '中线', 'long_term': '长线'}
type_text = type_map.get(signal_type, signal_type)
action = sig.get('action', 'wait')
action_map = {'buy': '🟢 做多', 'sell': '🔴 做空', 'wait': '⏸️ 观望'}
action_text = action_map.get(action, action)
grade = sig.get('grade', 'D')
confidence = sig.get('confidence', 0)
grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '', 'D': ''}.get(grade, '')
print(f"\n{type_text} {action_text} [{grade}{grade_icon}] {confidence}%")
entry = sig.get('entry_price', 0)
sl = sig.get('stop_loss', 0)
tp = sig.get('take_profit', 0)
if entry and sl and tp:
print(f" 入场: ${entry:,.2f}")
print(f" 止损: ${sl:,.2f} ({((sl - entry) / entry * 100):+.1f}%)")
print(f" 止盈: ${tp:,.2f} ({((tp - entry) / entry * 100):+.1f}%)")
reason = sig.get('reason', '')
if reason:
print(f" 理由: {reason}")
risk_warning = sig.get('risk_warning', '')
if risk_warning:
print(f" 风险提示: {risk_warning}")
# 6. 格式化通知
if signals:
print("\n" + "=" * 80)
print("📱 通知预览:")
print("-" * 80)
for sig in signals:
if sig.get('grade', 'D') != 'D':
formatted = llm_analyzer.format_signal_for_notification(symbol, sig)
print(f"\n{formatted['title']}")
print(formatted['content'])
except Exception as e:
print(f"\n❌ 分析 {symbol} 失败: {e}")
import traceback
traceback.print_exc()
async def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description='美股手动分析脚本')
parser.add_argument('symbols', nargs='*', help='股票代码(留空则分析配置的所有股票)')
args = parser.parse_args()
# 确定要分析的股票
if args.symbols:
symbols = [s.upper() for s in args.symbols]
else:
settings = get_settings()
symbols = settings.stock_symbols.split(',') if settings.stock_symbols else []
if not symbols or not symbols[0]:
print("❌ 未指定股票代码,且配置文件中未配置 STOCK_SYMBOLS")
print("\n用法:")
print(" python3 scripts/analyze_stock.py AAPL")
print(" python3 scripts/analyze_stock.py AAPL TSLA NVDA")
print(" python3 scripts/analyze_stock.py # 分析配置的所有股票")
sys.exit(1)
print("\n" + "=" * 80)
print("🤖 美股手动分析脚本")
print("=" * 80)
print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"股票: {', '.join(symbols)}")
print("=" * 80)
# 逐个分析
for symbol in symbols:
await analyze_stock(symbol)
print("\n" + "=" * 80)
print("✅ 分析完成!")
print("=" * 80)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n\n⚠️ 用户中断")
sys.exit(0)

View File

@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""
美股快速分析脚本简化版
用法:
cd backend && python3 ../scripts/analyze_stock_simple.py AAPL
cd backend && python3 ../scripts/analyze_stock_simple.py AAPL TSLA NVDA
"""
import sys
import os
import asyncio
# 添加项目路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'backend'))
from app.services.yfinance_service import get_yfinance_service
from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer
from app.utils.logger import logger
async def analyze(symbol: str):
"""分析单只股票"""
print(f"\n{'='*60}")
print(f"📊 分析 {symbol}")
print(f"{'='*60}")
try:
# 获取服务和数据
yf_service = get_yfinance_service()
llm = LLMSignalAnalyzer()
# 获取行情
ticker = yf_service.get_ticker(symbol)
if not ticker:
print(f"❌ 无法获取行情")
return
price = ticker['lastPrice']
change = ticker['priceChangePercent']
print(f"价格: ${price:,.2f} ({change:+.2f}%)")
# 获取K线数据
data = yf_service.get_multi_timeframe_data(symbol)
print(f"K线: {list(data.keys())}")
# LLM分析
print(f"\n🤖 LLM分析中...")
result = await llm.analyze(symbol, data, symbols=[symbol], position_info=None)
# 输出结果
summary = result.get('analysis_summary', '')
signals = result.get('signals', [])
print(f"\n状态: {summary}")
if signals:
print(f"\n🎯 发现 {len(signals)} 个信号:")
for sig in signals:
action = sig.get('action', 'wait')
grade = sig.get('grade', 'D')
conf = sig.get('confidence', 0)
action_text = {'buy': '🟢 做多', 'sell': '🔴 做空'}.get(action, action)
grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': ''}.get(grade, '')
print(f"\n {action_text} [{grade}{grade_icon}] {conf}%")
entry = sig.get('entry_price')
sl = sig.get('stop_loss')
tp = sig.get('take_profit')
if entry and sl and tp:
print(f" 入场: ${entry:,.2f}")
print(f" 止损: ${sl:,.2f}")
print(f" 止盈: ${tp:,.2f}")
reason = sig.get('reason', '')
if reason:
print(f" 理由: {reason[:100]}...")
else:
print(f"\n⏸️ 无交易信号")
except Exception as e:
print(f"❌ 错误: {e}")
async def main():
symbols = sys.argv[1:] if len(sys.argv) > 1 else ['AAPL']
print(f"🤖 美股快速分析")
print(f"股票: {', '.join(symbols)}")
for symbol in symbols:
await analyze(symbol.upper())
print(f"\n✅ 完成")
if __name__ == "__main__":
asyncio.run(main())

27
scripts/stock.sh Executable file
View File

@ -0,0 +1,27 @@
#!/bin/bash
# 美股分析快捷脚本
#
# 用法:
# ./scripts/stock.sh AAPL
# ./scripts/stock.sh AAPL TSLA
# ./scripts/stock.sh # 分析配置的所有股票(会发送通知)
cd "$(dirname "$0")/.." || exit 1
if [ $# -eq 0 ]; then
# 无参数,分析配置的所有股票
echo "📊 分析配置的所有股票(将发送通知)..."
python3 -c "
import sys
sys.path.insert(0, 'backend')
from app.config import get_settings
settings = get_settings()
symbols = settings.stock_symbols.split(',')
print(' '.join(symbols))
" 2>/dev/null | xargs python3 scripts/test_stock.py
else
# 分析指定的股票
SYMBOLS="$@"
echo "📊 分析股票: $SYMBOLS(将发送通知)"
python3 scripts/test_stock.py $SYMBOLS
fi

161
scripts/test_stock.py Executable file
View File

@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""
美股分析脚本修复版
用法:
python3 scripts/test_stock.py AAPL
python3 scripts/test_stock.py AAPL TSLA NVDA
"""
import sys
import os
# 确保路径正确
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(script_dir)
backend_dir = os.path.join(project_root, 'backend')
sys.path.insert(0, backend_dir)
import asyncio
from app.services.yfinance_service import get_yfinance_service
from app.services.feishu_service import get_feishu_service
from app.services.telegram_service import get_telegram_service
from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer
from app.utils.logger import logger
async def analyze(symbol: str, send_notification: bool = True):
"""分析单只股票
Args:
symbol: 股票代码
send_notification: 是否发送通知默认True
"""
print(f"\n{'='*60}")
print(f"📊 分析 {symbol}")
print(f"{'='*60}")
try:
# 获取服务
yf_service = get_yfinance_service()
llm = LLMSignalAnalyzer()
feishu = get_feishu_service()
telegram = get_telegram_service()
# 获取行情
print(f"获取行情...")
ticker = yf_service.get_ticker(symbol)
if not ticker:
print(f"❌ 无法获取 {symbol} 行情")
return
price = ticker['lastPrice']
change = ticker['priceChangePercent']
print(f"价格: ${price:,.2f} ({change:+.2f}%)")
print(f"成交量: {ticker['volume']:,}")
# 获取K线
print(f"获取K线数据...")
data = yf_service.get_multi_timeframe_data(symbol)
if not data:
print(f"❌ 无法获取K线数据")
return
print(f"时间周期: {', '.join(data.keys())}")
# LLM分析
print(f"\n🤖 LLM分析中...\n")
result = await llm.analyze(symbol, data, symbols=[symbol], position_info=None)
# 输出结果
summary = result.get('analysis_summary', '')
signals = result.get('signals', [])
print(f"市场状态: {summary}")
if signals:
print(f"\n🎯 发现 {len(signals)} 个信号:")
for sig in signals:
action = sig.get('action', 'wait')
grade = sig.get('grade', 'D')
conf = sig.get('confidence', 0)
action_text = {'buy': '🟢 做多', 'sell': '🔴 做空'}.get(action, action)
grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': ''}.get(grade, '')
print(f"\n{action_text} [{grade}{grade_icon}] {conf}%")
entry = sig.get('entry_price')
sl = sig.get('stop_loss')
tp = sig.get('take_profit')
if entry and sl and tp:
print(f" 入场: ${entry:,.2f}")
print(f" 止损: ${sl:,.2f}")
print(f" 止盈: ${tp:,.2f}")
reason = sig.get('reason', '')
if reason:
# 限制理由长度
short_reason = reason[:80] + "..." if len(reason) > 80 else reason
print(f" 理由: {short_reason}")
# 发送通知(仅发送置信度 >= 60% 的信号)
if send_notification:
best_signal = None
for sig in signals:
if sig.get('confidence', 0) >= 60 and sig.get('grade', 'D') != 'D':
best_signal = sig
break
if best_signal:
# 使用正确的方法格式化通知
card = llm.format_feishu_card(best_signal, symbol)
title = card['title']
content = card['content']
# 发送通知
await feishu.send_markdown(title, content)
await telegram.send_message(llm.format_signal_message(best_signal, symbol))
print(f"\n📬 通知已发送:{title}")
else:
print(f"\n⏸️ 置信度不足,不发送通知")
else:
print(f"\n⏸️ 无交易信号")
except Exception as e:
print(f"❌ 错误: {e}")
import traceback
traceback.print_exc()
async def main():
# 从命令行参数获取股票代码
if len(sys.argv) < 2:
print("用法: python3 scripts/test_stock.py AAPL [TSLA] [NVDA] ...")
print("\n示例:")
print(" python3 scripts/test_stock.py AAPL")
print(" python3 scripts/test_stock.py AAPL TSLA NVDA")
sys.exit(1)
symbols = sys.argv[1:]
print("="*60)
print("🤖 美股分析脚本")
print("="*60)
print(f"股票: {', '.join(symbols)}")
print(f"时间: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
for symbol in symbols:
await analyze(symbol.upper())
print("\n" + "="*60)
print("✅ 分析完成")
print("="*60)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n\n⚠️ 用户中断")