update
This commit is contained in:
parent
4d3472758e
commit
cf2dcfe2c7
File diff suppressed because it is too large
Load Diff
546
backend/app/crypto_agent/market_signal_analyzer.py
Normal file
546
backend/app/crypto_agent/market_signal_analyzer.py
Normal file
@ -0,0 +1,546 @@
|
||||
"""
|
||||
市场信号分析器 - 纯市场分析,不包含任何仓位信息
|
||||
|
||||
职责:
|
||||
1. 分析K线、量价、技术指标
|
||||
2. 分析新闻舆情
|
||||
3. 输出纯市场信号(buy/sell/hold + confidence + reasoning)
|
||||
|
||||
不负责:
|
||||
- 仓位管理
|
||||
- 风险控制
|
||||
- 具体下单决策
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
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 MarketSignalAnalyzer:
|
||||
"""市场信号分析器 - 只关注市场,输出客观信号"""
|
||||
|
||||
# 纯市场分析系统提示词(与旧版 CRYPTO_SYSTEM_PROMPT 保持一致,只移除仓位管理)
|
||||
MARKET_ANALYSIS_PROMPT = """你是一位专业的加密货币交易员和技术分析师。你的任务是综合分析**K线数据、量价关系、技术指标和新闻舆情**,给出交易信号。
|
||||
|
||||
## 核心理念
|
||||
加密货币市场波动大,每天都有交易机会。你的目标是:
|
||||
- **主动寻找机会**,而不是被动等待完美信号
|
||||
- 短线交易重点关注:超跌反弹、超涨回落、关键位突破
|
||||
- 中线交易重点关注:趋势回调、形态突破、多周期共振
|
||||
|
||||
## 一、量价分析(最重要)
|
||||
量价关系是判断趋势真假的核心:
|
||||
|
||||
### 1. 健康上涨信号
|
||||
- **放量上涨**:价格上涨 + 成交量放大(量比>1.5)= 上涨有效,可追多
|
||||
- **缩量回调**:上涨后回调 + 成交量萎缩(量比<0.7)= 回调健康,可低吸
|
||||
|
||||
### 2. 健康下跌信号
|
||||
- **放量下跌**:价格下跌 + 成交量放大 = 下跌有效,可追空
|
||||
- **缩量反弹**:下跌后反弹 + 成交量萎缩 = 反弹无力,可做空
|
||||
|
||||
### 3. 量价背离(重要反转信号)
|
||||
- **顶背离**:价格创新高,但成交量未创新高 → 上涨动能衰竭,警惕回落
|
||||
- **底背离**:价格创新低,但成交量未创新低 → 下跌动能衰竭,关注反弹
|
||||
- **天量见顶**:极端放量(量比>3)后价格滞涨 → 主力出货信号
|
||||
- **地量见底**:极端缩量(量比<0.3)后价格企稳 → 抛压枯竭信号
|
||||
|
||||
### 4. 突破确认
|
||||
- **有效突破**:突破关键位 + 放量确认(量比>1.5)= 真突破
|
||||
- **假突破**:突破关键位 + 缩量 = 假突破,可能回落
|
||||
|
||||
## 二、K线形态分析
|
||||
### 反转形态
|
||||
- **锤子线/倒锤子**:下跌趋势中出现,下影线长 = 底部信号
|
||||
- **吞没形态**:大阳吞没前一根阴线 = 看涨;大阴吞没前一根阳线 = 看跌
|
||||
- **十字星**:在高位/低位出现 = 变盘信号
|
||||
- **早晨之星/黄昏之星**:三根K线组合的反转信号
|
||||
|
||||
### 持续形态
|
||||
- **三连阳/三连阴**:趋势延续信号
|
||||
- **旗形整理**:趋势中的健康回调
|
||||
|
||||
## 三、技术指标分析
|
||||
### RSI(相对强弱指标)
|
||||
**RSI 是最重要的超买超卖指标,请注意细节:**
|
||||
- **RSI < 30**:超卖区,关注反弹机会
|
||||
- RSI 从 30 以下回升,交叉上穿 30:买入信号
|
||||
- RSI 底背离(价格新低但 RSI 未创新低):强买入信号
|
||||
- **RSI > 70**:超买区,关注回落风险
|
||||
- RSI 从 70 以上回落,交叉下穿 70:卖出信号
|
||||
- RSI 顶背离(价格新高但 RSI 未创新高):强卖出信号
|
||||
- **RSI 40-60**:震荡区,观望为主
|
||||
- **RSI 趋势**:RSI 自身的趋势变化比单一数值更重要
|
||||
|
||||
### MACD
|
||||
- 金叉(DIF 上穿 DEA):做多信号
|
||||
- 死叉(DIF 下穿 DEA):做空信号
|
||||
- 零轴上方金叉:强势做多
|
||||
- 零轴下方死叉:强势做空
|
||||
- MACD 柱状图背离:重要反转信号
|
||||
|
||||
### 布林带
|
||||
- 触及下轨 + 企稳:反弹做多
|
||||
- 触及上轨 + 受阻:回落做空
|
||||
- 布林带收口:即将变盘
|
||||
- 布林带开口:趋势启动
|
||||
|
||||
### 均线系统(重要)
|
||||
**均线系统是趋势判断的核心,请仔细分析:**
|
||||
- **多头排列**(MA5 > MA10 > MA20 > MA50):强势上涨趋势,回调做多
|
||||
- **空头排列**(MA5 < MA10 < MA20 < MA50):强势下跌趋势,反弹做空
|
||||
- **价格与 MA 的关系**:
|
||||
- 价格站稳 MA5/MA10 上方:短线上涨
|
||||
- 价格突破 MA20:中线转多
|
||||
- 价格跌破 MA20:中线转空
|
||||
- MA50 是中期趋势的分水岭
|
||||
- **均线金叉死叉**:
|
||||
- MA5 上穿 MA10:短线买入信号
|
||||
- MA5 下穿 MA10:短线卖出信号
|
||||
- MA10 上穿 MA20:中线买入信号
|
||||
- MA10 下穿 MA20:中线卖出信号
|
||||
|
||||
## 四、新闻舆情分析
|
||||
结合最新市场新闻判断:
|
||||
- **重大利好**:监管利好、机构入场、ETF 通过等 → 提高做多置信度
|
||||
- **重大利空**:监管打压、交易所暴雷、黑客攻击等 → 提高做空置信度
|
||||
- **市场情绪**:恐慌指数、社交媒体热度
|
||||
- **大户动向**:鲸鱼转账、交易所流入流出
|
||||
|
||||
## 五、多周期共振(关键分析框架)
|
||||
**多周期共振是提高信号质量的核心方法:**
|
||||
|
||||
### 周期层级关系
|
||||
- **4h(趋势层)**:决定中期大方向
|
||||
- **1h(主周期)**:主要交易周期
|
||||
- **15m(入场层)**:寻找入场时机
|
||||
- **5m(精确入场)**:确认最佳入场点
|
||||
|
||||
### 共振判断标准
|
||||
**强共振(A级信号)**:
|
||||
- 所有周期趋势同向(如 4h多 + 1h多 + 15m多)
|
||||
- 多周期 RSI 同时超买/超卖后出现背离
|
||||
- 多周期 MA 同时金叉/死叉
|
||||
|
||||
**中等共振(B级信号)**:
|
||||
- 大周期(4h+1h)同向
|
||||
- 主周期(1h)技术指标明确
|
||||
|
||||
**弱共振(C级信号)**:
|
||||
- 只有单一周期信号
|
||||
- 多周期方向不一致
|
||||
|
||||
### 实战策略
|
||||
- **顺势交易**:4h 和 1h 同向时,在 15m/5m 寻找入场点
|
||||
- **逆势谨慎**:只有 1h 信号但 4h 反向时,降低置信度
|
||||
- **突破交易**:多周期同时突破关键位,信号最强
|
||||
|
||||
## 六、入场方式
|
||||
根据市场分析综合判断入场方式:
|
||||
- **market**:现价立即入场
|
||||
- 信号强烈且明确(A级或高置信度B级)
|
||||
- 放量突破关键位,趋势明确
|
||||
- 多周期共振,等待可能错过机会
|
||||
- 市场波动大,等待可能价格变化太快
|
||||
- **limit**:挂单等待入场
|
||||
- 信号强度中等(B级或C级)
|
||||
- 当前价格距离理想入场位有一定距离
|
||||
- 判断市场可能回调到更好位置
|
||||
- 希望获得更优成交价格,愿意承担可能无法成交的风险
|
||||
|
||||
**重要**:
|
||||
- 必须同时输出 `entry_zone`(建议入场价)和 `entry_type`(入场方式)
|
||||
- 入场方式由你的市场分析判断,不是简单的价格距离计算
|
||||
- 如果选择 `limit`,`entry_zone` 应该是你建议的挂单价格
|
||||
|
||||
## 输出格式
|
||||
请严格按照以下 JSON 格式输出:
|
||||
|
||||
```json
|
||||
{
|
||||
"analysis_summary": "简要描述当前市场状态(50字以内)",
|
||||
"volume_analysis": "量价分析结论(30字以内)",
|
||||
"news_sentiment": "positive/negative/neutral",
|
||||
"news_impact": "新闻对市场的影响分析(30字以内)",
|
||||
"signals": [
|
||||
{
|
||||
"type": "short_term/medium_term/long_term",
|
||||
"action": "buy/sell",
|
||||
"entry_type": "market/limit",
|
||||
"confidence": 0-100,
|
||||
"grade": "A/B/C/D",
|
||||
"entry_zone": 66000,
|
||||
"stop_loss": 65500,
|
||||
"take_profit": 67500,
|
||||
"reasoning": "详细的入场理由(必须包含量价分析)",
|
||||
"key_factors": ["关键因素1", "关键因素2"]
|
||||
}
|
||||
],
|
||||
"key_levels": {
|
||||
"support": [65000, 64500],
|
||||
"resistance": [67000, 67500]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 重要说明
|
||||
- **所有价格必须是纯数字**,不要加 $ 符号、逗号或其他格式
|
||||
- `entry_zone`、`stop_loss`、`take_profit` 必须是数字类型,不要是字符串
|
||||
- `key_levels` 中的支撑位和阻力位也必须是数字数组
|
||||
|
||||
## 信号等级与置信度
|
||||
- **A级**(80-100):量价配合 + 多指标共振 + 多周期确认
|
||||
- **B级**(60-79):量价配合 + 主要指标确认
|
||||
- **C级**(40-59):有机会但量价不够理想
|
||||
- **D级**(<40):量价背离或信号矛盾
|
||||
|
||||
## 注意事项
|
||||
1. **只在有明确的做多或做空机会时才输出信号**(action 为 buy 或 sell)
|
||||
2. 如果市场不明朗,没有明确交易机会,**不要输出任何信号**(signals 为空数组 [])
|
||||
3. 信号强度(confidence)要合理,不要随意给高分
|
||||
4. 60-70分:一般信号,可轻仓试探
|
||||
5. 75-85分:较强信号,可正常仓位
|
||||
6. 90+分:强信号,但也要控制风险
|
||||
7. 不要输出 action 为 "wait" 的信号,如果没有交易机会就不输出
|
||||
|
||||
记住:你只负责分析市场,输出客观的交易信号,不需要考虑仓位管理和风险控制!
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.news_service = get_news_service()
|
||||
|
||||
async def analyze(self, symbol: str, data: Dict[str, Any],
|
||||
symbols: List[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
分析市场并生成信号
|
||||
|
||||
Args:
|
||||
symbol: 交易对
|
||||
data: 多周期K线数据
|
||||
symbols: 所有监控的交易对(用于市场对比)
|
||||
|
||||
Returns:
|
||||
市场信号字典
|
||||
"""
|
||||
try:
|
||||
# 1. 准备市场数据
|
||||
market_context = self._prepare_market_context(symbol, data, symbols)
|
||||
|
||||
# 2. 获取新闻舆情
|
||||
news_context = await self._get_news_context(symbol)
|
||||
|
||||
# 3. 构建 LLM 提示词
|
||||
prompt = self._build_analysis_prompt(symbol, market_context, news_context)
|
||||
|
||||
# 4. 调用 LLM 分析
|
||||
messages = [
|
||||
{"role": "system", "content": self.MARKET_ANALYSIS_PROMPT},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
response = await llm_service.achat(messages)
|
||||
|
||||
# 5. 解析结果
|
||||
result = self._parse_llm_response(response, symbol)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"市场信号分析失败: {e}")
|
||||
import traceback
|
||||
logger.debug(traceback.format_exc())
|
||||
return self._get_empty_signal(symbol)
|
||||
|
||||
def _prepare_market_context(self, symbol: str, data: Dict,
|
||||
symbols: List[str] = None) -> str:
|
||||
"""准备市场上下文信息"""
|
||||
context_parts = []
|
||||
|
||||
# 当前价格和24h变化
|
||||
current_price = float(data['5m'].iloc[-1]['close'])
|
||||
price_change_24h = self._calculate_price_change_24h(data['1h'])
|
||||
context_parts.append(f"当前价格: ${current_price:,.2f} ({price_change_24h})")
|
||||
|
||||
# 多周期数据
|
||||
for tf_name, df in data.items():
|
||||
if df is None or len(df) == 0:
|
||||
continue
|
||||
|
||||
latest = df.iloc[-1]
|
||||
context_parts.append(f"\n## {tf_name} 数据")
|
||||
context_parts.append(f"开: {latest['open']}, 高: {latest['high']}, 低: {latest['low']}, 收: {latest['close']}")
|
||||
context_parts.append(f"成交量: {latest.get('volume', 'N/A')}")
|
||||
|
||||
# 技术指标
|
||||
if 'rsi' in df.columns:
|
||||
rsi = df['rsi'].iloc[-1]
|
||||
context_parts.append(f"RSI: {rsi:.2f}")
|
||||
if 'macd' in df.columns:
|
||||
macd = df['macd'].iloc[-1]
|
||||
signal = df['macd_signal'].iloc[-1]
|
||||
context_parts.append(f"MACD: {macd:.4f}, 信号线: {signal:.4f}")
|
||||
if 'bb_upper' in df.columns:
|
||||
bb_upper = df['bb_upper'].iloc[-1]
|
||||
bb_lower = df['bb_lower'].iloc[-1]
|
||||
context_parts.append(f"布林带: 上轨 {bb_upper:.2f}, 下轨 {bb_lower:.2f}")
|
||||
|
||||
# 均线系统
|
||||
context_parts.append(f"\n## 均线系统")
|
||||
df_1h = data.get('1h')
|
||||
if df_1h is not None and len(df_1h) > 0:
|
||||
latest = df_1h.iloc[-1]
|
||||
context_parts.append(f"MA5: {latest.get('ma5', 'N/A')}")
|
||||
context_parts.append(f"MA10: {latest.get('ma10', 'N/A')}")
|
||||
context_parts.append(f"MA20: {latest.get('ma20', 'N/A')}")
|
||||
context_parts.append(f"MA50: {latest.get('ma50', 'N/A')}")
|
||||
|
||||
# 判断均线排列
|
||||
ma5 = latest.get('ma5', 0)
|
||||
ma10 = latest.get('ma10', 0)
|
||||
ma20 = latest.get('ma20', 0)
|
||||
ma50 = latest.get('ma50', 0)
|
||||
|
||||
if all([ma5, ma10, ma20, ma50]):
|
||||
if ma5 > ma10 > ma20 > ma50:
|
||||
context_parts.append("均线排列: 多头排列 📈")
|
||||
elif ma5 < ma10 < ma20 < ma50:
|
||||
context_parts.append("均线排列: 空头排列 📉")
|
||||
else:
|
||||
context_parts.append("均线排列: 交织,方向不明")
|
||||
|
||||
# 量比分析
|
||||
df_5m = data.get('5m')
|
||||
if df_5m is not None and len(df_5m) >= 20:
|
||||
vol_latest = df_5m['volume'].iloc[-1]
|
||||
vol_ma20 = df_5m['volume'].iloc[-20:-1].mean()
|
||||
volume_ratio = vol_latest / vol_ma20 if vol_ma20 > 0 else 1
|
||||
context_parts.append(f"\n## 量价分析")
|
||||
context_parts.append(f"最新成交量: {vol_latest:.0f}")
|
||||
context_parts.append(f"20周期均量: {vol_ma20:.0f}")
|
||||
context_parts.append(f"量比: {volume_ratio:.2f}")
|
||||
|
||||
if volume_ratio > 1.5:
|
||||
context_parts.append("量价状态: 放量 📊")
|
||||
elif volume_ratio < 0.7:
|
||||
context_parts.append("量价状态: 缩量 📉")
|
||||
else:
|
||||
context_parts.append("量价状态: 平量 ➖")
|
||||
|
||||
return "\n".join(context_parts)
|
||||
|
||||
async def _get_news_context(self, symbol: str) -> str:
|
||||
"""获取新闻舆情上下文"""
|
||||
try:
|
||||
news_result = await self.news_service.get_crypto_news(symbol)
|
||||
|
||||
if not news_result or not news_result.get('articles'):
|
||||
return "无最新新闻"
|
||||
|
||||
articles = news_result['articles'][:5] # 只取前5条
|
||||
context_parts = ["\n## 最新新闻"]
|
||||
|
||||
for article in articles:
|
||||
title = article.get('title', '')
|
||||
source = article.get('source', '')
|
||||
published_at = article.get('publishedAt', '')
|
||||
time_str = published_at.split('T')[1][:5] if 'T' in published_at else ''
|
||||
|
||||
context_parts.append(f"- [{time_str}] {title} ({source})")
|
||||
|
||||
return "\n".join(context_parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"获取新闻失败: {e}")
|
||||
return "新闻获取失败"
|
||||
|
||||
def _build_analysis_prompt(self, symbol: str, market_context: str,
|
||||
news_context: str) -> str:
|
||||
"""构建分析提示词"""
|
||||
return f"""请分析 {symbol} 的市场情况:
|
||||
|
||||
{market_context}
|
||||
|
||||
{news_context}
|
||||
|
||||
请根据以上数据,给出你的市场判断和交易信号。
|
||||
"""
|
||||
|
||||
def _parse_llm_response(self, response: str, symbol: str) -> Dict[str, Any]:
|
||||
"""解析 LLM 响应"""
|
||||
try:
|
||||
# 尝试提取 JSON
|
||||
json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response)
|
||||
if json_match:
|
||||
json_str = json_match.group(1)
|
||||
else:
|
||||
json_match = re.search(r'\{[\s\S]*\}', response)
|
||||
if json_match:
|
||||
json_str = json_match.group(0)
|
||||
else:
|
||||
raise ValueError("无法找到 JSON 响应")
|
||||
|
||||
# 清理 JSON 字符串(移除可能导致解析错误的注释等)
|
||||
json_str = self._clean_json_string(json_str)
|
||||
|
||||
logger.debug(f"解析的 JSON 字符串: {json_str[:500]}...") # 打印前500字符用于调试
|
||||
|
||||
result = json.loads(json_str)
|
||||
|
||||
# 清理价格字段 - 转换为 float
|
||||
result = self._clean_price_fields(result)
|
||||
|
||||
# 添加元数据
|
||||
result['symbol'] = symbol
|
||||
result['timestamp'] = datetime.now().isoformat()
|
||||
result['raw_response'] = response
|
||||
|
||||
# 兼容处理:确保 signals 中的字段与旧格式一致
|
||||
if 'signals' in result:
|
||||
for sig in result['signals']:
|
||||
# LLM 输出的 "type" 是 timeframe (short_term/medium_term/long_term)
|
||||
# 需要映射为 "timeframe",而 "action" 才是 buy/sell/wait
|
||||
if 'type' in sig:
|
||||
# 如果 type 是 short_term/medium_term/long_term,映射为 timeframe
|
||||
if sig['type'] in ['short_term', 'medium_term', 'long_term']:
|
||||
sig['timeframe'] = sig.pop('type')
|
||||
# 如果 type 是 buy/sell/wait,映射为 action
|
||||
elif sig['type'] in ['buy', 'sell', 'wait']:
|
||||
sig['action'] = sig.pop('type')
|
||||
|
||||
# 确保 action 字段存在
|
||||
if 'action' not in sig and 'timeframe' in sig:
|
||||
# 从 reasoning 或其他字段推断 action
|
||||
sig['action'] = 'wait'
|
||||
|
||||
# 确保 grade 字段存在
|
||||
if 'grade' not in sig:
|
||||
# 根据 confidence 推断 grade
|
||||
confidence = sig.get('confidence', 0)
|
||||
if confidence >= 80:
|
||||
sig['grade'] = 'A'
|
||||
elif confidence >= 60:
|
||||
sig['grade'] = 'B'
|
||||
elif confidence >= 40:
|
||||
sig['grade'] = 'C'
|
||||
else:
|
||||
sig['grade'] = 'D'
|
||||
|
||||
# 从信号中推断 market_state 和 trend
|
||||
if 'signals' in result and result['signals']:
|
||||
# 找出置信度最高的信号
|
||||
best_signal = max(result['signals'], key=lambda s: s.get('confidence', 0))
|
||||
action = best_signal.get('action', 'wait')
|
||||
confidence = best_signal.get('confidence', 0)
|
||||
|
||||
# 推断市场状态
|
||||
if confidence >= 70:
|
||||
if action == 'buy':
|
||||
result['market_state'] = '强势上涨'
|
||||
elif action == 'sell':
|
||||
result['market_state'] = '强势下跌'
|
||||
else:
|
||||
result['market_state'] = '震荡整理'
|
||||
else:
|
||||
result['market_state'] = '震荡整理'
|
||||
|
||||
# 推断趋势
|
||||
if action == 'buy':
|
||||
result['trend'] = 'up'
|
||||
elif action == 'sell':
|
||||
result['trend'] = 'down'
|
||||
else:
|
||||
result['trend'] = 'sideways'
|
||||
else:
|
||||
result['market_state'] = '无明确信号'
|
||||
result['trend'] = 'sideways'
|
||||
|
||||
logger.info(f"✅ 市场信号分析完成: {symbol}")
|
||||
logger.debug(f"市场信号: {json.dumps(result, ensure_ascii=False, indent=2)}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"解析 LLM 响应失败: {e}")
|
||||
logger.warning(f"原始响应: {response[:1000]}...") # 打印前1000字符
|
||||
return self._get_empty_signal(symbol)
|
||||
|
||||
def _clean_json_string(self, json_str: str) -> str:
|
||||
"""清理 JSON 字符串,移除可能导致解析错误的内容"""
|
||||
# 移除单行注释 // ...
|
||||
json_str = re.sub(r'//.*?(?=\n|$)', '', json_str)
|
||||
# 移除多行注释 /* ... */
|
||||
json_str = re.sub(r'/\*[\s\S]*?\*/', '', json_str)
|
||||
# 移除尾随逗号(例如 {"a": 1,} -> {"a": 1})
|
||||
json_str = re.sub(r',\s*([}\]])', r'\1', json_str)
|
||||
return json_str
|
||||
|
||||
def _clean_price_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""清理价格字段,转换为 float"""
|
||||
def clean_price(price_value):
|
||||
if price_value is None:
|
||||
return None
|
||||
if isinstance(price_value, (int, float)):
|
||||
return float(price_value)
|
||||
if isinstance(price_value, str):
|
||||
# 移除 $ 符号和逗号
|
||||
cleaned = price_value.replace('$', '').replace(',', '').strip()
|
||||
if cleaned:
|
||||
try:
|
||||
return float(cleaned)
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
# 清理 key_levels 中的支撑位和阻力位
|
||||
if 'key_levels' in data and data['key_levels']:
|
||||
key_levels = data['key_levels']
|
||||
if 'support' in key_levels:
|
||||
data['key_levels']['support'] = [clean_price(s) for s in key_levels['support']]
|
||||
if 'resistance' in key_levels:
|
||||
data['key_levels']['resistance'] = [clean_price(r) for r in key_levels['resistance']]
|
||||
|
||||
# 清理 signals 中的价格字段
|
||||
if 'signals' in data:
|
||||
for sig in data['signals']:
|
||||
price_fields = ['entry_zone', 'stop_loss', 'take_profit']
|
||||
for field in price_fields:
|
||||
if field in sig:
|
||||
sig[field] = clean_price(sig[field])
|
||||
|
||||
return data
|
||||
|
||||
def _calculate_price_change_24h(self, df) -> str:
|
||||
"""计算24小时涨跌幅"""
|
||||
try:
|
||||
if df is None or len(df) < 24:
|
||||
return "N/A"
|
||||
|
||||
current_price = float(df['close'].iloc[-1])
|
||||
price_24h_ago = float(df['close'].iloc[-24])
|
||||
change = ((current_price - price_24h_ago) / price_24h_ago) * 100
|
||||
|
||||
sign = "+" if change >= 0 else ""
|
||||
return f"{sign}{change:.2f}%"
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"计算24h涨跌失败: {e}")
|
||||
return "N/A"
|
||||
|
||||
def _get_empty_signal(self, symbol: str) -> Dict[str, Any]:
|
||||
"""返回空信号"""
|
||||
return {
|
||||
'symbol': symbol,
|
||||
'analysis_summary': 'unknown',
|
||||
'volume_analysis': '分析失败',
|
||||
'news_sentiment': 'neutral',
|
||||
'news_impact': '无',
|
||||
'market_state': '分析失败',
|
||||
'trend': 'sideways',
|
||||
'signals': [],
|
||||
'key_levels': {},
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'error': '信号分析失败'
|
||||
}
|
||||
461
backend/app/crypto_agent/trading_decision_maker.py
Normal file
461
backend/app/crypto_agent/trading_decision_maker.py
Normal file
@ -0,0 +1,461 @@
|
||||
"""
|
||||
交易决策器 - 基于市场信号和当前状态做出交易决策
|
||||
|
||||
职责:
|
||||
1. 接收市场信号(不含仓位信息)
|
||||
2. 接收当前持仓状态
|
||||
3. 接收账户状态
|
||||
4. 做出具体交易决策(开仓/平仓/加仓/减仓/观望)
|
||||
"""
|
||||
import json
|
||||
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
|
||||
|
||||
|
||||
class TradingDecisionMaker:
|
||||
"""交易决策器 - 负责仓位管理和风险控制"""
|
||||
|
||||
# 交易决策系统提示词
|
||||
TRADING_DECISION_PROMPT = """你是一位专业的加密货币交易员。你的职责是**根据市场信号和当前仓位状态,做出交易决策**。
|
||||
|
||||
## 你的职责
|
||||
- 分析市场信号的质量
|
||||
- 结合当前持仓评估风险
|
||||
- 考虑账户整体状况
|
||||
- 做出具体交易决策
|
||||
|
||||
## 输入信息
|
||||
你将收到:
|
||||
1. 市场信号(方向、强度、理由)
|
||||
2. 当前持仓列表
|
||||
3. 账户状态(余额、已用保证金、杠杆等)
|
||||
|
||||
## 决策类型
|
||||
### 1. 开仓(OPEN)
|
||||
**时机**:无持仓或可以加仓时
|
||||
**要求**:
|
||||
- **A级信号(confidence >= 80)**:可开 heavy/medium/light 仓位
|
||||
- **B级信号(60 <= confidence < 80)**:只能开 medium/light 仓位
|
||||
- **C级信号(40 <= confidence < 60)**:只能开 light 仓位
|
||||
- **D级信号(confidence < 40)**:不开仓,返回 HOLD
|
||||
- 账户有足够的可用杠杆空间
|
||||
- 风险可控(止损明确)
|
||||
|
||||
### 2. 平仓(CLOSE)
|
||||
**时机**:
|
||||
- 触发止损/止盈
|
||||
- 信号反转
|
||||
- 风险过大
|
||||
|
||||
### 3. 加仓(ADD)
|
||||
**时机**:
|
||||
- 已有盈利持仓
|
||||
- 同向新信号
|
||||
- 趋势加强
|
||||
|
||||
### 4. 减仓(REDUCE)
|
||||
**时机**:
|
||||
- 部分止盈
|
||||
- 降低风险敞口
|
||||
- 不确定增加
|
||||
|
||||
### 5. 观望(HOLD)
|
||||
**时机**:
|
||||
- 信号不明确
|
||||
- 风险过大
|
||||
- 可用杠杆空间不足
|
||||
- 等待更好时机
|
||||
|
||||
## 仓位管理规则
|
||||
### 全仓模式(联合保证金)
|
||||
- **最大杠杆 20 倍**:最大仓位金额 = 账户余额 × 20
|
||||
- **当前杠杆**:当前杠杆 = 当前持仓价值 / 账户余额
|
||||
- **可用杠杆空间百分比**:(最大仓位金额 - 当前持仓价值) / 最大仓位金额 × 100%
|
||||
|
||||
### 仓位大小选择(综合考虑信号质量和可用空间)
|
||||
仓位大小由**信号等级**和**可用杠杆空间**共同决定:
|
||||
|
||||
#### 1. 信号等级决定最大仓位上限
|
||||
- **A级信号(80-100分)**:可选择 heavy/medium/light
|
||||
- **B级信号(60-79分)**:只能选择 medium/light
|
||||
- **C级信号(40-59分)**:只能选择 light
|
||||
- **D级信号(<40分)**:不开仓,返回 HOLD
|
||||
|
||||
#### 2. 可用杠杆空间决定是否可开仓
|
||||
- **可用空间 >= 10%**:可以开 heavy 仓位
|
||||
- **可用空间 >= 5%**:可以开 medium 仓位
|
||||
- **可用空间 >= 3%**:可以开 light 仓位
|
||||
- **可用空间 < 3%**:不新开仓,返回 HOLD
|
||||
|
||||
#### 3. 仓位大小与保证金金额
|
||||
- **heavy**:使用保证金 = 账户余额 × 12%
|
||||
- **medium**:使用保证金 = 账户余额 × 6%
|
||||
- **light**:使用保证金 = 账户余额 × 3%
|
||||
|
||||
#### 4. 选择逻辑示例
|
||||
假设当前可用杠杆空间为 50%:
|
||||
- A级信号 → 可以选择 heavy(空间足够,信号质量高)
|
||||
- B级信号 → 只能选择 medium/light(信号质量中等)
|
||||
- C级信号 → 只能选择 light(信号质量一般,保守仓位)
|
||||
|
||||
假设当前可用杠杆空间为 4%:
|
||||
- A级信号 → 只能选择 medium/light(空间不足)
|
||||
- B级信号 → 只能选择 light(空间不足)
|
||||
- C级信号 → 不开仓(空间不足)
|
||||
|
||||
**重要**:`quantity` 字段输出的是**保证金金额**,不是持仓价值。交易系统会使用杠杆自动计算实际持仓价值。
|
||||
|
||||
### 计算示例
|
||||
- 账户余额:$10,000
|
||||
- 最大仓位金额:$10,000 × 20 = $200,000
|
||||
- 当前持仓价值:$20,000(当前杠杆 2x)
|
||||
- 可用仓位金额:$200,000 - $20,000 = $180,000
|
||||
- 可用杠杆空间:$180,000 / $200,000 = 90%
|
||||
- 计算公式:保证金金额 = 账户余额 × 使用比例
|
||||
- heavy:保证金 $10,000 × 12% = $1,200 → 持仓价值 $1,200 × 20 = $24,000
|
||||
- medium:保证金 $10,000 × 6% = $600 → 持仓价值 $600 × 20 = $12,000
|
||||
- light:保证金 $10,000 × 3% = $300 → 持仓价值 $300 × 20 = $6,000
|
||||
|
||||
### 风险控制
|
||||
- 单笔最大亏损不超过账户 2%
|
||||
- 止损必须明确
|
||||
- 避免过度交易
|
||||
- 不追涨杀跌
|
||||
|
||||
## 决策输出格式
|
||||
请以 JSON 格式输出:
|
||||
|
||||
```json
|
||||
{
|
||||
"decision": "OPEN/CLOSE/ADD/REDUCE/HOLD",
|
||||
"symbol": "BTC/USDT",
|
||||
"side": "buy/sell",
|
||||
"action": "open_long/close_short/add_long/...",
|
||||
"position_size": "heavy/medium/light",
|
||||
"quantity": 1200,
|
||||
"confidence": 0-100,
|
||||
"reasoning": "决策理由",
|
||||
"risk_analysis": "风险分析",
|
||||
"stop_loss": 65500,
|
||||
"take_profit": 67500,
|
||||
"notes": "其他说明"
|
||||
}
|
||||
```
|
||||
|
||||
## 重要说明
|
||||
- **所有价格必须是纯数字**,不要加 $ 符号、逗号或其他格式
|
||||
- `stop_loss`、`take_profit` 必须是数字类型
|
||||
- **quantity 是保证金金额(USDT)**,交易系统会使用杠杆计算实际持仓价值
|
||||
- **position_size** 和 **quantity** 必须匹配(heavy 对应最大保证金金额)
|
||||
- **入场方式由市场信号决定**,你只需要根据市场信号的 `entry_type` 来执行交易
|
||||
|
||||
## 注意事项
|
||||
1. **安全第一**:宁可错过机会,也不要冒过大风险
|
||||
2. **遵守杠杆限制**:总杠杆永远不超过 20 倍
|
||||
3. **理性决策**:不要被 FOMO 情绪左右
|
||||
4. **灵活应变**:根据市场变化调整策略
|
||||
5. **记录决策**:清晰记录决策理由,便于复盘
|
||||
|
||||
记住:你是交易执行者,不是市场分析师。市场分析已经完成了,你只需要根据分析结果和当前状态做出理性的交易决策!
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def make_decision(self,
|
||||
market_signal: Dict[str, Any],
|
||||
positions: List[Dict[str, Any]],
|
||||
account: Dict[str, Any],
|
||||
current_price: float = None) -> Dict[str, Any]:
|
||||
"""
|
||||
做出交易决策
|
||||
|
||||
Args:
|
||||
market_signal: 市场信号(来自 MarketSignalAnalyzer)
|
||||
positions: 当前持仓列表
|
||||
account: 账户状态
|
||||
current_price: 当前价格(用于判断入场方式)
|
||||
|
||||
Returns:
|
||||
交易决策字典
|
||||
"""
|
||||
try:
|
||||
# 1. 准备决策上下文
|
||||
decision_context = self._prepare_decision_context(
|
||||
market_signal, positions, account, current_price
|
||||
)
|
||||
|
||||
# 2. 构建提示词
|
||||
prompt = self._build_decision_prompt(decision_context)
|
||||
|
||||
# 3. 调用 LLM 做决策
|
||||
messages = [
|
||||
{"role": "system", "content": self.TRADING_DECISION_PROMPT},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
response = await llm_service.achat(messages)
|
||||
|
||||
# 4. 解析结果
|
||||
result = self._parse_decision_response(response, market_signal['symbol'])
|
||||
|
||||
# 5. 验证决策安全性
|
||||
result = self._validate_decision(result, positions, account)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"交易决策失败: {e}")
|
||||
import traceback
|
||||
logger.debug(traceback.format_exc())
|
||||
return self._get_hold_decision(market_signal['symbol'], "决策系统异常")
|
||||
|
||||
def _prepare_decision_context(self,
|
||||
market_signal: Dict[str, Any],
|
||||
positions: List[Dict[str, Any]],
|
||||
account: Dict[str, Any],
|
||||
current_price: float = None) -> Dict[str, Any]:
|
||||
"""准备决策上下文"""
|
||||
context = {
|
||||
'symbol': market_signal.get('symbol'),
|
||||
'market_state': market_signal.get('market_state'),
|
||||
'trend': market_signal.get('trend'),
|
||||
'signals': market_signal.get('signals', []),
|
||||
'key_levels': market_signal.get('key_levels', {}),
|
||||
'positions': positions,
|
||||
'account': account,
|
||||
'current_price': current_price
|
||||
}
|
||||
|
||||
# 计算账户状态
|
||||
balance = float(account.get('current_balance', 0))
|
||||
total_position_value = float(account.get('total_position_value', 0))
|
||||
used_margin = float(account.get('used_margin', 0))
|
||||
|
||||
# 当前杠杆(全仓模式)
|
||||
max_leverage = 20
|
||||
max_position_value = balance * max_leverage # 最大仓位金额
|
||||
current_leverage = (total_position_value / balance) if balance > 0 else 0
|
||||
available_position_value = max(0, max_position_value - total_position_value) # 剩余可用仓位金额
|
||||
available_leverage_percent = (available_position_value / max_position_value * 100) if max_position_value > 0 else 0 # 可用杠杆空间百分比
|
||||
|
||||
context['leverage_info'] = {
|
||||
'balance': balance,
|
||||
'current_leverage': current_leverage,
|
||||
'total_position_value': total_position_value,
|
||||
'max_position_value': max_position_value,
|
||||
'available_position_value': available_position_value,
|
||||
'available_leverage_percent': available_leverage_percent,
|
||||
'max_leverage': max_leverage
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
def _build_decision_prompt(self, context: Dict[str, Any]) -> str:
|
||||
"""构建决策提示词"""
|
||||
prompt_parts = []
|
||||
|
||||
# 市场信号
|
||||
prompt_parts.append(f"## 市场信号")
|
||||
prompt_parts.append(f"交易对: {context['symbol']}")
|
||||
prompt_parts.append(f"市场状态: {context.get('market_state')}")
|
||||
prompt_parts.append(f"趋势: {context.get('trend')}")
|
||||
|
||||
# 当前价格(如果有)
|
||||
current_price = context.get('current_price')
|
||||
if current_price:
|
||||
prompt_parts.append(f"当前价格: ${current_price:,.2f}")
|
||||
|
||||
# 信号列表
|
||||
signals = context.get('signals', [])
|
||||
if signals:
|
||||
prompt_parts.append(f"\n## 信号列表")
|
||||
for i, sig in enumerate(signals, 1):
|
||||
# timeframe 是 short_term/medium_term/long_term
|
||||
timeframe = sig.get('timeframe', 'N/A')
|
||||
action = sig.get('action', 'N/A')
|
||||
prompt_parts.append(f"{i}. {timeframe} | {action}")
|
||||
prompt_parts.append(f" 信心度: {sig.get('confidence', 0)}")
|
||||
|
||||
# 添加入场价格信息
|
||||
entry_zone = sig.get('entry_zone')
|
||||
if entry_zone:
|
||||
prompt_parts.append(f" 建议入场价: ${entry_zone:,.2f}")
|
||||
|
||||
prompt_parts.append(f" 理由: {sig.get('reasoning', 'N/A')}")
|
||||
|
||||
# 关键价位
|
||||
key_levels = context.get('key_levels', {})
|
||||
if key_levels:
|
||||
prompt_parts.append(f"\n## 关键价位")
|
||||
if key_levels.get('support'):
|
||||
# 提取数字并格式化
|
||||
import re
|
||||
def extract_num(val):
|
||||
if isinstance(val, (int, float)):
|
||||
return float(val)
|
||||
if isinstance(val, str):
|
||||
match = re.search(r'[\d,]+\.?\d*', val.replace(',', ''))
|
||||
if match:
|
||||
return float(match.group())
|
||||
return None
|
||||
|
||||
supports = [extract_num(s) for s in key_levels['support'][:3]]
|
||||
supports_str = ', '.join([f"${s:,.2f}" for s in supports if s is not None])
|
||||
prompt_parts.append(f"支撑位: {supports_str}")
|
||||
if key_levels.get('resistance'):
|
||||
import re
|
||||
def extract_num(val):
|
||||
if isinstance(val, (int, float)):
|
||||
return float(val)
|
||||
if isinstance(val, str):
|
||||
match = re.search(r'[\d,]+\.?\d*', val.replace(',', ''))
|
||||
if match:
|
||||
return float(match.group())
|
||||
return None
|
||||
|
||||
resistances = [extract_num(r) for r in key_levels['resistance'][:3]]
|
||||
resistances_str = ', '.join([f"${r:,.2f}" for r in resistances if r is not None])
|
||||
prompt_parts.append(f"阻力位: {resistances_str}")
|
||||
|
||||
# 当前持仓
|
||||
positions = context.get('positions', [])
|
||||
prompt_parts.append(f"\n## 当前持仓")
|
||||
if positions:
|
||||
for pos in positions:
|
||||
if pos.get('holding', 0) > 0:
|
||||
prompt_parts.append(f"- {pos.get('symbol')}: {pos.get('side')} {pos.get('holding')} USDT")
|
||||
prompt_parts.append(f" 开仓价: ${pos.get('entry_price')}")
|
||||
prompt_parts.append(f" 止损: ${pos.get('stop_loss')}")
|
||||
prompt_parts.append(f" 止盈: ${pos.get('take_profit')}")
|
||||
else:
|
||||
prompt_parts.append("无持仓")
|
||||
|
||||
# 账户状态
|
||||
account = context.get('account', {})
|
||||
lev_info = context.get('leverage_info', {})
|
||||
prompt_parts.append(f"\n## 账户状态")
|
||||
prompt_parts.append(f"余额: ${account.get('current_balance', 0):.2f}")
|
||||
prompt_parts.append(f"可用: ${account.get('available', 0):.2f}")
|
||||
prompt_parts.append(f"已用保证金: ${account.get('used_margin', 0):.2f}")
|
||||
prompt_parts.append(f"持仓价值: ${account.get('total_position_value', 0):.2f}")
|
||||
prompt_parts.append(f"\n## 杠杆信息")
|
||||
prompt_parts.append(f"当前杠杆: {lev_info.get('current_leverage', 0):.1f}x")
|
||||
prompt_parts.append(f"最大仓位金额: ${lev_info.get('max_position_value', 0):,.2f}")
|
||||
prompt_parts.append(f"可用仓位金额: ${lev_info.get('available_position_value', 0):,.2f}")
|
||||
prompt_parts.append(f"可用杠杆空间: {lev_info.get('available_leverage_percent', 0):.1f}%")
|
||||
prompt_parts.append(f"最大杠杆限制: {lev_info.get('max_leverage', 20)}x")
|
||||
|
||||
prompt_parts.append(f"\n请根据以上信息,做出交易决策。")
|
||||
|
||||
return "\n".join(prompt_parts)
|
||||
|
||||
def _parse_decision_response(self, response: str, symbol: str) -> Dict[str, Any]:
|
||||
"""解析决策响应"""
|
||||
try:
|
||||
import re
|
||||
|
||||
# 尝试提取 JSON
|
||||
json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response)
|
||||
if json_match:
|
||||
json_str = json_match.group(1)
|
||||
else:
|
||||
json_match = re.search(r'\{[\s\S]*\}', response)
|
||||
if json_match:
|
||||
json_str = json_match.group(0)
|
||||
else:
|
||||
raise ValueError("无法找到 JSON 响应")
|
||||
|
||||
# 清理 JSON 字符串
|
||||
json_str = self._clean_json_string(json_str)
|
||||
|
||||
result = json.loads(json_str)
|
||||
|
||||
# 清理价格字段 - 转换为 float
|
||||
result = self._clean_price_fields(result)
|
||||
|
||||
# 添加元数据
|
||||
result['symbol'] = symbol
|
||||
result['timestamp'] = datetime.now().isoformat()
|
||||
result['raw_response'] = response
|
||||
|
||||
logger.info(f"✅ 交易决策完成: {symbol} | {result.get('decision', 'HOLD')}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"解析决策响应失败: {e}")
|
||||
logger.warning(f"原始响应: {response[:1000]}...") # 打印前1000字符
|
||||
return self._get_hold_decision(symbol, "解析失败,默认观望")
|
||||
|
||||
def _clean_price_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""清理价格字段,转换为 float"""
|
||||
def clean_price(price_value):
|
||||
if price_value is None:
|
||||
return None
|
||||
if isinstance(price_value, (int, float)):
|
||||
return float(price_value)
|
||||
if isinstance(price_value, str):
|
||||
# 移除 $ 符号和逗号
|
||||
cleaned = price_value.replace('$', '').replace(',', '').strip()
|
||||
if cleaned:
|
||||
try:
|
||||
return float(cleaned)
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
# 清理顶层价格字段
|
||||
price_fields = ['stop_loss', 'take_profit', 'quantity']
|
||||
for field in price_fields:
|
||||
if field in data:
|
||||
data[field] = clean_price(data[field])
|
||||
|
||||
return data
|
||||
|
||||
def _clean_json_string(self, json_str: str) -> str:
|
||||
"""清理 JSON 字符串,移除可能导致解析错误的内容"""
|
||||
import re
|
||||
# 移除单行注释 // ...
|
||||
json_str = re.sub(r'//.*?(?=\n|$)', '', json_str)
|
||||
# 移除多行注释 /* ... */
|
||||
json_str = re.sub(r'/\*[\s\S]*?\*/', '', json_str)
|
||||
# 移除尾随逗号(例如 {"a": 1,} -> {"a": 1})
|
||||
json_str = re.sub(r',\s*([}\]])', r'\1', json_str)
|
||||
return json_str
|
||||
|
||||
def _validate_decision(self, decision: Dict[str, Any],
|
||||
positions: List[Dict[str, Any]],
|
||||
account: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""验证决策安全性"""
|
||||
# 检查杠杆限制
|
||||
if decision.get('decision') in ['OPEN', 'ADD']:
|
||||
balance = float(account.get('current_balance', 0))
|
||||
total_position_value = float(account.get('total_position_value', 0))
|
||||
max_leverage = 20
|
||||
max_position_value = balance * max_leverage
|
||||
|
||||
# quantity 是保证金金额,需要乘以杠杆得到持仓价值
|
||||
margin = float(decision.get('quantity', 0))
|
||||
position_value = margin * max_leverage # 使用最大杠杆计算持仓价值
|
||||
new_total_value = total_position_value + position_value
|
||||
|
||||
if new_total_value > max_position_value:
|
||||
logger.warning(f"⚠️ 决策被拒绝: 超过最大仓位金额 (保证金 ${margin:.2f} → 持仓价值 ${position_value:.2f}, 总计 ${new_total_value:,.2f} > ${max_position_value:,.2f})")
|
||||
return self._get_hold_decision(
|
||||
decision['symbol'],
|
||||
f"超过最大仓位金额 (保证金 ${margin:.2f} → 持仓价值 ${position_value:.2f}, 总计 ${new_total_value:,.2f} > ${max_position_value:,.2f})"
|
||||
)
|
||||
|
||||
return decision
|
||||
|
||||
def _get_hold_decision(self, symbol: str, reason: str = "") -> Dict[str, Any]:
|
||||
"""返回观望决策"""
|
||||
return {
|
||||
'decision': 'HOLD',
|
||||
'symbol': symbol,
|
||||
'action': 'hold',
|
||||
'reasoning': f'观望: {reason}',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
@ -515,6 +515,58 @@ class NewsService:
|
||||
logger.debug(traceback.format_exc())
|
||||
return []
|
||||
|
||||
async def get_crypto_news(self, symbol: str, limit: int = 10) -> Dict[str, Any]:
|
||||
"""
|
||||
获取加密货币相关新闻
|
||||
|
||||
Args:
|
||||
symbol: 加密货币代码(如 BTCUSDT)
|
||||
limit: 最大结果数
|
||||
|
||||
Returns:
|
||||
包含 articles 列表的字典
|
||||
"""
|
||||
try:
|
||||
# 获取一般市场新闻(包含加密货币相关)
|
||||
all_news = await self.get_latest_news(limit=limit * 2)
|
||||
|
||||
# 筛选与该币种相关的新闻
|
||||
symbol_keywords = {
|
||||
'BTCUSDT': ['BTC', 'Bitcoin', '比特币', 'bitcoin'],
|
||||
'ETHUSDT': ['ETH', 'Ethereum', '以太坊', 'ethereum'],
|
||||
'SOLUSDT': ['SOL', 'Solana', 'solana'],
|
||||
'BNBUSDT': ['BNB', 'Binance', '币安'],
|
||||
'ADAUSDT': ['ADA', 'Cardano', 'cardano'],
|
||||
'XRPUSDT': ['XRP', 'Ripple'],
|
||||
'DOGEUSDT': ['DOGE', 'Dogecoin', '狗狗币'],
|
||||
'MATICUSDT': ['MATIC', 'Polygon'],
|
||||
}
|
||||
|
||||
# 通用加密货币关键词
|
||||
crypto_keywords = ['crypto', 'cryptocurrency', '加密货币', 'blockchain', '区块链']
|
||||
|
||||
keywords = symbol_keywords.get(symbol, []) + crypto_keywords
|
||||
|
||||
filtered_news = []
|
||||
for news in all_news:
|
||||
title = news.get('title', '').lower()
|
||||
description = news.get('description', '').lower()
|
||||
|
||||
if any(kw.lower() in title or kw.lower() in description for kw in keywords):
|
||||
filtered_news.append(news)
|
||||
|
||||
if len(filtered_news) >= limit:
|
||||
break
|
||||
|
||||
return {
|
||||
'articles': filtered_news[:limit],
|
||||
'total': len(filtered_news)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"获取加密货币新闻失败: {e}")
|
||||
return {'articles': [], 'total': 0}
|
||||
|
||||
def format_news_for_llm(self, news_list: List[Dict[str, Any]],
|
||||
max_items: int = 10) -> str:
|
||||
"""
|
||||
|
||||
@ -186,6 +186,16 @@ class PaperTradingService:
|
||||
return result
|
||||
|
||||
# === 动态仓位计算 ===
|
||||
# 优先使用信号中的 quantity(LLM 决策的保证金金额)
|
||||
quantity_from_signal = signal.get('quantity')
|
||||
if quantity_from_signal is not None and quantity_from_signal > 0:
|
||||
# LLM 决策的 quantity 是保证金金额
|
||||
margin = float(quantity_from_signal)
|
||||
# 计算持仓价值(保证金 × 杠杆)
|
||||
position_value = margin * self.leverage
|
||||
logger.debug(f"使用 LLM 决策保证金: ${margin:.2f}, 持仓价值: ${position_value:.2f}")
|
||||
else:
|
||||
# 回退到动态仓位计算
|
||||
position_size = signal.get('position_size', 'light')
|
||||
margin, position_value = self._calculate_dynamic_position(position_size, symbol)
|
||||
|
||||
@ -1150,6 +1160,7 @@ class PaperTradingService:
|
||||
'realized_pnl': round(realized_pnl, 2),
|
||||
'current_balance': round(current_balance, 2),
|
||||
'used_margin': round(used_margin, 2),
|
||||
'available': round(available_margin, 2), # 添加 available 字段,兼容性
|
||||
'available_margin': round(available_margin, 2),
|
||||
'leverage': self.leverage,
|
||||
'margin_per_order': self.margin_per_order,
|
||||
|
||||
@ -342,6 +342,25 @@ class RealTradingService:
|
||||
'message': f'当前杠杆已达 {current_leverage:.1f}x,已超最大限制 {max_total_leverage}x,无法开仓'
|
||||
}
|
||||
|
||||
# 优先使用信号中的 quantity(LLM 决策的保证金金额)
|
||||
quantity_from_signal = signal.get('quantity')
|
||||
if quantity_from_signal is not None and quantity_from_signal > 0:
|
||||
# LLM 决策的 quantity 是保证金金额
|
||||
margin = float(quantity_from_signal)
|
||||
# 计算持仓价值(保证金 × 杠杆)
|
||||
position_value = margin * self.default_leverage
|
||||
logger.info(f"使用 LLM 决策保证金: ${margin:.2f}, 持仓价值: ${position_value:.2f}")
|
||||
|
||||
# 验证:加仓后的总杠杆不超过 20 倍
|
||||
new_total_value = total_position_value + position_value
|
||||
new_leverage = new_total_value / balance if balance > 0 else 0
|
||||
if new_leverage > max_total_leverage:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'LLM 决策会导致总杠杆 {new_leverage:.1f}x 超过限制 {max_total_leverage}x (保证金 ${margin:.2f}, 持仓价值 ${position_value:.2f})'
|
||||
}
|
||||
else:
|
||||
# 回退到动态仓位计算
|
||||
# 根据可用杠杆空间动态调整仓位比例
|
||||
custom_ratios = {
|
||||
'heavy': 0.12, # heavy: 12% 可用杠杆空间
|
||||
|
||||
@ -33,6 +33,23 @@ class SignalDatabaseService:
|
||||
"""添加信号到数据库"""
|
||||
db = self.db_service.get_session()
|
||||
try:
|
||||
# 清理价格字段 - 移除 $ 符号和逗号
|
||||
def clean_price(price_value):
|
||||
"""清理价格字段,转换为 float"""
|
||||
if price_value is None:
|
||||
return None
|
||||
if isinstance(price_value, (int, float)):
|
||||
return float(price_value)
|
||||
if isinstance(price_value, str):
|
||||
# 移除 $ 符号和逗号
|
||||
cleaned = price_value.replace('$', '').replace(',', '').strip()
|
||||
if cleaned:
|
||||
try:
|
||||
return float(cleaned)
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
# 创建信号对象
|
||||
signal = TradingSignal(
|
||||
signal_type=signal_data.get('signal_type', 'crypto'),
|
||||
@ -40,10 +57,10 @@ class SignalDatabaseService:
|
||||
action=signal_data.get('action', 'hold'),
|
||||
grade=signal_data.get('grade', 'D'),
|
||||
confidence=signal_data.get('confidence', 0),
|
||||
entry_price=signal_data.get('entry_price'),
|
||||
stop_loss=signal_data.get('stop_loss'),
|
||||
take_profit=signal_data.get('take_profit'),
|
||||
current_price=signal_data.get('current_price'),
|
||||
entry_price=clean_price(signal_data.get('entry_price')),
|
||||
stop_loss=clean_price(signal_data.get('stop_loss')),
|
||||
take_profit=clean_price(signal_data.get('take_profit')),
|
||||
current_price=clean_price(signal_data.get('current_price')),
|
||||
signal_type_detail=signal_data.get('type'),
|
||||
entry_type=signal_data.get('entry_type'),
|
||||
position_size=signal_data.get('position_size'),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user