From 6ac175038e42022e1086276704b875d7de3617d1 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Mon, 27 Apr 2026 14:09:52 +0800 Subject: [PATCH] 1 --- backend/app/config.py | 10 +- backend/app/crypto_agent/crypto_agent.py | 137 +++++++--------------- backend/app/crypto_agent/regime_engine.py | 14 +-- backend/app/crypto_agent/setup_policy.py | 30 +++++ frontend/console.html | 3 +- 5 files changed, 89 insertions(+), 105 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index c690b8e..6ed9903 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -109,7 +109,9 @@ class Settings(BaseSettings): # 加密货币交易智能体配置 crypto_symbols: str = "BTCUSDT,ETHUSDT" # 监控的交易对,逗号分隔 - crypto_llm_threshold: float = 0.70 # 触发 LLM 分析的置信度阈值 + crypto_llm_threshold: float = 0.70 # 旧的统一阈值,兼容保留 + crypto_intraday_signal_threshold: float = 0.68 # 日内信号置信度阈值 + crypto_trend_signal_threshold: float = 0.72 # 趋势信号置信度阈值 # 价格监控模式配置 use_bitget_websocket: bool = True # 是否使用 Bitget WebSocket 实时价格(默认 False 使用 Binance 轮询) @@ -119,14 +121,14 @@ class Settings(BaseSettings): crypto_min_volatility_percent: float = 0.5 # 最小波动率(百分比),低于此值跳过分析 crypto_min_price_range_percent: float = 0.3 # 最小价格变动范围(百分比),低于此值跳过分析 crypto_5m_surge_threshold: float = 1.0 # 5分钟突发波动阈值(百分比),超过此值即使1小时波动率低也会触发分析 - crypto_intraday_llm_cooldown_minutes: int = 15 # 日内 LLM 分析冷却时间 - crypto_trend_llm_cooldown_minutes: int = 60 # 趋势 LLM 分析冷却时间 + crypto_intraday_llm_cooldown_minutes: int = 8 # 日内 LLM 分析冷却时间 + crypto_trend_llm_cooldown_minutes: int = 25 # 趋势 LLM 分析冷却时间 crypto_force_llm_surge_threshold: float = 1.2 # 15分钟突发波动强制触发 LLM 的阈值 crypto_force_llm_trade_zone_pct: float = 0.25 # 接近关键交易区时强制触发 LLM 的距离阈值 crypto_event_analysis_enabled: bool = True # 是否启用实时行情事件触发分析 crypto_event_analysis_window_minutes: int = 5 # 实时行情异动检测窗口 crypto_event_analysis_price_change_percent: float = 0.8 # 检测窗口内涨跌超过该阈值触发日内分析 - crypto_event_analysis_cooldown_minutes: int = 10 # 同一交易对事件触发分析冷却 + crypto_event_analysis_cooldown_minutes: int = 5 # 同一交易对事件触发分析冷却 # Brave Search API 配置 brave_api_key: str = "" diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index c2e482f..140319b 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -503,7 +503,8 @@ class CryptoAgent: last_symbol = self._analysis_monitor.get("last_analysis_symbol") or "-" last_status = self._analysis_monitor.get("last_analysis_status") or "unknown" last_detail = self._analysis_monitor.get("last_analysis_detail") or "最近一轮分析已完成" - threshold = self.settings.crypto_llm_threshold * 100 + intraday_threshold = self._get_signal_threshold_pct(signal_type='short_term') + trend_threshold = self._get_signal_threshold_pct(signal_type='medium_term') title = "💓 [分析心跳] 系统运行正常" content = "\n".join([ @@ -513,7 +514,7 @@ class CryptoAgent: f"**完成分析**: {symbol_completed} 个交易对", f"**跳过分析**: {symbol_skipped} 次", f"**分析异常**: {symbol_errors} 次", - f"**信号阈值**: {threshold:.0f}%", + f"**信号阈值**: 日内 {intraday_threshold:.0f}% / 趋势 {trend_threshold:.0f}%", f"**最近分析对象**: {last_symbol}", f"**最近状态**: {last_status}", f"**最近说明**: {last_detail}", @@ -615,20 +616,9 @@ class CryptoAgent: return False, "" def _resolve_llm_lanes_for_symbol(self, symbol: str, data: Dict[str, pd.DataFrame]) -> tuple[List[str], Dict[str, Any], str]: - now = datetime.now() state = self._get_lane_state(symbol) force, force_reason = self._detect_force_llm_trigger(symbol, data) - lanes: List[str] = [] - - intraday_last = self._parse_iso_datetime(state.get("last_intraday_at")) - trend_last = self._parse_iso_datetime(state.get("last_trend_at")) - intraday_cooldown = timedelta(minutes=self.settings.crypto_intraday_llm_cooldown_minutes) - trend_cooldown = timedelta(minutes=self.settings.crypto_trend_llm_cooldown_minutes) - - if force or intraday_last is None or not state.get("cached_intraday") or now - intraday_last >= intraday_cooldown: - lanes.append("intraday") - if force or trend_last is None or not state.get("cached_trend") or now - trend_last >= trend_cooldown: - lanes.append("trend") + lanes: List[str] = ["intraday", "trend"] cached_results = {} if state.get("cached_intraday"): @@ -636,9 +626,6 @@ class CryptoAgent: if state.get("cached_trend"): cached_results["trend"] = state["cached_trend"] - if not lanes and not cached_results: - lanes = ["intraday", "trend"] - state["last_force_reason"] = force_reason if force else "" return lanes, cached_results, force_reason @@ -1245,7 +1232,7 @@ class CryptoAgent: f"📊 **监控交易对**: {len(self.symbols)} 个", f" {', '.join(self.symbols)}", f"⏰ **运行频率**: 每5分钟轻扫描", - f"🧊 **LLM 冷却**: 日内 {self.settings.crypto_intraday_llm_cooldown_minutes} 分钟 / 趋势 {self.settings.crypto_trend_llm_cooldown_minutes} 分钟", + f"🧠 **LLM 执行**: 每轮双 lane 直接分析,不做冷却缓存", f"💰 **交易系统**: 已启用(后台统一监控)", f"🎯 **分析维度**: 技术面 + 资金面 + 情绪面", ] @@ -1497,25 +1484,6 @@ class CryptoAgent: price_change_24h = self._calculate_price_change(data['1h']) logger.info(f"💰 当前价格: ${current_price:,.2f} ({price_change_24h})") - paper_symbol_cooldown = None - if self.settings.paper_trading_enabled: - paper_symbol_cooldown = self._refresh_symbol_trade_cooldown('PaperTrading', symbol) - if paper_symbol_cooldown.get('should_cool_down'): - cooldown_until = paper_symbol_cooldown.get('cooldown_until') - cooldown_text = cooldown_until.isoformat() if cooldown_until else '' - self._record_analysis_event( - "symbol_trade_cooldown", - symbol=symbol, - status="warning", - detail=paper_symbol_cooldown.get('reason', '交易对处于冷却中'), - extra={ - "platform": "PaperTrading", - "losing_streak": paper_symbol_cooldown.get('losing_streak', 0), - "cooldown_until": cooldown_text, - }, - ) - logger.info(f"⏸️ 模拟盘交易对冷却: {paper_symbol_cooldown.get('reason')} | until={cooldown_text}") - # 1.5. 波动率检查(节省 LLM 调用) should_analyze, volatility_reason, volatility = self._check_volatility(symbol, data) if not should_analyze: @@ -1545,24 +1513,19 @@ class CryptoAgent: logger.info(f"\n🤖 【第一阶段:市场信号分析】 lanes={lanes_to_run or ['cache_only']}") if force_reason: logger.info(f" ⚡ 强制触发 LLM: {force_reason}") - elif not lanes_to_run: - logger.info(" 🧊 LLM 冷却中,使用上一轮 lane 缓存结果") - if lanes_to_run: - self._bump_analysis_stat("llm_analyses") - for lane in lanes_to_run: - self._bump_lane_call(lane) - else: - self._bump_analysis_stat("cache_only_runs") + self._bump_analysis_stat("llm_analyses") + for lane in lanes_to_run: + self._bump_lane_call(lane) self._record_analysis_event( "llm_lane_plan", symbol=symbol, status="info", - detail=force_reason or (f"本轮执行 lane: {', '.join(lanes_to_run)}" if lanes_to_run else "LLM 冷却中,使用缓存 lane 结果"), + detail=force_reason or f"本轮执行 lane: {', '.join(lanes_to_run)}", extra={ "lanes_to_run": lanes_to_run, - "cache_only": not bool(lanes_to_run), + "cache_only": False, "force_reason": force_reason, "trigger_source": trigger_source, }, @@ -1627,19 +1590,19 @@ class CryptoAgent: return # 检查是否有达到阈值的交易信号 - threshold = self.settings.crypto_llm_threshold * 100 # 转换为百分比 - valid_signals = [s for s in trade_signals if s.get('confidence', 0) >= threshold] + valid_signals = self._filter_valid_trade_signals(trade_signals) if not valid_signals: self._bump_analysis_stat("threshold_filtered_runs") self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat() self._analysis_monitor["last_analysis_status"] = "completed" - self._analysis_monitor["last_analysis_detail"] = f"完成分析,但无信号达到阈值 {threshold}%" + thresholds_text = f"日内 {self._get_signal_threshold_pct(signal_type='short_term'):.0f}% / 趋势 {self._get_signal_threshold_pct(signal_type='medium_term'):.0f}%" + self._analysis_monitor["last_analysis_detail"] = f"完成分析,但无信号达到阈值 {thresholds_text}" self._record_analysis_event( "symbol_analysis_completed", symbol=symbol, status="success", - detail=f"完成分析,但无信号达到阈值 {threshold}%", + detail=f"完成分析,但无信号达到阈值 {thresholds_text}", extra={ "trade_signals": len(trade_signals), "valid_signals": 0, @@ -1648,12 +1611,12 @@ class CryptoAgent: "post_regime_lane_signal_counts": market_signal.get("post_regime_lane_signal_counts") or {}, }, ) - logger.info(f"\n⏸️ 结论: 无交易信号达到置信度阈值 ({threshold}%),继续观望") + logger.info(f"\n⏸️ 结论: 无交易信号达到分周期阈值(日内 {self._get_signal_threshold_pct(signal_type='short_term'):.0f}% / 趋势 {self._get_signal_threshold_pct(signal_type='medium_term'):.0f}%),继续观望") return self._bump_analysis_stat("valid_signal_runs") self._bump_analysis_stat("valid_signals_total", len(valid_signals)) - logger.info(f"\n✅ 发现 {len(valid_signals)} 个有效交易信号(达到 {threshold}% 阈值)") + logger.info(f"\n✅ 发现 {len(valid_signals)} 个有效交易信号(按周期阈值过滤)") for signal in valid_signals: logger.info( f" - {signal.get('timeframe', signal.get('type', 'unknown'))} | " @@ -2036,18 +1999,34 @@ class CryptoAgent: profile['setup_type'] = setup_type return profile + def _get_signal_threshold_pct(self, signal: Optional[Dict[str, Any]] = None, signal_type: Optional[str] = None) -> float: + resolved_type = signal_type or (signal or {}).get('timeframe') or (signal or {}).get('type') or 'medium_term' + if resolved_type == 'short_term': + threshold = getattr(self.settings, 'crypto_intraday_signal_threshold', None) + elif resolved_type == 'medium_term': + threshold = getattr(self.settings, 'crypto_trend_signal_threshold', None) + else: + threshold = None + + if threshold is None: + threshold = getattr(self.settings, 'crypto_llm_threshold', 0.70) + return float(threshold) * 100 + + def _filter_valid_trade_signals(self, signals: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + return [ + signal for signal in (signals or []) + if signal.get('action') in {'buy', 'sell'} + and float(signal.get('confidence', 0) or 0) >= self._get_signal_threshold_pct(signal=signal) + ] + async def _execute_decisions(self, paper_decision: Dict[str, Any], bitget_decisions: Dict[str, Dict[str, Any]], market_signal: Dict[str, Any], current_price: float): """执行交易决策(模拟盘 + Bitget 独立)""" # 保存本轮所有达到阈值的可交易信号,避免分流后只落一条信号 - threshold = self.settings.crypto_llm_threshold * 100 symbol = market_signal.get('symbol') signals = market_signal.get('signals', []) - valid_signals = [ - signal for signal in signals - if signal.get('action') in {'buy', 'sell'} and signal.get('confidence', 0) >= threshold - ] + valid_signals = self._filter_valid_trade_signals(signals) for signal in valid_signals: signal_to_save = signal.copy() @@ -2673,12 +2652,8 @@ class CryptoAgent: current_price: float): """发送市场信号通知(第一阶段)- 调用前已确保有有效信号""" try: - # 获取配置的阈值 - threshold = self.settings.crypto_llm_threshold * 100 # 转换为百分比 - - # 过滤达到阈值的信号(防御性检查) signals = market_signal.get('signals', []) - valid_signals = [s for s in signals if s.get('confidence', 0) >= threshold] + valid_signals = self._filter_valid_trade_signals(signals) if not valid_signals: return @@ -4017,12 +3992,6 @@ class CryptoAgent: blocked_reasons = regime_profile.get('no_trade_reasons') or ['当前市场状态不允许交易'] return False, f"市场状态过滤: {';'.join(blocked_reasons[:2])}" - symbol_cooldown = self._get_symbol_trade_cooldown(platform_name, signal.get('symbol', '')) - if symbol_cooldown and symbol_cooldown.get('should_cool_down'): - cooldown_until = symbol_cooldown.get('cooldown_until') - until_text = cooldown_until.isoformat() if cooldown_until else '' - return False, f"交易对冷却中: {symbol_cooldown.get('reason', '').strip()} {until_text}".strip() - setup_passed, setup_reason = self._check_setup_execution_constraints(signal) if not setup_passed: return False, f"setup 过滤: {setup_reason}" @@ -4095,12 +4064,6 @@ class CryptoAgent: elif signal_action == 'sell' and fr_pct < -0.05: return False, f"资金费率过冷 {fr_pct:.4f}%(做空拥挤),拒绝做空" - # 6. 连败降温检查(不拒绝,但降低仓位) - streak_info = self._check_losing_streak(platform_name) - if streak_info.get('should_cool_down'): - signal['_streak_margin_multiplier'] = streak_info['margin_multiplier'] - logger.warning(f"[{platform_name}] ⚠️ {streak_info['reason']}") - return True, "通过风控检查" def execute_signal_with_rules(self, signal: Dict[str, Any], @@ -4440,20 +4403,10 @@ class CryptoAgent: return False confidence = signal.get('confidence', 0) - # 使用配置文件中的阈值 - threshold = self.settings.crypto_llm_threshold * 100 # 转换为百分比 + threshold = self._get_signal_threshold_pct(signal=signal) if confidence < threshold: return False - # 检查冷却时间(30分钟内不重复发送相同方向的信号) - if symbol in self.signal_cooldown: - cooldown_end = self.signal_cooldown[symbol] + timedelta(minutes=30) - if datetime.now() < cooldown_end: - if symbol in self.last_signals: - if self.last_signals[symbol].get('action') == action: - logger.debug(f"{symbol} 信号冷却中,跳过") - return False - return True async def _review_and_adjust_positions( @@ -4560,11 +4513,7 @@ class CryptoAgent: ) signals = market_signal.get('signals', []) - threshold = self.settings.crypto_llm_threshold * 100 - valid_signals = [ - signal for signal in signals - if signal.get('action') in {'buy', 'sell'} and signal.get('confidence', 0) >= threshold - ] + valid_signals = self._filter_valid_trade_signals(signals) execution_preview: Dict[str, Any] = {} @@ -4627,8 +4576,10 @@ class CryptoAgent: 'execution_guardian': self.execution_guardian.get_status(), 'llm_schedule': { 'scan_interval_minutes': 5, - 'intraday_cooldown_minutes': self.settings.crypto_intraday_llm_cooldown_minutes, - 'trend_cooldown_minutes': self.settings.crypto_trend_llm_cooldown_minutes, + 'intraday_cooldown_minutes': 0, + 'trend_cooldown_minutes': 0, + 'intraday_signal_threshold': self._get_signal_threshold_pct(signal_type='short_term'), + 'trend_signal_threshold': self._get_signal_threshold_pct(signal_type='medium_term'), 'force_surge_threshold': self.settings.crypto_force_llm_surge_threshold, 'force_trade_zone_pct': self.settings.crypto_force_llm_trade_zone_pct, 'event_analysis_enabled': self.settings.crypto_event_analysis_enabled, diff --git a/backend/app/crypto_agent/regime_engine.py b/backend/app/crypto_agent/regime_engine.py index f3ded19..bf41a37 100644 --- a/backend/app/crypto_agent/regime_engine.py +++ b/backend/app/crypto_agent/regime_engine.py @@ -90,15 +90,15 @@ class RegimeEngine: { "regime_key": "transition", "market_state_label": "过渡市", - "tradability": "avoid", + "tradability": "selective", "risk_mode": "defensive", - "allowed_lanes": [], - "preferred_lanes": [], - "allowed_setups": [], - "preferred_entry_types": [], + "allowed_lanes": ["short_term"], + "preferred_lanes": ["short_term", "medium_term"], + "allowed_setups": ["breakout_pullback", "breakout_confirmation", "range_reversal"], + "preferred_entry_types": ["limit", "market"], } ) - no_trade_reasons.append("趋势与震荡切换期,方向优势不足") + no_trade_reasons.append("趋势与震荡切换期,只做边界确认和突破回踩") elif range_regime in {"weak_trend", "strong_trend"} and trend_direction in {"uptrend", "downtrend"}: allow_reversal = bool(reversal_detection.get("is_reversing")) and trend_strength != "strong" @@ -127,7 +127,7 @@ class RegimeEngine: "risk_mode": "normal" if range_regime == "strong_trend" else "reduced", "allowed_lanes": ["medium_term", "short_term"], "preferred_lanes": ["medium_term", "short_term"], - "allowed_setups": ["trend_continuation_pullback"], + "allowed_setups": ["trend_continuation_pullback", "deep_pullback_continuation"], "preferred_entry_types": ["limit", "market"], } ) diff --git a/backend/app/crypto_agent/setup_policy.py b/backend/app/crypto_agent/setup_policy.py index a6a1011..1068d63 100644 --- a/backend/app/crypto_agent/setup_policy.py +++ b/backend/app/crypto_agent/setup_policy.py @@ -42,6 +42,14 @@ class SetupPolicy: continue if setup_type not in allowed_setups: + if tradability == "selective" and self._allow_selective_fallback(signal, lane=lane): + kept.append({ + **signal, + "setup_type": setup_type, + "setup_basis": setup_basis, + "entry_basis": entry_basis, + }) + continue reasons.append(f"{setup_type} 不在允许 setup 内") reason_counts[f"setup_blocked:{setup_type}"] += 1 continue @@ -55,6 +63,28 @@ class SetupPolicy: return kept, reasons, dict(reason_counts) + def _allow_selective_fallback(self, signal: Dict[str, Any], *, lane: str) -> bool: + confidence = float(signal.get("confidence", 0) or 0) + entry_type = signal.get("entry_type", "market") + location_tag = ((signal.get("market_location") or {}).get("location_tag") or "") + volume_context = signal.get("volume_price_context") or {} + pullback_quality = volume_context.get("pullback_quality") or signal.get("pullback_quality") + breakout_quality = volume_context.get("breakout_quality") or signal.get("breakout_quality") + + if lane == "medium_term" and entry_type == "limit" and confidence >= 74: + if location_tag in {"near_long_zone", "near_short_zone", "near_range_support", "near_range_resistance"}: + return True + if pullback_quality == "healthy_pullback": + return True + + if lane == "short_term" and confidence >= 72: + if breakout_quality in {"acceptance_breakout_up", "acceptance_breakout_down"}: + return True + if entry_type == "limit" and location_tag in {"near_long_zone", "near_short_zone", "near_range_support", "near_range_resistance"}: + return True + + return False + def _infer_setup_type(self, signal: Dict[str, Any]) -> str: lane = signal.get("timeframe") or signal.get("type") or "medium_term" action = signal.get("action") diff --git a/frontend/console.html b/frontend/console.html index db8e62c..62a2d62 100644 --- a/frontend/console.html +++ b/frontend/console.html @@ -3701,7 +3701,8 @@
最近分析说明${monitor.last_analysis_detail || '-'}
最近完成轮次${monitor.last_cycle_completed_at ? `${relativeTime(monitor.last_cycle_completed_at)} / ${formatTime(monitor.last_cycle_completed_at)}` : '暂无'}
-
日内 / 趋势冷却${schedule.intraday_cooldown_minutes || '-'}m / ${schedule.trend_cooldown_minutes || '-'}m
+
日内 / 趋势冷却${schedule.intraday_cooldown_minutes ?? '-'}m / ${schedule.trend_cooldown_minutes ?? '-'}m
+
日内 / 趋势阈值${schedule.intraday_signal_threshold || '-'}% / ${schedule.trend_signal_threshold || '-'}%
事件分析${schedule.event_analysis_enabled ? '开启' : '关闭'}
触发 / LLM${funnel.total_triggers || 0} / ${funnel.llm_analyses || 0}
波动率跳过 / 数据异常${funnel.volatility_skips || 0} / ${funnel.data_invalid_skips || 0}