1
This commit is contained in:
parent
918c7f914d
commit
dba6e569c6
@ -3,7 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import math
|
import math
|
||||||
from collections import deque
|
from collections import deque, defaultdict
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@ -291,6 +291,28 @@ class CryptoAgent:
|
|||||||
"last_signal_symbol": None,
|
"last_signal_symbol": None,
|
||||||
"last_heartbeat_notified_at": None,
|
"last_heartbeat_notified_at": None,
|
||||||
}
|
}
|
||||||
|
self._analysis_funnel_stats: Dict[str, Any] = {
|
||||||
|
"total_triggers": 0,
|
||||||
|
"scheduled_triggers": 0,
|
||||||
|
"event_triggers": 0,
|
||||||
|
"manual_triggers": 0,
|
||||||
|
"data_invalid_skips": 0,
|
||||||
|
"volatility_skips": 0,
|
||||||
|
"llm_lane_calls": {
|
||||||
|
"intraday": 0,
|
||||||
|
"trend": 0,
|
||||||
|
},
|
||||||
|
"llm_analyses": 0,
|
||||||
|
"cache_only_runs": 0,
|
||||||
|
"pre_regime_trade_signals": 0,
|
||||||
|
"post_regime_trade_signals": 0,
|
||||||
|
"regime_filtered_out": 0,
|
||||||
|
"no_trade_signal_runs": 0,
|
||||||
|
"threshold_filtered_runs": 0,
|
||||||
|
"valid_signal_runs": 0,
|
||||||
|
"valid_signals_total": 0,
|
||||||
|
"last_updated_at": None,
|
||||||
|
}
|
||||||
self._lane_analysis_state: Dict[str, Dict[str, Any]] = {}
|
self._lane_analysis_state: Dict[str, Dict[str, Any]] = {}
|
||||||
self._event_analysis_state: Dict[str, Dict[str, Any]] = {}
|
self._event_analysis_state: Dict[str, Dict[str, Any]] = {}
|
||||||
self._event_analysis_tasks: Dict[str, asyncio.Task] = {}
|
self._event_analysis_tasks: Dict[str, asyncio.Task] = {}
|
||||||
@ -800,6 +822,171 @@ class CryptoAgent:
|
|||||||
def get_recent_analysis_events(self, limit: int = 40) -> List[Dict[str, Any]]:
|
def get_recent_analysis_events(self, limit: int = 40) -> List[Dict[str, Any]]:
|
||||||
return list(self._analysis_events)[:limit]
|
return list(self._analysis_events)[:limit]
|
||||||
|
|
||||||
|
def _bump_analysis_stat(self, key: str, amount: int = 1):
|
||||||
|
self._analysis_funnel_stats[key] = int(self._analysis_funnel_stats.get(key, 0) or 0) + amount
|
||||||
|
self._analysis_funnel_stats["last_updated_at"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
def _bump_lane_call(self, lane: str, amount: int = 1):
|
||||||
|
lane_calls = self._analysis_funnel_stats.setdefault("llm_lane_calls", {})
|
||||||
|
lane_calls[lane] = int(lane_calls.get(lane, 0) or 0) + amount
|
||||||
|
self._analysis_funnel_stats["last_updated_at"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
def _summarize_recent_analysis_funnel(self, hours: int = 24) -> Dict[str, Any]:
|
||||||
|
cutoff = datetime.now() - timedelta(hours=hours)
|
||||||
|
events = []
|
||||||
|
for event in self._analysis_events:
|
||||||
|
timestamp = self._parse_iso_datetime(event.get("timestamp"))
|
||||||
|
if timestamp and timestamp >= cutoff:
|
||||||
|
events.append(event)
|
||||||
|
|
||||||
|
summary: Dict[str, Any] = {
|
||||||
|
"window_hours": hours,
|
||||||
|
"total_events": len(events),
|
||||||
|
"triggered_symbols": 0,
|
||||||
|
"llm_runs": 0,
|
||||||
|
"cache_only_runs": 0,
|
||||||
|
"data_invalid_skips": 0,
|
||||||
|
"volatility_skips": 0,
|
||||||
|
"no_trade_signal_runs": 0,
|
||||||
|
"threshold_filtered_runs": 0,
|
||||||
|
"valid_signal_runs": 0,
|
||||||
|
"valid_signals_total": 0,
|
||||||
|
"symbols": {},
|
||||||
|
"lane_calls": {
|
||||||
|
"intraday": 0,
|
||||||
|
"trend": 0,
|
||||||
|
},
|
||||||
|
"lane_signal_counts": {
|
||||||
|
"short_term_pre": 0,
|
||||||
|
"medium_term_pre": 0,
|
||||||
|
"short_term_post": 0,
|
||||||
|
"medium_term_post": 0,
|
||||||
|
},
|
||||||
|
"blocked_reason_counts": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
symbol_stats: Dict[str, Dict[str, Any]] = defaultdict(lambda: {
|
||||||
|
"triggers": 0,
|
||||||
|
"llm_runs": 0,
|
||||||
|
"cache_only_runs": 0,
|
||||||
|
"data_invalid_skips": 0,
|
||||||
|
"volatility_skips": 0,
|
||||||
|
"no_trade_signal_runs": 0,
|
||||||
|
"threshold_filtered_runs": 0,
|
||||||
|
"valid_signal_runs": 0,
|
||||||
|
"valid_signals_total": 0,
|
||||||
|
"last_status": None,
|
||||||
|
"last_detail": None,
|
||||||
|
"last_event_at": None,
|
||||||
|
"lane_calls": {
|
||||||
|
"intraday": 0,
|
||||||
|
"trend": 0,
|
||||||
|
},
|
||||||
|
"lane_signal_counts": {
|
||||||
|
"short_term_pre": 0,
|
||||||
|
"medium_term_pre": 0,
|
||||||
|
"short_term_post": 0,
|
||||||
|
"medium_term_post": 0,
|
||||||
|
},
|
||||||
|
"blocked_reason_counts": {},
|
||||||
|
})
|
||||||
|
|
||||||
|
blocked_reason_counts: Dict[str, int] = defaultdict(int)
|
||||||
|
|
||||||
|
for event in reversed(events):
|
||||||
|
event_type = event.get("event_type")
|
||||||
|
symbol = event.get("symbol") or ""
|
||||||
|
stats = symbol_stats[symbol] if symbol else None
|
||||||
|
|
||||||
|
if event_type == "symbol_analysis_started":
|
||||||
|
summary["triggered_symbols"] += 1
|
||||||
|
if stats is not None:
|
||||||
|
stats["triggers"] += 1
|
||||||
|
|
||||||
|
if event_type == "llm_lane_plan":
|
||||||
|
cache_only = bool(event.get("cache_only"))
|
||||||
|
lanes_to_run = event.get("lanes_to_run") or []
|
||||||
|
if cache_only:
|
||||||
|
summary["cache_only_runs"] += 1
|
||||||
|
if stats is not None:
|
||||||
|
stats["cache_only_runs"] += 1
|
||||||
|
else:
|
||||||
|
summary["llm_runs"] += 1
|
||||||
|
if stats is not None:
|
||||||
|
stats["llm_runs"] += 1
|
||||||
|
for lane in lanes_to_run:
|
||||||
|
if lane in summary["lane_calls"]:
|
||||||
|
summary["lane_calls"][lane] += 1
|
||||||
|
if stats is not None and lane in stats["lane_calls"]:
|
||||||
|
stats["lane_calls"][lane] += 1
|
||||||
|
|
||||||
|
if event_type == "symbol_analysis_skipped":
|
||||||
|
detail = str(event.get("detail") or "")
|
||||||
|
if "数据不完整" in detail:
|
||||||
|
summary["data_invalid_skips"] += 1
|
||||||
|
if stats is not None:
|
||||||
|
stats["data_invalid_skips"] += 1
|
||||||
|
if "波动率过低" in detail:
|
||||||
|
summary["volatility_skips"] += 1
|
||||||
|
if stats is not None:
|
||||||
|
stats["volatility_skips"] += 1
|
||||||
|
|
||||||
|
if event_type == "symbol_analysis_completed":
|
||||||
|
trade_signals = int(event.get("trade_signals", 0) or 0)
|
||||||
|
valid_signals = int(event.get("valid_signals", 0) or 0)
|
||||||
|
detail = str(event.get("detail") or "")
|
||||||
|
pre_lane_counts = event.get("pre_regime_lane_signal_counts") or {}
|
||||||
|
post_lane_counts = event.get("post_regime_lane_signal_counts") or {}
|
||||||
|
summary["lane_signal_counts"]["short_term_pre"] += int(pre_lane_counts.get("short_term", 0) or 0)
|
||||||
|
summary["lane_signal_counts"]["medium_term_pre"] += int(pre_lane_counts.get("medium_term", 0) or 0)
|
||||||
|
summary["lane_signal_counts"]["short_term_post"] += int(post_lane_counts.get("short_term", 0) or 0)
|
||||||
|
summary["lane_signal_counts"]["medium_term_post"] += int(post_lane_counts.get("medium_term", 0) or 0)
|
||||||
|
if stats is not None:
|
||||||
|
stats["lane_signal_counts"]["short_term_pre"] += int(pre_lane_counts.get("short_term", 0) or 0)
|
||||||
|
stats["lane_signal_counts"]["medium_term_pre"] += int(pre_lane_counts.get("medium_term", 0) or 0)
|
||||||
|
stats["lane_signal_counts"]["short_term_post"] += int(post_lane_counts.get("short_term", 0) or 0)
|
||||||
|
stats["lane_signal_counts"]["medium_term_post"] += int(post_lane_counts.get("medium_term", 0) or 0)
|
||||||
|
event_reason_counts = event.get("blocked_reason_counts") or {}
|
||||||
|
for reason_key, count in event_reason_counts.items():
|
||||||
|
blocked_reason_counts[str(reason_key)] += int(count or 0)
|
||||||
|
if stats is not None:
|
||||||
|
symbol_reason_counts = stats.setdefault("blocked_reason_counts", {})
|
||||||
|
symbol_reason_counts[str(reason_key)] = int(symbol_reason_counts.get(str(reason_key), 0) or 0) + int(count or 0)
|
||||||
|
if trade_signals == 0:
|
||||||
|
summary["no_trade_signal_runs"] += 1
|
||||||
|
if stats is not None:
|
||||||
|
stats["no_trade_signal_runs"] += 1
|
||||||
|
elif valid_signals == 0:
|
||||||
|
summary["threshold_filtered_runs"] += 1
|
||||||
|
if stats is not None:
|
||||||
|
stats["threshold_filtered_runs"] += 1
|
||||||
|
else:
|
||||||
|
summary["valid_signal_runs"] += 1
|
||||||
|
summary["valid_signals_total"] += valid_signals
|
||||||
|
if stats is not None:
|
||||||
|
stats["valid_signal_runs"] += 1
|
||||||
|
stats["valid_signals_total"] += valid_signals
|
||||||
|
|
||||||
|
if stats is not None:
|
||||||
|
stats["last_status"] = event.get("status")
|
||||||
|
stats["last_detail"] = detail
|
||||||
|
stats["last_event_at"] = event.get("timestamp")
|
||||||
|
|
||||||
|
summary["symbols"] = dict(sorted(
|
||||||
|
(
|
||||||
|
(symbol, payload)
|
||||||
|
for symbol, payload in symbol_stats.items()
|
||||||
|
if symbol
|
||||||
|
),
|
||||||
|
key=lambda item: (
|
||||||
|
-(item[1]["valid_signal_runs"] or 0),
|
||||||
|
-(item[1]["triggers"] or 0),
|
||||||
|
item[0],
|
||||||
|
)
|
||||||
|
))
|
||||||
|
summary["blocked_reason_counts"] = dict(sorted(blocked_reason_counts.items(), key=lambda item: (-item[1], item[0])))
|
||||||
|
return summary
|
||||||
|
|
||||||
def _on_price_update(self, symbol: str, price: float):
|
def _on_price_update(self, symbol: str, price: float):
|
||||||
"""处理实时价格更新(用于模拟交易)"""
|
"""处理实时价格更新(用于模拟交易)"""
|
||||||
if not self.paper_trading:
|
if not self.paper_trading:
|
||||||
@ -1262,6 +1449,14 @@ class CryptoAgent:
|
|||||||
symbol: 交易对,如 'BTCUSDT'
|
symbol: 交易对,如 'BTCUSDT'
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
self._bump_analysis_stat("total_triggers")
|
||||||
|
if trigger_source == "schedule":
|
||||||
|
self._bump_analysis_stat("scheduled_triggers")
|
||||||
|
elif trigger_source == "event":
|
||||||
|
self._bump_analysis_stat("event_triggers")
|
||||||
|
else:
|
||||||
|
self._bump_analysis_stat("manual_triggers")
|
||||||
|
|
||||||
# 更新活动时间
|
# 更新活动时间
|
||||||
monitor = get_system_monitor()
|
monitor = get_system_monitor()
|
||||||
monitor.update_activity("crypto_agent")
|
monitor.update_activity("crypto_agent")
|
||||||
@ -1284,6 +1479,7 @@ class CryptoAgent:
|
|||||||
data = self.exchange.get_multi_timeframe_data(symbol)
|
data = self.exchange.get_multi_timeframe_data(symbol)
|
||||||
|
|
||||||
if not self._validate_data(data):
|
if not self._validate_data(data):
|
||||||
|
self._bump_analysis_stat("data_invalid_skips")
|
||||||
self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat()
|
self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat()
|
||||||
self._analysis_monitor["last_analysis_status"] = "warning"
|
self._analysis_monitor["last_analysis_status"] = "warning"
|
||||||
self._analysis_monitor["last_analysis_detail"] = "数据不完整,跳过分析"
|
self._analysis_monitor["last_analysis_detail"] = "数据不完整,跳过分析"
|
||||||
@ -1323,6 +1519,7 @@ class CryptoAgent:
|
|||||||
# 1.5. 波动率检查(节省 LLM 调用)
|
# 1.5. 波动率检查(节省 LLM 调用)
|
||||||
should_analyze, volatility_reason, volatility = self._check_volatility(symbol, data)
|
should_analyze, volatility_reason, volatility = self._check_volatility(symbol, data)
|
||||||
if not should_analyze:
|
if not should_analyze:
|
||||||
|
self._bump_analysis_stat("volatility_skips")
|
||||||
self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat()
|
self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat()
|
||||||
self._analysis_monitor["last_analysis_status"] = "skipped"
|
self._analysis_monitor["last_analysis_status"] = "skipped"
|
||||||
self._analysis_monitor["last_analysis_detail"] = volatility_reason
|
self._analysis_monitor["last_analysis_detail"] = volatility_reason
|
||||||
@ -1351,6 +1548,13 @@ class CryptoAgent:
|
|||||||
elif not lanes_to_run:
|
elif not lanes_to_run:
|
||||||
logger.info(" 🧊 LLM 冷却中,使用上一轮 lane 缓存结果")
|
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._record_analysis_event(
|
self._record_analysis_event(
|
||||||
"llm_lane_plan",
|
"llm_lane_plan",
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
@ -1390,8 +1594,15 @@ class CryptoAgent:
|
|||||||
# 过滤掉 wait 信号,只保留 buy/sell 信号
|
# 过滤掉 wait 信号,只保留 buy/sell 信号
|
||||||
signals = market_signal.get('signals', [])
|
signals = market_signal.get('signals', [])
|
||||||
trade_signals = [s for s in signals if s.get('action') in ['buy', 'sell']]
|
trade_signals = [s for s in signals if s.get('action') in ['buy', 'sell']]
|
||||||
|
pre_regime_trade_signals = int(market_signal.get('pre_regime_trade_signal_count', len(trade_signals)) or 0)
|
||||||
|
self._bump_analysis_stat("pre_regime_trade_signals", pre_regime_trade_signals)
|
||||||
|
self._bump_analysis_stat("post_regime_trade_signals", len(trade_signals))
|
||||||
|
filtered_out = max(0, pre_regime_trade_signals - len(trade_signals))
|
||||||
|
if filtered_out:
|
||||||
|
self._bump_analysis_stat("regime_filtered_out", filtered_out)
|
||||||
|
|
||||||
if not trade_signals:
|
if not trade_signals:
|
||||||
|
self._bump_analysis_stat("no_trade_signal_runs")
|
||||||
self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat()
|
self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat()
|
||||||
self._analysis_monitor["last_analysis_status"] = "completed"
|
self._analysis_monitor["last_analysis_status"] = "completed"
|
||||||
self._analysis_monitor["last_analysis_detail"] = "完成分析,无交易信号"
|
self._analysis_monitor["last_analysis_detail"] = "完成分析,无交易信号"
|
||||||
@ -1400,7 +1611,13 @@ class CryptoAgent:
|
|||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
status="success",
|
status="success",
|
||||||
detail="完成分析,无交易信号",
|
detail="完成分析,无交易信号",
|
||||||
extra={"trade_signals": 0, "valid_signals": 0},
|
extra={
|
||||||
|
"trade_signals": 0,
|
||||||
|
"valid_signals": 0,
|
||||||
|
"blocked_reason_counts": market_signal.get("blocked_reason_counts") or {},
|
||||||
|
"pre_regime_lane_signal_counts": market_signal.get("pre_regime_lane_signal_counts") or {},
|
||||||
|
"post_regime_lane_signal_counts": market_signal.get("post_regime_lane_signal_counts") or {},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
blocked_reasons = market_signal.get('blocked_reasons') or []
|
blocked_reasons = market_signal.get('blocked_reasons') or []
|
||||||
if blocked_reasons:
|
if blocked_reasons:
|
||||||
@ -1414,6 +1631,7 @@ class CryptoAgent:
|
|||||||
valid_signals = [s for s in trade_signals if s.get('confidence', 0) >= threshold]
|
valid_signals = [s for s in trade_signals if s.get('confidence', 0) >= threshold]
|
||||||
|
|
||||||
if not valid_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_completed_at"] = datetime.now().isoformat()
|
||||||
self._analysis_monitor["last_analysis_status"] = "completed"
|
self._analysis_monitor["last_analysis_status"] = "completed"
|
||||||
self._analysis_monitor["last_analysis_detail"] = f"完成分析,但无信号达到阈值 {threshold}%"
|
self._analysis_monitor["last_analysis_detail"] = f"完成分析,但无信号达到阈值 {threshold}%"
|
||||||
@ -1422,11 +1640,19 @@ class CryptoAgent:
|
|||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
status="success",
|
status="success",
|
||||||
detail=f"完成分析,但无信号达到阈值 {threshold}%",
|
detail=f"完成分析,但无信号达到阈值 {threshold}%",
|
||||||
extra={"trade_signals": len(trade_signals), "valid_signals": 0},
|
extra={
|
||||||
|
"trade_signals": len(trade_signals),
|
||||||
|
"valid_signals": 0,
|
||||||
|
"blocked_reason_counts": market_signal.get("blocked_reason_counts") or {},
|
||||||
|
"pre_regime_lane_signal_counts": market_signal.get("pre_regime_lane_signal_counts") or {},
|
||||||
|
"post_regime_lane_signal_counts": market_signal.get("post_regime_lane_signal_counts") or {},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
logger.info(f"\n⏸️ 结论: 无交易信号达到置信度阈值 ({threshold}%),继续观望")
|
logger.info(f"\n⏸️ 结论: 无交易信号达到置信度阈值 ({threshold}%),继续观望")
|
||||||
return
|
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)} 个有效交易信号(达到 {threshold}% 阈值)")
|
||||||
for signal in valid_signals:
|
for signal in valid_signals:
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -1535,7 +1761,13 @@ class CryptoAgent:
|
|||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
status="success",
|
status="success",
|
||||||
detail=f"完成分析,产生 {len(valid_signals)} 个有效信号",
|
detail=f"完成分析,产生 {len(valid_signals)} 个有效信号",
|
||||||
extra={"trade_signals": len(trade_signals), "valid_signals": len(valid_signals)},
|
extra={
|
||||||
|
"trade_signals": len(trade_signals),
|
||||||
|
"valid_signals": len(valid_signals),
|
||||||
|
"blocked_reason_counts": market_signal.get("blocked_reason_counts") or {},
|
||||||
|
"pre_regime_lane_signal_counts": market_signal.get("pre_regime_lane_signal_counts") or {},
|
||||||
|
"post_regime_lane_signal_counts": market_signal.get("post_regime_lane_signal_counts") or {},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -4388,6 +4620,8 @@ class CryptoAgent:
|
|||||||
'target_execution_controls': self.get_target_execution_status(),
|
'target_execution_controls': self.get_target_execution_status(),
|
||||||
'analysis_monitor': self._analysis_monitor,
|
'analysis_monitor': self._analysis_monitor,
|
||||||
'analysis_notifications': self._analysis_notification_state,
|
'analysis_notifications': self._analysis_notification_state,
|
||||||
|
'analysis_funnel_stats': self._analysis_funnel_stats,
|
||||||
|
'analysis_funnel_24h': self._summarize_recent_analysis_funnel(hours=24),
|
||||||
'lane_analysis_state': self._lane_analysis_state,
|
'lane_analysis_state': self._lane_analysis_state,
|
||||||
'event_analysis_state': self._event_analysis_state,
|
'event_analysis_state': self._event_analysis_state,
|
||||||
'execution_guardian': self.execution_guardian.get_status(),
|
'execution_guardian': self.execution_guardian.get_status(),
|
||||||
|
|||||||
@ -1574,6 +1574,11 @@ class MarketSignalAnalyzer:
|
|||||||
)[:2]
|
)[:2]
|
||||||
|
|
||||||
result['signals'] = merged_signals
|
result['signals'] = merged_signals
|
||||||
|
result['pre_regime_trade_signal_count'] = len(merged_signals)
|
||||||
|
result['pre_regime_lane_signal_counts'] = {
|
||||||
|
'short_term': len(intraday_signals),
|
||||||
|
'medium_term': len(trend_signals),
|
||||||
|
}
|
||||||
result['key_levels'] = {
|
result['key_levels'] = {
|
||||||
'support': self._dedupe_levels(
|
'support': self._dedupe_levels(
|
||||||
(intraday_result.get('key_levels', {}) or {}).get('support', []) +
|
(intraday_result.get('key_levels', {}) or {}).get('support', []) +
|
||||||
@ -1665,12 +1670,17 @@ class MarketSignalAnalyzer:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
filtered_signals, blocked_reasons = self.setup_policy.filter_signals(enriched_signals, regime_profile)
|
filtered_signals, blocked_reasons, blocked_reason_counts = self.setup_policy.filter_signals(enriched_signals, regime_profile)
|
||||||
|
|
||||||
normalized = dict(market_signal)
|
normalized = dict(market_signal)
|
||||||
normalized["signals"] = filtered_signals
|
normalized["signals"] = filtered_signals
|
||||||
normalized["regime_profile"] = regime_profile
|
normalized["regime_profile"] = regime_profile
|
||||||
normalized["blocked_reasons"] = blocked_reasons[:6]
|
normalized["blocked_reasons"] = blocked_reasons[:6]
|
||||||
|
normalized["blocked_reason_counts"] = blocked_reason_counts
|
||||||
|
normalized["post_regime_lane_signal_counts"] = {
|
||||||
|
'short_term': len([signal for signal in filtered_signals if (signal.get("timeframe") or signal.get("type")) == "short_term"]),
|
||||||
|
'medium_term': len([signal for signal in filtered_signals if (signal.get("timeframe") or signal.get("type")) == "medium_term"]),
|
||||||
|
}
|
||||||
|
|
||||||
if not filtered_signals and blocked_reasons:
|
if not filtered_signals and blocked_reasons:
|
||||||
normalized["analysis_summary"] = self._truncate_summary(regime_profile.get("summary") or "当前状态不交易")
|
normalized["analysis_summary"] = self._truncate_summary(regime_profile.get("summary") or "当前状态不交易")
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
市场状态到交易行为的硬约束策略
|
市场状态到交易行为的硬约束策略
|
||||||
"""
|
"""
|
||||||
|
from collections import defaultdict
|
||||||
from typing import Any, Dict, List, Tuple
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
|
|
||||||
@ -15,15 +16,18 @@ class SetupPolicy:
|
|||||||
self,
|
self,
|
||||||
signals: List[Dict[str, Any]],
|
signals: List[Dict[str, Any]],
|
||||||
regime_profile: Dict[str, Any],
|
regime_profile: Dict[str, Any],
|
||||||
) -> Tuple[List[Dict[str, Any]], List[str]]:
|
) -> Tuple[List[Dict[str, Any]], List[str], Dict[str, int]]:
|
||||||
allowed_lanes = set(regime_profile.get("allowed_lanes") or [])
|
allowed_lanes = set(regime_profile.get("allowed_lanes") or [])
|
||||||
allowed_setups = set(regime_profile.get("allowed_setups") or [])
|
allowed_setups = set(regime_profile.get("allowed_setups") or [])
|
||||||
tradability = regime_profile.get("tradability", "avoid")
|
tradability = regime_profile.get("tradability", "avoid")
|
||||||
reasons: List[str] = []
|
reasons: List[str] = []
|
||||||
|
reason_counts: Dict[str, int] = defaultdict(int)
|
||||||
|
|
||||||
if tradability == "avoid" or not allowed_lanes or not allowed_setups:
|
if tradability == "avoid" or not allowed_lanes or not allowed_setups:
|
||||||
reasons.extend(regime_profile.get("no_trade_reasons") or ["当前市场状态不允许交易"])
|
base_reasons = regime_profile.get("no_trade_reasons") or ["当前市场状态不允许交易"]
|
||||||
return [], reasons
|
reasons.extend(base_reasons)
|
||||||
|
reason_counts["tradability_avoid"] += max(1, len(signals or []))
|
||||||
|
return [], reasons, dict(reason_counts)
|
||||||
|
|
||||||
kept: List[Dict[str, Any]] = []
|
kept: List[Dict[str, Any]] = []
|
||||||
for signal in signals or []:
|
for signal in signals or []:
|
||||||
@ -34,10 +38,12 @@ class SetupPolicy:
|
|||||||
|
|
||||||
if lane not in allowed_lanes:
|
if lane not in allowed_lanes:
|
||||||
reasons.append(f"{lane} 不在允许交易周期内")
|
reasons.append(f"{lane} 不在允许交易周期内")
|
||||||
|
reason_counts[f"lane_blocked:{lane}"] += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if setup_type not in allowed_setups:
|
if setup_type not in allowed_setups:
|
||||||
reasons.append(f"{setup_type} 不在允许 setup 内")
|
reasons.append(f"{setup_type} 不在允许 setup 内")
|
||||||
|
reason_counts[f"setup_blocked:{setup_type}"] += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
kept.append({
|
kept.append({
|
||||||
@ -47,7 +53,7 @@ class SetupPolicy:
|
|||||||
"entry_basis": entry_basis,
|
"entry_basis": entry_basis,
|
||||||
})
|
})
|
||||||
|
|
||||||
return kept, reasons
|
return kept, reasons, dict(reason_counts)
|
||||||
|
|
||||||
def _infer_setup_type(self, signal: Dict[str, Any]) -> str:
|
def _infer_setup_type(self, signal: Dict[str, Any]) -> str:
|
||||||
lane = signal.get("timeframe") or signal.get("type") or "medium_term"
|
lane = signal.get("timeframe") or signal.get("type") or "medium_term"
|
||||||
|
|||||||
@ -3164,6 +3164,7 @@
|
|||||||
const attentionItems = management.attention_items || [];
|
const attentionItems = management.attention_items || [];
|
||||||
const haltedCount = countHalted(cryptoAgent.platform_halts || {});
|
const haltedCount = countHalted(cryptoAgent.platform_halts || {});
|
||||||
const latestSignals = data.signals?.latest || [];
|
const latestSignals = data.signals?.latest || [];
|
||||||
|
const funnel = cryptoAgent.analysis_funnel_stats || {};
|
||||||
const heartbeatHeadline = monitor.last_heartbeat_at ? relativeTime(monitor.last_heartbeat_at) : '无心跳';
|
const heartbeatHeadline = monitor.last_heartbeat_at ? relativeTime(monitor.last_heartbeat_at) : '无心跳';
|
||||||
const heartbeatTone = toneClassForHealth(cryptoAgent.running ? (monitor.last_cycle_status || monitor.last_analysis_status) : 'stopped');
|
const heartbeatTone = toneClassForHealth(cryptoAgent.running ? (monitor.last_cycle_status || monitor.last_analysis_status) : 'stopped');
|
||||||
const exposureNotional = positions.reduce((sum, item) => {
|
const exposureNotional = positions.reduce((sum, item) => {
|
||||||
@ -3184,7 +3185,7 @@
|
|||||||
<div class="focus-summary-card">
|
<div class="focus-summary-card">
|
||||||
<div class="kicker">最近信号</div>
|
<div class="kicker">最近信号</div>
|
||||||
<div class="headline ${signalTone}">${latestSignals.length}</div>
|
<div class="headline ${signalTone}">${latestSignals.length}</div>
|
||||||
<div class="detail">最新 ${latestSignals[0]?.symbol || '-'} / ${latestSignals[0]?.signal_type || '-'}</div>
|
<div class="detail">有效轮次 ${funnel.valid_signal_runs || 0} / 无信号 ${funnel.no_trade_signal_runs || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="focus-summary-card">
|
<div class="focus-summary-card">
|
||||||
<div class="kicker">持仓暴露</div>
|
<div class="kicker">持仓暴露</div>
|
||||||
@ -3279,6 +3280,29 @@
|
|||||||
return 'good';
|
return 'good';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBlockedReason(reasonKey) {
|
||||||
|
const key = String(reasonKey || '');
|
||||||
|
if (key === 'tradability_avoid') return '市场状态禁止交易';
|
||||||
|
if (key.startsWith('lane_blocked:')) {
|
||||||
|
const lane = key.split(':')[1] || '';
|
||||||
|
return lane === 'short_term' ? '日内 lane 被禁止' : lane === 'medium_term' ? '趋势 lane 被禁止' : `lane 被禁止: ${lane}`;
|
||||||
|
}
|
||||||
|
if (key.startsWith('setup_blocked:')) {
|
||||||
|
const setup = key.split(':')[1] || '';
|
||||||
|
const setupMap = {
|
||||||
|
range_reversal: '区间反转 setup 被禁止',
|
||||||
|
trend_continuation_pullback: '趋势回调延续 setup 被禁止',
|
||||||
|
deep_pullback_continuation: '深回踩延续 setup 被禁止',
|
||||||
|
trend_reversal: '趋势反转 setup 被禁止',
|
||||||
|
breakout_confirmation: '突破确认 setup 被禁止',
|
||||||
|
breakout_pullback: '突破回踩 setup 被禁止',
|
||||||
|
unknown: '未识别 setup 被禁止',
|
||||||
|
};
|
||||||
|
return setupMap[setup] || `setup 被禁止: ${setup}`;
|
||||||
|
}
|
||||||
|
return key || '-';
|
||||||
|
}
|
||||||
|
|
||||||
function renderHealthRibbon(data) {
|
function renderHealthRibbon(data) {
|
||||||
const container = document.getElementById('healthRibbon');
|
const container = document.getElementById('healthRibbon');
|
||||||
const cryptoAgent = data.crypto_agent || {};
|
const cryptoAgent = data.crypto_agent || {};
|
||||||
@ -3641,9 +3665,14 @@
|
|||||||
const monitor = analysisMonitor || {};
|
const monitor = analysisMonitor || {};
|
||||||
const notifications = cachedConsoleData?.crypto_agent?.analysis_notifications || {};
|
const notifications = cachedConsoleData?.crypto_agent?.analysis_notifications || {};
|
||||||
const schedule = cachedConsoleData?.crypto_agent?.llm_schedule || {};
|
const schedule = cachedConsoleData?.crypto_agent?.llm_schedule || {};
|
||||||
|
const funnel = cachedConsoleData?.crypto_agent?.analysis_funnel_stats || {};
|
||||||
|
const funnel24h = cachedConsoleData?.crypto_agent?.analysis_funnel_24h || {};
|
||||||
const cycleStatus = monitor.last_cycle_status || 'idle';
|
const cycleStatus = monitor.last_cycle_status || 'idle';
|
||||||
const lastSignalAt = notifications.last_signal_at;
|
const lastSignalAt = notifications.last_signal_at;
|
||||||
const lastSignalSymbol = notifications.last_signal_symbol || '-';
|
const lastSignalSymbol = notifications.last_signal_symbol || '-';
|
||||||
|
const topSymbols = Object.entries(funnel24h.symbols || {}).slice(0, 3);
|
||||||
|
const topBlockedReasons = Object.entries(funnel24h.blocked_reason_counts || {}).slice(0, 3);
|
||||||
|
const laneSignalCounts = funnel24h.lane_signal_counts || {};
|
||||||
|
|
||||||
if (!heartbeat || !summaryCard) return;
|
if (!heartbeat || !summaryCard) return;
|
||||||
|
|
||||||
@ -3674,6 +3703,33 @@
|
|||||||
<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>${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.event_analysis_enabled ? '开启' : '关闭'}</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>
|
||||||
|
<div class="runtime-summary-row"><span>硬规则过滤 / 阈值过滤</span><strong>${funnel.regime_filtered_out || 0} / ${funnel.threshold_filtered_runs || 0}</strong></div>
|
||||||
|
<div class="runtime-summary-row"><span>24h 触发 / LLM</span><strong>${funnel24h.triggered_symbols || 0} / ${funnel24h.llm_runs || 0}</strong></div>
|
||||||
|
<div class="runtime-summary-row"><span>24h 日内 / 趋势</span><strong>${funnel24h.lane_calls?.intraday || 0} / ${funnel24h.lane_calls?.trend || 0}</strong></div>
|
||||||
|
<div class="runtime-summary-row"><span>24h 日内前后</span><strong>${laneSignalCounts.short_term_pre || 0} / ${laneSignalCounts.short_term_post || 0}</strong></div>
|
||||||
|
<div class="runtime-summary-row"><span>24h 趋势前后</span><strong>${laneSignalCounts.medium_term_pre || 0} / ${laneSignalCounts.medium_term_post || 0}</strong></div>
|
||||||
|
</div>
|
||||||
|
<div class="lane-state-list">
|
||||||
|
${topSymbols.length ? topSymbols.map(([symbol, stats]) => `
|
||||||
|
<div class="lane-state-item">
|
||||||
|
<div class="lane-state-symbol">${symbol}</div>
|
||||||
|
<div class="lane-state-detail">
|
||||||
|
24h 触发 ${stats.triggers || 0} / LLM ${stats.llm_runs || 0} / 有效 ${stats.valid_signal_runs || 0}
|
||||||
|
<br>日内前后 ${stats.lane_signal_counts?.short_term_pre || 0}/${stats.lane_signal_counts?.short_term_post || 0} / 趋势前后 ${stats.lane_signal_counts?.medium_term_pre || 0}/${stats.lane_signal_counts?.medium_term_post || 0}
|
||||||
|
<br>波动率跳过 ${stats.volatility_skips || 0} / 阈值过滤 ${stats.threshold_filtered_runs || 0}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('') : '<div class="analysis-log-detail">近 24 小时还没有形成 symbol 级分析统计。</div>'}
|
||||||
|
</div>
|
||||||
|
<div class="lane-state-list" style="margin-top: 10px;">
|
||||||
|
${topBlockedReasons.length ? topBlockedReasons.map(([reason, count]) => `
|
||||||
|
<div class="lane-state-item">
|
||||||
|
<div class="lane-state-symbol">${count}</div>
|
||||||
|
<div class="lane-state-detail">${formatBlockedReason(reason)}</div>
|
||||||
|
</div>
|
||||||
|
`).join('') : '<div class="analysis-log-detail">近 24 小时没有记录到明确的硬规则过滤原因。</div>'}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user