This commit is contained in:
aaron 2026-02-07 00:51:58 +08:00
parent 60a5410907
commit 0198dd5345
4 changed files with 1030 additions and 223 deletions

View File

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

View File

@ -0,0 +1,443 @@
"""
LLM 驱动的信号分析器 - LLM 自主分析市场数据并给出交易信号
"""
import json
import re
import pandas as pd
from typing import Dict, Any, Optional, List
from datetime import datetime
from app.utils.logger import logger
from app.services.llm_service import llm_service
from app.services.news_service import get_news_service
class LLMSignalAnalyzer:
"""LLM 驱动的交易信号分析器"""
# 系统提示词 - 让 LLM 自主分析
SYSTEM_PROMPT = """你是一位专业的加密货币技术分析师。你的任务是综合分析市场数据和新闻舆情,判断是否存在交易机会。
## 你的分析方法
你可以自由运用你所知道的任何技术分析方法包括但不限于
- 趋势分析均线趋势线高低点
- 动量指标RSIMACDKDJ
- 波动率分析布林带ATR
- 价格形态K线形态图表形态
- 支撑阻力位
- 成交量分析
- 多周期共振
## 新闻舆情分析
你还需要结合最新的市场新闻进行分析
- 重大利好/利空消息
- 市场情绪恐慌/贪婪
- 大户/机构动向
- 监管政策变化
- 宏观经济影响
## 信号类型
请判断是否存在以下三种类型的交易机会
1. **短线信号**持仓 4小时 - 1
- 适合快速的超跌反弹或超涨回落
- 风险较高需要快速止盈止损
2. **中线信号**持仓 1-7
- 波段交易顺势回调入场
- 风险适中有明确的止损止盈
3. **长线信号**持仓 1周以上
- 趋势交易大级别趋势确认
- 风险较低止损较宽
## 输出格式
请严格按照以下 JSON 格式输出你的分析结果
```json
{
"analysis_summary": "简要描述当前市场状态50字以内",
"news_sentiment": "positive/negative/neutral",
"news_impact": "新闻对市场的影响分析30字以内",
"signals": [
{
"type": "short_term/medium_term/long_term",
"action": "buy/sell/wait",
"confidence": 0-100,
"grade": "A/B/C/D",
"entry_price": 建议入场价,
"stop_loss": 止损价,
"take_profit": 止盈价,
"reason": "详细的入场理由,说明你看到了什么技术信号和消息面因素",
"risk_warning": "风险提示"
}
],
"key_levels": {
"support": [支撑位列表],
"resistance": [阻力位列表]
}
}
```
## 信号等级说明
- **A级**技术面+消息面共振高置信度80+强烈建议入场
- **B级**信号较好置信度中等60-80可以入场
- **C级**有机会但需谨慎40-60轻仓试探
- **D级**不建议交易<40继续观望
## 重要原则
1. 宁可错过不要做错 - 没有明确信号时输出空的 signals 数组
2. 每种类型最多输出一个信号
3. 止损必须明确风险收益比至少 1:1.5
4. 如果市场混乱或数据不足直接建议观望
5. reason 字段要具体说明你看到了什么"15M RSI 从 25 回升到 35同时 MACD 金叉,且有大户加仓消息"
6. 消息面和技术面冲突时优先考虑技术面但要在 risk_warning 中提示"""
def __init__(self):
"""初始化分析器"""
self.news_service = get_news_service()
logger.info("LLM 信号分析器初始化完成(含新闻舆情)")
async def analyze(self, symbol: str, data: Dict[str, pd.DataFrame],
symbols: List[str] = None) -> Dict[str, Any]:
"""
使用 LLM 分析市场数据
Args:
symbol: 交易对 'BTCUSDT'
data: 多周期K线数据 {'5m': df, '15m': df, '1h': df, '4h': df}
symbols: 所有监控的交易对用于过滤相关新闻
Returns:
分析结果
"""
try:
# 获取新闻数据
news_text = await self._get_news_context(symbol, symbols or [symbol])
# 构建数据提示
data_prompt = self._build_data_prompt(symbol, data, news_text)
# 调用 LLM
response = llm_service.chat([
{"role": "system", "content": self.SYSTEM_PROMPT},
{"role": "user", "content": data_prompt}
])
if not response:
logger.warning(f"{symbol} LLM 分析无响应")
return self._empty_result(symbol, "LLM 无响应")
# 解析响应
result = self._parse_response(response)
result['symbol'] = symbol
result['timestamp'] = datetime.now().isoformat()
# 记录日志
signals = result.get('signals', [])
if signals:
for sig in signals:
logger.info(f"{symbol} [{sig['type']}] {sig['action']} "
f"置信度:{sig['confidence']}% 等级:{sig['grade']} "
f"原因:{sig['reason'][:50]}...")
else:
logger.info(f"{symbol} 无交易信号 - {result.get('analysis_summary', '观望')}")
return result
except Exception as e:
logger.error(f"{symbol} LLM 分析出错: {e}")
import traceback
logger.error(traceback.format_exc())
return self._empty_result(symbol, str(e))
async def _get_news_context(self, symbol: str, symbols: List[str]) -> str:
"""获取新闻上下文"""
try:
# 获取最新新闻
all_news = await self.news_service.get_latest_news(limit=30)
# 过滤相关新闻最近4小时
relevant_news = self.news_service.filter_relevant_news(
all_news, symbols=symbols, hours=4
)
if not relevant_news:
return "暂无相关新闻"
# 格式化新闻
return self.news_service.format_news_for_llm(relevant_news, max_items=8)
except Exception as e:
logger.warning(f"获取新闻失败: {e}")
return "新闻数据暂时不可用"
def _build_data_prompt(self, symbol: str, data: Dict[str, pd.DataFrame],
news_text: str = "") -> str:
"""构建数据提示词"""
parts = [f"# {symbol} 市场数据分析\n"]
parts.append(f"分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
# 当前价格
if '5m' in data and not data['5m'].empty:
current_price = float(data['5m'].iloc[-1]['close'])
parts.append(f"**当前价格**: ${current_price:,.2f}\n")
# 各周期数据
for interval in ['4h', '1h', '15m', '5m']:
df = data.get(interval)
if df is None or df.empty:
continue
parts.append(f"\n## {interval.upper()} 周期数据")
# 最新指标
latest = df.iloc[-1]
parts.append(self._format_indicators(latest))
# 最近 K 线数据
parts.append(self._format_recent_klines(df, interval))
# 添加新闻数据
if news_text and news_text != "暂无相关新闻":
parts.append(f"\n{news_text}")
parts.append("\n---")
parts.append("请综合分析以上技术数据和新闻舆情,判断是否存在短线、中线或长线的交易机会。")
parts.append("如果没有明确的交易机会signals 数组返回空即可。")
return "\n".join(parts)
def _format_indicators(self, row: pd.Series) -> str:
"""格式化指标数据"""
lines = []
# 价格
close = row.get('close', 0)
open_price = row.get('open', 0)
high = row.get('high', 0)
low = row.get('low', 0)
change = ((close - open_price) / open_price * 100) if open_price else 0
lines.append(f"- K线: O={open_price:.2f} H={high:.2f} L={low:.2f} C={close:.2f} ({change:+.2f}%)")
# 均线
ma5 = row.get('ma5', 0)
ma10 = row.get('ma10', 0)
ma20 = row.get('ma20', 0)
ma50 = row.get('ma50', 0)
if pd.notna(ma20):
ma_str = f"- 均线: MA5={ma5:.2f}, MA10={ma10:.2f}, MA20={ma20:.2f}"
if pd.notna(ma50):
ma_str += f", MA50={ma50:.2f}"
lines.append(ma_str)
# RSI
rsi = row.get('rsi', 0)
if pd.notna(rsi):
rsi_status = "超卖" if rsi < 30 else ("超买" if rsi > 70 else "中性")
lines.append(f"- RSI: {rsi:.1f} ({rsi_status})")
# MACD
macd = row.get('macd', 0)
macd_signal = row.get('macd_signal', 0)
macd_hist = row.get('macd_hist', 0)
if pd.notna(macd):
macd_status = "多头" if macd > macd_signal else "空头"
lines.append(f"- MACD: DIF={macd:.4f}, DEA={macd_signal:.4f}, 柱={macd_hist:.4f} ({macd_status})")
# KDJ
k = row.get('k', 0)
d = row.get('d', 0)
j = row.get('j', 0)
if pd.notna(k):
lines.append(f"- KDJ: K={k:.1f}, D={d:.1f}, J={j:.1f}")
# 布林带
bb_upper = row.get('bb_upper', 0)
bb_middle = row.get('bb_middle', 0)
bb_lower = row.get('bb_lower', 0)
if pd.notna(bb_upper):
lines.append(f"- 布林带: 上={bb_upper:.2f}, 中={bb_middle:.2f}, 下={bb_lower:.2f}")
# ATR
atr = row.get('atr', 0)
if pd.notna(atr):
lines.append(f"- ATR: {atr:.2f}")
# 成交量
volume = row.get('volume', 0)
volume_ratio = row.get('volume_ratio', 0)
if pd.notna(volume_ratio):
vol_status = "放量" if volume_ratio > 1.5 else ("缩量" if volume_ratio < 0.5 else "正常")
lines.append(f"- 成交量: {volume:.2f}, 量比={volume_ratio:.2f} ({vol_status})")
return "\n".join(lines)
def _format_recent_klines(self, df: pd.DataFrame, interval: str) -> str:
"""格式化最近 K 线"""
# 根据周期决定显示数量
count = {'4h': 6, '1h': 12, '15m': 8, '5m': 6}.get(interval, 6)
if len(df) < count:
count = len(df)
lines = [f"\n最近 {count} 根 K 线:"]
lines.append("| 时间 | 开盘 | 最高 | 最低 | 收盘 | 涨跌 | RSI |")
lines.append("|------|------|------|------|------|------|-----|")
for i in range(-count, 0):
row = df.iloc[i]
change = ((row['close'] - row['open']) / row['open'] * 100) if row['open'] else 0
change_str = f"{change:+.2f}%"
time_str = row['open_time'].strftime('%m-%d %H:%M') if pd.notna(row.get('open_time')) else 'N/A'
rsi = row.get('rsi', 0)
rsi_str = f"{rsi:.0f}" if pd.notna(rsi) else "-"
lines.append(f"| {time_str} | {row['open']:.2f} | {row['high']:.2f} | "
f"{row['low']:.2f} | {row['close']:.2f} | {change_str} | {rsi_str} |")
return "\n".join(lines)
def _parse_response(self, response: str) -> Dict[str, Any]:
"""解析 LLM 响应"""
result = {
'raw_response': response,
'analysis_summary': '',
'signals': [],
'key_levels': {'support': [], 'resistance': []}
}
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
parsed = json.loads(json_str)
result['analysis_summary'] = parsed.get('analysis_summary', '')
result['signals'] = parsed.get('signals', [])
result['key_levels'] = parsed.get('key_levels', {'support': [], 'resistance': []})
# 验证和清理信号
valid_signals = []
for sig in result['signals']:
if self._validate_signal(sig):
valid_signals.append(sig)
result['signals'] = valid_signals
except json.JSONDecodeError:
logger.warning("LLM 响应不是有效 JSON尝试提取关键信息")
result['analysis_summary'] = self._extract_summary(response)
return result
def _validate_signal(self, signal: Dict[str, Any]) -> bool:
"""验证信号是否有效"""
required_fields = ['type', 'action', 'confidence', 'grade', 'reason']
for field in required_fields:
if field not in signal:
return False
# 验证类型
if signal['type'] not in ['short_term', 'medium_term', 'long_term']:
return False
# 验证动作
if signal['action'] not in ['buy', 'sell', 'wait']:
return False
# wait 动作不算有效信号
if signal['action'] == 'wait':
return False
# 验证置信度
confidence = signal.get('confidence', 0)
if not isinstance(confidence, (int, float)) or confidence < 40:
return False
return True
def _extract_summary(self, text: str) -> str:
"""从文本中提取摘要"""
text = text.strip()
if len(text) > 100:
return text[:100] + "..."
return text
def _empty_result(self, symbol: str, reason: str = "") -> Dict[str, Any]:
"""返回空结果"""
return {
'symbol': symbol,
'timestamp': datetime.now().isoformat(),
'analysis_summary': reason or '无法分析',
'signals': [],
'key_levels': {'support': [], 'resistance': []},
'error': reason
}
def get_best_signal(self, result: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
从分析结果中获取最佳信号
Args:
result: analyze() 的返回结果
Returns:
最佳信号如果没有则返回 None
"""
signals = result.get('signals', [])
if not signals:
return None
# 按置信度排序
sorted_signals = sorted(signals, key=lambda x: x.get('confidence', 0), reverse=True)
return sorted_signals[0]
def format_signal_message(self, signal: Dict[str, Any], symbol: str) -> str:
"""
格式化信号消息用于通知
Args:
signal: 信号数据
symbol: 交易对
Returns:
格式化的消息文本
"""
type_map = {
'short_term': '短线',
'medium_term': '中线',
'long_term': '长线'
}
action_map = {
'buy': '做多',
'sell': '做空'
}
signal_type = type_map.get(signal['type'], signal['type'])
action = action_map.get(signal['action'], signal['action'])
grade = signal.get('grade', 'C')
confidence = signal.get('confidence', 0)
# 等级图标
grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '', 'D': ''}.get(grade, '')
message = f"""📊 {symbol} {signal_type}信号
方向: {action}
等级: {grade} {grade_icon}
置信度: {confidence}%
入场价: ${signal.get('entry_price', 0):,.2f}
止损价: ${signal.get('stop_loss', 0):,.2f}
止盈价: ${signal.get('take_profit', 0):,.2f}
📝 分析理由:
{signal.get('reason', '')}
风险提示:
{signal.get('risk_warning', '请注意风险控制')}"""
return message

View File

@ -0,0 +1,272 @@
"""
新闻舆情服务 - 获取加密货币相关新闻
"""
import re
import html
import aiohttp
import xml.etree.ElementTree as ET
from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
from app.utils.logger import logger
class NewsService:
"""新闻舆情服务"""
# 律动快讯 RSS
BLOCKBEATS_RSS = "https://api.theblockbeats.news/v2/rss/newsflash"
def __init__(self):
"""初始化新闻服务"""
self._cache: List[Dict[str, Any]] = []
self._cache_time: Optional[datetime] = None
self._cache_duration = timedelta(minutes=5) # 缓存5分钟
logger.info("新闻舆情服务初始化完成")
async def get_latest_news(self, limit: int = 20) -> List[Dict[str, Any]]:
"""
获取最新新闻
Args:
limit: 获取数量
Returns:
新闻列表
"""
# 检查缓存
if self._cache and self._cache_time:
if datetime.now() - self._cache_time < self._cache_duration:
return self._cache[:limit]
try:
news = await self._fetch_blockbeats_news()
self._cache = news
self._cache_time = datetime.now()
return news[:limit]
except Exception as e:
logger.error(f"获取新闻失败: {e}")
return self._cache[:limit] if self._cache else []
async def _fetch_blockbeats_news(self) -> List[Dict[str, Any]]:
"""获取律动快讯"""
news_list = []
try:
async with aiohttp.ClientSession() as session:
async with session.get(self.BLOCKBEATS_RSS, timeout=10) as response:
if response.status != 200:
logger.error(f"获取律动快讯失败: HTTP {response.status}")
return []
content = await response.text()
# 解析 XML
root = ET.fromstring(content)
channel = root.find('channel')
if channel is None:
return []
for item in channel.findall('item'):
title_elem = item.find('title')
desc_elem = item.find('description')
pub_date_elem = item.find('pubDate')
link_elem = item.find('link')
if title_elem is None:
continue
# 提取标题
title = self._clean_cdata(title_elem.text or '')
# 提取描述(去除 HTML 标签)
description = ''
if desc_elem is not None and desc_elem.text:
description = self._clean_html(self._clean_cdata(desc_elem.text))
# 解析时间
pub_time = None
if pub_date_elem is not None and pub_date_elem.text:
pub_time = self._parse_rss_date(self._clean_cdata(pub_date_elem.text))
# 链接
link = ''
if link_elem is not None and link_elem.text:
link = self._clean_cdata(link_elem.text)
news_list.append({
'title': title,
'description': description[:500], # 限制长度
'time': pub_time,
'time_str': pub_time.strftime('%m-%d %H:%M') if pub_time else '',
'link': link,
'source': '律动BlockBeats'
})
logger.info(f"获取到 {len(news_list)} 条律动快讯")
return news_list
except Exception as e:
logger.error(f"解析律动快讯失败: {e}")
return []
def _clean_cdata(self, text: str) -> str:
"""清理 CDATA 标记"""
if not text:
return ''
# 移除 CDATA 包装
text = re.sub(r'<!\[CDATA\[(.*?)\]\]>', r'\1', text, flags=re.DOTALL)
return text.strip()
def _clean_html(self, text: str) -> str:
"""清理 HTML 标签"""
if not text:
return ''
# 移除 HTML 标签
text = re.sub(r'<[^>]+>', '', text)
# 解码 HTML 实体
text = html.unescape(text)
# 清理多余空白
text = re.sub(r'\s+', ' ', text)
return text.strip()
def _parse_rss_date(self, date_str: str) -> Optional[datetime]:
"""解析 RSS 日期格式"""
if not date_str:
return None
# RSS 日期格式: "Sat, 07 Feb 2026 00:30:33 +0800"
formats = [
'%a, %d %b %Y %H:%M:%S %z',
'%a, %d %b %Y %H:%M:%S',
'%Y-%m-%d %H:%M:%S'
]
for fmt in formats:
try:
return datetime.strptime(date_str, fmt)
except ValueError:
continue
return None
def filter_relevant_news(self, news_list: List[Dict[str, Any]],
symbols: List[str] = None,
hours: int = 4) -> List[Dict[str, Any]]:
"""
过滤相关新闻
Args:
news_list: 新闻列表
symbols: 关注的交易对 ['BTCUSDT', 'ETHUSDT']
hours: 只保留最近几小时的新闻
Returns:
过滤后的新闻
"""
if not news_list:
return []
# 时间过滤
cutoff_time = datetime.now() - timedelta(hours=hours)
filtered = []
# 关键词映射
symbol_keywords = {
'BTCUSDT': ['比特币', 'BTC', 'Bitcoin'],
'ETHUSDT': ['以太坊', 'ETH', 'Ethereum'],
'BNBUSDT': ['BNB', 'Binance'],
'SOLUSDT': ['SOL', 'Solana'],
}
# 通用关键词(影响整体市场)
market_keywords = [
'市场', '行情', '反弹', '下跌', '暴跌', '暴涨', '清算',
'资金费率', '多单', '空单', '杠杆', '爆仓',
'美联储', 'Fed', '利率', '通胀',
'监管', 'SEC', 'ETF',
'鲸鱼', '巨鲸', '大户',
'交易所', 'Binance', 'Coinbase'
]
for news in news_list:
# 时间过滤
if news.get('time'):
# 处理带时区的时间
news_time = news['time']
if news_time.tzinfo:
news_time = news_time.replace(tzinfo=None)
if news_time < cutoff_time:
continue
title = news.get('title', '')
desc = news.get('description', '')
content = title + ' ' + desc
# 检查是否与关注的交易对相关
is_relevant = False
if symbols:
for symbol in symbols:
keywords = symbol_keywords.get(symbol, [])
for kw in keywords:
if kw.lower() in content.lower():
is_relevant = True
news['related_symbol'] = symbol
break
if is_relevant:
break
# 检查是否包含市场关键词
if not is_relevant:
for kw in market_keywords:
if kw.lower() in content.lower():
is_relevant = True
news['related_symbol'] = 'MARKET'
break
if is_relevant:
filtered.append(news)
return filtered
def format_news_for_llm(self, news_list: List[Dict[str, Any]],
max_items: int = 10) -> str:
"""
格式化新闻供 LLM 分析
Args:
news_list: 新闻列表
max_items: 最大条数
Returns:
格式化的新闻文本
"""
if not news_list:
return "暂无相关新闻"
lines = ["## 最新市场新闻\n"]
for i, news in enumerate(news_list[:max_items], 1):
time_str = news.get('time_str', '')
title = news.get('title', '')
desc = news.get('description', '')[:200] # 限制描述长度
lines.append(f"### {i}. [{time_str}] {title}")
if desc:
lines.append(f"{desc}")
lines.append("")
return "\n".join(lines)
# 全局实例
_news_service: Optional[NewsService] = None
def get_news_service() -> NewsService:
"""获取新闻服务实例"""
global _news_service
if _news_service is None:
_news_service = NewsService()
return _news_service

View File

@ -364,6 +364,74 @@
border-color: #ff4444; border-color: #ff4444;
color: #ff4444; color: #ff4444;
} }
/* 持仓汇总 */
.position-summary {
display: flex;
gap: 24px;
padding: 16px 20px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 4px;
margin-bottom: 16px;
}
.summary-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.summary-label {
font-size: 12px;
color: var(--text-secondary);
}
.summary-value {
font-size: 18px;
font-weight: 500;
color: var(--text-primary);
}
.summary-value.positive {
color: #00ff41;
}
.summary-value.negative {
color: #ff4444;
}
/* 现价样式 */
.current-price {
font-family: monospace;
font-weight: 500;
}
.current-price.price-up {
color: #00ff41;
}
.current-price.price-down {
color: #ff4444;
}
/* 警告价格(接近止损) */
.warning-price {
color: #ff4444;
font-weight: 500;
}
/* 成功价格(接近止盈) */
.success-price {
color: #00ff41;
font-weight: 500;
}
/* 盈亏小字 */
.pnl small {
font-size: 11px;
opacity: 0.8;
}
</style> </style>
</head> </head>
<body> <body>
@ -438,6 +506,25 @@
<!-- 活跃订单 --> <!-- 活跃订单 -->
<div v-if="activeTab === 'active'"> <div v-if="activeTab === 'active'">
<!-- 持仓汇总 -->
<div v-if="activeOrders.length > 0" class="position-summary">
<div class="summary-item">
<span class="summary-label">持仓数量</span>
<span class="summary-value">{{ activeOrders.length }}</span>
</div>
<div class="summary-item">
<span class="summary-label">总仓位</span>
<span class="summary-value">${{ totalPosition.toFixed(2) }}</span>
</div>
<div class="summary-item">
<span class="summary-label">浮动盈亏</span>
<span class="summary-value" :class="totalUnrealizedPnl >= 0 ? 'positive' : 'negative'">
{{ totalUnrealizedPnl >= 0 ? '+' : '' }}${{ totalUnrealizedPnl.toFixed(2) }}
({{ totalUnrealizedPnlPercent >= 0 ? '+' : '' }}{{ totalUnrealizedPnlPercent.toFixed(2) }}%)
</span>
</div>
</div>
<div v-if="loading" class="loading">加载中...</div> <div v-if="loading" class="loading">加载中...</div>
<div v-else-if="activeOrders.length === 0" class="empty-state"> <div v-else-if="activeOrders.length === 0" class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
@ -453,6 +540,8 @@
<th>方向</th> <th>方向</th>
<th>等级</th> <th>等级</th>
<th>入场价</th> <th>入场价</th>
<th>现价</th>
<th>浮动盈亏</th>
<th>止损</th> <th>止损</th>
<th>止盈</th> <th>止盈</th>
<th>仓位</th> <th>仓位</th>
@ -467,8 +556,28 @@
<td><span class="side-badge" :class="order.side">{{ order.side === 'long' ? '做多' : '做空' }}</span></td> <td><span class="side-badge" :class="order.side">{{ order.side === 'long' ? '做多' : '做空' }}</span></td>
<td><span class="grade-badge" :class="order.signal_grade">{{ order.signal_grade }}</span></td> <td><span class="grade-badge" :class="order.signal_grade">{{ order.signal_grade }}</span></td>
<td>${{ order.entry_price?.toLocaleString() }}</td> <td>${{ order.entry_price?.toLocaleString() }}</td>
<td>${{ order.stop_loss?.toLocaleString() }}</td> <td>
<td>${{ order.take_profit?.toLocaleString() }}</td> <span class="current-price" :class="getPriceChangeClass(order)">
${{ getCurrentPrice(order.symbol)?.toLocaleString() || '-' }}
</span>
</td>
<td>
<span class="pnl" :class="getUnrealizedPnl(order).pnl >= 0 ? 'positive' : 'negative'">
{{ getUnrealizedPnl(order).pnl >= 0 ? '+' : '' }}{{ getUnrealizedPnl(order).percent.toFixed(2) }}%
<br>
<small>(${{ getUnrealizedPnl(order).pnl >= 0 ? '+' : '' }}{{ getUnrealizedPnl(order).pnl.toFixed(2) }})</small>
</span>
</td>
<td>
<span :class="isNearStopLoss(order) ? 'warning-price' : ''">
${{ order.stop_loss?.toLocaleString() }}
</span>
</td>
<td>
<span :class="isNearTakeProfit(order) ? 'success-price' : ''">
${{ order.take_profit?.toLocaleString() }}
</span>
</td>
<td>${{ order.quantity }}</td> <td>${{ order.quantity }}</td>
<td>{{ formatTime(order.opened_at) }}</td> <td>{{ formatTime(order.opened_at) }}</td>
<td> <td>
@ -611,10 +720,10 @@
}, },
mounted() { mounted() {
this.refreshData(); this.refreshData();
// 每10秒自动刷新 // 每3秒自动刷新实时价格更新
this.refreshInterval = setInterval(() => { this.refreshInterval = setInterval(() => {
this.refreshData(); this.refreshData();
}, 10000); }, 3000);
}, },
beforeUnmount() { beforeUnmount() {
if (this.refreshInterval) { if (this.refreshInterval) {
@ -719,6 +828,82 @@
'closed_manual': '手动平仓' 'closed_manual': '手动平仓'
}; };
return map[status] || status; return map[status] || status;
},
// 获取当前价格
getCurrentPrice(symbol) {
return this.latestPrices[symbol] || null;
},
// 计算未实现盈亏
getUnrealizedPnl(order) {
const currentPrice = this.getCurrentPrice(order.symbol);
if (!currentPrice || !order.entry_price) {
return { pnl: 0, percent: 0 };
}
let pnlPercent;
if (order.side === 'long') {
pnlPercent = ((currentPrice - order.entry_price) / order.entry_price) * 100;
} else {
pnlPercent = ((order.entry_price - currentPrice) / order.entry_price) * 100;
}
const pnlAmount = (order.quantity || 0) * pnlPercent / 100;
return {
pnl: pnlAmount,
percent: pnlPercent
};
},
// 获取价格变化样式类
getPriceChangeClass(order) {
const currentPrice = this.getCurrentPrice(order.symbol);
if (!currentPrice || !order.entry_price) return '';
if (order.side === 'long') {
return currentPrice >= order.entry_price ? 'price-up' : 'price-down';
} else {
return currentPrice <= order.entry_price ? 'price-up' : 'price-down';
}
},
// 检查是否接近止损(距离止损 < 1%
isNearStopLoss(order) {
const currentPrice = this.getCurrentPrice(order.symbol);
if (!currentPrice || !order.stop_loss) return false;
const distance = Math.abs(currentPrice - order.stop_loss) / currentPrice;
return distance < 0.01;
},
// 检查是否接近止盈(距离止盈 < 1%
isNearTakeProfit(order) {
const currentPrice = this.getCurrentPrice(order.symbol);
if (!currentPrice || !order.take_profit) return false;
const distance = Math.abs(currentPrice - order.take_profit) / currentPrice;
return distance < 0.01;
}
},
computed: {
// 总仓位
totalPosition() {
return this.activeOrders.reduce((sum, order) => sum + (order.quantity || 0), 0);
},
// 总浮动盈亏
totalUnrealizedPnl() {
return this.activeOrders.reduce((sum, order) => {
return sum + this.getUnrealizedPnl(order).pnl;
}, 0);
},
// 总浮动盈亏百分比
totalUnrealizedPnlPercent() {
if (this.totalPosition === 0) return 0;
return (this.totalUnrealizedPnl / this.totalPosition) * 100;
} }
} }
}).mount('#app'); }).mount('#app');