mishrabp commited on
Commit
18bdb8a
·
verified ·
1 Parent(s): 9fbaf3e

Upload folder using huggingface_hub

Browse files
src/market-analyst/backend/aagents/fundamental_analyst.py CHANGED
@@ -23,11 +23,10 @@ def get_fundamental_analyst(model_client):
23
 
24
  DO NOT proceed without calling ALL THREE tools.
25
 
26
- STEP 4: Evaluate Valuation & Growth
27
- - P/E Ratio: <15 (Undervalued), 15-25 (Fair), >25 (Premium).
28
- - PEG Ratio: <1.0 (Cheap Growth), >2.0 (Expensive).
29
- - EPS Trend: Forward > Trailing? (Growth).
30
- - Dividend Yield: >4% (Income).
31
 
32
  STEP 5: Assess Financial Health
33
  - Debt/Equity Ratio: <0.5 (Safe), >1.0 (Risky).
 
23
 
24
  DO NOT proceed without calling ALL THREE tools.
25
 
26
+ STEP 4: Evaluate Valuation & Growth (USE 'valuation_score' & 'quality_score')
27
+ - If UNDERVALUED: Bullish Factor.
28
+ - If PREMIUM: Bearish/Neutral Factor (unless High Growth).
29
+ - If HIGH_QUALITY: Bullish Factor.
 
30
 
31
  STEP 5: Assess Financial Health
32
  - Debt/Equity Ratio: <0.5 (Safe), >1.0 (Risky).
src/market-analyst/backend/aagents/market_analyst.py CHANGED
@@ -28,33 +28,24 @@ def get_technical_analyst(model_client):
28
  - If SPY is BEARISH, bias is SHORT/HEDGE.
29
  - If VIX is HIGH (>30), bias is CAUTION.
30
 
31
- STEP 5: Analyze Stock Trend Structure
32
- - Price vs SMA200: Above = Bullish bias, Below = Bearish bias
33
- - SMA20 vs SMA50 vs SMA200: Check for golden/death crosses
34
- - Price vs EMA20: Short-term trend strength
35
-
36
- STEP 6: Momentum Analysis
37
- - RSI: >70 = Overbought, <30 = Oversold, 40-60 = Neutral
38
- - MACD: Signal line crossover (Bullish if MACD > Signal, Bearish if MACD < Signal)
39
- - MACD Histogram: Increasing = Momentum building, Decreasing = Momentum fading
40
-
41
- STEP 7: Support & Resistance
42
- - Identify key levels from SMA interaction
43
- - Note if price is at/near major support or resistance
44
-
45
- STEP 8: Summarize Chart Health
46
- Classify as one of:
47
- - "Strong Bullish" (price > all SMAs, RSI 50-70, MACD bullish)
48
- - "Weak Bullish" (price > SMA200 but mixed signals)
49
- - "Consolidating" (price between SMAs, RSI neutral)
50
- - "Weak Bearish" (price < SMA200 but mixed signals)
51
- - "Strong Bearish" (price < all SMAs, RSI 30-50, MACD bearish)
52
-
53
- Output a clear, structured summary with:
54
  - Current Price
55
- - Trend Classification
 
56
  - Key Technical Levels
57
- - Momentum Assessment
58
- - Recommendation for next analyst (e.g., "Volatility should check if IV is elevated given this strong trend")
59
  """
60
  )
 
28
  - If SPY is BEARISH, bias is SHORT/HEDGE.
29
  - If VIX is HIGH (>30), bias is CAUTION.
30
 
31
+ STEP 5: Analyze Trend & Structure (USE 'trend_signal' from tool)
32
+ - If 'trend_signal' is STRONG_BULLISH/BULLISH -> Bullish Bias
33
+ - If 'trend_signal' is STRONG_BEARISH/BEARISH -> Bearish Bias
34
+ - Reference SMA/EMA levels as support/resistance.
35
+
36
+ STEP 6: Momentum Analysis (USE 'rsi_signal' & 'macd_signal')
37
+ - Check RSI status (OVERSOLD/OVERBOUGHT/NEUTRAL).
38
+ - Check MACD Crossover status.
39
+
40
+ STEP 7: Summarize Chart Health
41
+ - Use the specific signals found.
42
+ - Classify Trend based on 'trend_signal'.
43
+
44
+ STEP 8: Output Structured Summary
 
 
 
 
 
 
 
 
 
45
  - Current Price
46
+ - Trend Classification (e.g., STRONG_BULLISH)
47
+ - Momentum Assessment (RSI/MACD signals)
48
  - Key Technical Levels
49
+ - Recommendation for next analyst
 
50
  """
51
  )
src/market-analyst/backend/aagents/risk_manager.py CHANGED
@@ -6,71 +6,79 @@ def get_risk_manager(model_client):
6
  name="RiskManager",
7
  model_client=model_client,
