update
This commit is contained in:
parent
d242a5978f
commit
9166e4a987
@ -24,8 +24,76 @@ from app.services.news_service import get_news_service
|
|||||||
class MarketSignalAnalyzer:
|
class MarketSignalAnalyzer:
|
||||||
"""市场信号分析器 - 只关注市场,输出客观信号"""
|
"""市场信号分析器 - 只关注市场,输出客观信号"""
|
||||||
|
|
||||||
# 纯市场分析系统提示词(日内交易优化版 + 多级别反转检测)
|
# 纯市场分析系统提示词(日内交易优化版 + 多级别反转检测 + 市场状态动态调整)
|
||||||
MARKET_ANALYSIS_PROMPT = """你是一位专业的加密货币**日内交易员**和技术分析师。你的任务是综合分析**趋势方向、K线数据、量价关系、技术指标和新闻舆情**,给出**适合日内快进快出**的交易信号。
|
MARKET_ANALYSIS_PROMPT = """你是一位专业的加密货币**智能交易员**和技术分析师。你的任务是综合分析**趋势方向、市场状态、K线数据、量价关系、技术指标和新闻舆情**,给出**适合当前市场状态**的交易信号。
|
||||||
|
|
||||||
|
## 🎯 核心策略:根据市场状态动态调整(最重要!)
|
||||||
|
|
||||||
|
### 📊 第一步:判断市场状态
|
||||||
|
|
||||||
|
**市场状态分类**:
|
||||||
|
- **震荡市(Range-bound)**:价格在一定区间内波动,无明确方向
|
||||||
|
- **趋势市(Trending)**:价格有明确方向(上涨/下跌),持续突破/跌破
|
||||||
|
|
||||||
|
**判断标准**:
|
||||||
|
1. **1h EMA 趋势判断**:
|
||||||
|
- EMA5/10/20 多头/空头排列 → 趋势市
|
||||||
|
- EMA 纠缠,无明确方向 → 震荡市
|
||||||
|
|
||||||
|
2. **30m 波动率判断(ATR)**:
|
||||||
|
- ATR 收缩(较前期下降 > 20%) → 震荡市
|
||||||
|
- ATR 扩张(较前期上升 > 20%) → 趋势市启动
|
||||||
|
|
||||||
|
3. **15m 价格动量判断**:
|
||||||
|
- 价格在区间内波动(±2%) → 震荡市
|
||||||
|
- 价格持续创新高/新低 → 趋势市
|
||||||
|
|
||||||
|
### 📈 第二步:根据市场状态选择策略
|
||||||
|
|
||||||
|
#### 策略A:震荡市策略
|
||||||
|
**特点**:无明确方向,价格在区间内波动
|
||||||
|
**核心操作**:**5分钟级别高抛低吸,快进快出**
|
||||||
|
|
||||||
|
**震荡市入场时机**:
|
||||||
|
- ✅ 回调到下沿支撑 → 5m 反弹信号 → 做多(目标 1-2%)
|
||||||
|
- ✅ 反弹到上沿压力 → 5m 下跌信号 → 做空(目标 1-2%)
|
||||||
|
- ✅ RSI 40-60 震荡,超卖区做多,超买区做空
|
||||||
|
- ✅ 盈亏比 ≥ 1:1.2(震荡市目标小,盈亏比可适当放宽)
|
||||||
|
- ✅ 持仓时间:30分钟-2小时(快速进出)
|
||||||
|
|
||||||
|
**震荡市严禁**:
|
||||||
|
- ❌ 追涨杀跌(价格突破后不要追,通常会回落)
|
||||||
|
- ❌ 期待大行情(震荡市无大趋势,不要贪婪)
|
||||||
|
- ❌ 持仓过久(区间内快速进出)
|
||||||
|
|
||||||
|
#### 策略B:趋势市策略
|
||||||
|
**特点**:价格有明确方向,持续突破/跌破
|
||||||
|
**核心操作**:**跟随趋势,等待回调/反弹入场,持仓更长**
|
||||||
|
|
||||||
|
**趋势市入场时机**:
|
||||||
|
- ✅ 趋势回调/反弹到 EMA20/支撑压力位 → 顺势入场
|
||||||
|
- ✅ 放量突破/跌破关键位 → 等待回踩确认后入场
|
||||||
|
- ✅ 盈亏比 ≥ 1:1.5(趋势市目标更大,要求更严格)
|
||||||
|
- ✅ 持仓时间:2-6小时(跟随趋势)
|
||||||
|
|
||||||
|
**趋势市严禁**:
|
||||||
|
- ❌ **逆势做超短线**(趋势明确时不要反向操作!)
|
||||||
|
- ❌ 频繁进出(趋势要持仓,不要被小波动洗出)
|
||||||
|
- ❌ 小波动就止盈(让利润奔跑)
|
||||||
|
|
||||||
|
### 🔄 策略切换规则(关键!)
|
||||||
|
|
||||||
|
**震荡 → 趋势**:
|
||||||
|
- 1h EMA 多头/空头排列形成
|
||||||
|
- 放量突破/跌破震荡区间
|
||||||
|
- ATR 显著扩张(> 20%)
|
||||||
|
- **立即切换到趋势市策略,停止超短线反向操作**
|
||||||
|
|
||||||
|
**趋势 → 震荡**:
|
||||||
|
- 趋势减弱,EMA 开始纠缠
|
||||||
|
- 波动率收缩(ATR 下降 > 20%)
|
||||||
|
- 价格不再创新高/新低
|
||||||
|
- **切换到震荡市策略,准备高抛低吸**
|
||||||
|
|
||||||
## 🎯 日内交易核心定位
|
## 🎯 日内交易核心定位
|
||||||
**日内交易 = 快进快出 + 盈亏比第一 + 严控风险**
|
**日内交易 = 快进快出 + 盈亏比第一 + 严控风险**
|
||||||
@ -544,6 +612,7 @@ class MarketSignalAnalyzer:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"market_state": "ranging/trending",
|
||||||
"trend_direction": "uptrend/downtrend/neutral",
|
"trend_direction": "uptrend/downtrend/neutral",
|
||||||
"trend_strength": "strong/medium/weak",
|
"trend_strength": "strong/medium/weak",
|
||||||
"analysis_summary": "简要描述当前市场状态(50字以内)",
|
"analysis_summary": "简要描述当前市场状态(50字以内)",
|
||||||
@ -571,6 +640,31 @@ class MarketSignalAnalyzer:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 重要说明 - market_state 字段(新增)
|
||||||
|
- **market_state**:**必须明确判断当前是震荡市还是趋势市**
|
||||||
|
- `ranging`(震荡市):价格在区间内波动,无明确方向,使用5分钟高抛低吸策略
|
||||||
|
- `trending`(趋势市):价格有明确方向,跟随趋势,等待回调/反弹入场
|
||||||
|
|
||||||
|
**判断标准**:
|
||||||
|
1. **1h EMA 纠缠** → ranging
|
||||||
|
2. **30m ATR 收缩 > 20%** → ranging
|
||||||
|
3. **1h EMA 多头/空头排列** → trending
|
||||||
|
4. **价格持续创新高/新低** → trending
|
||||||
|
|
||||||
|
**策略差异**:
|
||||||
|
- **震荡市(ranging)**:
|
||||||
|
- 5分钟级别操作
|
||||||
|
- 支撑位做多,压力位做空
|
||||||
|
- 目标 1-2%,快速进出
|
||||||
|
- 盈亏比 ≥ 1:1.2
|
||||||
|
|
||||||
|
- **趋势市(trending)**:
|
||||||
|
- 30分钟/1小时级别操作
|
||||||
|
- 等待回调/反弹到 EMA20 顺势入场
|
||||||
|
- 目标 3-5%,跟随趋势
|
||||||
|
- 盈亏比 ≥ 1:1.5
|
||||||
|
- **严禁逆势做超短线**
|
||||||
|
|
||||||
## 重要说明
|
## 重要说明
|
||||||
- `entry_price`:建议入场价格(单一值)
|
- `entry_price`:建议入场价格(单一值)
|
||||||
- `entry_type`:入场方式 - `market`(现价立即入场)或 `limit`(挂单等待)
|
- `entry_type`:入场方式 - `market`(现价立即入场)或 `limit`(挂单等待)
|
||||||
@ -887,10 +981,11 @@ class MarketSignalAnalyzer:
|
|||||||
return "新闻获取失败"
|
return "新闻获取失败"
|
||||||
|
|
||||||
def _analyze_trend_position(self, data: Dict[str, pd.DataFrame]) -> str:
|
def _analyze_trend_position(self, data: Dict[str, pd.DataFrame]) -> str:
|
||||||
"""分析趋势位置和日内交易机会(使用 EMA)"""
|
"""分析趋势位置和日内交易机会(使用 EMA)+ 市场状态判断(震荡/趋势)"""
|
||||||
try:
|
try:
|
||||||
df_30m = data.get('30m')
|
df_30m = data.get('30m')
|
||||||
df_15m = data.get('15m')
|
df_15m = data.get('15m')
|
||||||
|
df_1h = data.get('1h')
|
||||||
|
|
||||||
if df_30m is None or len(df_30m) < 50:
|
if df_30m is None or len(df_30m) < 50:
|
||||||
return ""
|
return ""
|
||||||
@ -906,6 +1001,61 @@ class MarketSignalAnalyzer:
|
|||||||
if not all([ema5_30m, ema10_30m, ema20_30m]):
|
if not all([ema5_30m, ema10_30m, ema20_30m]):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
# ========== 新增:市场状态判断(震荡 vs 趋势) ==========
|
||||||
|
market_state = "unknown"
|
||||||
|
market_state_reason = []
|
||||||
|
|
||||||
|
# 1h EMA 趋势判断
|
||||||
|
if df_1h is not None and len(df_1h) >= 20:
|
||||||
|
latest_1h = df_1h.iloc[-1]
|
||||||
|
ema5_1h = latest_1h.get('ma5')
|
||||||
|
ema10_1h = latest_1h.get('ma10')
|
||||||
|
ema20_1h = latest_1h.get('ma20')
|
||||||
|
|
||||||
|
if ema5_1h and ema10_1h and ema20_1h:
|
||||||
|
# 1h EMA 多头/空头排列 → 趋势市
|
||||||
|
if ema5_1h > ema10_1h > ema20_1h:
|
||||||
|
market_state = "trending"
|
||||||
|
market_state_reason.append("1h EMA 多头排列")
|
||||||
|
elif ema5_1h < ema10_1h < ema20_1h:
|
||||||
|
market_state = "trending"
|
||||||
|
market_state_reason.append("1h EMA 空头排列")
|
||||||
|
else:
|
||||||
|
market_state = "ranging"
|
||||||
|
market_state_reason.append("1h EMA 纠缠")
|
||||||
|
|
||||||
|
# 波动率判断(ATR 变化)
|
||||||
|
if df_30m is not None and len(df_30m) >= 24 and 'atr' in df_30m.columns:
|
||||||
|
recent_atr = df_30m['atr'].iloc[-6:].mean() # 最近3小时
|
||||||
|
older_atr = df_30m['atr'].iloc[-12:-6].mean() # 之前3小时
|
||||||
|
|
||||||
|
if pd.notna(recent_atr) and pd.notna(older_atr) and older_atr > 0:
|
||||||
|
atr_change = (recent_atr - older_atr) / older_atr * 100
|
||||||
|
|
||||||
|
if atr_change > 20:
|
||||||
|
if market_state != "trending":
|
||||||
|
market_state = "trending"
|
||||||
|
market_state_reason.append(f"ATR 扩张 {atr_change:.0f}%")
|
||||||
|
elif atr_change < -20:
|
||||||
|
if market_state != "ranging":
|
||||||
|
market_state = "ranging"
|
||||||
|
market_state_reason.append(f"ATR 收缩 {abs(atr_change):.0f}%")
|
||||||
|
|
||||||
|
# 价格动量判断(15m)
|
||||||
|
if df_15m is not None and len(df_15m) >= 20:
|
||||||
|
recent_high = df_15m['high'].iloc[-20:].max()
|
||||||
|
recent_low = df_15m['low'].iloc[-20:].min()
|
||||||
|
price_range = (recent_high - recent_low) / current_price * 100
|
||||||
|
|
||||||
|
if price_range < 2.5: # 15分钟内波动小于2.5% → 震荡
|
||||||
|
if market_state != "trending":
|
||||||
|
market_state = "ranging"
|
||||||
|
market_state_reason.append(f"15m 波动 {price_range:.1f}% 较小")
|
||||||
|
elif price_range > 4: # 15分钟内波动大于4% → 趋势
|
||||||
|
if market_state != "ranging":
|
||||||
|
market_state = "trending"
|
||||||
|
market_state_reason.append(f"15m 波动 {price_range:.1f}% 较大")
|
||||||
|
|
||||||
# 判断日内趋势(30m EMA 为主)
|
# 判断日内趋势(30m EMA 为主)
|
||||||
if ema5_30m > ema10_30m > ema20_30m:
|
if ema5_30m > ema10_30m > ema20_30m:
|
||||||
intraday_trend = "上升"
|
intraday_trend = "上升"
|
||||||
@ -917,7 +1067,33 @@ class MarketSignalAnalyzer:
|
|||||||
intraday_trend = "震荡"
|
intraday_trend = "震荡"
|
||||||
intraday_emoji = "➖"
|
intraday_emoji = "➖"
|
||||||
|
|
||||||
analysis = [f"日内趋势(30m EMA): {intraday_emoji} {intraday_trend}"]
|
# 构建市场状态分析
|
||||||
|
analysis_parts = []
|
||||||
|
|
||||||
|
# 市场状态显示(新增)
|
||||||
|
if market_state == "trending":
|
||||||
|
state_emoji = "📊"
|
||||||
|
state_text = f"{state_emoji} **市场状态: 趋势市**"
|
||||||
|
analysis_parts.append(state_text)
|
||||||
|
analysis_parts.append(f" 判断依据: {', '.join(market_state_reason)}")
|
||||||
|
analysis_parts.append(f" 策略: 跟随趋势,等待回调/反弹到 EMA20 顺势入场")
|
||||||
|
analysis_parts.append(f" 目标: 3-5%,盈亏比 ≥ 1:1.5")
|
||||||
|
analysis_parts.append(f" 严禁: 逆势做超短线")
|
||||||
|
elif market_state == "ranging":
|
||||||
|
state_emoji = "🔄"
|
||||||
|
state_text = f"{state_emoji} **市场状态: 震荡市**"
|
||||||
|
analysis_parts.append(state_text)
|
||||||
|
analysis_parts.append(f" 判断依据: {', '.join(market_state_reason)}")
|
||||||
|
analysis_parts.append(f" 策略: 5分钟级别高抛低吸,支撑位多、压力位空")
|
||||||
|
analysis_parts.append(f" 目标: 1-2%,盈亏比 ≥ 1:1.2")
|
||||||
|
analysis_parts.append(f" 严禁: 追涨杀跌")
|
||||||
|
else:
|
||||||
|
analysis_parts.append(f"⚠️ 市场状态: 不明确,观望为主")
|
||||||
|
|
||||||
|
analysis_parts.append(f"")
|
||||||
|
analysis_parts.append(f"日内趋势(30m EMA): {intraday_emoji} {intraday_trend}")
|
||||||
|
|
||||||
|
analysis = analysis_parts
|
||||||
|
|
||||||
# 检查15分钟级别入场时机
|
# 检查15分钟级别入场时机
|
||||||
if df_15m is not None and len(df_15m) >= 20:
|
if df_15m is not None and len(df_15m) >= 20:
|
||||||
|
|||||||
@ -32,6 +32,10 @@ class NewsAnalyzer:
|
|||||||
self.batch_size = 10 # 每次最多分析 10 条新闻(只传标题,可以增加数量)
|
self.batch_size = 10 # 每次最多分析 10 条新闻(只传标题,可以增加数量)
|
||||||
self.max_retries = 2
|
self.max_retries = 2
|
||||||
|
|
||||||
|
# 余额错误通知冷却时间(秒)
|
||||||
|
self._balance_error_cooldown = 3600 # 1小时内只通知一次
|
||||||
|
self._balance_error_last_notified = None
|
||||||
|
|
||||||
def _build_analysis_prompt(self, news_item: NewsItem) -> str:
|
def _build_analysis_prompt(self, news_item: NewsItem) -> str:
|
||||||
"""构建单条新闻的分析提示词"""
|
"""构建单条新闻的分析提示词"""
|
||||||
|
|
||||||
@ -315,6 +319,13 @@ class NewsAnalyzer:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"分析失败 (尝试 {attempt + 1}/{self.max_retries}): {e}")
|
logger.warning(f"分析失败 (尝试 {attempt + 1}/{self.max_retries}): {e}")
|
||||||
|
|
||||||
|
# 检查是否是余额不足错误 (402)
|
||||||
|
error_str = str(e)
|
||||||
|
error_code = str(e).split('Error code: ')[1].split(' -')[0] if 'Error code:' in error_str else ''
|
||||||
|
if error_code == '402' or ('402' in error_str and 'insufficient balance' in error_str.lower()):
|
||||||
|
await self._notify_balance_error(e)
|
||||||
|
break # 余额不足不再重试
|
||||||
|
|
||||||
logger.error(f"新闻分析失败,已达最大重试次数: {news_item.title[:50]}")
|
logger.error(f"新闻分析失败,已达最大重试次数: {news_item.title[:50]}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -369,11 +380,57 @@ class NewsAnalyzer:
|
|||||||
results.extend([None] * len(batch))
|
results.extend([None] * len(batch))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
error_str = str(e)
|
||||||
|
error_code = str(e).split('Error code: ')[1].split(' -')[0] if 'Error code:' in error_str else ''
|
||||||
|
|
||||||
logger.error(f"批量分析失败: {e}")
|
logger.error(f"批量分析失败: {e}")
|
||||||
|
|
||||||
|
# 检查是否是余额不足错误 (402)
|
||||||
|
if error_code == '402' or ('402' in error_str and 'insufficient balance' in error_str.lower()):
|
||||||
|
await self._notify_balance_error(e)
|
||||||
|
|
||||||
results.extend([None] * len(batch))
|
results.extend([None] * len(batch))
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
async def _notify_balance_error(self, error: Exception):
|
||||||
|
"""
|
||||||
|
发送余额不足的飞书通知
|
||||||
|
|
||||||
|
Args:
|
||||||
|
error: 异常对象
|
||||||
|
"""
|
||||||
|
# 检查冷却时间
|
||||||
|
now = datetime.now()
|
||||||
|
if self._balance_error_last_notified:
|
||||||
|
time_since_last = (now - self._balance_error_last_notified).total_seconds()
|
||||||
|
if time_since_last < self._balance_error_cooldown:
|
||||||
|
logger.info(f"余额错误通知冷却中,剩余 {int(self._balance_error_cooldown - time_since_last)} 秒")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 发送通知
|
||||||
|
try:
|
||||||
|
from app.services.feishu_service import get_feishu_service
|
||||||
|
feishu = get_feishu_service()
|
||||||
|
|
||||||
|
message = f"""🚨 **新闻分析 LLM API 余额不足警告**
|
||||||
|
|
||||||
|
**服务商**: DeepSeek
|
||||||
|
**错误类型**: 余额不足 (Insufficient Balance)
|
||||||
|
**错误信息**: {str(error)[:200]}
|
||||||
|
**时间**: {now.strftime('%Y-%m-%d %H:%M:%S')}
|
||||||
|
|
||||||
|
⚠️ 请及时充值,否则新闻智能体将无法正常工作"""
|
||||||
|
|
||||||
|
await feishu.send_text(message)
|
||||||
|
logger.warning("已发送 DeepSeek 余额不足飞书通知")
|
||||||
|
|
||||||
|
# 记录通知时间
|
||||||
|
self._balance_error_last_notified = now
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送余额不足通知失败: {e}")
|
||||||
|
|
||||||
def calculate_priority(self, analysis: Dict[str, Any], quality_score: float = 0.5) -> float:
|
def calculate_priority(self, analysis: Dict[str, Any], quality_score: float = 0.5) -> float:
|
||||||
"""
|
"""
|
||||||
根据分析结果计算优先级
|
根据分析结果计算优先级
|
||||||
|
|||||||
@ -152,7 +152,7 @@ class MultiLLMService:
|
|||||||
|
|
||||||
async def _notify_balance_error(self, provider: str, error: Exception):
|
async def _notify_balance_error(self, provider: str, error: Exception):
|
||||||
"""
|
"""
|
||||||
发送余额不足的Telegram通知
|
发送余额不足的通知(Telegram + 飞书)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
provider: LLM提供商
|
provider: LLM提供商
|
||||||
@ -170,14 +170,17 @@ class MultiLLMService:
|
|||||||
# 发送通知
|
# 发送通知
|
||||||
try:
|
try:
|
||||||
from app.services.telegram_service import get_telegram_service
|
from app.services.telegram_service import get_telegram_service
|
||||||
|
from app.services.feishu_service import get_feishu_service
|
||||||
telegram = get_telegram_service()
|
telegram = get_telegram_service()
|
||||||
|
feishu = get_feishu_service()
|
||||||
|
|
||||||
provider_name = {
|
provider_name = {
|
||||||
'zhipu': '智谱AI (GLM-4)',
|
'zhipu': '智谱AI (GLM-4)',
|
||||||
'deepseek': 'DeepSeek'
|
'deepseek': 'DeepSeek'
|
||||||
}.get(provider, provider)
|
}.get(provider, provider)
|
||||||
|
|
||||||
message = f"""🚨 <b>LLM API 余额不足警告</b>
|
# Telegram 通知
|
||||||
|
telegram_message = f"""🚨 <b>LLM API 余额不足警告</b>
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
@ -189,8 +192,21 @@ class MultiLLMService:
|
|||||||
|
|
||||||
<i>请及时充值,否则智能体将无法正常工作</i>"""
|
<i>请及时充值,否则智能体将无法正常工作</i>"""
|
||||||
|
|
||||||
await telegram.send_message(message, parse_mode="HTML")
|
await telegram.send_message(telegram_message, parse_mode="HTML")
|
||||||
logger.warning(f"已发送 {provider} 余额不足Telegram通知")
|
logger.info(f"已发送 {provider} 余额不足 Telegram 通知")
|
||||||
|
|
||||||
|
# 飞书通知
|
||||||
|
feishu_message = f"""🚨 **LLM API 余额不足警告**
|
||||||
|
|
||||||
|
**服务商**: {provider_name}
|
||||||
|
**错误类型**: 余额不足 (Insufficient Balance)
|
||||||
|
**错误信息**: {str(error)[:200]}
|
||||||
|
**时间**: {now.strftime('%Y-%m-%d %H:%M:%S')}
|
||||||
|
|
||||||
|
⚠️ 请及时充值,否则智能体将无法正常工作"""
|
||||||
|
|
||||||
|
await feishu.send_text(feishu_message)
|
||||||
|
logger.info(f"已发送 {provider} 余额不足飞书通知")
|
||||||
|
|
||||||
# 记录通知时间
|
# 记录通知时间
|
||||||
self._balance_error_notified[provider] = now
|
self._balance_error_notified[provider] = now
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user