diff --git a/backend/app/crypto_agent/llm_signal_analyzer.py b/backend/app/crypto_agent/llm_signal_analyzer.py index 4fe1bcb..2c6493a 100644 --- a/backend/app/crypto_agent/llm_signal_analyzer.py +++ b/backend/app/crypto_agent/llm_signal_analyzer.py @@ -50,6 +50,11 @@ class LLMSignalAnalyzer: - 趋势交易,大级别趋势确认 - 风险较低,止损较宽 +## 入场方式 +你需要明确指定入场方式: +- **market**:现价立即入场 - 当前价格就是好的入场点,建议立即开仓 +- **limit**:挂单等待入场 - 等价格回调/突破到指定位置再入场 + ## 输出格式 请严格按照以下 JSON 格式输出你的分析结果: @@ -62,6 +67,7 @@ class LLMSignalAnalyzer: { "type": "short_term/medium_term/long_term", "action": "buy/sell/wait", + "entry_type": "market/limit", "confidence": 0-100, "grade": "A/B/C/D", "entry_price": 建议入场价, @@ -90,7 +96,9 @@ class LLMSignalAnalyzer: 3. 止损必须明确,风险收益比至少 1:1.5 4. 如果市场混乱或数据不足,直接建议观望 5. reason 字段要具体说明你看到了什么(如"15M RSI 从 25 回升到 35,同时 MACD 金叉,且有大户加仓消息") -6. 消息面和技术面冲突时,优先考虑技术面,但要在 risk_warning 中提示""" +6. 消息面和技术面冲突时,优先考虑技术面,但要在 risk_warning 中提示 +7. entry_type 必须明确:如果当前价格合适立即入场用 market,如果需要等待更好价位用 limit +8. limit 挂单的 entry_price 应该是你期望的入场价位,而不是当前价格""" def __init__(self): """初始化分析器""" @@ -352,11 +360,16 @@ class LLMSignalAnalyzer: if signal['action'] == 'wait': return False - # 验证置信度 + # 验证置信度(必须 >= 80 才算有效信号) confidence = signal.get('confidence', 0) - if not isinstance(confidence, (int, float)) or confidence < 40: + if not isinstance(confidence, (int, float)) or confidence < 80: return False + # 验证入场类型(默认为 market) + entry_type = signal.get('entry_type', 'market') + if entry_type not in ['market', 'limit']: + signal['entry_type'] = 'market' # 默认现价入场 + return True def _extract_summary(self, text: str) -> str: @@ -420,6 +433,7 @@ class LLMSignalAnalyzer: action = action_map.get(signal['action'], signal['action']) grade = signal.get('grade', 'C') confidence = signal.get('confidence', 0) + entry_type = signal.get('entry_type', 'market') # 等级图标 grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(grade, '') @@ -427,6 +441,10 @@ class LLMSignalAnalyzer: # 方向图标 action_icon = '🟢' if signal['action'] == 'buy' else '🔴' + # 入场类型 + entry_type_text = '现价入场' if entry_type == 'market' else '挂单等待' + entry_type_icon = '⚡' if entry_type == 'market' else '⏳' + # 计算风险收益比 entry = signal.get('entry_price', 0) sl = signal.get('stop_loss', 0) @@ -437,6 +455,7 @@ class LLMSignalAnalyzer: message = f"""📊 {symbol} {signal_type}信号 {action_icon} **方向**: {action} +{entry_type_icon} **入场**: {entry_type_text} ⭐ **等级**: {grade} {grade_icon} 📈 **置信度**: {confidence}% @@ -477,16 +496,21 @@ class LLMSignalAnalyzer: action = action_map.get(signal['action'], signal['action']) grade = signal.get('grade', 'C') confidence = signal.get('confidence', 0) + entry_type = signal.get('entry_type', 'market') # 等级图标 grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(grade, '') + # 入场类型 + entry_type_text = '现价入场' if entry_type == 'market' else '挂单等待' + entry_type_icon = '⚡' if entry_type == 'market' else '⏳' + # 标题和颜色 if signal['action'] == 'buy': - title = f"🟢 {symbol} {signal_type}做多信号" + title = f"🟢 {symbol} {signal_type}做多信号 [{entry_type_text}]" color = "green" else: - title = f"🔴 {symbol} {signal_type}做空信号" + title = f"🔴 {symbol} {signal_type}做空信号 [{entry_type_text}]" color = "red" # 计算风险收益比 @@ -499,6 +523,7 @@ class LLMSignalAnalyzer: # 构建 Markdown 内容 content_parts = [ f"**{signal_type}** | **{grade}**{grade_icon} | **{confidence}%** 置信度", + f"{entry_type_icon} **入场方式**: {entry_type_text}", "", f"💰 **入场**: ${entry:,.2f}", f"🛑 **止损**: ${sl:,.2f} ({sl_percent:+.1f}%)", diff --git a/backend/app/models/paper_trading.py b/backend/app/models/paper_trading.py index a8d2b55..c8cdf69 100644 --- a/backend/app/models/paper_trading.py +++ b/backend/app/models/paper_trading.py @@ -31,6 +31,12 @@ class SignalGrade(str, Enum): D = "D" +class EntryType(str, Enum): + """入场类型""" + MARKET = "market" # 现价入场 + LIMIT = "limit" # 挂单入场 + + class PaperOrder(Base): """模拟交易订单表""" __tablename__ = "paper_orders" @@ -59,6 +65,7 @@ class PaperOrder(Base): signal_type = Column(String(20), default="swing") # swing / short_term confidence = Column(Float, default=0) # 置信度 (0-100) trend = Column(String(20), nullable=True) # 趋势方向 + entry_type = Column(SQLEnum(EntryType), default=EntryType.MARKET) # 入场类型 # 订单状态 status = Column(SQLEnum(OrderStatus), default=OrderStatus.PENDING, index=True) @@ -98,6 +105,7 @@ class PaperOrder(Base): 'signal_type': self.signal_type, 'confidence': self.confidence, 'trend': self.trend, + 'entry_type': self.entry_type.value if self.entry_type else 'market', 'status': self.status.value if self.status else None, 'pnl_amount': self.pnl_amount, 'pnl_percent': self.pnl_percent, diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index 7b0ece9..85e5274 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -5,7 +5,7 @@ import uuid from datetime import datetime, timedelta from typing import Dict, Any, List, Optional -from app.models.paper_trading import PaperOrder, OrderStatus, OrderSide, SignalGrade +from app.models.paper_trading import PaperOrder, OrderStatus, OrderSide, SignalGrade, EntryType from app.services.db_service import db_service from app.config import get_settings from app.utils.logger import logger @@ -59,7 +59,7 @@ class PaperTradingService: finally: db.close() - def create_order_from_signal(self, signal: Dict[str, Any]) -> Optional[PaperOrder]: + def create_order_from_signal(self, signal: Dict[str, Any], current_price: float = None) -> Optional[PaperOrder]: """ 从交易信号创建模拟订单 @@ -67,14 +67,15 @@ class PaperTradingService: signal: 交易信号 - symbol: 交易对 - action: 'buy' 或 'sell' - - price: 入场价 + - entry_type: 'market' 或 'limit' + - price / entry_price: 入场价 - stop_loss: 止损价 - take_profit: 止盈价 - confidence: 置信度 - - signal_grade: 信号等级 - - signal_type: 信号类型 - - reasons: 入场原因 - - indicators: 技术指标 + - signal_grade / grade: 信号等级 + - signal_type / type: 信号类型 + - reason: 入场原因 + current_price: 当前价格(用于市价单) Returns: 创建的订单或 None @@ -84,7 +85,7 @@ class PaperTradingService: return None # 获取信号等级 - grade = signal.get('signal_grade', 'D') + grade = signal.get('signal_grade') or signal.get('grade', 'D') if grade == 'D': logger.info(f"D级信号不开仓: {signal.get('symbol')}") return None @@ -97,28 +98,48 @@ class PaperTradingService: # 确定订单方向 side = OrderSide.LONG if action == 'buy' else OrderSide.SHORT + # 确定入场类型 + entry_type_str = signal.get('entry_type', 'market') + entry_type = EntryType.LIMIT if entry_type_str == 'limit' else EntryType.MARKET + + # 获取入场价 + entry_price = signal.get('entry_price') or signal.get('price', 0) + # 生成订单ID symbol = signal.get('symbol', 'UNKNOWN') order_id = f"PT-{symbol}-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}" + # 确定订单状态和成交价 + if entry_type == EntryType.MARKET: + # 现价单:立即开仓 + status = OrderStatus.OPEN + filled_price = current_price if current_price else entry_price + opened_at = datetime.utcnow() + else: + # 挂单:等待触发 + status = OrderStatus.PENDING + filled_price = None + opened_at = None + db = db_service.get_session() try: order = PaperOrder( order_id=order_id, symbol=symbol, side=side, - entry_price=signal.get('price', 0), + entry_price=entry_price, stop_loss=signal.get('stop_loss', 0), take_profit=signal.get('take_profit', 0), - filled_price=signal.get('price', 0), # 市价成交 + filled_price=filled_price, quantity=quantity, signal_grade=SignalGrade(grade), - signal_type=signal.get('signal_type', 'swing'), + signal_type=signal.get('signal_type') or signal.get('type', 'swing'), confidence=signal.get('confidence', 0), trend=signal.get('trend'), - status=OrderStatus.OPEN, - opened_at=datetime.utcnow(), - entry_reasons=signal.get('reasons', []), + entry_type=entry_type, + status=status, + opened_at=opened_at, + entry_reasons=[signal.get('reason', '')] if signal.get('reason') else signal.get('reasons', []), indicators=signal.get('indicators', {}) ) @@ -129,7 +150,9 @@ class PaperTradingService: # 添加到活跃订单缓存 self.active_orders[order.order_id] = order - logger.info(f"创建模拟订单: {order_id} | {symbol} {side.value} @ ${order.entry_price:,.2f} | 仓位: ${quantity}") + entry_type_text = "现价" if entry_type == EntryType.MARKET else "挂单" + status_text = "已开仓" if status == OrderStatus.OPEN else "等待触发" + logger.info(f"创建模拟订单: {order_id} | {symbol} {side.value} [{entry_type_text}] @ ${entry_price:,.2f} | {status_text} | 仓位: ${quantity}") return order except Exception as e: @@ -141,22 +164,32 @@ class PaperTradingService: def check_price_triggers(self, symbol: str, current_price: float) -> List[Dict[str, Any]]: """ - 检查当前价格是否触发止盈止损 + 检查当前价格是否触发挂单入场或止盈止损 Args: symbol: 交易对 current_price: 当前价格 Returns: - 触发的订单结果列表 + 触发的订单结果列表(平仓结果) """ triggered = [] - orders_to_check = [ + + # 1. 检查挂单是否触发入场 + pending_orders = [ + order for order in self.active_orders.values() + if order.symbol == symbol and order.status == OrderStatus.PENDING + ] + for order in pending_orders: + if self._check_pending_entry(order, current_price): + logger.info(f"挂单触发入场: {order.order_id} | {symbol} @ ${current_price:,.2f}") + + # 2. 检查持仓订单是否触发止盈止损 + open_orders = [ order for order in self.active_orders.values() if order.symbol == symbol and order.status == OrderStatus.OPEN ] - - for order in orders_to_check: + for order in open_orders: result = self._check_order_trigger(order, current_price) if result: triggered.append(result) @@ -166,6 +199,49 @@ class PaperTradingService: return triggered + def _check_pending_entry(self, order: PaperOrder, current_price: float) -> bool: + """ + 检查挂单是否触发入场 + + 做多挂单:价格下跌到入场价时触发(买入) + 做空挂单:价格上涨到入场价时触发(卖出) + """ + should_trigger = False + + if order.side == OrderSide.LONG: + # 做多:价格 <= 入场价 触发 + if current_price <= order.entry_price: + should_trigger = True + else: + # 做空:价格 >= 入场价 触发 + if current_price >= order.entry_price: + should_trigger = True + + if should_trigger: + return self._activate_pending_order(order, current_price) + + return False + + def _activate_pending_order(self, order: PaperOrder, filled_price: float) -> bool: + """激活挂单,转为持仓""" + db = db_service.get_session() + try: + order.status = OrderStatus.OPEN + order.filled_price = filled_price + order.opened_at = datetime.utcnow() + + db.merge(order) + db.commit() + + logger.info(f"挂单已激活: {order.order_id} | {order.symbol} {order.side.value} @ ${filled_price:,.2f}") + return True + except Exception as e: + logger.error(f"激活挂单失败: {e}") + db.rollback() + return False + finally: + db.close() + def _check_order_trigger(self, order: PaperOrder, current_price: float) -> Optional[Dict[str, Any]]: """检查单个订单是否触发""" triggered = False @@ -268,14 +344,50 @@ class PaperTradingService: order.max_drawdown = current_pnl_percent def close_order_manual(self, order_id: str, exit_price: float) -> Optional[Dict[str, Any]]: - """手动平仓""" + """手动平仓或取消挂单""" if order_id not in self.active_orders: logger.warning(f"订单不存在或已平仓: {order_id}") return None order = self.active_orders[order_id] + + # 如果是挂单,取消而不是平仓 + if order.status == OrderStatus.PENDING: + return self._cancel_pending_order(order) + return self._close_order(order, OrderStatus.CLOSED_MANUAL, exit_price) + def _cancel_pending_order(self, order: PaperOrder) -> Dict[str, Any]: + """取消挂单""" + db = db_service.get_session() + try: + order.status = OrderStatus.CANCELLED + order.closed_at = datetime.utcnow() + + db.merge(order) + db.commit() + + # 从活跃订单缓存中移除 + if order.order_id in self.active_orders: + del self.active_orders[order.order_id] + + logger.info(f"挂单已取消: {order.order_id} | {order.symbol}") + + return { + 'order_id': order.order_id, + 'symbol': order.symbol, + 'side': order.side.value, + 'status': 'cancelled', + 'entry_price': order.entry_price, + 'message': '挂单已取消' + } + except Exception as e: + logger.error(f"取消挂单失败: {e}") + db.rollback() + return None + finally: + db.close() + def get_active_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]: """获取活跃订单""" orders = list(self.active_orders.values()) diff --git a/frontend/paper-trading.html b/frontend/paper-trading.html index 9de3be6..ee8491b 100644 --- a/frontend/paper-trading.html +++ b/frontend/paper-trading.html @@ -165,6 +165,11 @@ font-size: 12px; } + .status-badge.pending { + background: rgba(255, 165, 0, 0.1); + color: orange; + } + .status-badge.open { background: rgba(0, 255, 65, 0.1); color: #00ff41; @@ -567,6 +572,7 @@ 订单ID 交易对 方向 + 状态 等级 入场价 现价 @@ -574,7 +580,7 @@ 止损 止盈 仓位 - 开仓时间 + 时间 操作 @@ -583,6 +589,11 @@ {{ order.order_id.slice(-12) }} {{ order.symbol }} {{ order.side === 'long' ? '做多' : '做空' }} + + + {{ order.status === 'pending' ? '⏳ 挂单' : '✅ 持仓' }} + + {{ order.signal_grade }} ${{ order.entry_price?.toLocaleString() }} @@ -591,11 +602,16 @@ - - {{ getUnrealizedPnl(order).pnl >= 0 ? '+' : '' }}{{ getUnrealizedPnl(order).percent.toFixed(2) }}% -
- (${{ getUnrealizedPnl(order).pnl >= 0 ? '+' : '' }}{{ getUnrealizedPnl(order).pnl.toFixed(2) }}) -
+ + @@ -608,9 +624,11 @@ ${{ order.quantity }} - {{ formatTime(order.opened_at) }} + {{ formatTime(order.status === 'open' ? order.opened_at : order.created_at) }} - +