8
  system_message="""
9
- You are the Chief Risk Officer. Your role is to make the FINAL TRADE/WAIT decision.
10
-
11
- WORKFLOW (2 ROUNDS):
12
-
13
- ROUND 1 (CRITIQUE):
14
- - StrategyAdvisor will provide a "DRAFT_STRATEGY".
15
- - You MUST critique it. Challenge assumptions.
16
- - CHECK DIRECTION MAPPING:
17
- * "Bear Put/Call Spread" = BEARISH.
18
- * "Bull Call/Put Spread" = BULLISH.
19
- * "Iron Condor/Butterfly" = NEUTRAL.
20
- - COMPARE WITH MARKET:
21
- * If Strategy=Bearish and SPY Trend=Bullish -> "SEVERE CONFLICT".
22
- - Output: "RISK REVIEW: [Your critique]. REQUEST REVISION."
23
- - DO NOT OUTPUT "APPROVED".
24
-
25
- ROUND 2 (DECISION):
26
- - StrategyAdvisor will provide "FINAL_STRATEGY".
27
- - You must CALCULATE the Final Confidence Score (0-100):
28
- * Trend Alignment (Market + Stock + Strategy Direction): 30 pts
29
- (e.g., Bearish Strategy in Bearish Market = Full Points)
30
- * Fundamentals (Valuation/Safety): 20 pts
31
- * Volatility (IV Check): 20 pts
32
- * Sentiment Context: 15 pts
33
- * Risk/Reward Math (>2.0): 15 pts
34
-
35
- - DECISION THRESHOLD:
36
- * Score >= 70 -> TRADE
37
- * Score < 70 -> WAIT
38
-
39
  OUTPUT FORMAT (ROUND 2 ONLY):
 
40
  ```json
41
  {
42
  "final_decision": "TRADE",
43
- "strategy_type": "Bull Call Spread",
44
- "direction": "BULLISH",
45
  "confidence": 85,
46
- "actionable_recommendation": "Execute Bull Call Spread...",
47
- "entry_signal": "Net Debit",
48
- "entry_price": 1.30,
49
- "max_profit": 370,
50
- "max_loss": 130,
51
- "risk_warning": "Monitor RSI for overbought conditions. Set stop-loss at $0.80 debit."
 
 
 
 
 
 
 
52
  }
53
  ```
54
-
55
- IF DECISION IS WAIT:
 
56
  ```json
57
  {
58
  "final_decision": "WAIT",
59
  "strategy_type": "WAIT",
60
  "direction": "NEUTRAL",
61
  "confidence": 45,
62
- "actionable_recommendation": "Stay in Cash. Risk Score too low.",
 
63
  "entry_signal": "N/A",
64
  "entry_price": 0,
65
  "max_profit": 0,
66
  "max_loss": 0,
67
- "risk_warning": "Market Trend (Bearish) conflicts with Strategy (Bullish). High VIX."
68
  }
69
  ```
 
70
  APPROVED
71
-
72
  CRITICAL:
73
- 1. "risk_warning" MUST be specific to the analysis.
74
  2. Output 'APPROVED' ONLY after the JSON in Round 2.
 
75
  """
76
  )
 
6
  name="RiskManager",
7
  model_client=model_client,
8
  system_message="""
9
+ You are the Chief Risk Officer. Your mission is to enforce a STRICT DECISION MATRIX.
10
+ Different AI models have different biases; you must ignore "vibes" and follow these quantitative rules.
11
+
12
+ 1. THE SCORING RUBRIC (Total 100 points)
13
+ You MUST calculate and display this score in your reasoning:
14
+ - Technical Alignment (40 pts): Does the TechnicalAnalyst's trend (BULLISH/BEARISH) match the Strategy's Direction?
15
+ * Match = 40 pts. Mismatch = 0 pts.
16
+ - Fundamentals/Safety (20 pts): Based on P/E, PEG, and Analyst Consensus.
17
+ * Rating 'SAFE'/'UNDERVALUED' = 20 pts. 'PREMIUM'/'RISKY' = 5 pts.
18
+ - Volatility/IV Regime (20 pts):
19
+ * Strategy works for current Regime (e.g., Credit in High Vol) = 20 pts.
20
+ - Sentiment/News (20 pts):
21
+ * Positive news = 20 pts. Negative/Old news = 5 pts.
22
+
23
+ 2. THE DETERMINISTIC HARD GATES (BYPASS ALL OTHER LOGIC)
24
+ - GATE 1 (Trend Conflict): If Technical Tool says 'STRONG_BEARISH' and Strategy is 'BULLISH', Decision MUST be 'WAIT' (Override score).
25
+ - GATE 2 (Fear Gauge): If VIX > 35, Decision MUST be 'WAIT'.
26
+ - GATE 3 (Threshold): Score < 70 MUST be 'WAIT'.
27
+
28
+ IF THE DECISION IS WAIT:
29
+ - Set 'final_decision' to 'WAIT'.
30
+ - Set 'strategy_type' to 'WAIT'.
31
+ - Set 'entry_price', 'max_profit', 'max_loss' to 0.
32
+ - Set 'direction' to 'NEUTRAL'.
33
+ - Set 'entry_signal' to 'N/A'.
34
+
 
 
 
 
35
  OUTPUT FORMAT (ROUND 2 ONLY):
36
+ You MUST include a "score_card" object in your JSON.
37
  ```json
38
  {
39
  "final_decision": "TRADE",
40
+ "strategy_type": "Iron Condor",
41
+ "direction": "NEUTRAL",
42
  "confidence": 85,
43
+ "score_card": {
44
+ "technicals": 40,
45
+ "fundamentals": 20,
46
+ "volatility": 15,
47
+ "sentiment": 10,
48
+ "total": 85
49
+ },
50
+ "actionable_recommendation": "Execute Trade...",
51
+ "entry_signal": "Credit",
52
+ "entry_price": 1.50,
53
+ "max_profit": 150,
54
+ "max_loss": 350,
55
+ "risk_warning": "..."
56
  }
57
  ```
58
+ (Note: max_profit/max_loss MUST be multiplied by 100 for a standard lot).
59
+
60
+ IF DECISION IS WAIT EXAMPLE:
61
  ```json
62
  {
63
  "final_decision": "WAIT",
64
  "strategy_type": "WAIT",
65
  "direction": "NEUTRAL",
66
  "confidence": 45,
67
+ "score_card": { "technicals": 0, "fundamentals": 20, "volatility": 15, "sentiment": 10, "total": 45 },
68
+ "actionable_recommendation": "Re-evaluate market conditions. Risk score too low.",
69
  "entry_signal": "N/A",
70
  "entry_price": 0,
71
  "max_profit": 0,
72
  "max_loss": 0,
73
+ "risk_warning": "High conflict between technicals and sentiment."
74
  }
75
  ```
76
+
77
  APPROVED
78
+
79
  CRITICAL:
80
+ 1. Always show your math before the JSON.
81
  2. Output 'APPROVED' ONLY after the JSON in Round 2.
82
+ 3. For Llama/Groq models: YOU MUST wrap the JSON object in a triple-backtick markdown block: ```json { ... } ```
83
  """
