diff --git a/backend/app/config.py b/backend/app/config.py index ddf291e..12bdb57 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -89,6 +89,21 @@ class Settings(BaseSettings): # CORS配置 cors_origins: str = "http://localhost:8000,http://127.0.0.1:8000" + # Binance 配置(公开数据不需要 API 密钥) + binance_api_key: str = "" + binance_api_secret: str = "" + + # 飞书机器人配置 + feishu_webhook_url: str = "https://open.feishu.cn/open-apis/bot/v2/hook/8a1dcf69-6753-41e2-a393-edc4f7822db0" + + # 加密货币交易智能体配置 + crypto_symbols: str = "BTCUSDT,ETHUSDT" # 监控的交易对,逗号分隔 + crypto_analysis_interval: int = 60 # 分析间隔(秒) + crypto_llm_threshold: float = 0.7 # 触发 LLM 分析的置信度阈值 + + # Brave Search API 配置 + brave_api_key: str = "" + class Config: env_file = find_env_file() case_sensitive = False diff --git a/backend/app/crypto_agent/__init__.py b/backend/app/crypto_agent/__init__.py new file mode 100644 index 0000000..30c8a09 --- /dev/null +++ b/backend/app/crypto_agent/__init__.py @@ -0,0 +1,8 @@ +""" +加密货币交易智能体模块 +""" +from app.crypto_agent.crypto_agent import CryptoAgent +from app.crypto_agent.signal_analyzer import SignalAnalyzer +from app.crypto_agent.strategy import TrendFollowingStrategy + +__all__ = ['CryptoAgent', 'SignalAnalyzer', 'TrendFollowingStrategy'] diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py new file mode 100644 index 0000000..c5d3e57 --- /dev/null +++ b/backend/app/crypto_agent/crypto_agent.py @@ -0,0 +1,258 @@ +""" +加密货币交易智能体 - 主控制器 +""" +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.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.analyzer = SignalAnalyzer() + self.strategy = TrendFollowingStrategy() + + # 状态管理 + 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}") + + async def run(self): + """主运行循环""" + self.running = True + logger.info("加密货币智能体开始运行...") + + # 发送启动通知 + await self.feishu.send_text( + f"🚀 加密货币智能体已启动\n" + f"监控交易对: {', '.join(self.symbols)}\n" + f"分析间隔: {self.analysis_interval}秒" + ) + + while self.running: + try: + for symbol in self.symbols: + await self.analyze_symbol(symbol) + + # 等待下一次分析 + await asyncio.sleep(self.analysis_interval) + + except Exception as e: + logger.error(f"分析循环出错: {e}") + await asyncio.sleep(10) # 出错后等待10秒再继续 + + def stop(self): + """停止运行""" + self.running = False + logger.info("加密货币智能体已停止") + + async def analyze_symbol(self, symbol: str): + """ + 分析单个交易对 + + Args: + symbol: 交易对,如 'BTCUSDT' + """ + try: + logger.info(f"开始分析 {symbol}...") + + # 1. 获取多周期数据 + data = self.binance.get_multi_timeframe_data(symbol) + + if not self._validate_data(data): + logger.warning(f"{symbol} 数据不完整,跳过分析") + return + + # 2. 分析趋势(1H + 4H)- 返回详细趋势信息 + trend = self.analyzer.analyze_trend(data['1h'], data['4h']) + trend_direction = trend.get('direction', 'neutral') if isinstance(trend, dict) else trend + + # 3. 检查趋势变化 + last_direction = self.last_trends.get(symbol, {}) + if isinstance(last_direction, dict): + last_direction = last_direction.get('direction', 'neutral') + if last_direction and last_direction != trend_direction: + await self._handle_trend_change(symbol, last_direction, trend_direction, data) + + self.last_trends[symbol] = trend + + # 4. 分析进场信号(15M 为主,5M 辅助) + signal = self.analyzer.analyze_entry_signal(data['5m'], data['15m'], trend) + signal['symbol'] = symbol + signal['trend'] = trend_direction + signal['trend_info'] = trend if isinstance(trend, dict) else {'direction': trend} + signal['price'] = float(data['5m'].iloc[-1]['close']) + signal['timestamp'] = datetime.now() + + # 5. 检查是否需要发送信号 + if self._should_send_signal(symbol, signal): + # 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) + + # 7. LLM 深度分析(置信度超过阈值时) + if signal['confidence'] >= self.llm_threshold * 100: + 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 建议观望') + 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', '') + else: + signal['llm_analysis'] = llm_result.get('summary', llm_result.get('raw', '')[:200]) + + # 8. 发送飞书通知(置信度仍然足够高时) + if signal['confidence'] >= 50: + await self.feishu.send_trading_signal(signal) + + # 9. 更新状态 + self.last_signals[symbol] = signal + self.signal_cooldown[symbol] = datetime.now() + + logger.info(f"{symbol} 发送{signal['action']}信号,置信度: {signal['confidence']}%") + + except Exception as e: + logger.error(f"分析 {symbol} 出错: {e}") + + 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) + 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() diff --git a/backend/app/crypto_agent/signal_analyzer.py b/backend/app/crypto_agent/signal_analyzer.py new file mode 100644 index 0000000..d3ec3c7 --- /dev/null +++ b/backend/app/crypto_agent/signal_analyzer.py @@ -0,0 +1,670 @@ +""" +信号分析器 - 多周期技术分析和 LLM 深度分析 +""" +import pandas as pd +from typing import Dict, Any, Optional, List +from app.utils.logger import logger +from app.services.llm_service import llm_service + + +class SignalAnalyzer: + """交易信号分析器 - 波段交易优化版""" + + # LLM 系统提示词 - 波段交易版 + CRYPTO_ANALYST_PROMPT = """你是一位经验丰富的加密货币波段交易员,专注于捕捉 1-7 天的中等波段行情。 + +## 交易风格 +- **波段交易**:持仓 1-7 天,不做超短线 +- **顺势回调**:在趋势中寻找回调入场机会 +- **风险控制**:单笔亏损不超过本金 2% + +## 多周期分析框架 +1. **4H 周期**:判断主趋势方向和强度 + - 趋势明确:价格在 MA20 同侧运行 3 根以上 K 线 + - 趋势强度:看 MACD 柱状图是否放大 + +2. **1H 周期**:确认趋势 + 寻找回调位置 + - 上涨趋势中:等待回调到 MA20 或前低支撑 + - 下跌趋势中:等待反弹到 MA20 或前高阻力 + +3. **15M 周期**:入场信号确认 + - 做多:RSI 从超卖回升 + MACD 金叉 + K 线企稳 + - 做空:RSI 从超买回落 + MACD 死叉 + K 线见顶 + +## 入场条件(波段做多) +1. 4H 趋势向上(价格 > MA20,MACD > 0 或底背离) +2. 1H 回调到支撑位(MA20 附近或前低) +3. 15M 出现止跌信号(RSI < 40 回升,或 MACD 金叉) +4. 止损明确(前低下方),风险收益比 >= 1:2 + +## 入场条件(波段做空) +1. 4H 趋势向下(价格 < MA20,MACD < 0 或顶背离) +2. 1H 反弹到阻力位(MA20 附近或前高) +3. 15M 出现见顶信号(RSI > 60 回落,或 MACD 死叉) +4. 止损明确(前高上方),风险收益比 >= 1:2 + +## 特殊情况处理 +- **极度超卖(RSI < 20)**:不追空,等待反弹做多机会 +- **极度超买(RSI > 80)**:不追多,等待回调做空机会 +- **震荡市**:观望,等待突破方向 + +## 输出格式(JSON) +```json +{ + "market_structure": { + "trend": "uptrend/downtrend/sideways", + "strength": "strong/moderate/weak", + "phase": "impulse/correction/reversal" + }, + "key_levels": { + "resistance": [阻力位1, 阻力位2], + "support": [支撑位1, 支撑位2] + }, + "signal": { + "quality": "A/B/C/D", + "action": "buy/sell/wait", + "confidence": 0-100, + "entry_zone": [入场区间下限, 入场区间上限], + "stop_loss": 止损价, + "targets": [目标1, 目标2], + "reason": "入场理由" + }, + "risk_warning": "风险提示" +} +``` + +信号质量说明: +- A级:趋势明确 + 回调到位 + 多重信号共振(置信度 80+) +- B级:趋势明确 + 信号较好(置信度 60-80) +- C级:有机会但需要更多确认(置信度 40-60) +- D级:不建议交易(置信度 < 40) + +重要:波段交易要有耐心,宁可错过也不要在不理想的位置入场。""" + + def __init__(self): + """初始化信号分析器""" + logger.info("信号分析器初始化完成") + + def analyze_trend(self, h1_data: pd.DataFrame, h4_data: pd.DataFrame) -> Dict[str, Any]: + """ + 分析趋势方向和强度(波段交易优化版) + + Args: + h1_data: 1小时K线数据(含技术指标) + h4_data: 4小时K线数据(含技术指标) + + Returns: + { + 'direction': 'bullish' | 'bearish' | 'neutral', + 'strength': 'strong' | 'moderate' | 'weak', + 'phase': 'impulse' | 'correction' | 'reversal', + 'h4_score': float, + 'h1_score': float + } + """ + if h1_data.empty or h4_data.empty: + return { + 'direction': 'neutral', + 'strength': 'weak', + 'phase': 'sideways', + 'h4_score': 0, + 'h1_score': 0 + } + + # 获取最新数据 + h1_latest = h1_data.iloc[-1] + h4_latest = h4_data.iloc[-1] + + # 计算各周期的趋势得分 + h4_score, h4_details = self._calculate_trend_score(h4_latest) + h1_score, h1_details = self._calculate_trend_score(h1_latest) + + # 判断趋势方向(4H 为主) + if h4_score > 0.3: + direction = 'bullish' + elif h4_score < -0.3: + direction = 'bearish' + else: + direction = 'neutral' + + # 判断趋势强度 + strength = self._assess_trend_strength(h4_data, h1_data) + + # 判断当前阶段(是主升/主跌还是回调) + phase = self._detect_market_phase(h4_data, h1_data, direction) + + # 检查极端情况 + h4_rsi = h4_latest.get('rsi', 50) + extreme_warning = "" + if pd.notna(h4_rsi): + if h4_rsi < 20: + extreme_warning = f" [RSI={h4_rsi:.1f} 极度超卖]" + phase = 'oversold' + elif h4_rsi > 80: + extreme_warning = f" [RSI={h4_rsi:.1f} 极度超买]" + phase = 'overbought' + + logger.info(f"趋势分析: 方向={direction}, 强度={strength}, 阶段={phase} | " + f"4H={h4_score:.2f}{h4_details}, 1H={h1_score:.2f}{h1_details}{extreme_warning}") + + return { + 'direction': direction, + 'strength': strength, + 'phase': phase, + 'h4_score': h4_score, + 'h1_score': h1_score + } + + def _assess_trend_strength(self, h4_data: pd.DataFrame, h1_data: pd.DataFrame) -> str: + """评估趋势强度""" + if len(h4_data) < 5: + return 'weak' + + h4_latest = h4_data.iloc[-1] + + # 检查 MACD 柱状图是否放大 + macd_hist = h4_data['macd_hist'].tail(5) + macd_expanding = False + if len(macd_hist) >= 3: + recent_abs = abs(macd_hist.iloc[-1]) + prev_abs = abs(macd_hist.iloc[-3]) + if recent_abs > prev_abs * 1.2: + macd_expanding = True + + # 检查价格是否持续在 MA20 同侧 + close_prices = h4_data['close'].tail(5) + ma20_values = h4_data['ma20'].tail(5) + consistent_side = True + if pd.notna(ma20_values.iloc[-1]): + above_ma = close_prices > ma20_values + consistent_side = above_ma.all() or (~above_ma).all() + + # 检查 RSI 是否在趋势区间 + rsi = h4_latest.get('rsi', 50) + rsi_trending = 40 < rsi < 60 # 中性区间表示趋势不强 + + if macd_expanding and consistent_side and not rsi_trending: + return 'strong' + elif consistent_side: + return 'moderate' + else: + return 'weak' + + def _detect_market_phase(self, h4_data: pd.DataFrame, h1_data: pd.DataFrame, + direction: str) -> str: + """检测市场阶段(主升/主跌 vs 回调)""" + if len(h1_data) < 10: + return 'unknown' + + h1_latest = h1_data.iloc[-1] + h4_latest = h4_data.iloc[-1] + + # 获取 1H 的短期趋势 + h1_ma5 = h1_latest.get('ma5', 0) + h1_ma20 = h1_latest.get('ma20', 0) + h1_close = h1_latest.get('close', 0) + + if not (pd.notna(h1_ma5) and pd.notna(h1_ma20)): + return 'unknown' + + # 判断 1H 是否在回调 + if direction == 'bullish': + # 上涨趋势中,1H 价格回落到 MA20 附近 = 回调 + if h1_close < h1_ma5 and h1_close > h1_ma20 * 0.98: + return 'correction' # 回调中,可能是入场机会 + elif h1_close > h1_ma5: + return 'impulse' # 主升浪 + elif direction == 'bearish': + # 下跌趋势中,1H 价格反弹到 MA20 附近 = 反弹 + if h1_close > h1_ma5 and h1_close < h1_ma20 * 1.02: + return 'correction' # 反弹中,可能是做空机会 + elif h1_close < h1_ma5: + return 'impulse' # 主跌浪 + + return 'sideways' + + def _calculate_trend_score(self, data: pd.Series) -> tuple: + """ + 计算单周期趋势得分 + + Args: + data: 包含技术指标的数据行 + + Returns: + (得分, 详情字符串) + """ + score = 0.0 + count = 0 + details = [] + + # 价格与均线关系 + if 'close' in data and 'ma20' in data and pd.notna(data['ma20']): + if data['close'] > data['ma20']: + score += 1 + details.append("价格>MA20") + else: + score -= 1 + details.append("价格 data['ma20']: + score += 1 + details.append("MA5>MA20") + else: + score -= 1 + details.append("MA5 data['macd_signal']: + score += 1 + details.append("MACD多") + else: + score -= 1 + details.append("MACD空") + count += 1 + + # RSI + if 'rsi' in data and pd.notna(data['rsi']): + if data['rsi'] > 50: + score += 0.5 + else: + score -= 0.5 + count += 0.5 + + final_score = score / count if count > 0 else 0 + detail_str = f"({','.join(details)})" if details else "" + return final_score, detail_str + + def analyze_entry_signal(self, m5_data: pd.DataFrame, m15_data: pd.DataFrame, + trend: Dict[str, Any]) -> Dict[str, Any]: + """ + 分析 15M 进场信号(波段交易优化版) + + Args: + m5_data: 5分钟K线数据(用于精确入场) + m15_data: 15分钟K线数据(主要入场周期) + trend: 趋势分析结果 + + Returns: + { + 'action': 'buy' | 'sell' | 'hold', + 'confidence': 0-100, + 'signal_grade': 'A' | 'B' | 'C' | 'D', + 'reasons': [...], + 'indicators': {...} + } + """ + if m5_data.empty or m15_data.empty: + return {'action': 'hold', 'confidence': 0, 'signal_grade': 'D', + 'reasons': ['数据不足'], 'indicators': {}} + + # 兼容旧格式(如果 trend 是字符串) + if isinstance(trend, str): + trend_direction = trend + trend_phase = 'unknown' + trend_strength = 'moderate' + else: + trend_direction = trend.get('direction', 'neutral') + trend_phase = trend.get('phase', 'unknown') + trend_strength = trend.get('strength', 'moderate') + + m15_latest = m15_data.iloc[-1] + + # 收集信号 + buy_signals = [] + sell_signals = [] + signal_weights = {'buy': 0, 'sell': 0} + + # === RSI 信号 === + if 'rsi' in m15_latest and pd.notna(m15_latest['rsi']): + rsi = m15_latest['rsi'] + if rsi < 30: + buy_signals.append(f"RSI超卖({rsi:.1f})") + signal_weights['buy'] += 2 + elif rsi < 40 and len(m15_data) >= 2: + # RSI 从低位回升 + prev_rsi = m15_data.iloc[-2].get('rsi', 50) + if pd.notna(prev_rsi) and rsi > prev_rsi: + buy_signals.append(f"RSI回升({prev_rsi:.1f}→{rsi:.1f})") + signal_weights['buy'] += 1.5 + elif rsi > 70: + sell_signals.append(f"RSI超买({rsi:.1f})") + signal_weights['sell'] += 2 + elif rsi > 60 and len(m15_data) >= 2: + prev_rsi = m15_data.iloc[-2].get('rsi', 50) + if pd.notna(prev_rsi) and rsi < prev_rsi: + sell_signals.append(f"RSI回落({prev_rsi:.1f}→{rsi:.1f})") + signal_weights['sell'] += 1.5 + + # === MACD 信号 === + if len(m15_data) >= 2: + prev = m15_data.iloc[-2] + if 'macd' in m15_latest and 'macd_signal' in m15_latest: + if pd.notna(m15_latest['macd']) and pd.notna(prev['macd']): + # 金叉 + if prev['macd'] <= prev['macd_signal'] and m15_latest['macd'] > m15_latest['macd_signal']: + buy_signals.append("MACD金叉") + signal_weights['buy'] += 2 + # 死叉 + elif prev['macd'] >= prev['macd_signal'] and m15_latest['macd'] < m15_latest['macd_signal']: + sell_signals.append("MACD死叉") + signal_weights['sell'] += 2 + # MACD 柱状图缩小(趋势减弱) + elif abs(m15_latest['macd_hist']) < abs(prev['macd_hist']) * 0.7: + if m15_latest['macd_hist'] > 0: + sell_signals.append("MACD动能减弱") + signal_weights['sell'] += 0.5 + else: + buy_signals.append("MACD动能减弱") + signal_weights['buy'] += 0.5 + + # === 布林带信号 === + if 'close' in m15_latest and 'bb_lower' in m15_latest and 'bb_upper' in m15_latest: + if pd.notna(m15_latest['bb_lower']) and pd.notna(m15_latest['bb_upper']): + bb_middle = m15_latest.get('bb_middle', (m15_latest['bb_upper'] + m15_latest['bb_lower']) / 2) + if m15_latest['close'] < m15_latest['bb_lower']: + buy_signals.append("触及布林下轨") + signal_weights['buy'] += 1.5 + elif m15_latest['close'] > m15_latest['bb_upper']: + sell_signals.append("触及布林上轨") + signal_weights['sell'] += 1.5 + # 突破中轨 + elif len(m15_data) >= 2: + prev_close = m15_data.iloc[-2]['close'] + if prev_close < bb_middle and m15_latest['close'] > bb_middle: + buy_signals.append("突破布林中轨") + signal_weights['buy'] += 1 + elif prev_close > bb_middle and m15_latest['close'] < bb_middle: + sell_signals.append("跌破布林中轨") + signal_weights['sell'] += 1 + + # === KDJ 信号 === + if 'k' in m15_latest and 'd' in m15_latest and len(m15_data) >= 2: + prev = m15_data.iloc[-2] + if pd.notna(m15_latest['k']) and pd.notna(prev['k']): + if prev['k'] <= prev['d'] and m15_latest['k'] > m15_latest['d']: + if m15_latest['k'] < 30: + buy_signals.append("KDJ低位金叉") + signal_weights['buy'] += 1.5 + else: + buy_signals.append("KDJ金叉") + signal_weights['buy'] += 0.5 + elif prev['k'] >= prev['d'] and m15_latest['k'] < m15_latest['d']: + if m15_latest['k'] > 70: + sell_signals.append("KDJ高位死叉") + signal_weights['sell'] += 1.5 + else: + sell_signals.append("KDJ死叉") + signal_weights['sell'] += 0.5 + + # === 根据趋势和阶段决定动作 === + action = 'hold' + confidence = 0 + reasons = [] + signal_grade = 'D' + + # 波段交易核心逻辑:在回调中寻找入场机会 + if trend_direction == 'bullish': + if trend_phase == 'correction' and signal_weights['buy'] >= 3: + # 上涨趋势 + 回调 + 买入信号 = 最佳做多机会 + action = 'buy' + confidence = min(40 + signal_weights['buy'] * 10, 95) + reasons = buy_signals + [f"上涨趋势回调({trend_strength})"] + signal_grade = 'A' if confidence >= 80 else ('B' if confidence >= 60 else 'C') + elif trend_phase == 'impulse' and signal_weights['buy'] >= 4: + # 主升浪中追多需要更强信号 + action = 'buy' + confidence = min(30 + signal_weights['buy'] * 8, 80) + reasons = buy_signals + ["主升浪追多"] + signal_grade = 'B' if confidence >= 60 else 'C' + elif trend_phase in ['oversold', 'overbought']: + reasons = ['极端行情,等待企稳'] + + elif trend_direction == 'bearish': + if trend_phase == 'correction' and signal_weights['sell'] >= 3: + # 下跌趋势 + 反弹 + 卖出信号 = 最佳做空机会 + action = 'sell' + confidence = min(40 + signal_weights['sell'] * 10, 95) + reasons = sell_signals + [f"下跌趋势反弹({trend_strength})"] + signal_grade = 'A' if confidence >= 80 else ('B' if confidence >= 60 else 'C') + elif trend_phase == 'impulse' and signal_weights['sell'] >= 4: + action = 'sell' + confidence = min(30 + signal_weights['sell'] * 8, 80) + reasons = sell_signals + ["主跌浪追空"] + signal_grade = 'B' if confidence >= 60 else 'C' + elif trend_phase in ['oversold', 'overbought']: + reasons = ['极端行情,等待企稳'] + + else: # neutral + # 震荡市不交易 + reasons = ['趋势不明确,观望'] + + if not reasons: + reasons = ['信号不足,继续观望'] + + # 收集指标数据 + indicators = {} + for col in ['rsi', 'macd', 'macd_signal', 'macd_hist', 'k', 'd', 'j', 'close', 'ma20']: + if col in m15_latest and pd.notna(m15_latest[col]): + indicators[col] = float(m15_latest[col]) + + return { + 'action': action, + 'confidence': confidence, + 'signal_grade': signal_grade, + 'reasons': reasons, + 'indicators': indicators, + 'trend_info': { + 'direction': trend_direction, + 'phase': trend_phase, + 'strength': trend_strength + } + } + + async def llm_analyze(self, data: Dict[str, pd.DataFrame], signal: Dict[str, Any], + symbol: str) -> Dict[str, Any]: + """ + 使用 LLM 进行深度分析 + + Args: + data: 多周期K线数据 + signal: 初步信号分析结果 + symbol: 交易对 + + Returns: + LLM 分析结果(结构化数据) + """ + try: + # 构建分析提示 + prompt = self._build_analysis_prompt(data, signal, symbol) + + # 调用 LLM + response = llm_service.chat([ + {"role": "system", "content": self.CRYPTO_ANALYST_PROMPT}, + {"role": "user", "content": prompt} + ]) + + if response: + logger.info(f"LLM 分析完成: {symbol}") + # 解析 JSON 响应 + return self._parse_llm_response(response) + else: + return {"error": "LLM 分析暂时不可用", "raw": ""} + + except Exception as e: + logger.error(f"LLM 分析失败: {e}") + return {"error": str(e), "raw": ""} + + def _parse_llm_response(self, response: str) -> Dict[str, Any]: + """ + 解析 LLM 的 JSON 响应 + + Args: + response: LLM 原始响应 + + Returns: + 解析后的结构化数据 + """ + import json + import re + + result = { + "raw": response, + "parsed": None, + "summary": "" + } + + try: + # 尝试提取 JSON 块 + json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response) + if json_match: + json_str = json_match.group(1) + else: + # 尝试直接解析整个响应 + json_str = response + + # 解析 JSON + parsed = json.loads(json_str) + result["parsed"] = parsed + + # 生成摘要 + if parsed: + recommendation = parsed.get("recommendation", {}) + action = recommendation.get("action", "wait") + confidence = recommendation.get("confidence", 0) + reason = recommendation.get("reason", "") + + if action == "wait": + result["summary"] = f"建议观望。{parsed.get('risk_warning', '')}" + else: + action_text = "做多" if action == "buy" else "做空" + result["summary"] = f"建议{action_text},置信度{confidence}%。{reason}" + + except json.JSONDecodeError: + # JSON 解析失败,提取关键信息 + logger.warning("LLM 响应不是有效 JSON,尝试提取关键信息") + result["summary"] = self._extract_summary_from_text(response) + + return result + + def _extract_summary_from_text(self, text: str) -> str: + """从非 JSON 文本中提取摘要""" + # 简单提取前 200 字符作为摘要 + text = text.strip() + if len(text) > 200: + return text[:200] + "..." + return text + + def _build_analysis_prompt(self, data: Dict[str, pd.DataFrame], signal: Dict[str, Any], + symbol: str) -> str: + """构建 LLM 分析提示 - 优化版""" + parts = [f"# {symbol} 技术分析数据\n"] + + # 当前价格 + current_price = float(data['5m'].iloc[-1]['close']) + parts.append(f"**当前价格**: ${current_price:,.2f}\n") + + # 添加各周期指标摘要 + for interval in ['4h', '1h', '15m']: + df = data.get(interval) + if df is None or df.empty: + continue + + latest = df.iloc[-1] + parts.append(f"\n## {interval.upper()} 周期指标") + + # 价格与均线关系 + close = latest.get('close', 0) + ma20 = latest.get('ma20', 0) + ma50 = latest.get('ma50', 0) + if pd.notna(ma20): + position = "上方" if close > ma20 else "下方" + parts.append(f"- 价格在 MA20 {position} (MA20={ma20:.2f})") + if pd.notna(ma50): + parts.append(f"- MA50: {ma50:.2f}") + + # RSI + rsi = latest.get('rsi', 0) + if pd.notna(rsi): + rsi_status = "超卖" if rsi < 30 else ("超买" if rsi > 70 else "中性") + parts.append(f"- RSI: {rsi:.1f} ({rsi_status})") + + # MACD + macd = latest.get('macd', 0) + macd_signal = latest.get('macd_signal', 0) + if pd.notna(macd) and pd.notna(macd_signal): + macd_status = "多头" if macd > macd_signal else "空头" + parts.append(f"- MACD: {macd:.2f}, Signal: {macd_signal:.2f} ({macd_status})") + + # 布林带 + bb_upper = latest.get('bb_upper', 0) + bb_lower = latest.get('bb_lower', 0) + if pd.notna(bb_upper) and pd.notna(bb_lower): + parts.append(f"- 布林带: 上轨={bb_upper:.2f}, 下轨={bb_lower:.2f}") + + # 添加最近 5 根 15M K 线(让 LLM 看形态) + parts.append("\n## 最近 5 根 15M K线") + parts.append("| 时间 | 开盘 | 最高 | 最低 | 收盘 | 涨跌 |") + parts.append("|------|------|------|------|------|------|") + + df_15m = data.get('15m') + if df_15m is not None and len(df_15m) >= 5: + for i in range(-5, 0): + row = df_15m.iloc[i] + change = ((row['close'] - row['open']) / row['open']) * 100 + change_str = f"+{change:.2f}%" if change >= 0 else f"{change:.2f}%" + time_str = row['open_time'].strftime('%H:%M') if pd.notna(row['open_time']) else 'N/A' + parts.append(f"| {time_str} | {row['open']:.2f} | {row['high']:.2f} | {row['low']:.2f} | {row['close']:.2f} | {change_str} |") + + # 计算关键价位 + parts.append("\n## 关键价位参考") + df_1h = data.get('1h') + if df_1h is not None and len(df_1h) >= 20: + recent_high = df_1h['high'].tail(20).max() + recent_low = df_1h['low'].tail(20).min() + parts.append(f"- 近期高点: ${recent_high:,.2f}") + parts.append(f"- 近期低点: ${recent_low:,.2f}") + + # 初步信号分析结果 + parts.append(f"\n## 规则引擎初步判断") + parts.append(f"- 趋势: {signal.get('trend', 'unknown')}") + parts.append(f"- 信号: {signal.get('action', 'hold')}") + parts.append(f"- 置信度: {signal.get('confidence', 0)}%") + parts.append(f"- 触发原因: {', '.join(signal.get('reasons', []))}") + + parts.append("\n---") + parts.append("请基于以上数据进行分析,严格按照 JSON 格式输出你的判断。") + + return "\n".join(parts) + + def calculate_stop_loss_take_profit(self, price: float, action: str, + atr: float) -> Dict[str, float]: + """ + 计算止损止盈位置 + + Args: + price: 当前价格 + action: 'buy' 或 'sell' + atr: ATR 值 + + Returns: + {'stop_loss': float, 'take_profit': float} + """ + if action == 'buy': + stop_loss = price - atr * 2 + take_profit = price + atr * 3 + elif action == 'sell': + stop_loss = price + atr * 2 + take_profit = price - atr * 3 + else: + stop_loss = 0 + take_profit = 0 + + return { + 'stop_loss': round(stop_loss, 2), + 'take_profit': round(take_profit, 2) + } diff --git a/backend/app/crypto_agent/strategy.py b/backend/app/crypto_agent/strategy.py new file mode 100644 index 0000000..31c3878 --- /dev/null +++ b/backend/app/crypto_agent/strategy.py @@ -0,0 +1,189 @@ +""" +交易策略 - 趋势跟踪策略定义 +""" +from typing import Dict, List, Any +from app.utils.logger import logger + + +class TrendFollowingStrategy: + """趋势跟踪策略""" + + # 趋势判断规则(1H + 4H) + TREND_RULES = { + 'bullish': { + 'description': '看涨趋势', + 'conditions': [ + {'name': 'price_above_ma20', 'desc': '价格在MA20上方'}, + {'name': 'ma5_above_ma20', 'desc': 'MA5在MA20上方'}, + {'name': 'macd_positive', 'desc': 'MACD在信号线上方'}, + {'name': 'rsi_above_50', 'desc': 'RSI大于50'} + ] + }, + 'bearish': { + 'description': '看跌趋势', + 'conditions': [ + {'name': 'price_below_ma20', 'desc': '价格在MA20下方'}, + {'name': 'ma5_below_ma20', 'desc': 'MA5在MA20下方'}, + {'name': 'macd_negative', 'desc': 'MACD在信号线下方'}, + {'name': 'rsi_below_50', 'desc': 'RSI小于50'} + ] + } + } + + # 进场规则(5M + 15M) + ENTRY_RULES = { + 'buy': { + 'description': '做多进场', + 'conditions': [ + {'name': 'rsi_oversold_recovery', 'desc': 'RSI从超卖区回升', 'weight': 2}, + {'name': 'macd_golden_cross', 'desc': 'MACD金叉', 'weight': 2}, + {'name': 'price_break_bb_middle', 'desc': '价格突破布林中轨', 'weight': 1}, + {'name': 'kdj_golden_cross', 'desc': 'KDJ低位金叉', 'weight': 1}, + {'name': 'volume_increase', 'desc': '成交量放大', 'weight': 1} + ], + 'min_score': 3 # 最低触发分数 + }, + 'sell': { + 'description': '做空进场', + 'conditions': [ + {'name': 'rsi_overbought_decline', 'desc': 'RSI从超买区回落', 'weight': 2}, + {'name': 'macd_death_cross', 'desc': 'MACD死叉', 'weight': 2}, + {'name': 'price_break_bb_middle_down', 'desc': '价格跌破布林中轨', 'weight': 1}, + {'name': 'kdj_death_cross', 'desc': 'KDJ高位死叉', 'weight': 1}, + {'name': 'volume_increase', 'desc': '成交量放大', 'weight': 1} + ], + 'min_score': 3 + } + } + + # 出场规则 + EXIT_RULES = { + 'take_profit': { + 'description': '止盈', + 'conditions': [ + {'name': 'target_reached', 'desc': '达到目标价位'}, + {'name': 'rsi_extreme', 'desc': 'RSI达到极值'}, + {'name': 'trend_reversal', 'desc': '趋势反转信号'} + ] + }, + 'stop_loss': { + 'description': '止损', + 'conditions': [ + {'name': 'price_hit_stop', 'desc': '价格触及止损位'}, + {'name': 'trend_break', 'desc': '趋势破坏'} + ] + } + } + + # 风险管理参数 + RISK_PARAMS = { + 'max_position_size': 0.1, # 最大仓位比例 + 'stop_loss_atr_multiplier': 2.0, # 止损 ATR 倍数 + 'take_profit_atr_multiplier': 3.0, # 止盈 ATR 倍数 + 'max_daily_trades': 5, # 每日最大交易次数 + 'min_risk_reward_ratio': 1.5 # 最小风险收益比 + } + + def __init__(self): + """初始化策略""" + logger.info("趋势跟踪策略初始化完成") + + def get_trend_rules(self, trend: str) -> Dict[str, Any]: + """获取趋势判断规则""" + return self.TREND_RULES.get(trend, {}) + + def get_entry_rules(self, action: str) -> Dict[str, Any]: + """获取进场规则""" + return self.ENTRY_RULES.get(action, {}) + + def get_exit_rules(self, exit_type: str) -> Dict[str, Any]: + """获取出场规则""" + return self.EXIT_RULES.get(exit_type, {}) + + def calculate_position_size(self, account_balance: float, risk_per_trade: float, + entry_price: float, stop_loss: float) -> float: + """ + 计算仓位大小 + + Args: + account_balance: 账户余额 + risk_per_trade: 单笔风险比例(如 0.02 表示 2%) + entry_price: 入场价格 + stop_loss: 止损价格 + + Returns: + 建议仓位大小 + """ + risk_amount = account_balance * risk_per_trade + price_risk = abs(entry_price - stop_loss) + + if price_risk == 0: + return 0 + + position_size = risk_amount / price_risk + + # 限制最大仓位 + max_position = account_balance * self.RISK_PARAMS['max_position_size'] / entry_price + position_size = min(position_size, max_position) + + return round(position_size, 6) + + def validate_trade(self, entry_price: float, stop_loss: float, + take_profit: float) -> Dict[str, Any]: + """ + 验证交易是否符合风险管理规则 + + Args: + entry_price: 入场价格 + stop_loss: 止损价格 + take_profit: 止盈价格 + + Returns: + 验证结果 + """ + risk = abs(entry_price - stop_loss) + reward = abs(take_profit - entry_price) + + if risk == 0: + return { + 'valid': False, + 'reason': '止损距离为0', + 'risk_reward_ratio': 0 + } + + risk_reward_ratio = reward / risk + + if risk_reward_ratio < self.RISK_PARAMS['min_risk_reward_ratio']: + return { + 'valid': False, + 'reason': f'风险收益比({risk_reward_ratio:.2f})低于最低要求({self.RISK_PARAMS["min_risk_reward_ratio"]})', + 'risk_reward_ratio': risk_reward_ratio + } + + return { + 'valid': True, + 'reason': '符合风险管理规则', + 'risk_reward_ratio': risk_reward_ratio + } + + def get_strategy_description(self) -> str: + """获取策略描述""" + return """ +## 趋势跟踪策略 + +### 核心理念 +顺势而为,在大周期确认趋势后,在小周期寻找最佳进场点。 + +### 趋势判断(4H + 1H) +- 看涨:价格在MA20上方,MA5>MA20,MACD>信号线,RSI>50 +- 看跌:价格在MA20下方,MA5 pd.DataFrame: + """ + 获取 K 线数据 + + Args: + symbol: 交易对,如 'BTCUSDT' + interval: K线周期,如 '5m', '15m', '1h', '4h' + limit: 获取数量 + + Returns: + DataFrame 包含 OHLCV 数据 + """ + try: + binance_interval = self.INTERVALS.get(interval, interval) + klines = self.client.get_klines( + symbol=symbol, + interval=binance_interval, + limit=limit + ) + return self._parse_klines(klines) + except Exception as e: + logger.error(f"获取 {symbol} {interval} K线数据失败: {e}") + return pd.DataFrame() + + def get_multi_timeframe_data(self, symbol: str) -> Dict[str, pd.DataFrame]: + """ + 获取多周期 K 线数据 + + Args: + symbol: 交易对 + + Returns: + 包含 5m, 15m, 1h, 4h 数据的字典 + """ + data = {} + for interval in ['5m', '15m', '1h', '4h']: + df = self.get_klines(symbol, interval, limit=100) + if not df.empty: + df = self.calculate_indicators(df) + data[interval] = df + + logger.info(f"获取 {symbol} 多周期数据完成") + return data + + def _parse_klines(self, klines: List) -> pd.DataFrame: + """解析 K 线数据为 DataFrame""" + if not klines: + return pd.DataFrame() + + df = pd.DataFrame(klines, columns=[ + 'open_time', 'open', 'high', 'low', 'close', 'volume', + 'close_time', 'quote_volume', 'trades', + 'taker_buy_base', 'taker_buy_quote', 'ignore' + ]) + + # 转换数据类型 + df['open_time'] = pd.to_datetime(df['open_time'], unit='ms') + df['close_time'] = pd.to_datetime(df['close_time'], unit='ms') + + for col in ['open', 'high', 'low', 'close', 'volume', 'quote_volume']: + df[col] = df[col].astype(float) + + df['trades'] = df['trades'].astype(int) + + # 只保留需要的列 + df = df[['open_time', 'open', 'high', 'low', 'close', 'volume', 'trades']] + + return df + + def calculate_indicators(self, df: pd.DataFrame) -> pd.DataFrame: + """ + 计算技术指标 + + Args: + df: K线数据 DataFrame + + Returns: + 添加了技术指标的 DataFrame + """ + if df.empty: + return df + + # 移动平均线 + df['ma5'] = self._calculate_ma(df['close'], 5) + df['ma10'] = self._calculate_ma(df['close'], 10) + df['ma20'] = self._calculate_ma(df['close'], 20) + df['ma50'] = self._calculate_ma(df['close'], 50) + + # EMA + df['ema12'] = self._calculate_ema(df['close'], 12) + df['ema26'] = self._calculate_ema(df['close'], 26) + + # RSI + df['rsi'] = self._calculate_rsi(df['close'], 14) + + # MACD + df['macd'], df['macd_signal'], df['macd_hist'] = self._calculate_macd(df['close']) + + # 布林带 + df['bb_upper'], df['bb_middle'], df['bb_lower'] = self._calculate_bollinger(df['close']) + + # KDJ + df['k'], df['d'], df['j'] = self._calculate_kdj(df['high'], df['low'], df['close']) + + # ATR + df['atr'] = self._calculate_atr(df['high'], df['low'], df['close']) + + return df + + @staticmethod + def _calculate_ma(data: pd.Series, period: int) -> pd.Series: + """简单移动平均线""" + return data.rolling(window=period).mean() + + @staticmethod + def _calculate_ema(data: pd.Series, period: int) -> pd.Series: + """指数移动平均线""" + return data.ewm(span=period, adjust=False).mean() + + @staticmethod + def _calculate_rsi(data: pd.Series, period: int = 14) -> pd.Series: + """RSI 指标""" + delta = data.diff() + gain = (delta.where(delta > 0, 0)).rolling(window=period).mean() + loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean() + rs = gain / loss + rsi = 100 - (100 / (1 + rs)) + return rsi + + @staticmethod + def _calculate_macd(data: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9): + """MACD 指标""" + ema_fast = data.ewm(span=fast, adjust=False).mean() + ema_slow = data.ewm(span=slow, adjust=False).mean() + + macd = ema_fast - ema_slow + signal_line = macd.ewm(span=signal, adjust=False).mean() + histogram = macd - signal_line + + return macd, signal_line, histogram + + @staticmethod + def _calculate_bollinger(data: pd.Series, period: int = 20, std_dev: float = 2.0): + """布林带""" + middle = data.rolling(window=period).mean() + std = data.rolling(window=period).std() + + upper = middle + (std * std_dev) + lower = middle - (std * std_dev) + + return upper, middle, lower + + @staticmethod + def _calculate_kdj(high: pd.Series, low: pd.Series, close: pd.Series, + period: int = 9, k_period: int = 3, d_period: int = 3): + """KDJ 指标""" + low_min = low.rolling(window=period).min() + high_max = high.rolling(window=period).max() + + rsv = (close - low_min) / (high_max - low_min) * 100 + + k = rsv.ewm(com=k_period - 1, adjust=False).mean() + d = k.ewm(com=d_period - 1, adjust=False).mean() + j = 3 * k - 2 * d + + return k, d, j + + @staticmethod + def _calculate_atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14): + """ATR 平均真实波幅""" + tr1 = high - low + tr2 = abs(high - close.shift()) + tr3 = abs(low - close.shift()) + + tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) + atr = tr.rolling(window=period).mean() + + return atr + + def get_current_price(self, symbol: str) -> Optional[float]: + """获取当前价格""" + try: + ticker = self.client.get_symbol_ticker(symbol=symbol) + return float(ticker['price']) + except Exception as e: + logger.error(f"获取 {symbol} 当前价格失败: {e}") + return None + + def get_24h_stats(self, symbol: str) -> Optional[Dict[str, Any]]: + """获取 24 小时统计数据""" + try: + stats = self.client.get_ticker(symbol=symbol) + return { + 'price': float(stats['lastPrice']), + 'price_change': float(stats['priceChange']), + 'price_change_percent': float(stats['priceChangePercent']), + 'high': float(stats['highPrice']), + 'low': float(stats['lowPrice']), + 'volume': float(stats['volume']), + 'quote_volume': float(stats['quoteVolume']) + } + except Exception as e: + logger.error(f"获取 {symbol} 24h 统计失败: {e}") + return None + + +# 全局实例 +binance_service = BinanceService() diff --git a/backend/app/services/feishu_service.py b/backend/app/services/feishu_service.py new file mode 100644 index 0000000..e4a578b --- /dev/null +++ b/backend/app/services/feishu_service.py @@ -0,0 +1,296 @@ +""" +飞书通知服务 - 通过 Webhook 发送交易信号通知 +""" +import json +import httpx +from typing import Dict, Any, Optional +from app.utils.logger import logger +from app.config import get_settings + + +class FeishuService: + """飞书机器人通知服务""" + + def __init__(self, webhook_url: str = ""): + """ + 初始化飞书服务 + + Args: + webhook_url: 飞书机器人 Webhook URL + """ + settings = get_settings() + self.webhook_url = webhook_url or getattr(settings, 'feishu_webhook_url', '') + self.enabled = bool(self.webhook_url) + + if self.enabled: + logger.info("飞书通知服务初始化完成") + else: + logger.warning("飞书 Webhook URL 未配置,通知功能已禁用") + + async def send_text(self, message: str) -> bool: + """ + 发送文本消息 + + Args: + message: 消息内容 + + Returns: + 是否发送成功 + """ + if not self.enabled: + logger.warning("飞书服务未启用,跳过发送") + return False + + data = { + "msg_type": "text", + "content": { + "text": message + } + } + + return await self._send(data) + + async def send_card(self, title: str, content: str, color: str = "blue") -> bool: + """ + 发送卡片消息 + + Args: + title: 卡片标题 + content: 卡片内容(支持 Markdown) + color: 标题颜色 (blue, green, red, orange, purple) + + Returns: + 是否发送成功 + """ + if not self.enabled: + logger.warning("飞书服务未启用,跳过发送") + return False + + # 颜色映射 + color_map = { + "blue": "blue", + "green": "green", + "red": "red", + "orange": "orange", + "purple": "purple" + } + + data = { + "msg_type": "interactive", + "card": { + "header": { + "title": { + "tag": "plain_text", + "content": title + }, + "template": color_map.get(color, "blue") + }, + "elements": [ + { + "tag": "markdown", + "content": content + } + ] + } + } + + return await self._send(data) + + async def send_trading_signal(self, signal: Dict[str, Any]) -> bool: + """ + 发送交易信号卡片 + + Args: + signal: 交易信号数据 + - symbol: 交易对 + - action: 'buy' | 'sell' + - price: 当前价格 + - trend: 趋势方向 + - confidence: 信号强度 (0-100) + - indicators: 技术指标数据 + - llm_analysis: LLM 分析结果(可选) + - stop_loss: 建议止损价 + - take_profit: 建议止盈价 + + Returns: + 是否发送成功 + """ + if not self.enabled: + logger.warning("飞书服务未启用,跳过发送") + return False + + action = signal.get('action', 'hold') + symbol = signal.get('symbol', 'UNKNOWN') + price = signal.get('price', 0) + trend = signal.get('trend', 'neutral') + confidence = signal.get('confidence', 0) + indicators = signal.get('indicators', {}) + llm_analysis = signal.get('llm_analysis', '') + stop_loss = signal.get('stop_loss', 0) + take_profit = signal.get('take_profit', 0) + + # 确定标题和颜色 + if action == 'buy': + title = f"🟢 买入信号 - {symbol}" + color = "green" + action_text = "做多" + elif action == 'sell': + title = f"🔴 卖出信号 - {symbol}" + color = "red" + action_text = "做空" + else: + title = f"⚪ 观望 - {symbol}" + color = "blue" + action_text = "观望" + + # 趋势文本 + trend_text = { + 'bullish': '看涨 📈', + 'bearish': '看跌 📉', + 'neutral': '震荡 ↔️' + }.get(trend, '未知') + + # 构建内容 + content_parts = [ + f"**当前价格**: ${price:,.2f}", + f"**趋势方向**: {trend_text}", + f"**信号强度**: {confidence}%", + "", + "---", + "", + "**技术指标**:" + ] + + # 添加技术指标 + if indicators: + rsi = indicators.get('rsi', 0) + macd = indicators.get('macd', 0) + macd_signal = indicators.get('macd_signal', 0) + + rsi_status = "超卖 ↑" if rsi < 30 else ("超买 ↓" if rsi > 70 else "中性") + macd_status = "金叉" if macd > macd_signal else "死叉" + + content_parts.extend([ + f"• RSI(14): {rsi:.1f} ({rsi_status})", + f"• MACD: {macd_status}", + ]) + + if 'k' in indicators: + content_parts.append(f"• KDJ: K={indicators['k']:.1f}, D={indicators['d']:.1f}") + + # 添加 LLM 分析 + if llm_analysis: + content_parts.extend([ + "", + "---", + "", + "**AI 分析**:", + llm_analysis[:200] + "..." if len(llm_analysis) > 200 else llm_analysis + ]) + + # 添加止损止盈建议 + if stop_loss > 0 or take_profit > 0: + content_parts.extend([ + "", + "---", + "", + "**风险管理**:" + ]) + if stop_loss > 0: + sl_percent = ((stop_loss - price) / price) * 100 + content_parts.append(f"• 建议止损: ${stop_loss:,.2f} ({sl_percent:+.1f}%)") + if take_profit > 0: + tp_percent = ((take_profit - price) / price) * 100 + content_parts.append(f"• 建议止盈: ${take_profit:,.2f} ({tp_percent:+.1f}%)") + + # 添加免责声明 + content_parts.extend([ + "", + "---", + "", + "*⚠️ 仅供参考,不构成投资建议*" + ]) + + content = "\n".join(content_parts) + + return await self.send_card(title, content, color) + + async def send_trend_change(self, symbol: str, old_trend: str, new_trend: str, price: float) -> bool: + """ + 发送趋势变化通知 + + Args: + symbol: 交易对 + old_trend: 旧趋势 + new_trend: 新趋势 + price: 当前价格 + + Returns: + 是否发送成功 + """ + trend_emoji = { + 'bullish': '📈', + 'bearish': '📉', + 'neutral': '↔️' + } + + trend_text = { + 'bullish': '看涨', + 'bearish': '看跌', + 'neutral': '震荡' + } + + title = f"🔄 趋势变化 - {symbol}" + content = f"""**{symbol}** 趋势发生变化 + +**变化**: {trend_text.get(old_trend, old_trend)} {trend_emoji.get(old_trend, '')} → {trend_text.get(new_trend, new_trend)} {trend_emoji.get(new_trend, '')} + +**当前价格**: ${price:,.2f} + +*请关注后续交易信号*""" + + return await self.send_card(title, content, "orange") + + async def _send(self, data: Dict[str, Any]) -> bool: + """ + 发送消息到飞书 + + Args: + data: 消息数据 + + Returns: + 是否发送成功 + """ + try: + async with httpx.AsyncClient() as client: + response = await client.post( + self.webhook_url, + json=data, + headers={"Content-Type": "application/json"}, + timeout=10.0 + ) + + result = response.json() + + if result.get('code') == 0 or result.get('StatusCode') == 0: + logger.info("飞书消息发送成功") + return True + else: + logger.error(f"飞书消息发送失败: {result}") + return False + + except Exception as e: + logger.error(f"飞书消息发送异常: {e}") + return False + + +# 全局实例(延迟初始化) +_feishu_service: Optional[FeishuService] = None + + +def get_feishu_service() -> FeishuService: + """获取飞书服务实例""" + global _feishu_service + if _feishu_service is None: + _feishu_service = FeishuService() + return _feishu_service diff --git a/backend/requirements.txt b/backend/requirements.txt index bba367f..7f06878 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -19,3 +19,7 @@ yfinance>=0.2.36 PyJWT==2.8.0 tencentcloud-sdk-python==3.0.1100 python-jose[cryptography]==3.3.0 + +# 加密货币交易智能体依赖 +python-binance>=1.0.19 +httpx>=0.27.0 diff --git a/backend/run_crypto.sh b/backend/run_crypto.sh new file mode 100755 index 0000000..a6ad8f1 --- /dev/null +++ b/backend/run_crypto.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# 加密货币智能体启动脚本 + +echo "================================" +echo "加密货币交易信号智能体" +echo "================================" +echo "" + +cd /Users/aaron/source_code/Stock_Agent/backend + +# 激活虚拟环境 +if [ ! -d "venv" ]; then + echo "❌ 虚拟环境不存在,请先运行 ../install.sh" + exit 1 +fi + +source venv/bin/activate + +# 检查依赖 +echo "1. 检查依赖..." +python3 -c "import binance; print(' ✓ python-binance')" 2>/dev/null || { + echo " 安装 python-binance..." + pip install python-binance -q +} +python3 -c "import httpx; print(' ✓ httpx')" 2>/dev/null || { + echo " 安装 httpx..." + pip install httpx -q +} + +# 测试导入 +echo "" +echo "2. 测试模块导入..." +python3 << 'EOF' +try: + from app.services.binance_service import binance_service + print(" ✓ Binance 服务") + + from app.services.feishu_service import get_feishu_service + print(" ✓ 飞书服务") + + from app.crypto_agent.crypto_agent import crypto_agent + print(" ✓ 加密货币智能体") + + print("\n所有模块导入成功!") + +except Exception as e: + print(f"\n❌ 导入失败: {e}") + import traceback + traceback.print_exc() + exit(1) +EOF + +if [ $? -ne 0 ]; then + echo "" + echo "模块导入失败,请检查错误信息" + exit 1 +fi + +# 检查配置 +echo "" +echo "3. 检查配置..." +python3 << 'EOF' +from app.config import get_settings +settings = get_settings() + +print(f" 监控交易对: {settings.crypto_symbols}") +print(f" 分析间隔: {settings.crypto_analysis_interval}秒") +print(f" 飞书 Webhook: {'✓ 已配置' if settings.feishu_webhook_url else '❌ 未配置'}") +print(f" LLM 服务: {'✓ 已配置' if settings.zhipuai_api_key or settings.deepseek_api_key else '⚠️ 未配置'}") + +if not settings.feishu_webhook_url: + print("\n⚠️ 警告: 飞书 Webhook 未配置,将无法发送通知") +EOF + +echo "" +echo "================================" +echo "启动智能体..." +echo "================================" +echo "" +echo "按 Ctrl+C 停止" +echo "" + +# 启动智能体 +python3 run_crypto_agent.py diff --git a/backend/run_crypto_agent.py b/backend/run_crypto_agent.py new file mode 100644 index 0000000..3cc3aaa --- /dev/null +++ b/backend/run_crypto_agent.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +""" +加密货币智能体启动脚本 +""" +import asyncio +import sys +import os + +# 添加项目路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app.crypto_agent.crypto_agent import crypto_agent +from app.utils.logger import logger + + +async def main(): + """主函数""" + logger.info("=" * 50) + logger.info("加密货币交易信号智能体") + logger.info("=" * 50) + + try: + await crypto_agent.run() + except KeyboardInterrupt: + logger.info("收到停止信号,正在关闭...") + crypto_agent.stop() + except Exception as e: + logger.error(f"运行出错: {e}") + raise + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/test_crypto_agent.py b/backend/test_crypto_agent.py new file mode 100644 index 0000000..c42e58a --- /dev/null +++ b/backend/test_crypto_agent.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +""" +测试加密货币智能体 - 单次分析 +""" +import asyncio +import sys +import os + +# 添加项目路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + +async def test_binance_service(): + """测试 Binance 数据服务""" + print("\n" + "=" * 50) + print("测试 Binance 数据服务") + print("=" * 50) + + from app.services.binance_service import binance_service + + # 测试获取 K 线数据 + print("\n1. 获取 BTCUSDT 1小时 K线数据...") + df = binance_service.get_klines('BTCUSDT', '1h', limit=10) + if not df.empty: + print(f" ✓ 获取成功,共 {len(df)} 条数据") + print(f" 最新收盘价: ${df.iloc[-1]['close']:,.2f}") + else: + print(" ✗ 获取失败") + return False + + # 测试多周期数据 + print("\n2. 获取 BTCUSDT 多周期数据...") + data = binance_service.get_multi_timeframe_data('BTCUSDT') + for interval, df in data.items(): + if not df.empty: + print(f" ✓ {interval}: {len(df)} 条数据") + else: + print(f" ✗ {interval}: 无数据") + + # 测试技术指标 + print("\n3. 检查技术指标...") + df = data['1h'] + indicators = ['ma5', 'ma20', 'rsi', 'macd', 'bb_upper', 'k', 'd', 'atr'] + for ind in indicators: + if ind in df.columns: + value = df.iloc[-1][ind] + print(f" ✓ {ind}: {value:.4f}" if value else f" - {ind}: N/A") + + return True + + +async def test_signal_analyzer(): + """测试信号分析器""" + print("\n" + "=" * 50) + print("测试信号分析器") + print("=" * 50) + + from app.services.binance_service import binance_service + from app.crypto_agent.signal_analyzer import SignalAnalyzer + + analyzer = SignalAnalyzer() + + # 获取数据 + data = binance_service.get_multi_timeframe_data('BTCUSDT') + + # 测试趋势分析 + print("\n1. 分析趋势...") + trend = analyzer.analyze_trend(data['1h'], data['4h']) + print(f" 趋势: {trend}") + + # 测试进场信号 + print("\n2. 分析进场信号...") + signal = analyzer.analyze_entry_signal(data['5m'], data['15m'], trend) + print(f" 动作: {signal['action']}") + print(f" 置信度: {signal['confidence']}%") + print(f" 原因: {', '.join(signal['reasons'])}") + + return True + + +async def test_feishu_service(): + """测试飞书通知服务""" + print("\n" + "=" * 50) + print("测试飞书通知服务") + print("=" * 50) + + from app.services.feishu_service import get_feishu_service + + feishu = get_feishu_service() + + if not feishu.enabled: + print(" ⚠ 飞书服务未配置,跳过测试") + return True + + # 发送测试消息 + print("\n1. 发送测试文本消息...") + result = await feishu.send_text("🧪 这是一条测试消息,来自加密货币智能体") + print(f" {'✓ 发送成功' if result else '✗ 发送失败'}") + + return result + + +async def test_full_analysis(): + """测试完整分析流程""" + print("\n" + "=" * 50) + print("测试完整分析流程") + print("=" * 50) + + from app.crypto_agent.crypto_agent import CryptoAgent + + agent = CryptoAgent() + + for symbol in ['BTCUSDT', 'ETHUSDT']: + print(f"\n分析 {symbol}...") + result = await agent.analyze_once(symbol) + + if 'error' in result: + print(f" ✗ 错误: {result['error']}") + else: + print(f" 价格: ${result['price']:,.2f}") + print(f" 趋势: {result['trend']}") + print(f" 动作: {result['action']}") + print(f" 置信度: {result['confidence']}%") + if result.get('stop_loss'): + print(f" 止损: ${result['stop_loss']:,.2f}") + if result.get('take_profit'): + print(f" 止盈: ${result['take_profit']:,.2f}") + + return True + + +async def main(): + """主测试函数""" + print("\n" + "=" * 60) + print("加密货币智能体测试") + print("=" * 60) + + tests = [ + ("Binance 数据服务", test_binance_service), + ("信号分析器", test_signal_analyzer), + ("飞书通知服务", test_feishu_service), + ("完整分析流程", test_full_analysis), + ] + + results = [] + for name, test_func in tests: + try: + result = await test_func() + results.append((name, result)) + except Exception as e: + print(f"\n✗ {name} 测试出错: {e}") + results.append((name, False)) + + # 打印总结 + print("\n" + "=" * 60) + print("测试总结") + print("=" * 60) + for name, result in results: + status = "✓ 通过" if result else "✗ 失败" + print(f" {name}: {status}") + + all_passed = all(r for _, r in results) + print("\n" + ("所有测试通过!" if all_passed else "部分测试失败")) + + return all_passed + + +if __name__ == "__main__": + success = asyncio.run(main()) + sys.exit(0 if success else 1)