f43312a858
Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
169 lines
7.6 KiB
Python
169 lines
7.6 KiB
Python
"""
|
|
Multi-dimensional analysis service.
|
|
Uses OpenRouter via the internal LLMService and the multi-agent coordinator.
|
|
Local-only: this project does not implement any paid/credit system itself.
|
|
"""
|
|
import json
|
|
import traceback
|
|
from typing import Dict, Any, Optional
|
|
|
|
from app.utils.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class AnalysisService:
|
|
"""Multi-dimensional analyzer powered by agent coordinator."""
|
|
|
|
# Class-level guard to avoid circular-init recursion
|
|
_initializing = False
|
|
|
|
def __init__(self, use_multi_agent: bool = None):
|
|
"""
|
|
Args:
|
|
use_multi_agent: Deprecated; kept for frontend compatibility
|
|
"""
|
|
# Avoid circular-init recursion
|
|
if AnalysisService._initializing:
|
|
logger.warning("AnalysisService is initializing; skipping duplicate initialization")
|
|
self.coordinator = None
|
|
return
|
|
|
|
self.coordinator = None
|
|
|
|
try:
|
|
# Mark initializing
|
|
AnalysisService._initializing = True
|
|
|
|
# Lazy import to avoid circular imports
|
|
from app.services.agents.coordinator import AgentCoordinator
|
|
self.coordinator = AgentCoordinator(
|
|
enable_memory=True,
|
|
max_debate_rounds=2
|
|
)
|
|
logger.info("Multi-agent coordinator initialized")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Coordinator init failed: {e}")
|
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
self.coordinator = None
|
|
finally:
|
|
AnalysisService._initializing = False
|
|
|
|
def analyze(self, market: str, symbol: str, language: str = 'en-US', model: str = None) -> Dict[str, Any]:
|
|
"""
|
|
Args:
|
|
market: Market (AShare, USStock, HShare, Crypto, Forex, Futures)
|
|
symbol: Symbol
|
|
language: Output language tag (e.g. en-US, zh-CN, zh-TW)
|
|
model: Optional OpenRouter model id
|
|
Returns:
|
|
Result dict
|
|
"""
|
|
logger.info(f"Starting analysis {market}:{symbol}, language={language}, mode=multi-agent")
|
|
|
|
# Default result structure (keeps frontend compatible even when coordinator fails).
|
|
result = {
|
|
"overview": {"report": "Initializing..."},
|
|
"fundamental": {"report": "Initializing..."},
|
|
"technical": {"report": "Initializing..."},
|
|
"news": {"report": "Initializing..."},
|
|
"sentiment": {"report": "Initializing..."},
|
|
"risk": {"report": "Initializing..."},
|
|
"error": None
|
|
}
|
|
|
|
if not self.coordinator:
|
|
result["error"] = "Analysis service is not ready (coordinator init failed)"
|
|
return result
|
|
|
|
try:
|
|
logger.info(f"Run coordinator: {market}:{symbol}")
|
|
agent_result = self.coordinator.run_analysis(market, symbol, language, model=model)
|
|
|
|
logger.info(f"Coordinator result keys: {list(agent_result.keys())}")
|
|
|
|
# Validate expected keys (defensive)
|
|
debate = agent_result.get("debate", {})
|
|
trader_decision = agent_result.get("trader_decision", {})
|
|
risk_debate = agent_result.get("risk_debate", {})
|
|
final_decision = agent_result.get("final_decision", {})
|
|
|
|
# Keep frontend-compatible shape and fill defaults if empty
|
|
if "debate" in agent_result and "trader_decision" in agent_result and "risk_debate" in agent_result and "final_decision" in agent_result:
|
|
if not debate or (isinstance(debate, dict) and len(debate) == 0):
|
|
logger.warning("debate is empty; using defaults")
|
|
agent_result["debate"] = {"bull": {}, "bear": {}, "research_decision": "Analyzing..."}
|
|
if not trader_decision or (isinstance(trader_decision, dict) and len(trader_decision) == 0):
|
|
logger.warning("trader_decision is empty; using defaults")
|
|
agent_result["trader_decision"] = {"decision": "HOLD", "confidence": 50, "reasoning": "Analyzing..."}
|
|
if not risk_debate or (isinstance(risk_debate, dict) and len(risk_debate) == 0):
|
|
logger.warning("risk_debate is empty; using defaults")
|
|
agent_result["risk_debate"] = {"risky": {}, "neutral": {}, "safe": {}}
|
|
if not final_decision or (isinstance(final_decision, dict) and len(final_decision) == 0):
|
|
logger.warning("final_decision is empty; using defaults")
|
|
agent_result["final_decision"] = {"decision": "HOLD", "confidence": 50, "reasoning": "Analyzing..."}
|
|
|
|
return agent_result
|
|
else:
|
|
logger.warning("Coordinator result format is incomplete; filling defaults")
|
|
return {
|
|
"overview": agent_result.get("overview", {"report": "Analyzing..."}),
|
|
"fundamental": agent_result.get("fundamental", {"report": "Analyzing..."}),
|
|
"technical": agent_result.get("technical", {"report": "Analyzing..."}),
|
|
"news": agent_result.get("news", {"report": "Analyzing..."}),
|
|
"sentiment": agent_result.get("sentiment", {"report": "Analyzing..."}),
|
|
"risk": agent_result.get("risk", {"report": "Analyzing..."}),
|
|
"debate": agent_result.get("debate", {"bull": {}, "bear": {}, "research_decision": "Analyzing..."}),
|
|
"trader_decision": agent_result.get("trader_decision", {"decision": "HOLD", "confidence": 50, "reasoning": "Analyzing..."}),
|
|
"risk_debate": agent_result.get("risk_debate", {"risky": {}, "neutral": {}, "safe": {}}),
|
|
"final_decision": agent_result.get("final_decision", {"decision": "HOLD", "confidence": 50, "reasoning": "Analyzing..."}),
|
|
"error": agent_result.get("error")
|
|
}
|
|
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
logger.error(f"Analysis failed {market}:{symbol} - {error_msg}")
|
|
|
|
# If OpenRouter returns 402, it's an upstream billing/credit issue (not a QuantDinger fee).
|
|
if "402" in error_msg or "Payment Required" in error_msg:
|
|
result["error"] = f"OpenRouter returned 402 (billing/credits). Please check your OpenRouter account. Details: {error_msg}"
|
|
else:
|
|
result["error"] = f"Analysis failed: {error_msg}"
|
|
|
|
return result
|
|
|
|
|
|
def multi_analysis(market: str, symbol: str, language: str = 'en-US', use_multi_agent: bool = None) -> Dict[str, Any]:
|
|
"""
|
|
Convenience entrypoint for multi-dimensional analysis.
|
|
|
|
Args:
|
|
market: Market (AShare, USStock, HShare, Crypto, Forex, Futures)
|
|
symbol: Symbol
|
|
language: Output language tag
|
|
use_multi_agent: Deprecated; kept for compatibility
|
|
"""
|
|
analyzer = AnalysisService()
|
|
return analyzer.analyze(market, symbol, language)
|
|
|
|
|
|
def reflect_analysis(market: str, symbol: str, decision: str, returns: float = None, result: str = None):
|
|
"""
|
|
Reflection hook: learn from post-trade outcomes (local-only).
|
|
|
|
Args:
|
|
market: Market
|
|
symbol: Symbol
|
|
decision: Decision (BUY/SELL/HOLD)
|
|
returns: Return percentage
|
|
result: Free-text outcome
|
|
"""
|
|
try:
|
|
analyzer = AnalysisService()
|
|
if analyzer.coordinator:
|
|
analyzer.coordinator.reflect_and_learn(market, symbol, decision, returns, result)
|
|
logger.info(f"Reflection completed: {market}:{symbol}")
|
|
except Exception as e:
|
|
logger.error(f"Reflection failed: {e}")
|