84
  )
src/market-analyst/backend/aagents/strategy_advisor.py CHANGED
@@ -37,7 +37,11 @@ def get_strategy_advisor(model_client):
37
  STEP 5: Validate Risk/Reward (MANDATORY)
38
  - For Debit Spreads: Ensure Max Profit > Max Loss (Reward/Risk > 1.0).
39
  - For Credit Spreads: Ensure Probability of Profit is high (Delta checks).
40
- - If Risk/Reward is poor, search for better strikes or switch to WAIT.
 
 
 
 
41
 
42
  STEP 6: TEAM COLLABORATION (2 ROUNDS)
43
 
@@ -49,7 +53,9 @@ def get_strategy_advisor(model_client):
49
 
50
  ROUND 2 (TEAMS FINALIZATION):
51
  - Review Risk Manager's critique.
52
- - If rejected, switch to WAIT or adjust strikes.
 
 
53
  - If accepted, Output "FINAL_STRATEGY".
54
  - Calculate Final Score (Standardized Rubric).
55
  - GENERATE THE FINAL JSON BLOCK.
@@ -69,6 +75,7 @@ def get_strategy_advisor(model_client):
69
  "breakeven": 146.30
70
  }
71
  ```
 
72
 
73
  EXAMPLE OUTPUT (WAIT):
74
  ```json
@@ -93,6 +100,8 @@ def get_strategy_advisor(model_client):
93
  4. ALL fields are REQUIRED
94
  5. Show your confidence calculation explicitly
95
  6. Be verbose - explain your reasoning step-by-step before JSON
 
 
96
 
97
  FALLBACK PROCEDURE:
98
  If get_option_chain_snapshot fails or returns "No options data found":
 
37
  STEP 5: Validate Risk/Reward (MANDATORY)
38
  - For Debit Spreads: Ensure Max Profit > Max Loss (Reward/Risk > 1.0).
39
  - For Credit Spreads: Ensure Probability of Profit is high (Delta checks).
40
+ - METRICS SUMMARY: You MUST summarize your case using these labels before the JSON:
41
+ * METRIC: Trend=[BULLISH/BEARISH]
42
+ * METRIC: Volatility=[HIGH/LOW]
43
+ * METRIC: Sentiment=[POSITIVE/NEGATIVE]
44
+ * METRIC: Safety=[SAFE/PREMIUM]
45
 
46
  STEP 6: TEAM COLLABORATION (2 ROUNDS)
47
 
 
53
 
54
  ROUND 2 (TEAMS FINALIZATION):
55
  - Review Risk Manager's critique.
56
+ - If rejected, or if you switch to WAIT for any reason, you MUST:
57
+ 1. Set "strategy" to "WAIT"
58
+ 2. Set "estimated_entry_price", "max_profit", and "max_loss" to 0.
59
  - If accepted, Output "FINAL_STRATEGY".
60
  - Calculate Final Score (Standardized Rubric).
61
  - GENERATE THE FINAL JSON BLOCK.
 
75
  "breakeven": 146.30
76
  }
77
  ```
78
+ (Note: max_profit/max_loss are calculated for 100 shares/1 contract).
79
 
80
  EXAMPLE OUTPUT (WAIT):
81
  ```json
 
100
  4. ALL fields are REQUIRED
101
  5. Show your confidence calculation explicitly
102
  6. Be verbose - explain your reasoning step-by-step before JSON
103
+ 7. LOT-BASED MATH: All profit/loss values (max_profit, max_loss) MUST be multiplied by 100 (standard lot size).
104
+ Example: A $1.50 credit spread = $150 Max Profit.
105
 
