添加币安

This commit is contained in:
aaron 2026-02-05 22:23:41 +08:00
parent 68f38d4062
commit 15add26b15
11 changed files with 1974 additions and 0 deletions

View File

@ -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

View File

@ -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']

View File

@ -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()

View File

@ -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 趋势向上价格 > MA20MACD > 0 或底背离
2. 1H 回调到支撑位MA20 附近或前低
3. 15M 出现止跌信号RSI < 40 回升 MACD 金叉
4. 止损明确前低下方风险收益比 >= 1:2
## 入场条件(波段做空)
1. 4H 趋势向下价格 < MA20MACD < 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("价格<MA20")
count += 1
# MA5 与 MA20 关系
if 'ma5' in data and 'ma20' in data and pd.notna(data['ma5']) and pd.notna(data['ma20']):
if data['ma5'] > data['ma20']:
score += 1
details.append("MA5>MA20")
else:
score -= 1
details.append("MA5<MA20")
count += 1
# MACD
if 'macd' in data and 'macd_signal' in data and pd.notna(data['macd']):
if data['macd'] > 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)
}

View File

@ -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>MA20MACD>信号线RSI>50
- 看跌价格在MA20下方MA5<MA20MACD<信号线RSI<50
### 进场信号15M + 5M
- 做多RSI超卖回升MACD金叉突破布林中轨KDJ低位金叉
- 做空RSI超买回落MACD死叉跌破布林中轨KDJ高位死叉
### 风险管理
- 止损2倍ATR
- 止盈3倍ATR
- 最小风险收益比1.5
"""

View File

@ -0,0 +1,246 @@
"""
Binance 数据服务 - 获取加密货币 K 线数据和技术指标
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Any
from binance.client import Client
from binance.enums import (
KLINE_INTERVAL_5MINUTE,
KLINE_INTERVAL_15MINUTE,
KLINE_INTERVAL_1HOUR,
KLINE_INTERVAL_4HOUR
)
from app.utils.logger import logger
class BinanceService:
"""Binance 数据服务"""
# K线周期映射
INTERVALS = {
'5m': KLINE_INTERVAL_5MINUTE,
'15m': KLINE_INTERVAL_15MINUTE,
'1h': KLINE_INTERVAL_1HOUR,
'4h': KLINE_INTERVAL_4HOUR
}
def __init__(self, api_key: str = "", api_secret: str = ""):
"""
初始化 Binance 客户端
Args:
api_key: API 密钥可选公开数据不需要
api_secret: API 密钥可选
"""
self.client = Client(api_key=api_key, api_secret=api_secret)
logger.info("Binance 服务初始化完成")
def get_klines(self, symbol: str, interval: str, limit: int = 100) -> 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()

View File

@ -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

View File

@ -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

85
backend/run_crypto.sh Executable file
View File

@ -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

View File

@ -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())

View File

@ -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)