stock-ai-agent/backend/app/crypto_agent/crypto_agent.py
2026-02-06 23:11:06 +08:00

476 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
加密货币交易智能体 - 主控制器
"""
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.binance_service import binance_service
from app.services.feishu_service import get_feishu_service
from app.services.telegram_service import get_telegram_service
from app.services.paper_trading_service import get_paper_trading_service
from app.services.price_monitor_service import get_price_monitor_service
from app.crypto_agent.signal_analyzer import SignalAnalyzer
from app.crypto_agent.strategy import TrendFollowingStrategy
class CryptoAgent:
"""加密货币交易信号智能体"""
def __init__(self):
"""初始化智能体"""
self.settings = get_settings()
self.binance = binance_service
self.feishu = get_feishu_service()
self.telegram = get_telegram_service()
self.analyzer = SignalAnalyzer()
self.strategy = TrendFollowingStrategy()
# 模拟交易服务
self.paper_trading_enabled = self.settings.paper_trading_enabled
if self.paper_trading_enabled:
self.paper_trading = get_paper_trading_service()
self.price_monitor = get_price_monitor_service()
# 注册价格回调
self.price_monitor.add_price_callback(self._on_price_update)
else:
self.paper_trading = None
self.price_monitor = None
# 状态管理
self.last_signals: Dict[str, Dict[str, Any]] = {} # 上次信号
self.last_trends: Dict[str, str] = {} # 上次趋势
self.signal_cooldown: Dict[str, datetime] = {} # 信号冷却时间
# 配置
self.symbols = self.settings.crypto_symbols.split(',')
self.analysis_interval = self.settings.crypto_analysis_interval
self.llm_threshold = self.settings.crypto_llm_threshold
# 运行状态
self.running = False
logger.info(f"加密货币智能体初始化完成,监控交易对: {self.symbols}")
if self.paper_trading_enabled:
logger.info(f"模拟交易已启用")
def _on_price_update(self, symbol: str, price: float):
"""处理实时价格更新(用于模拟交易)"""
if not self.paper_trading:
return
# 检查是否有订单触发止盈止损
triggered = self.paper_trading.check_price_triggers(symbol, price)
for result in triggered:
# 异步发送平仓通知
asyncio.create_task(self._notify_order_closed(result))
async def _notify_order_closed(self, result: Dict[str, Any]):
"""发送订单平仓通知"""
is_win = result.get('is_win', False)
status = result.get('status', '')
# 确定图标和文本
if status == 'closed_tp':
emoji = "🎯"
status_text = "止盈平仓"
elif status == 'closed_sl':
emoji = "🛑"
status_text = "止损平仓"
else:
emoji = "📤"
status_text = "手动平仓"
win_text = "盈利" if is_win else "亏损"
side_text = "做多" if result.get('side') == 'long' else "做空"
# 构建消息
message = f"""{emoji} 订单{status_text}
交易对: {result.get('symbol')}
方向: {side_text}
入场: ${result.get('entry_price', 0):,.2f}
出场: ${result.get('exit_price', 0):,.2f}
{win_text}: {result.get('pnl_percent', 0):+.2f}% (${result.get('pnl_amount', 0):+.2f})
持仓时间: {result.get('hold_duration', 'N/A')}"""
# 发送通知
await self.feishu.send_text(message)
await self.telegram.send_message(message)
logger.info(f"已发送订单平仓通知: {result.get('order_id')}")
def _get_seconds_until_next_5min(self) -> int:
"""计算距离下一个5分钟整点的秒数"""
now = datetime.now()
current_minute = now.minute
current_second = now.second
# 计算下一个5的倍数分钟
minutes_past = current_minute % 5
if minutes_past == 0 and current_second == 0:
# 刚好在整点,立即执行
return 0
minutes_to_wait = 5 - minutes_past if minutes_past > 0 else 5
seconds_to_wait = minutes_to_wait * 60 - current_second
return seconds_to_wait
async def run(self):
"""主运行循环 - 在5的倍数分钟执行"""
self.running = True
# 启动横幅
logger.info("\n" + "=" * 60)
logger.info("🚀 加密货币交易信号智能体")
logger.info("=" * 60)
logger.info(f" 监控交易对: {', '.join(self.symbols)}")
logger.info(f" 运行模式: 每5分钟整点执行 (:00, :05, :10, ...)")
logger.info(f" LLM阈值: {self.llm_threshold * 100:.0f}%")
if self.paper_trading_enabled:
logger.info(f" 模拟交易: 已启用")
logger.info("=" * 60 + "\n")
# 启动价格监控(用于模拟交易)
if self.paper_trading_enabled and self.price_monitor:
for symbol in self.symbols:
self.price_monitor.subscribe_symbol(symbol)
logger.info(f"已启动 WebSocket 价格监控: {', '.join(self.symbols)}")
# 发送启动通知(飞书 + Telegram
await self.feishu.send_text(
f"🚀 加密货币智能体已启动\n"
f"监控交易对: {', '.join(self.symbols)}\n"
f"运行时间: 每5分钟整点 (:00, :05, :10, ...)"
)
await self.telegram.send_startup_notification(self.symbols)
while self.running:
try:
# 等待到下一个5分钟整点
wait_seconds = self._get_seconds_until_next_5min()
if wait_seconds > 0:
next_run = datetime.now() + timedelta(seconds=wait_seconds)
logger.info(f"⏳ 等待 {wait_seconds} 秒,下次运行: {next_run.strftime('%H:%M:%S')}")
await asyncio.sleep(wait_seconds)
# 执行分析
run_time = datetime.now()
logger.info("\n" + "=" * 60)
logger.info(f"⏰ 定时任务执行 [{run_time.strftime('%Y-%m-%d %H:%M:%S')}]")
logger.info("=" * 60)
for symbol in self.symbols:
await self.analyze_symbol(symbol)
logger.info("\n" + "" * 60)
logger.info(f"✅ 本轮分析完成,共分析 {len(self.symbols)} 个交易对")
logger.info("" * 60 + "\n")
# 等待几秒确保不会在同一分钟内重复执行
await asyncio.sleep(2)
except Exception as e:
logger.error(f"❌ 分析循环出错: {e}")
await asyncio.sleep(10) # 出错后等待10秒再继续
def stop(self):
"""停止运行"""
self.running = False
# 停止价格监控
if self.price_monitor:
self.price_monitor.stop()
logger.info("加密货币智能体已停止")
async def analyze_symbol(self, symbol: str):
"""
分析单个交易对
Args:
symbol: 交易对,如 'BTCUSDT'
"""
try:
# 分隔线
logger.info(f"\n{'' * 50}")
logger.info(f"📊 {symbol} 分析开始")
logger.info(f"{'' * 50}")
# 1. 获取多周期数据
data = self.binance.get_multi_timeframe_data(symbol)
if not self._validate_data(data):
logger.warning(f"⚠️ {symbol} 数据不完整,跳过分析")
return
# 获取当前价格
current_price = float(data['5m'].iloc[-1]['close'])
price_change_24h = self._calculate_price_change(data['1h'])
logger.info(f"💰 当前价格: ${current_price:,.2f} ({price_change_24h})")
# 2. 分析趋势1H + 4H- 返回详细趋势信息
logger.info(f"\n📈 【趋势分析】")
trend = self.analyzer.analyze_trend(data['1h'], data['4h'])
trend_direction = trend.get('direction', 'neutral') if isinstance(trend, dict) else trend
trend_strength = trend.get('strength', 'unknown') if isinstance(trend, dict) else 'unknown'
trend_phase = trend.get('phase', 'unknown') if isinstance(trend, dict) else 'unknown'
# 趋势方向图标
trend_icon = {'bullish': '🟢 看涨', 'bearish': '🔴 看跌', 'neutral': '⚪ 震荡'}.get(trend_direction, '')
phase_text = {'impulse': '主升/主跌浪', 'correction': '回调/反弹', 'oversold': '极度超卖',
'overbought': '极度超买', 'sideways': '横盘'}.get(trend_phase, trend_phase)
logger.info(f" 方向: {trend_icon} | 强度: {trend_strength} | 阶段: {phase_text}")
# 3. 检查趋势变化(只有之前有记录时才检查,避免启动时误报)
if symbol in self.last_trends:
last_trend = self.last_trends[symbol]
if isinstance(last_trend, dict):
last_direction = last_trend.get('direction', 'neutral')
else:
last_direction = last_trend
if last_direction != trend_direction:
await self._handle_trend_change(symbol, last_direction, trend_direction, data)
self.last_trends[symbol] = trend
# 4. 分析进场信号15M 为主5M 辅助,传入 1H 数据用于支撑阻力位)
logger.info(f"\n🎯 【信号分析】")
signal = self.analyzer.analyze_entry_signal(data['5m'], data['15m'], trend, data['1h'])
signal['symbol'] = symbol
signal['trend'] = trend_direction
signal['trend_info'] = trend if isinstance(trend, dict) else {'direction': trend}
signal['price'] = current_price
signal['timestamp'] = datetime.now()
# 输出信号详情
action_icon = {'buy': '🟢 买入', 'sell': '🔴 卖出', 'hold': '⏸️ 观望'}.get(signal['action'], '')
grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '', 'D': ''}.get(signal.get('signal_grade', 'D'), '')
signal_type = signal.get('signal_type', 'swing')
type_text = '📈短线' if signal_type == 'short_term' else '📊波段'
logger.info(f" 信号: {action_icon} | 类型: {type_text} | 置信度: {signal['confidence']}% | 等级: {signal.get('signal_grade', 'D')} {grade_icon}")
# 输出触发原因
if signal.get('reasons'):
logger.info(f" 原因: {', '.join(signal['reasons'][:5])}") # 最多显示5个原因
# 输出权重详情
weights = signal.get('signal_weights', {})
if weights:
logger.info(f" 权重: 买入={weights.get('buy', 0):.1f} | 卖出={weights.get('sell', 0):.1f}")
# 输出K线形态
patterns = signal.get('patterns', {})
if patterns.get('bullish_patterns') or patterns.get('bearish_patterns'):
all_patterns = patterns.get('bullish_patterns', []) + patterns.get('bearish_patterns', [])
logger.info(f" 形态: {', '.join(all_patterns)}")
# 输出成交量分析
vol = signal.get('volume_analysis', {})
if vol:
vol_icon = {'high': '📈', 'low': '📉', 'normal': ''}.get(vol.get('volume_signal', 'normal'), '')
confirm_text = '✓ 确认' if vol.get('volume_confirms') else '✗ 未确认'
logger.info(f" 成交量: {vol_icon} {vol.get('volume_signal', 'normal')} | {confirm_text}")
# 输出支撑阻力位
levels = signal.get('levels', {})
if levels.get('nearest_support') or levels.get('nearest_resistance'):
support = f"${levels['nearest_support']:,.2f}" if levels.get('nearest_support') else '-'
resistance = f"${levels['nearest_resistance']:,.2f}" if levels.get('nearest_resistance') else '-'
logger.info(f" 关键位: 支撑={support} | 阻力={resistance}")
# 5. 检查是否需要发送信号
if self._should_send_signal(symbol, signal):
logger.info(f"\n📤 【发送通知】")
# 6. 计算止损止盈
atr = float(data['15m'].iloc[-1].get('atr', 0))
if atr > 0:
sl_tp = self.analyzer.calculate_stop_loss_take_profit(
signal['price'], signal['action'], atr
)
signal.update(sl_tp)
logger.info(f" 止损: ${signal['stop_loss']:,.2f} | 止盈: ${signal['take_profit']:,.2f}")
# 7. LLM 深度分析(置信度超过阈值时)
if signal['confidence'] >= self.llm_threshold * 100:
logger.info(f" 🤖 触发 LLM 深度分析...")
llm_result = await self.analyzer.llm_analyze(data, signal, symbol)
# 处理 LLM 分析结果
if llm_result.get('parsed'):
parsed = llm_result['parsed']
# 新格式使用 signal 而不是 recommendation
recommendation = parsed.get('signal', parsed.get('recommendation', {}))
# 如果 LLM 建议观望,降低置信度
if recommendation.get('action') == 'wait':
signal['confidence'] = min(signal['confidence'], 40)
signal['llm_analysis'] = llm_result.get('summary', 'LLM 建议观望')
logger.info(f" 🤖 LLM 建议: 观望")
else:
# 使用 LLM 的止损止盈建议
if recommendation.get('stop_loss'):
signal['stop_loss'] = recommendation['stop_loss']
if recommendation.get('targets'):
signal['take_profit'] = recommendation['targets'][0]
elif recommendation.get('take_profit'):
signal['take_profit'] = recommendation['take_profit']
signal['llm_analysis'] = llm_result.get('summary', '')
logger.info(f" 🤖 LLM 建议: {recommendation.get('action', 'N/A')}")
else:
signal['llm_analysis'] = llm_result.get('summary', llm_result.get('raw', '')[:200])
# 8. 发送通知(飞书 + Telegram置信度仍然足够高时
if signal['confidence'] >= 50:
await self.feishu.send_trading_signal(signal)
await self.telegram.send_trading_signal(signal)
# 9. 更新状态
self.last_signals[symbol] = signal
self.signal_cooldown[symbol] = datetime.now()
action_text = '买入' if signal['action'] == 'buy' else '卖出'
logger.info(f" ✅ 已发送 {action_text} 信号通知(飞书+Telegram")
# 10. 创建模拟订单
if self.paper_trading_enabled and self.paper_trading:
if signal.get('signal_grade', 'D') != 'D':
order = self.paper_trading.create_order_from_signal(signal)
if order:
logger.info(f" 📝 已创建模拟订单: {order.order_id}")
else:
logger.info(f" ⏸️ 置信度不足({signal['confidence']}%),不发送通知")
else:
# 输出为什么不发送
if signal['action'] == 'hold':
logger.info(f"\n⏸️ 结论: 观望,无交易机会")
elif signal['confidence'] < 50:
logger.info(f"\n⏸️ 结论: 置信度不足({signal['confidence']}%),继续观望")
else:
logger.info(f"\n⏸️ 结论: 信号冷却中,跳过")
except Exception as e:
logger.error(f"❌ 分析 {symbol} 出错: {e}")
import traceback
logger.error(traceback.format_exc())
def _calculate_price_change(self, h1_data: pd.DataFrame) -> str:
"""计算24小时价格变化"""
if len(h1_data) < 24:
return "N/A"
price_now = h1_data.iloc[-1]['close']
price_24h_ago = h1_data.iloc[-24]['close']
change = ((price_now - price_24h_ago) / price_24h_ago) * 100
if change >= 0:
return f"+{change:.2f}%"
return f"{change:.2f}%"
def _validate_data(self, data: Dict[str, pd.DataFrame]) -> bool:
"""验证数据完整性"""
required_intervals = ['5m', '15m', '1h', '4h']
for interval in required_intervals:
if interval not in data or data[interval].empty:
return False
if len(data[interval]) < 20: # 至少需要20条数据
return False
return True
def _should_send_signal(self, symbol: str, signal: Dict[str, Any]) -> bool:
"""
判断是否应该发送信号
Args:
symbol: 交易对
signal: 信号数据
Returns:
是否发送
"""
# 如果是观望,不发送
if signal['action'] == 'hold':
return False
# 置信度太低,不发送
if signal['confidence'] < 50:
return False
# 检查冷却时间同一交易对30分钟内不重复发送相同方向的信号
if symbol in self.signal_cooldown:
cooldown_end = self.signal_cooldown[symbol] + timedelta(minutes=30)
if datetime.now() < cooldown_end:
# 检查是否是相同方向的信号
if symbol in self.last_signals:
if self.last_signals[symbol]['action'] == signal['action']:
logger.debug(f"{symbol} 信号冷却中,跳过")
return False
return True
async def _handle_trend_change(self, symbol: str, old_trend: str, new_trend: str,
data: Dict[str, pd.DataFrame]):
"""处理趋势变化"""
price = float(data['1h'].iloc[-1]['close'])
await self.feishu.send_trend_change(symbol, old_trend, new_trend, price)
await self.telegram.send_trend_change(symbol, old_trend, new_trend, price)
logger.info(f"{symbol} 趋势变化: {old_trend} -> {new_trend}")
async def analyze_once(self, symbol: str) -> Dict[str, Any]:
"""
单次分析(用于测试或手动触发)
Args:
symbol: 交易对
Returns:
分析结果
"""
data = self.binance.get_multi_timeframe_data(symbol)
if not self._validate_data(data):
return {'error': '数据不完整'}
trend = self.analyzer.analyze_trend(data['1h'], data['4h'])
signal = self.analyzer.analyze_entry_signal(data['5m'], data['15m'], trend)
signal['symbol'] = symbol
signal['trend'] = trend
signal['price'] = float(data['5m'].iloc[-1]['close'])
# 计算止损止盈
atr = float(data['15m'].iloc[-1].get('atr', 0))
if atr > 0:
sl_tp = self.analyzer.calculate_stop_loss_take_profit(
signal['price'], signal['action'], atr
)
signal.update(sl_tp)
return signal
def get_status(self) -> Dict[str, Any]:
"""获取智能体状态"""
return {
'running': self.running,
'symbols': self.symbols,
'analysis_interval': self.analysis_interval,
'last_signals': {
symbol: {
'action': sig.get('action'),
'confidence': sig.get('confidence'),
'timestamp': sig.get('timestamp').isoformat() if sig.get('timestamp') else None
}
for symbol, sig in self.last_signals.items()
},
'last_trends': self.last_trends
}
# 全局实例
crypto_agent = CryptoAgent()