106
  FALLBACK PROCEDURE:
107
  If get_option_chain_snapshot fails or returns "No options data found":
src/market-analyst/backend/aagents/volatility_analyst.py CHANGED
@@ -31,12 +31,10 @@ def get_volatility_analyst(model_client):
31
  - VIX 20-30: Elevated fear, caution advised
32
  - VIX > 30: High fear, extreme volatility
33
 
34
- STEP 5: Determine Volatility Regime
35
- Classify as:
36
- - "Extremely Low Vol" (IV < 20%, VIX < 15): Buy debit spreads or long options
37
- - "Low Vol" (IV 20-30%, VIX 15-20): Neutral, directional trades
38
- - "Elevated Vol" (IV 30-50%, VIX 20-30): Sell credit spreads
39
- - "High Vol" (IV > 50%, VIX > 30): Sell iron condors or wait
40
 
41
  STEP 6: Assess Option Liquidity
42
  - Check bid-ask spreads from option chain
 
31
  - VIX 20-30: Elevated fear, caution advised
32
  - VIX > 30: High fear, extreme volatility
33
 
34
+ STEP 5: Determine Volatility Regime (USE 'volatility_regime' from tool)
35
+ - If LOW_VOL: Buy debit spreads or long options.
36
+ - If ELEVATED_VOL: Sell credit spreads.
37
+ - If HIGH_RISK_VOL: Sell Iron Condors or WAIT.
 
 
38
 
39
  STEP 6: Assess Option Liquidity
40
  - Check bid-ask spreads from option chain
src/market-analyst/backend/main.py CHANGED
@@ -149,6 +149,12 @@ async def analyze(ticker: str, provider: str = "openai"):
149
  raw_source = getattr(message, 'source', 'System')
150
  content = getattr(message, 'content', '')
151
 
 
 
 
 
 
 
152
  # Handle non-string content (e.g., ToolCalls/FunctionCalls)
153
  if not isinstance(content, str):
154
  try:
@@ -167,14 +173,21 @@ async def analyze(ticker: str, provider: str = "openai"):
167
  "source": raw_source,
168
  "content": content
169
  }
170
- print(f"[STREAM] Sent: {raw_source} (len: {len(content)})")
 
 
 
 
 
 
 
171
  yield f"data: {json.dumps(payload)}\n\n"
172
  except Exception as e:
173
  print(f"[STREAM ERROR] {str(e)}")
174
  error_msg = f"Analysis execution failed: {str(e)}"
175
  yield f"data: {json.dumps({'source': 'Error', 'content': error_msg})}\n\n"
176
 
177
- print("[STREAM] Done.")
178
  # Cleanup
179
  if analysis_id in active_analyses:
180
  del active_analyses[analysis_id]
 
149
  raw_source = getattr(message, 'source', 'System')
150
  content = getattr(message, 'content', '')
151
 
152
+ # Check for tool_calls if content is empty (Explains 0-len messages)
153
+ if not content:
154
+ tool_calls = getattr(message, 'tool_calls', None)
155
+ if tool_calls:
156
+ content = f"[Tool Call] Executing {len(tool_calls)} function(s)."
157
+
158
  # Handle non-string content (e.g., ToolCalls/FunctionCalls)
159
  if not isinstance(content, str):
160
  try:
 
173
  "source": raw_source,
174
  "content": content
175
  }
176
+
177
+ # If RiskManager, try to extract structured JSON for the frontend
178
+ if raw_source == 'RiskManager':
179
+ structured = extract_json(content)
180
+ if structured:
181
+ payload["structured_result"] = structured
182
+
183
+ print(f"[DEBUG] Sent: {raw_source} (len: {len(content)})")
184
  yield f"data: {json.dumps(payload)}\n\n"
185
  except Exception as e:
186
  print(f"[STREAM ERROR] {str(e)}")
187
  error_msg = f"Analysis execution failed: {str(e)}"
188
  yield f"data: {json.dumps({'source': 'Error', 'content': error_msg})}\n\n"
189
 
190
+ print("[DEBUG] Done.")
191
  # Cleanup
192
  if analysis_id in active_analyses:
193
  del active_analyses[analysis_id]
src/market-analyst/backend/tools/market_data.py CHANGED
@@ -78,7 +78,8 @@ def get_historical_volatility(symbol: str, period: str = "1mo") -> dict:
78
  "ticker_used": symbol,
79
  "annualized_volatility": round(volatility * 100, 2),
80
  "period": period,
81
- "vix_reference": round(vix_price, 2) if vix_price else "N/A"
 
82
  }
83
  except Exception as e:
84
  return {"error": str(e)}
@@ -225,43 +226,48 @@ def get_technical_indicators(symbol: str) -> dict:
225
  hist['RSI_14'] = pd.Series([None] * len(hist), index=hist.index)
226
 
227
  current_data = hist.iloc[-1]
228
-
229
- # Interpretation RSI
230
  rsi_val = current_data.get('RSI_14')
