This commit is contained in:
aaron 2026-04-27 14:09:52 +08:00
parent dba6e569c6
commit 6ac175038e
5 changed files with 89 additions and 105 deletions

View File

@ -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 = ""

View File

@ -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,

View File

@ -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"],
}
)

View File

@ -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")

View File

@ -3701,7 +3701,8 @@
<div class="runtime-summary-meta">
<div class="runtime-summary-row"><span>最近分析说明</span><strong>${monitor.last_analysis_detail || '-'}</strong></div>
<div class="runtime-summary-row"><span>最近完成轮次</span><strong>${monitor.last_cycle_completed_at ? `${relativeTime(monitor.last_cycle_completed_at)} / ${formatTime(monitor.last_cycle_completed_at)}` : '暂无'}</strong></div>
<div class="runtime-summary-row"><span>日内 / 趋势冷却</span><strong>${schedule.intraday_cooldown_minutes || '-'}m / ${schedule.trend_cooldown_minutes || '-'}m</strong></div>
<div class="runtime-summary-row"><span>日内 / 趋势冷却</span><strong>${schedule.intraday_cooldown_minutes ?? '-'}m / ${schedule.trend_cooldown_minutes ?? '-'}m</strong></div>
<div class="runtime-summary-row"><span>日内 / 趋势阈值</span><strong>${schedule.intraday_signal_threshold || '-'}% / ${schedule.trend_signal_threshold || '-'}%</strong></div>
<div class="runtime-summary-row"><span>事件分析</span><strong>${schedule.event_analysis_enabled ? '开启' : '关闭'}</strong></div>
<div class="runtime-summary-row"><span>触发 / LLM</span><strong>${funnel.total_triggers || 0} / ${funnel.llm_analyses || 0}</strong></div>
<div class="runtime-summary-row"><span>波动率跳过 / 数据异常</span><strong>${funnel.volatility_skips || 0} / ${funnel.data_invalid_skips || 0}</strong></div>