This commit is contained in:
aaron 2026-02-07 01:43:24 +08:00
parent 8e55d8ad1f
commit 182afdc26b
4 changed files with 197 additions and 34 deletions

View File

@ -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}%)",

View File

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

View File

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

View File

@ -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 @@
<th>订单ID</th>
<th>交易对</th>
<th>方向</th>
<th>状态</th>
<th>等级</th>
<th>入场价</th>
<th>现价</th>
@ -574,7 +580,7 @@
<th>止损</th>
<th>止盈</th>
<th>仓位</th>
<th>开仓时间</th>
<th>时间</th>
<th>操作</th>
</tr>
</thead>
@ -583,6 +589,11 @@
<td>{{ order.order_id.slice(-12) }}</td>
<td>{{ order.symbol }}</td>
<td><span class="side-badge" :class="order.side">{{ order.side === 'long' ? '做多' : '做空' }}</span></td>
<td>
<span class="status-badge" :class="order.status">
{{ order.status === 'pending' ? '⏳ 挂单' : '✅ 持仓' }}
</span>
</td>
<td><span class="grade-badge" :class="order.signal_grade">{{ order.signal_grade }}</span></td>
<td>${{ order.entry_price?.toLocaleString() }}</td>
<td>
@ -591,11 +602,16 @@
</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>
<template v-if="order.status === 'open'">
<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>
</template>
<template v-else>
<span style="color: var(--text-secondary);">等待入场</span>
</template>
</td>
<td>
<span :class="isNearStopLoss(order) ? 'warning-price' : ''">
@ -608,9 +624,11 @@
</span>
</td>
<td>${{ order.quantity }}</td>
<td>{{ formatTime(order.opened_at) }}</td>
<td>{{ formatTime(order.status === 'open' ? order.opened_at : order.created_at) }}</td>
<td>
<button class="action-btn danger" @click="closeOrder(order)">平仓</button>
<button class="action-btn danger" @click="closeOrder(order)">
{{ order.status === 'pending' ? '取消' : '平仓' }}
</button>
</td>
</tr>
</tbody>