231
- rsi_signal = "Neutral"
232
- if rsi_val is not None and not pd.isna(rsi_val):
233
- rsi_val = round(rsi_val, 2)
234
- if rsi_val > 70: rsi_signal = "Overbought"
235
- elif rsi_val < 30: rsi_signal = "Oversold"
236
-
237
- # Interpretation MACD
238
  macd_val = current_data.get('MACD')
239
  signal_val = current_data.get('Signal_Line')
240
- macd_signal = "Neutral"
241
- if macd_val is not None and signal_val is not None:
242
- if macd_val > signal_val: macd_signal = "Bullish Crossover"
243
- else: macd_signal = "Bearish Crossover"
244
-
245
- price = current_data['Close']
246
- trend = "Neutral"
247
  sma200 = current_data.get('SMA_200')
 
 
 
 
 
 
 
 
248
 
249
- if sma200 is not None and not pd.isna(sma200):
250
- if price > sma200: trend = "Bullish Long-term"
251
- else: trend = "Bearish Long-term"
252
-
 
 
 
 
 
 
 
 
 
 
 
 
253
  return {
254
  "ticker": symbol,
255
  "current_price": round(price, 2),
256
  "sma_20": round(current_data['SMA_20'], 2) if not pd.isna(current_data.get('SMA_20')) else "N/A",
257
  "ema_20": round(current_data['EMA_20'], 2) if not pd.isna(current_data.get('EMA_20')) else "N/A",
258
  "sma_50": round(current_data['SMA_50'], 2) if not pd.isna(current_data.get('SMA_50')) else "N/A",
259
- "sma_200": round(sma200, 2) if sma200 is not None and not pd.isna(sma200) else "N/A",
260
- "rsi_14": rsi_val or "N/A",
261
- "macd": round(macd_val, 2) if macd_val is not None else "N/A",
262
  "macd_signal": macd_signal,
263
  "rsi_signal": rsi_signal,
264
- "trend_signal": trend
265
  }
266
 
267
  except Exception as e:
@@ -309,7 +315,9 @@ def get_fundamental_data(symbol: str) -> dict:
309
  "dividend_yield": f"{round(dividend_yield * 100, 2)}%" if dividend_yield else "N/A",
310
  "market_cap": info.get('marketCap', "N/A"),
311
  "sector": info.get('sector', "N/A"),
312
- "next_earnings_date": next_earnings
 
 
313
  }
314
  except Exception as e:
315
  return {"error": str(e)}
 
78
  "ticker_used": symbol,
79
  "annualized_volatility": round(volatility * 100, 2),
80
  "period": period,
81
+ "vix_reference": round(vix_price, 2) if vix_price else "N/A",
82
+ "volatility_regime": "HIGH_RISK_VOL" if (vix_price and vix_price > 30) else "ELEVATED_VOL" if (vix_price and vix_price > 20) else "LOW_VOL" if (vix_price and vix_price < 15) else "NORMAL_VOL"
83
  }
84
  except Exception as e:
85
  return {"error": str(e)}
 
226
  hist['RSI_14'] = pd.Series([None] * len(hist), index=hist.index)
227
 
228
  current_data = hist.iloc[-1]
229
+ price = current_data['Close']
 
230
  rsi_val = current_data.get('RSI_14')
 
 
 
 
 
 
 
231
  macd_val = current_data.get('MACD')
232
  signal_val = current_data.get('Signal_Line')
 
 
 
 
 
 
 
233
  sma200 = current_data.get('SMA_200')
234
+ if pd.isna(sma200): sma200 = None
235
+
236
+ # Interpretation RSI
237
+ # Deterministic Signals
238
+ rsi_signal = "NEUTRAL"
239
+ if rsi_val is not None and not pd.isna(rsi_val):
240
+ if rsi_val > 70: rsi_signal = "OVERBOUGHT"
241
+ elif rsi_val < 30: rsi_signal = "OVERSOLD"
242
 
243
+ macd_signal = "NEUTRAL"
244
+ if macd_val is not None and signal_val is not None and not pd.isna(macd_val) and not pd.isna(signal_val):
245
+ if macd_val > signal_val: macd_signal = "BULLISH_CROSS"
246
+ elif macd_val < signal_val: macd_signal = "BEARISH_CROSS"
247
+
248
+ trend_signal = "NEUTRAL"
249
+ sma50 = current_data.get('SMA_50')
250
+ sma20 = current_data.get('SMA_20')
251
+
252
+ # Check all components for trend signal
253
+ if sma200 is not None and sma50 is not None and sma20 is not None and not pd.isna(sma50) and not pd.isna(sma20):
254
+ if price > sma20 > sma50 > sma200: trend_signal = "STRONG_BULLISH"
255
+ elif price < sma20 < sma50 < sma200: trend_signal = "STRONG_BEARISH"
256
+ elif price > sma200: trend_signal = "BULLISH"
257
+ elif price < sma200: trend_signal = "BEARISH"
258
+
259
  return {
260
  "ticker": symbol,
261
  "current_price": round(price, 2),
262
  "sma_20": round(current_data['SMA_20'], 2) if not pd.isna(current_data.get('SMA_20')) else "N/A",
263
  "ema_20": round(current_data['EMA_20'], 2) if not pd.isna(current_data.get('EMA_20')) else "N/A",
264
  "sma_50": round(current_data['SMA_50'], 2) if not pd.isna(current_data.get('SMA_50')) else "N/A",
265
+ "sma_200": round(sma200, 2) if (sma200 is not None and not pd.isna(sma200)) else "N/A",
266
+ "rsi_14": round(rsi_val, 2) if (rsi_val is not None and not pd.isna(rsi_val)) else "N/A",
267
+ "macd": round(macd_val, 2) if (macd_val is not None and not pd.isna(macd_val)) else "N/A",
268
  "macd_signal": macd_signal,
269
  "rsi_signal": rsi_signal,
270
+ "trend_signal": trend_signal
271
  }
