diff --git a/backend/app/crypto_agent/market_signal_analyzer.py b/backend/app/crypto_agent/market_signal_analyzer.py
index 70fcea2..b593727 100644
--- a/backend/app/crypto_agent/market_signal_analyzer.py
+++ b/backend/app/crypto_agent/market_signal_analyzer.py
@@ -24,10 +24,78 @@ from app.services.news_service import get_news_service
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%)
+ - 价格不再创新高/新低
+ - **切换到震荡市策略,准备高抛低吸**
+
+ ## 🎯 日内交易核心定位
**日内交易 = 快进快出 + 盈亏比第一 + 严控风险**
- 目标:2-3% 快速获利,不是波段行情
- 时限:单笔持仓不超过 4 小时
@@ -544,6 +612,7 @@ class MarketSignalAnalyzer:
```json
{
+ "market_state": "ranging/trending",
"trend_direction": "uptrend/downtrend/neutral",
"trend_strength": "strong/medium/weak",
"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_type`:入场方式 - `market`(现价立即入场)或 `limit`(挂单等待)
@@ -887,10 +981,11 @@ class MarketSignalAnalyzer:
return "新闻获取失败"
def _analyze_trend_position(self, data: Dict[str, pd.DataFrame]) -> str:
- """分析趋势位置和日内交易机会(使用 EMA)"""
+ """分析趋势位置和日内交易机会(使用 EMA)+ 市场状态判断(震荡/趋势)"""
try:
df_30m = data.get('30m')
df_15m = data.get('15m')
+ df_1h = data.get('1h')
if df_30m is None or len(df_30m) < 50:
return ""
@@ -906,6 +1001,61 @@ class MarketSignalAnalyzer:
if not all([ema5_30m, ema10_30m, ema20_30m]):
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 为主)
if ema5_30m > ema10_30m > ema20_30m:
intraday_trend = "上升"
@@ -917,7 +1067,33 @@ class MarketSignalAnalyzer:
intraday_trend = "震荡"
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分钟级别入场时机
if df_15m is not None and len(df_15m) >= 20:
diff --git a/backend/app/news_agent/analyzer.py b/backend/app/news_agent/analyzer.py
index 3186d7e..541dab5 100644
--- a/backend/app/news_agent/analyzer.py
+++ b/backend/app/news_agent/analyzer.py
@@ -32,6 +32,10 @@ class NewsAnalyzer:
self.batch_size = 10 # 每次最多分析 10 条新闻(只传标题,可以增加数量)
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:
"""构建单条新闻的分析提示词"""
@@ -315,6 +319,13 @@ class NewsAnalyzer:
except Exception as 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]}")
return None
@@ -369,11 +380,57 @@ class NewsAnalyzer:
results.extend([None] * len(batch))
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}")
+
+ # 检查是否是余额不足错误 (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))
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:
"""
根据分析结果计算优先级
diff --git a/backend/app/services/multi_llm_service.py b/backend/app/services/multi_llm_service.py
index 343ab59..8dd1758 100644
--- a/backend/app/services/multi_llm_service.py
+++ b/backend/app/services/multi_llm_service.py
@@ -152,7 +152,7 @@ class MultiLLMService:
async def _notify_balance_error(self, provider: str, error: Exception):
"""
- 发送余额不足的Telegram通知
+ 发送余额不足的通知(Telegram + 飞书)
Args:
provider: LLM提供商
@@ -170,14 +170,17 @@ class MultiLLMService:
# 发送通知
try:
from app.services.telegram_service import get_telegram_service
+ from app.services.feishu_service import get_feishu_service
telegram = get_telegram_service()
+ feishu = get_feishu_service()
provider_name = {
'zhipu': '智谱AI (GLM-4)',
'deepseek': 'DeepSeek'
}.get(provider, provider)
- message = f"""🚨 LLM API 余额不足警告
+ # Telegram 通知
+ telegram_message = f"""🚨 LLM API 余额不足警告
━━━━━━━━━━━━━━━━━━━━
@@ -189,8 +192,21 @@ class MultiLLMService:
请及时充值,否则智能体将无法正常工作"""
- await telegram.send_message(message, parse_mode="HTML")
- logger.warning(f"已发送 {provider} 余额不足Telegram通知")
+ await telegram.send_message(telegram_message, parse_mode="HTML")
+ 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