""" Fast Analysis Service 3.0 系统性重构版本 - 使用统一的数据采集器 核心改进: 1. 数据源统一 - 使用 MarketDataCollector,与K线模块、自选列表完全一致 2. 宏观数据 - 新增美元指数、VIX、利率等宏观经济指标 3. 多维新闻 - 使用结构化API,无需深度阅读 4. 单次LLM调用 - 强约束prompt,输出结构化分析 """ import json import time from typing import Dict, Any, Optional, List from decimal import Decimal, ROUND_HALF_UP from app.utils.logger import get_logger from app.services.llm import LLMService from app.services.market_data_collector import get_market_data_collector logger = get_logger(__name__) class FastAnalysisService: """ 快速分析服务 3.0 架构: 1. 数据采集层 - MarketDataCollector (统一数据源) 2. 分析层 - 单次LLM调用 (强约束prompt) 3. 记忆层 - 分析历史存储和检索 """ def __init__(self): self.llm_service = LLMService() self.data_collector = get_market_data_collector() self._memory_db = None # Lazy init # ==================== Data Collection Layer ==================== def _collect_market_data(self, market: str, symbol: str, timeframe: str = "1D") -> Dict[str, Any]: """ 使用统一的数据采集器收集市场数据 数据层次: 1. 核心数据: 价格、K线、技术指标 2. 基本面: 公司信息、财务数据 3. 宏观数据: DXY、VIX、TNX、黄金等 4. 情绪数据: 新闻、市场情绪 """ return self.data_collector.collect_all( market=market, symbol=symbol, timeframe=timeframe, include_macro=True, include_news=True, timeout=30 ) def _calculate_indicators(self, kline_data: List[Dict]) -> Dict[str, Any]: """ Calculate technical indicators using rules (no LLM). Returns actionable signals, not raw numbers. """ if not kline_data or len(kline_data) < 5: return {"error": "Insufficient data"} try: # Use tools' built-in calculation raw_indicators = self.tools.calculate_technical_indicators(kline_data) # Extract key values closes = [float(k.get("close", 0)) for k in kline_data if k.get("close")] if not closes: return {"error": "No close prices"} current_price = closes[-1] # RSI interpretation rsi = raw_indicators.get("RSI", 50) if rsi < 30: rsi_signal = "oversold" rsi_action = "potential_buy" elif rsi > 70: rsi_signal = "overbought" rsi_action = "potential_sell" else: rsi_signal = "neutral" rsi_action = "hold" # MACD interpretation macd = raw_indicators.get("MACD", 0) macd_signal_line = raw_indicators.get("MACD_Signal", 0) macd_hist = raw_indicators.get("MACD_Hist", 0) if macd > macd_signal_line and macd_hist > 0: macd_signal = "bullish" macd_trend = "golden_cross" if macd_hist > 0 and len(kline_data) > 1 else "bullish" elif macd < macd_signal_line and macd_hist < 0: macd_signal = "bearish" macd_trend = "death_cross" if macd_hist < 0 and len(kline_data) > 1 else "bearish" else: macd_signal = "neutral" macd_trend = "consolidating" # Moving averages ma5 = sum(closes[-5:]) / 5 if len(closes) >= 5 else current_price ma10 = sum(closes[-10:]) / 10 if len(closes) >= 10 else current_price ma20 = sum(closes[-20:]) / 20 if len(closes) >= 20 else current_price if current_price > ma5 > ma10 > ma20: ma_trend = "strong_uptrend" elif current_price > ma20: ma_trend = "uptrend" elif current_price < ma5 < ma10 < ma20: ma_trend = "strong_downtrend" elif current_price < ma20: ma_trend = "downtrend" else: ma_trend = "sideways" # Support/Resistance (simple: recent highs/lows) recent_highs = [float(k.get("high", 0)) for k in kline_data[-14:] if k.get("high")] recent_lows = [float(k.get("low", 0)) for k in kline_data[-14:] if k.get("low")] resistance = max(recent_highs) if recent_highs else current_price * 1.05 support = min(recent_lows) if recent_lows else current_price * 0.95 # Volatility (ATR-like) if len(kline_data) >= 14: ranges = [] for k in kline_data[-14:]: h = float(k.get("high", 0)) l = float(k.get("low", 0)) if h > 0 and l > 0: ranges.append(h - l) atr = sum(ranges) / len(ranges) if ranges else 0 volatility_pct = (atr / current_price * 100) if current_price > 0 else 0 if volatility_pct > 5: volatility = "high" elif volatility_pct > 2: volatility = "medium" else: volatility = "low" else: volatility = "unknown" volatility_pct = 0 return { "current_price": round(current_price, 6), "rsi": { "value": round(rsi, 2), "signal": rsi_signal, "action": rsi_action, }, "macd": { "value": round(macd, 6), "signal_line": round(macd_signal_line, 6), "histogram": round(macd_hist, 6), "signal": macd_signal, "trend": macd_trend, }, "moving_averages": { "ma5": round(ma5, 6), "ma10": round(ma10, 6), "ma20": round(ma20, 6), "trend": ma_trend, }, "levels": { "support": round(support, 6), "resistance": round(resistance, 6), }, "volatility": { "level": volatility, "pct": round(volatility_pct, 2), }, "raw": raw_indicators, } except Exception as e: logger.error(f"Indicator calculation failed: {e}") return {"error": str(e)} def _format_news_summary(self, news_data: List[Dict], max_items: int = 5) -> str: """Format news into a concise summary for the prompt.""" if not news_data: return "No recent news available." summaries = [] for item in news_data[:max_items]: title = item.get("title", item.get("headline", "")) sentiment = item.get("sentiment", "neutral") date = item.get("date", item.get("datetime", ""))[:10] if item.get("date") or item.get("datetime") else "" if title: summaries.append(f"- [{sentiment}] {title} ({date})") return "\n".join(summaries) if summaries else "No recent news available." # ==================== Memory Layer ==================== def _get_memory_context(self, market: str, symbol: str, current_indicators: Dict) -> str: """ Retrieve relevant historical analysis for similar market conditions. """ try: from app.services.analysis_memory import get_analysis_memory memory = get_analysis_memory() # Get similar patterns patterns = memory.get_similar_patterns(market, symbol, current_indicators, limit=3) if not patterns: return "No similar historical patterns found in memory." context_lines = ["Historical patterns with similar conditions:"] for p in patterns: outcome = "" if p.get("was_correct") is not None: outcome = f" (Outcome: {'Correct' if p['was_correct'] else 'Incorrect'}" if p.get("actual_return_pct"): outcome += f", Return: {p['actual_return_pct']:.2f}%" outcome += ")" context_lines.append( f"- Decision: {p['decision']} at ${p.get('price', 'N/A')}{outcome}" ) return "\n".join(context_lines) except Exception as e: logger.warning(f"Memory retrieval failed: {e}") return "Memory retrieval failed." # ==================== Prompt Engineering ==================== def _build_analysis_prompt(self, data: Dict[str, Any], language: str) -> tuple: """ Build the single, comprehensive analysis prompt. Key: Strong constraints to prevent absurd recommendations. """ price_data = data.get("price") or {} current_price = price_data.get("price", 0) if price_data else 0 change_24h = price_data.get("changePercent", 0) if price_data else 0 # Ensure all data fields have safe defaults (may be None from failed fetches) indicators = data.get("indicators") or {} fundamental = data.get("fundamental") or {} company = data.get("company") or {} news_summary = self._format_news_summary(data.get("news") or []) # Language instruction - MUST be enforced strictly lang_map = { 'zh-CN': '⚠️ 重要:你必须用简体中文回答所有内容,包括summary、key_reasons、risks等所有文本字段。不要使用英文。', 'zh-TW': '⚠️ 重要:你必須用繁體中文回答所有內容,包括summary、key_reasons、risks等所有文本字段。不要使用英文。', 'en-US': '⚠️ IMPORTANT: You MUST answer ALL content in English, including summary, key_reasons, risks, and all text fields. Do NOT use Chinese.', 'ja-JP': '⚠️ 重要:すべての内容を日本語で回答してください。summary、key_reasons、risksなど、すべてのテキストフィールドを日本語で記述してください。', } lang_instruction = lang_map.get(language, '⚠️ IMPORTANT: Answer ALL content in English.') # Get pre-calculated trading levels from technical analysis levels = indicators.get("levels", {}) trading_levels = indicators.get("trading_levels", {}) volatility = indicators.get("volatility", {}) support = levels.get("support", current_price * 0.95) resistance = levels.get("resistance", current_price * 1.05) pivot = levels.get("pivot", current_price) # Use ATR-based suggestions if available, otherwise use percentage atr = volatility.get("atr", current_price * 0.02) suggested_stop_loss = trading_levels.get("suggested_stop_loss", current_price - 2 * atr) suggested_take_profit = trading_levels.get("suggested_take_profit", current_price + 3 * atr) risk_reward_ratio = trading_levels.get("risk_reward_ratio", 1.5) # Price bounds (still enforce max 10% deviation) if current_price > 0: price_lower_bound = round(max(suggested_stop_loss, current_price * 0.90), 6) price_upper_bound = round(min(suggested_take_profit, current_price * 1.10), 6) entry_range_low = round(current_price * 0.98, 6) entry_range_high = round(current_price * 1.02, 6) else: price_lower_bound = price_upper_bound = entry_range_low = entry_range_high = 0 system_prompt = f"""You are QuantDinger's Senior Financial Analyst with 20+ years of experience. Provide professional, detailed analysis like a Wall Street analyst report. {lang_instruction} 📐 TECHNICAL LEVELS (Pre-calculated from chart data): - Support: ${support} | Resistance: ${resistance} | Pivot: ${pivot} - ATR (14-day): ${atr:.4f} ({volatility.get('pct', 0)}% volatility) - Suggested Stop Loss: ${suggested_stop_loss:.4f} (based on 2x ATR below support) - Suggested Take Profit: ${suggested_take_profit:.4f} (based on 3x ATR above resistance) - Risk/Reward Ratio: {risk_reward_ratio} ⚠️ CRITICAL PRICE RULES: 1. Current price: ${current_price} 2. Your stop_loss MUST be near ${suggested_stop_loss:.4f} (range: ${price_lower_bound:.4f} ~ ${current_price}) 3. Your take_profit MUST be near ${suggested_take_profit:.4f} (range: ${current_price} ~ ${price_upper_bound:.4f}) 4. Entry price: ${entry_range_low:.4f} ~ ${entry_range_high:.4f} 5. These levels are based on ATR and support/resistance analysis - use them as reference! 📊 YOUR ANALYSIS MUST INCLUDE: 1. **Technical Analysis**: Interpret the indicators, explain why support/resistance levels matter 2. **Fundamental Analysis**: Evaluate valuation, growth if data available 3. **Sentiment Analysis**: Assess market mood, news impact, macro factors 4. **Risk Assessment**: Explain why the stop loss level is appropriate 5. **Clear Recommendation**: BUY/SELL/HOLD with entry, stop loss (near suggested), take profit (near suggested) Output ONLY valid JSON (do NOT include word counts or format hints in your actual response): {{ "decision": "BUY" | "SELL" | "HOLD", "confidence": 0-100, "summary": "Executive summary in 2-3 sentences", "analysis": {{ "technical": "Your detailed technical analysis here - interpret RSI, MACD, MA, support/resistance", "fundamental": "Your fundamental assessment here - valuation, growth, competitive position", "sentiment": "Your market sentiment analysis here - news impact, macro factors, mood" }}, "entry_price": number, "stop_loss": number, "take_profit": number, "position_size_pct": 1-100, "timeframe": "short" | "medium" | "long", "key_reasons": ["First key reason for this decision", "Second key reason", "Third key reason"], "risks": ["Primary risk with potential impact", "Secondary risk"], "technical_score": 0-100, "fundamental_score": 0-100, "sentiment_score": 0-100 }} ⚠️ IMPORTANT: The analysis fields should contain your ACTUAL analysis text, NOT the format description above.""" # Format indicator data for prompt (ensure safe defaults) rsi_data = indicators.get("rsi") or {} macd_data = indicators.get("macd") or {} ma_data = indicators.get("moving_averages") or {} vol_data = indicators.get("volatility") or {} levels = indicators.get("levels") or {} # Format macro data macro = data.get("macro") or {} macro_summary = self._format_macro_summary(macro, data.get("market", "")) user_prompt = f"""Analyze {data['symbol']} in {data['market']} market. 📊 REAL-TIME DATA: - Current Price: ${current_price} - 24h Change: {change_24h}% - Support: ${support} - Resistance: ${resistance} 📈 TECHNICAL INDICATORS: - RSI(14): {rsi_data.get('value', 'N/A')} ({rsi_data.get('signal', 'N/A')}) - MACD: {macd_data.get('signal', 'N/A')} ({macd_data.get('trend', 'N/A')}) - MA Trend: {ma_data.get('trend', 'N/A')} - Volatility: {vol_data.get('level', 'N/A')} ({vol_data.get('pct', 0)}%) - Trend: {indicators.get('trend', 'N/A')} - Price Position (20d): {indicators.get('price_position', 'N/A')}% 🌐 MACRO ENVIRONMENT: {macro_summary} 📰 MARKET NEWS ({len(data.get('news') or [])} items): {news_summary} 💼 FUNDAMENTALS: - Company: {company.get('name', data['symbol'])} - Industry: {company.get('industry', 'N/A')} - P/E Ratio: {fundamental.get('pe_ratio', 'N/A')} - P/B Ratio: {fundamental.get('pb_ratio', 'N/A')} - Market Cap: {fundamental.get('market_cap', 'N/A')} - 52W High/Low: {fundamental.get('52w_high', 'N/A')} / {fundamental.get('52w_low', 'N/A')} - ROE: {fundamental.get('roe', 'N/A')} IMPORTANT: Consider the macro environment (especially DXY, VIX, rates) when making your recommendation. Provide your analysis now. Remember: all prices must be within 10% of ${current_price}.""" return system_prompt, user_prompt def _format_macro_summary(self, macro: Dict[str, Any], market: str) -> str: """格式化宏观数据摘要""" if not macro: return "宏观数据暂不可用" lines = [] # 美元指数 if 'DXY' in macro: dxy = macro['DXY'] direction = "↑" if dxy.get('change', 0) > 0 else "↓" lines.append(f"- {dxy.get('name', 'USD Index')}: {dxy.get('price', 'N/A')} ({direction}{abs(dxy.get('changePercent', 0)):.2f}%)") # 美元强弱对不同资产的影响 if market == 'Crypto': impact = "利空加密货币" if dxy.get('change', 0) > 0 else "利好加密货币" lines.append(f" ⚠️ 美元{direction} {impact}") elif market == 'Forex': lines.append(f" ⚠️ 美元{direction} 直接影响外汇走势") # VIX恐慌指数 if 'VIX' in macro: vix = macro['VIX'] vix_value = vix.get('price', 0) if vix_value > 30: level = "极度恐慌 (>30)" elif vix_value > 20: level = "较高恐慌 (20-30)" elif vix_value > 15: level = "正常 (15-20)" else: level = "低波动 (<15)" lines.append(f"- {vix.get('name', 'VIX')}: {vix_value:.2f} - {level}") # 美债收益率 if 'TNX' in macro: tnx = macro['TNX'] direction = "↑" if tnx.get('change', 0) > 0 else "↓" lines.append(f"- {tnx.get('name', '10Y Treasury')}: {tnx.get('price', 'N/A'):.3f}% ({direction})") if tnx.get('price', 0) > 4.5: lines.append(" ⚠️ 高利率环境,对估值不利") # 黄金 if 'GOLD' in macro: gold = macro['GOLD'] direction = "↑" if gold.get('change', 0) > 0 else "↓" lines.append(f"- {gold.get('name', 'Gold')}: ${gold.get('price', 'N/A'):.2f} ({direction}{abs(gold.get('changePercent', 0)):.2f}%)") # 标普500 if 'SPY' in macro: spy = macro['SPY'] direction = "↑" if spy.get('change', 0) > 0 else "↓" lines.append(f"- {spy.get('name', 'S&P 500')}: ${spy.get('price', 'N/A'):.2f} ({direction}{abs(spy.get('changePercent', 0)):.2f}%)") # 比特币 (作为风险指标) if 'BTC' in macro and market != 'Crypto': btc = macro['BTC'] direction = "↑" if btc.get('change', 0) > 0 else "↓" lines.append(f"- {btc.get('name', 'BTC')}: ${btc.get('price', 'N/A'):,.0f} ({direction}{abs(btc.get('changePercent', 0)):.2f}%) [风险偏好指标]") return "\n".join(lines) if lines else "宏观数据暂不可用" # ==================== Main Analysis ==================== def analyze(self, market: str, symbol: str, language: str = 'en-US', model: str = None, timeframe: str = "1D") -> Dict[str, Any]: """ Run fast single-call analysis. Returns: Complete analysis result with actionable recommendations. """ start_time = time.time() result = { "market": market, "symbol": symbol, "language": language, "timeframe": timeframe, "analysis_time_ms": 0, "error": None, } try: # Phase 1: Data collection (parallel) logger.info(f"Fast analysis starting: {market}:{symbol}") data = self._collect_market_data(market, symbol, timeframe) # Validate we have essential data - with fallback to indicators current_price = None # 优先从 price 数据获取 if data.get("price") and data["price"].get("price"): current_price = data["price"]["price"] # Fallback: 从 indicators 获取 (如果 K 线成功计算了) if not current_price and data.get("indicators"): current_price = data["indicators"].get("current_price") if current_price: logger.info(f"Using price from indicators: ${current_price}") # 构建简化的 price 数据 data["price"] = { "price": current_price, "change": 0, "changePercent": 0, "source": "indicators_fallback" } # Fallback: 从 kline 最后一根获取 if not current_price and data.get("kline"): klines = data["kline"] if klines and len(klines) > 0: current_price = float(klines[-1].get("close", 0)) if current_price > 0: logger.info(f"Using price from kline: ${current_price}") prev_close = float(klines[-2].get("close", current_price)) if len(klines) > 1 else current_price change = current_price - prev_close change_pct = (change / prev_close * 100) if prev_close > 0 else 0 data["price"] = { "price": current_price, "change": round(change, 6), "changePercent": round(change_pct, 2), "source": "kline_fallback" } if not current_price or current_price <= 0: result["error"] = "Failed to fetch current price from all sources" logger.error(f"Price fetch failed for {market}:{symbol}, all sources exhausted") return result # Phase 2: Build prompt system_prompt, user_prompt = self._build_analysis_prompt(data, language) # Phase 3: Single LLM call logger.info(f"Calling LLM for analysis...") llm_start = time.time() analysis = self.llm_service.safe_call_llm( system_prompt, user_prompt, default_structure={ "decision": "HOLD", "confidence": 50, "summary": "Analysis failed", "entry_price": current_price, "stop_loss": current_price * 0.95, "take_profit": current_price * 1.05, "position_size_pct": 10, "timeframe": "medium", "key_reasons": ["Unable to analyze"], "risks": ["Analysis error"], "technical_score": 50, "fundamental_score": 50, "sentiment_score": 50, }, model=model ) llm_time = int((time.time() - llm_start) * 1000) logger.info(f"LLM call completed in {llm_time}ms") # Phase 4: Validate and constrain output analysis = self._validate_and_constrain(analysis, current_price) # Build final result total_time = int((time.time() - start_time) * 1000) # Extract detailed analysis sections detailed_analysis = analysis.get("analysis", {}) if isinstance(detailed_analysis, str): # If AI returned a string instead of dict, use it as technical analysis detailed_analysis = {"technical": detailed_analysis, "fundamental": "", "sentiment": ""} result.update({ "decision": analysis.get("decision", "HOLD"), "confidence": analysis.get("confidence", 50), "summary": analysis.get("summary", ""), "detailed_analysis": { "technical": detailed_analysis.get("technical", ""), "fundamental": detailed_analysis.get("fundamental", ""), "sentiment": detailed_analysis.get("sentiment", ""), }, "trading_plan": { "entry_price": analysis.get("entry_price"), "stop_loss": analysis.get("stop_loss"), "take_profit": analysis.get("take_profit"), "position_size_pct": analysis.get("position_size_pct", 10), "timeframe": analysis.get("timeframe", "medium"), }, "reasons": analysis.get("key_reasons", []), "risks": analysis.get("risks", []), "scores": { "technical": analysis.get("technical_score", 50), "fundamental": analysis.get("fundamental_score", 50), "sentiment": analysis.get("sentiment_score", 50), "overall": self._calculate_overall_score(analysis), }, "market_data": { "current_price": current_price, "change_24h": data["price"].get("changePercent", 0), "support": data["indicators"].get("levels", {}).get("support"), "resistance": data["indicators"].get("levels", {}).get("resistance"), }, "indicators": data.get("indicators", {}), "analysis_time_ms": total_time, "llm_time_ms": llm_time, "data_collection_time_ms": data.get("collection_time_ms", 0), }) # Store in memory for future retrieval and get memory_id for feedback memory_id = self._store_analysis_memory(result) if memory_id: result["memory_id"] = memory_id logger.info(f"Fast analysis completed in {total_time}ms: {market}:{symbol} -> {result['decision']} (memory_id={memory_id})") except Exception as e: logger.error(f"Fast analysis failed: {e}", exc_info=True) result["error"] = str(e) return result def _validate_and_constrain(self, analysis: Dict, current_price: float) -> Dict: """ Validate LLM output and constrain prices to reasonable ranges. This prevents absurd recommendations like "BTC at 95000, buy at 75000". """ if not current_price or current_price <= 0: return analysis # Price bounds min_price = current_price * 0.90 max_price = current_price * 1.10 # Constrain entry price entry = analysis.get("entry_price", current_price) if entry and (entry < min_price or entry > max_price): logger.warning(f"Entry price {entry} out of bounds, constraining to current price {current_price}") analysis["entry_price"] = round(current_price, 6) # Constrain stop loss stop_loss = analysis.get("stop_loss", current_price * 0.95) if stop_loss and (stop_loss < min_price or stop_loss > current_price): analysis["stop_loss"] = round(current_price * 0.95, 6) # Constrain take profit take_profit = analysis.get("take_profit", current_price * 1.05) if take_profit and (take_profit < current_price or take_profit > max_price): analysis["take_profit"] = round(current_price * 1.05, 6) # Constrain confidence confidence = analysis.get("confidence", 50) analysis["confidence"] = max(0, min(100, int(confidence))) # Constrain scores for score_key in ["technical_score", "fundamental_score", "sentiment_score"]: score = analysis.get(score_key, 50) analysis[score_key] = max(0, min(100, int(score))) # Validate decision decision = str(analysis.get("decision", "HOLD")).upper() if decision not in ["BUY", "SELL", "HOLD"]: analysis["decision"] = "HOLD" else: analysis["decision"] = decision return analysis def _calculate_overall_score(self, analysis: Dict) -> int: """Calculate weighted overall score.""" tech = analysis.get("technical_score", 50) fund = analysis.get("fundamental_score", 50) sent = analysis.get("sentiment_score", 50) # Weights: technical 40%, fundamental 35%, sentiment 25% overall = tech * 0.40 + fund * 0.35 + sent * 0.25 # Adjust based on decision decision = analysis.get("decision", "HOLD") confidence = analysis.get("confidence", 50) if decision == "BUY": overall = overall * 0.6 + (50 + confidence * 0.5) * 0.4 elif decision == "SELL": overall = overall * 0.6 + (50 - confidence * 0.5) * 0.4 return max(0, min(100, int(overall))) def _store_analysis_memory(self, result: Dict) -> Optional[int]: """Store analysis result for future learning. Returns memory_id.""" try: from app.services.analysis_memory import get_analysis_memory memory = get_analysis_memory() memory_id = memory.store(result) return memory_id except Exception as e: logger.warning(f"Memory storage failed: {e}") return None # ==================== Backward Compatibility ==================== def analyze_legacy_format(self, market: str, symbol: str, language: str = 'en-US', model: str = None, timeframe: str = "1D") -> Dict[str, Any]: """ Returns analysis in legacy multi-agent format for backward compatibility. """ fast_result = self.analyze(market, symbol, language, model, timeframe) if fast_result.get("error"): return { "overview": {"report": f"Analysis failed: {fast_result['error']}"}, "fundamental": {"report": "N/A"}, "technical": {"report": "N/A"}, "news": {"report": "N/A"}, "sentiment": {"report": "N/A"}, "risk": {"report": "N/A"}, "error": fast_result["error"], } # Convert to legacy format decision = fast_result.get("decision", "HOLD") confidence = fast_result.get("confidence", 50) scores = fast_result.get("scores", {}) return { "overview": { "overallScore": scores.get("overall", 50), "recommendation": decision, "confidence": confidence, "dimensionScores": { "fundamental": scores.get("fundamental", 50), "technical": scores.get("technical", 50), "news": scores.get("sentiment", 50), "sentiment": scores.get("sentiment", 50), "risk": 100 - confidence, # Inverse of confidence }, "report": fast_result.get("summary", ""), }, "fundamental": { "score": scores.get("fundamental", 50), "report": f"Fundamental score: {scores.get('fundamental', 50)}/100", }, "technical": { "score": scores.get("technical", 50), "report": f"Technical score: {scores.get('technical', 50)}/100", "indicators": fast_result.get("indicators", {}), }, "news": { "score": scores.get("sentiment", 50), "report": "See sentiment analysis", }, "sentiment": { "score": scores.get("sentiment", 50), "report": f"Sentiment score: {scores.get('sentiment', 50)}/100", }, "risk": { "score": 100 - confidence, "report": "\n".join(fast_result.get("risks", [])), }, "debate": { "bull": {"confidence": confidence if decision == "BUY" else 50}, "bear": {"confidence": confidence if decision == "SELL" else 50}, "research_decision": fast_result.get("summary", ""), }, "trader_decision": { "decision": decision, "confidence": confidence, "reasoning": fast_result.get("summary", ""), "trading_plan": fast_result.get("trading_plan", {}), "report": "\n".join(fast_result.get("reasons", [])), }, "risk_debate": { "risky": {"recommendation": ""}, "neutral": {"recommendation": fast_result.get("summary", "")}, "safe": {"recommendation": ""}, }, "final_decision": { "decision": decision, "confidence": confidence, "reasoning": fast_result.get("summary", ""), "risk_summary": { "risks": fast_result.get("risks", []), }, "recommendation": "\n".join(fast_result.get("reasons", [])), }, "fast_analysis": fast_result, # Include new format for gradual migration "error": None, } # Singleton instance _fast_analysis_service = None def get_fast_analysis_service() -> FastAnalysisService: """Get singleton FastAnalysisService instance.""" global _fast_analysis_service if _fast_analysis_service is None: _fast_analysis_service = FastAnalysisService() return _fast_analysis_service def fast_analyze(market: str, symbol: str, language: str = 'en-US', model: str = None, timeframe: str = "1D") -> Dict[str, Any]: """Convenience function for fast analysis.""" service = get_fast_analysis_service() return service.analyze(market, symbol, language, model, timeframe)