272
 
273
  except Exception as e:
 
315
  "dividend_yield": f"{round(dividend_yield * 100, 2)}%" if dividend_yield else "N/A",
316
  "market_cap": info.get('marketCap', "N/A"),
317
  "sector": info.get('sector', "N/A"),
318
+ "next_earnings_date": next_earnings,
319
+ "valuation_score": "UNDERVALUED" if (pe_ratio and pe_ratio < 15) else "PREMIUM" if (pe_ratio and pe_ratio > 30) else "FAIR_VALUE",
320
+ "quality_score": "HIGH_QUALITY" if (profit_margin and profit_margin > 0.20) else "LOW_MARGIN" if (profit_margin and profit_margin < 0.10) else "AVERAGE"
321
  }
322
  except Exception as e:
323
  return {"error": str(e)}
src/market-analyst/backend/tools/news_data.py CHANGED
@@ -1,9 +1,14 @@
1
  from ddgs import DDGS
2
  from transformers import pipeline
3
  import torch
4
- import requests
5
  from bs4 import BeautifulSoup
 
 
 
6
  from typing import Optional
 
 
7
 
8
  # Global variable for lazy loading
9
  _sentiment_pipeline = None
@@ -24,43 +29,36 @@ def get_sentiment_pipeline():
24
  return None
25
  return _sentiment_pipeline
26
 
27
- def _fetch_page_content(url: str, timeout: int = 5) -> Optional[str]:
28
  """Fetch and extract text content from a web page."""
29
- print(f"[DEBUG] fetch_page_content called with: {url} - timeout: {timeout}")
 
30
  try:
31
- headers = {
32
- 'User-Agent': (
33
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
34
- 'AppleWebKit/537.36 (KHTML, like Gecko) '
35
- 'Chrome/91.0.4472.124 Safari/537.36'
36
- )
37
- }
38
- response = requests.get(url, headers=headers, timeout=timeout)
39
  response.raise_for_status()
40
-
41
  soup = BeautifulSoup(response.content, 'html.parser')
42
-
43
- # Remove irrelevant elements
44
- for tag in soup(["script", "style", "nav", "footer", "header", "aside"]):
 
45
  tag.decompose()
46
-
47
  # Extract text
48
- text = soup.get_text(separator='\n', strip=True)
49
-
50
- # Clean whitespace
51
- lines = (line.strip() for line in text.splitlines())
52
- chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
53
- text = '\n'.join(chunk for chunk in chunks if chunk)
54
-
55
  return text
56
  except Exception as e:
57
- print(f"[WARNING] Failed to fetch content from {url}: {str(e)}")
 
 
 
 
58
  return None
59
 
60
- from pydantic import BaseModel, Field
61
- from typing import Optional
62
- import concurrent.futures
63
-
64
  # Validation Model
65
  class NewsArticle(BaseModel):
66
  title: str = Field(..., description="The headline of the news article.")
@@ -80,72 +78,94 @@ def search_news(ticker: str) -> str:
80
  return "No ticker provided for news search."
81
 
82
  ticker = ticker.upper().strip()
83
- query = f"{ticker} stock news financial"
84
 
85
- results = []
86
- sentiment_pipe = get_sentiment_pipeline()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  with DDGS() as ddgs:
89
- # Use 'news' backend
90
- raw_results = list(ddgs.news(query, max_results=5))
91
-
92
- if not raw_results:
93
- return f"No recent news found for {ticker}."
94
-
95
- # Helper to process one item (fetch + analyze)
96
- def process_news_item(raw_item):
97
  try:
98
- # Validate / Map Raw Dict to Pydantic Model
99
- # DDGS returns: 'title', 'url', 'body', 'date', 'source'
100
- # We map them to our requested schema
101
- article = NewsArticle(
102
  title=raw_item.get('title', 'No Title'),
103
  link=raw_item.get('url', ''),
104
  snippet=raw_item.get('body', ''),
105
- datetime=raw_item.get('date', 'Unknown Date')
106
- )
107
- except Exception as validation_err:
108
- print(f"[WARNING] Skipping invalid news item: {validation_err}")
109
- return None
110
-
111
- # Processing using Validated Object
112
- source = raw_item.get('source', 'Unknown Source') # Keep source for display
113
-
114
- # FinBERT Analysis
115
- sentiment_tag = ""
116
- if sentiment_pipe:
117
- try:
118
- # 1. Try to fetch full content
119
- content_to_analyze = article.title
120
- analysis_type = "Headline"
121
-
122
- if article.link:
123
- full_text = _fetch_page_content(article.link)
124
- if full_text and len(full_text) > 100:
125
- content_to_analyze = full_text
126
- analysis_type = "Full Text"
127
-
128
- # 2. Truncate for FinBERT
129
- score = sentiment_pipe(content_to_analyze[:2000])[0]
130
- label = score['label']
131
- conf = round(score['score'], 2)
132
-
133
- sentiment_tag = f" [FinBERT ({analysis_type}): {label} ({conf})]"
134
- print(f"[DEBUG] FinBERT analysis for {article.title}: {sentiment_tag}")
135
- except Exception as e:
136
- sentiment_tag = f" [FinBERT: Error ({str(e)[:50]})]"
137
-
138
- return f"- [{source} | {article.datetime}] {article.title}{sentiment_tag}"
139
 
