update
This commit is contained in:
parent
8e55d8ad1f
commit
182afdc26b
@ -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}%)",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user