Files
DinQuant/backend_api_python/app/services/analysis.py
T
TIANHE f43312a858 creat
Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
2025-12-29 03:06:49 +08:00

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