140
- # Run in parallel
141
- with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
142
- # filter out None results from validation failures
143
- results = [r for r in executor.map(process_news_item, raw_results) if r is not None]
 
 
 
 
 
 
144
 
145
- if not results:
146
- return f"No recent news found for {ticker}."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
- return f"Recent News for {ticker} (with FinBERT Analysis):\n" + "\n".join(results)
149
 
150
  except Exception as e:
151
  return f"Error fetching news for {ticker}: {str(e)}"
 
1
  from ddgs import DDGS
2
  from transformers import pipeline
3
  import torch
4
+ from curl_cffi import requests
5
  from bs4 import BeautifulSoup
6
+ import yfinance as yf
7
+ import datetime
8
+ import time
9
  from typing import Optional
10
+ from pydantic import BaseModel, Field
11
+ import concurrent.futures
12
 
13
  # Global variable for lazy loading
14
  _sentiment_pipeline = None
 
29
  return None
30
  return _sentiment_pipeline
31
 
32
+ def _fetch_page_content(url: str, timeout: int = 15) -> Optional[str]:
33
  """Fetch and extract text content from a web page."""
34
+ print(f"[DEBUG] Fetching: {url}")
35
+ start_time = time.time()
36
  try:
37
+ # Use curl_cffi to impersonate Chrome 110 (Bypasses TLS Fingerprinting)
38
+ # Headers are auto-managed by impersonate
39
+ response = requests.get(url, timeout=timeout, impersonate="chrome110")
 
 
 
 
 
40
  response.raise_for_status()
41
+
42
  soup = BeautifulSoup(response.content, 'html.parser')
43
+
44
+ # Remove ads, popups, and non-content elements
45
+ # Targeted classes: .ad, .popup, .modal, .cookie-banner, etc.
46
+ for tag in soup.select("script, style, nav, footer, header, aside, form, iframe, .ad, .popup, .modal, .cookie-banner, [id*='popup'], [class*='popup'], [class*='ad-'], [class*='banner']"):
47
  tag.decompose()
48
+
49
  # Extract text
50
+ text = soup.get_text(separator=' ', strip=True)
51
+ duration = round(time.time() - start_time, 2)
52
+ print(f"[DEBUG] Fetch success ({duration}s): {url}")
 
 
 
 
53
  return text
54
  except Exception as e:
55
+ msg = str(e)
56
+ if "403" in msg:
57
+ print(f"[INFO] Access denied (403) for {url}. Falling back to snippet.")
58
+ else:
59
+ print(f"[WARNING] Failed to fetch content from {url}: {msg}")
60
  return None
61
 
 
 
 
 
62
  # Validation Model
63
  class NewsArticle(BaseModel):
64
  title: str = Field(..., description="The headline of the news article.")
 
78
  return "No ticker provided for news search."
79
 
80
  ticker = ticker.upper().strip()
81
+ articles_pool = []
82
 
83
+ # 1. Fetch from Yahoo Finance API (Reliable)
84
+ try:
85
+ print("[DEBUG] Fetching YF API news...")
86
+ yf_ticker = yf.Ticker(ticker)
87
+ yf_raw = yf_ticker.news
88
+ if yf_raw:
89
+ for item in yf_raw:
90
+ ts = item.get('providerPublishTime')
91
+ date_str = datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d') if ts else 'Unknown'
92
+ articles_pool.append(NewsArticle(
93
+ title=item.get('title', 'No Title'),
94
+ link=item.get('link', ''),
95
+ snippet=f"Source: {item.get('publisher')} - {date_str}",
96
+ datetime=date_str
97
+ ))
98
+ except Exception as e:
99
+ print(f"[WARNING] YF API failed: {e}")
100
+
101
+ # 2. Add DuckDuckGo Targeted Search (Secondary)
102
+ # Targeted sites: CNBC, Bloomberg, Investing.com, MarketWatch
103
+ query = f"{ticker} stock news (site:cnbc.com OR site:bloomberg.com OR site:investing.com OR site:marketwatch.com)"
104
 
105
  with DDGS() as ddgs:
106
+ raw_results = list(ddgs.news(query, max_results=10))
107
+ for raw_item in raw_results:
 
 
 
 
 
 
108
  try:
