添加币安
This commit is contained in:
parent
68f38d4062
commit
15add26b15
@ -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
|
||||
|
||||
8
backend/app/crypto_agent/__init__.py
Normal file
8
backend/app/crypto_agent/__init__.py
Normal 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']
|
||||
258
backend/app/crypto_agent/crypto_agent.py
Normal file
258
backend/app/crypto_agent/crypto_agent.py
Normal 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()
|
||||
670
backend/app/crypto_agent/signal_analyzer.py
Normal file
670
backend/app/crypto_agent/signal_analyzer.py
Normal 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 趋势向上(价格 > 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("价格<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)
|
||||
}
|
||||
189
backend/app/crypto_agent/strategy.py
Normal file
189
backend/app/crypto_agent/strategy.py
Normal 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>MA20,MACD>信号线,RSI>50
|
||||
- 看跌:价格在MA20下方,MA5<MA20,MACD<信号线,RSI<50
|
||||
|
||||
### 进场信号(15M + 5M)
|
||||
- 做多:RSI超卖回升、MACD金叉、突破布林中轨、KDJ低位金叉
|
||||
- 做空:RSI超买回落、MACD死叉、跌破布林中轨、KDJ高位死叉
|
||||
|
||||
### 风险管理
|
||||
- 止损:2倍ATR
|
||||
- 止盈:3倍ATR
|
||||
- 最小风险收益比:1.5
|
||||
"""
|
||||
246
backend/app/services/binance_service.py
Normal file
246
backend/app/services/binance_service.py
Normal 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()
|
||||
296
backend/app/services/feishu_service.py
Normal file
296
backend/app/services/feishu_service.py
Normal 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
|
||||
@ -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
85
backend/run_crypto.sh
Executable 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
|
||||
33
backend/run_crypto_agent.py
Normal file
33
backend/run_crypto_agent.py
Normal 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())
|
||||
170
backend/test_crypto_agent.py
Normal file
170
backend/test_crypto_agent.py
Normal 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)
|
||||
Loading…
Reference in New Issue
Block a user