update
This commit is contained in:
parent
ae743f9d39
commit
b61b9a0f0b
12
.env.example
12
.env.example
@ -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
131
backend/app/api/stocks.py
Normal 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))
|
||||
@ -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()
|
||||
|
||||
@ -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")
|
||||
|
||||
246
backend/app/services/yfinance_service.py
Normal file
246
backend/app/services/yfinance_service.py
Normal 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
|
||||
6
backend/app/stock_agent/__init__.py
Normal file
6
backend/app/stock_agent/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""
|
||||
美股交易智能体包
|
||||
"""
|
||||
from app.stock_agent.stock_agent import StockAgent, get_stock_agent
|
||||
|
||||
__all__ = ['StockAgent', 'get_stock_agent']
|
||||
356
backend/app/stock_agent/stock_agent.py
Normal file
356
backend/app/stock_agent/stock_agent.py
Normal 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
134
scripts/README.md
Normal 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. 数据来源:YFinance(Yahoo 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
198
scripts/STOCK_USAGE.md
Normal 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 - Palantir(AI 数据分析)
|
||||
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
213
scripts/analyze_stock.py
Executable 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)
|
||||
100
scripts/analyze_stock_simple.py
Normal file
100
scripts/analyze_stock_simple.py
Normal 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
27
scripts/stock.sh
Executable 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
161
scripts/test_stock.py
Executable 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⚠️ 用户中断")
|
||||
Loading…
Reference in New Issue
Block a user