109
+ articles_pool.append(NewsArticle(
 
 
 
110
  title=raw_item.get('title', 'No Title'),
111
  link=raw_item.get('url', ''),
112
  snippet=raw_item.get('body', ''),
113
+ datetime=raw_item.get('date', 'Unknown')
114
+ ))
115
+ except: continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ if not articles_pool:
118
+ return f"No recent news found for {ticker}."
119
+
120
+ # Deduplicate by Title
121
+ seen_titles = set()
122
+ unique_articles = []
123
+ for a in articles_pool:
124
+ if a.title not in seen_titles:
125
+ seen_titles.add(a.title)
126
+ unique_articles.append(a)
127
 
128
+ # Analyze Top 5
129
+ top_articles = unique_articles[:5]
130
+ sentiment_pipe = get_sentiment_pipeline()
131
+
132
+ results = []
133
+
134
+ # Helper to process
135
+ def process_article(article):
136
+ # FinBERT Analysis
137
+ sentiment_tag = ""
138
+ if sentiment_pipe:
139
+ try:
140
+ # Prefer full text fetch, fallback to Snippet
141
+ content = article.snippet if article.snippet else article.title
142
+ analysis_type = "Snippet" if article.snippet else "Headline"
143
+
144
+
145
+ # Try fetch full text with improved headers
146
+ if article.link:
147
+ full_text = _fetch_page_content(article.link)
148
+ if full_text and len(full_text) > 100:
149
+ content = full_text
150
+ analysis_type = "Full Text"
151
+ else:
152
+ # Fallback log
153
+ print(f"[DEBUG] Content too short/failed for {article.title[:30]}... using Snippet.")
154
+
155
+ # Truncate for BERT
156
+ score = sentiment_pipe(content[:2000])[0]
157
+ label = score['label']
158
+ conf = round(score['score'], 2)
159
+ sentiment_tag = f" [FinBERT ({analysis_type}): {label} ({conf})]"
160
+ except Exception as e:
161
+ sentiment_tag = f" [FinBERT: Error]"
162
+
163
+ return f"- [{article.datetime}] {article.title}{sentiment_tag}"
164
+
165
+ with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
166
+ results = list(executor.map(process_article, top_articles))
167
 
168
+ return f"Recent News for {ticker} (Sources: YF, CNBC, Bloomberg, Investing):\n" + "\n".join(results)
169
 
170
  except Exception as e:
171
  return f"Error fetching news for {ticker}: {str(e)}"
src/market-analyst/frontend/src/App.vue CHANGED
@@ -211,22 +211,31 @@ const analyzeTicker = (symbol) => {
211
  scrollToBottom()
212
  }
213
 
214
- // If RiskManager output, try to parse JSON for summary
215
  if (data.source === 'RiskManager') {
216
- const jsonMatch = data.content.match(/```json\s*([\s\S]*?)\s*```/) || data.content.match(/\{[\s\S]*"final_decision"[\s\S]*\}/)
217
- if (jsonMatch) {
218
- try {
219
- const rawJson = jsonMatch[1] || jsonMatch[0]
220
- const parsed = JSON.parse(rawJson)
221
-
222
  pendingResult.value = {
223
  ticker: symbol,
224
  model: providers.find(p => p.id === provider.value)?.name || provider.value,
225
- ...parsed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  }
227
- } catch (e) {
228
- console.log("Failed to parse result JSON", e)
229
- }
230
  }
231
  }
232
  } catch (e) {
@@ -785,6 +794,16 @@ onMounted(() => {
785
  color: var(--text-primary);
786
  }
787
 
 
 
 
 
 
 
 
 
 
 
788
  .log-viewport {
789
  flex: 1;
790
  overflow-y: auto;
 
211
  scrollToBottom()
212
  }
213
 
214
+ // Use backend-provided structured result if available (highly recommended for Groq/Llama)
215
  if (data.source === 'RiskManager') {
216
+ if (data.structured_result) {
217
+ console.log("Using backend-provided structured result");
 
 
 
 
218
  pendingResult.value = {
219
  ticker: symbol,
220
  model: providers.find(p => p.id === provider.value)?.name || provider.value,
221
+ ...data.structured_result
222
+ }
223
+ } else {
224
+ // Fallback to local regex if structured_result is missing
225
+ const jsonMatch = data.content.match(/```json\s*([\s\S]*?)\s*```/) || data.content.match(/\{[\s\S]*"final_decision"[\s\S]*\}/)
226
+ if (jsonMatch) {
227
+ try {
228
+ const rawJson = jsonMatch[1] || jsonMatch[0]
229
+ const parsed = JSON.parse(rawJson)
230
+ pendingResult.value = {
231
+ ticker: symbol,
232
+ model: providers.find(p => p.id === provider.value)?.name || provider.value,
233
+ ...parsed
234
+ }
235
+ } catch (e) {
236
+ console.log("Failed to parse result JSON", e)
237
+ }
238
  }
 
 
 
239
  }
240
  }
241
  } catch (e) {
 
794
  color: var(--text-primary);
795
  }
796
 
797
+ .header-hint {
798
+ font-size: 11px;
799
+ color: var(--text-tertiary);
800
+ background: rgba(255, 255, 255, 0.05);
801
+ padding: 2px 8px;
802
+ border-radius: 4px;
803
+ text-transform: uppercase;
804
+ letter-spacing: 0.5px;
805
+ }
806
+
807
  .log-viewport {
808
  flex: 1;
809
  overflow-y: auto;