Matthew-Anyiam commited on
Commit
a0cc0bb
·
1 Parent(s): 559cfa3

Add comprehensive Equity Research agent

Browse files
Files changed (2) hide show
  1. app.py +123 -93
  2. eugene/agents/equity_research.py +527 -0
app.py CHANGED
@@ -1,134 +1,161 @@
1
  """
2
  Eugene Intelligence — Gradio Web App
3
- Financial data infrastructure for AI agents and humans.
4
  """
5
 
6
  import gradio as gr
7
  from eugene.config import Config
8
  from eugene.agents.health import HealthMonitor
9
  from eugene.agents.earnings import EarningsAgent
 
10
  from eugene.sources.yahoo import get_stock_prices, get_earnings_data
11
  from eugene.sources.insider import get_insider_transactions
12
 
13
  config = Config()
14
  health = HealthMonitor(config)
15
  earnings_agent = EarningsAgent(config)
 
16
 
17
- def analyze_fundamentals(tickers_input: str) -> str:
18
- if not tickers_input.strip():
19
- return "Enter a ticker symbol (e.g., AAPL)"
20
  tickers = [t.strip().upper() for t in tickers_input.split(",")]
21
  if len(tickers) == 1:
22
  report = health.analyze(tickers[0])
23
  return f"## {report.company_name} ({report.ticker})\n\n" + "\n".join([f"**{k}:** {v}" for k, v in report.to_dict().get("metrics", {}).items()])
24
- else:
25
- result = health.compare(tickers[:5])
26
- return f"```json\n{result}\n```"
27
 
28
- def analyze_prices(ticker: str, period: str = "1y") -> str:
29
- if not ticker.strip():
30
- return "Enter a ticker symbol"
31
  data = get_stock_prices(ticker.strip().upper(), period, "1d")
32
- if "error" in data:
33
- return f"❌ {data['error']}"
34
-
35
- quote = data.get("current_quote", {})
36
- stats = data.get("summary_stats", {})
37
-
38
  output = f"## {data.get('company_name', ticker)} ({data['ticker']})\n"
39
  output += f"**Price:** ${quote.get('price', 'N/A')} | **P/E:** {quote.get('pe_ratio', 'N/A')} | **Beta:** {quote.get('beta', 'N/A')}\n\n"
40
- output += f"### Performance ({period})\n"
41
- output += f"- **Return:** {stats.get('total_return_pct', 'N/A')}%\n"
42
- output += f"- **52W High:** ${stats.get('52_week_high', 'N/A')} | **52W Low:** ${stats.get('52_week_low', 'N/A')}\n"
43
- output += f"- **50 SMA:** ${stats.get('sma_50', 'N/A')} | **200 SMA:** ${stats.get('sma_200', 'N/A')}\n"
44
-
45
- prices = data.get("prices", [])[-10:]
46
- if prices:
47
- output += "\n### Recent Prices\n| Date | Close | Volume |\n|------|-------|--------|\n"
48
- for p in reversed(prices):
49
- output += f"| {p['date']} | ${p['close']} | {p['volume']:,} |\n"
50
  return output
51
 
52
- def analyze_earnings(ticker: str) -> str:
53
- if not ticker.strip():
54
- return "Enter a ticker symbol"
55
  data = get_earnings_data(ticker.strip().upper())
56
- if "error" in data:
57
- return f"❌ {data['error']}"
58
-
59
  track = data.get("track_record", {})
60
  output = f"## {data.get('company_name', ticker)} — Earnings\n\n"
61
- output += f"**Beat Rate:** {track.get('beat_rate_pct', 'N/A')}% ({track.get('beats', 0)} beats, {track.get('misses', 0)} misses)\n\n"
62
-
63
- if data.get("next_earnings"):
64
- output += f"**Next Earnings:** {data['next_earnings'].get('date', 'TBD')}\n\n"
65
-
66
- hist = [e for e in data.get("earnings_history", []) if e.get("result") in ("beat", "miss", "inline")][:8]
67
- if hist:
68
- output += "### History\n| Date | EPS Est | EPS Actual | Result |\n|------|---------|------------|--------|\n"
69
- for e in hist:
70
- emoji = "✅" if e["result"] == "beat" else "❌" if e["result"] == "miss" else "➖"
71
- output += f"| {e['date']} | ${e.get('eps_estimate', 'N/A')} | ${e.get('eps_actual', 'N/A')} | {emoji} |\n"
72
  return output
73
 
74
- def analyze_insider(ticker: str, days: int = 365) -> str:
75
- if not ticker.strip():
76
- return "Enter a ticker symbol"
77
  data = get_insider_transactions(ticker.strip().upper(), int(days))
78
- if "error" in data:
79
- return f"❌ {data['error']}"
80
-
81
  s = data.get("summary", {})
82
  signal = s.get("signal", "neutral")
83
  emoji = "🟢" if signal == "net_buying" else "🔴" if signal == "net_selling" else "⚪"
84
-
85
  output = f"## {data.get('company_name', ticker)} — Insider Activity\n\n"
86
  output += f"### Signal: {emoji} {signal.upper()}\n\n"
87
- output += f"- **Buys:** {s.get('total_buys', 0)} (${s.get('total_buy_value', 0):,.0f})\n"
88
- output += f"- **Sells:** {s.get('total_sells', 0)} (${s.get('total_sell_value', 0):,.0f})\n"
89
- output += f"- **Net:** ${s.get('net_value', 0):,.0f}\n\n"
90
-
91
- txns = data.get("transactions", [])[:10]
92
- if txns:
93
- output += "### Recent Transactions\n| Date | Insider | Type | Value |\n|------|---------|------|-------|\n"
94
- for t in txns:
95
- val = f"${t['total_value']:,.0f}" if t.get('total_value') else "—"
96
- output += f"| {t.get('date', '—')} | {t.get('owner_name', '—')[:20]} | {t.get('transaction_type', '—')[:15]} | {val} |\n"
97
  return output
98
 
99
- def analyze_earnings_calendar(tickers_input: str) -> str:
100
- if not tickers_input.strip():
101
- return "Enter ticker symbols (e.g., AAPL, NVDA, TSLA)"
102
  tickers = [t.strip().upper() for t in tickers_input.split(",")]
103
  data = earnings_agent.get_earnings_calendar(tickers)
104
-
105
- output = f"## Earnings Calendar\n*{data['upcoming_earnings']} upcoming from {data['tickers_checked']} tickers*\n\n"
106
- output += "| Ticker | Company | Date | Days | Beat Rate |\n|--------|---------|------|------|----------|\n"
107
-
108
  for e in data.get("calendar", []):
109
- output += f"| {e['ticker']} | {e['company_name'][:20]} | {e['earnings_date']} | {e['days_until']} | {e.get('beat_rate_pct', 'N/A')}% |\n"
110
-
111
  return output
112
 
113
- def analyze_post_earnings(ticker: str) -> str:
114
- if not ticker.strip():
115
- return "Enter a ticker symbol"
116
  data = earnings_agent.analyze_post_earnings_moves(ticker.strip().upper(), 8)
117
- if "error" in data:
118
- return f"❌ {data['error']}"
119
-
120
  s = data.get("summary", {})
121
  output = f"## {data.get('company_name', ticker)} — Post-Earnings Moves\n\n"
122
- output += f"**Avg Move on Beat:** {s.get('avg_move_on_beat_pct', 'N/A')}%\n"
123
- output += f"**Avg Move on Miss:** {s.get('avg_move_on_miss_pct', 'N/A')}%\n"
124
- output += f"**Beat Rate:** {s.get('beat_rate_pct', 'N/A')}%\n\n"
125
-
126
- hist = data.get("history", [])[:8]
127
- if hist:
128
- output += "### Recent Earnings Reactions\n| Date | Result | EPS | 1D Move | 5D Move |\n|------|--------|-----|---------|--------|\n"
129
- for h in hist:
130
- result_emoji = "✅" if h["result"] == "beat" else "❌" if h["result"] == "miss" else "➖"
131
- output += f"| {h['date']} | {result_emoji} | ${h.get('eps_actual', 'N/A')} | {h.get('move_1d_pct', 'N/A')}% | {h.get('move_5d_pct', 'N/A')}% |\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  return output
134
 
@@ -136,15 +163,22 @@ with gr.Blocks(title="Eugene Intelligence") as demo:
136
  gr.Markdown("# 🏛️ Eugene Intelligence\n**Financial data infrastructure for AI agents.**")
137
 
138
  with gr.Tabs():
 
 
 
 
 
 
 
 
 
139
  with gr.TabItem("📊 Fundamentals"):
140
- gr.Markdown("SEC XBRL financial metrics")
141
  f_input = gr.Textbox(label="Ticker(s)", placeholder="AAPL or JPM, BAC, GS")
142
  f_btn = gr.Button("Analyze", variant="primary")
143
  f_output = gr.Markdown()
144
  f_btn.click(analyze_fundamentals, inputs=f_input, outputs=f_output)
145
 
146
  with gr.TabItem("📈 Stock Prices"):
147
- gr.Markdown("20+ years of price history")
148
  with gr.Row():
149
  p_ticker = gr.Textbox(label="Ticker", placeholder="AAPL")
150
  p_period = gr.Dropdown(choices=["1mo", "3mo", "6mo", "1y", "2y", "5y", "max"], value="1y", label="Period")
@@ -153,32 +187,28 @@ with gr.Blocks(title="Eugene Intelligence") as demo:
153
  p_btn.click(analyze_prices, inputs=[p_ticker, p_period], outputs=p_output)
154
 
155
  with gr.TabItem("💰 Earnings"):
156
- gr.Markdown("EPS beat/miss history")
157
  e_input = gr.Textbox(label="Ticker", placeholder="NVDA")
158
  e_btn = gr.Button("Get Earnings", variant="primary")
159
  e_output = gr.Markdown()
160
  e_btn.click(analyze_earnings, inputs=e_input, outputs=e_output)
161
 
162
  with gr.TabItem("📅 Earnings Calendar"):
163
- gr.Markdown("Upcoming earnings for your watchlist")
164
  cal_input = gr.Textbox(label="Tickers", placeholder="AAPL, NVDA, TSLA, META, GOOGL")
165
  cal_btn = gr.Button("Get Calendar", variant="primary")
166
  cal_output = gr.Markdown()
167
  cal_btn.click(analyze_earnings_calendar, inputs=cal_input, outputs=cal_output)
168
 
169
  with gr.TabItem("📉 Post-Earnings Moves"):
170
- gr.Markdown("How does the stock move after earnings?")
171
  pe_input = gr.Textbox(label="Ticker", placeholder="NVDA")
172
- pe_btn = gr.Button("Analyze Moves", variant="primary")
173
  pe_output = gr.Markdown()
174
  pe_btn.click(analyze_post_earnings, inputs=pe_input, outputs=pe_output)
175
 
176
  with gr.TabItem("🕵️ Insider Trades"):
177
- gr.Markdown("SEC Form 4 insider activity")
178
  with gr.Row():
179
  i_ticker = gr.Textbox(label="Ticker", placeholder="TSLA")
180
  i_days = gr.Slider(minimum=30, maximum=730, value=365, label="Days Back")
181
- i_btn = gr.Button("Get Insider Activity", variant="primary")
182
  i_output = gr.Markdown()
183
  i_btn.click(analyze_insider, inputs=[i_ticker, i_days], outputs=i_output)
184
 
 
1
  """
2
  Eugene Intelligence — Gradio Web App
 
3
  """
4
 
5
  import gradio as gr
6
  from eugene.config import Config
7
  from eugene.agents.health import HealthMonitor
8
  from eugene.agents.earnings import EarningsAgent
9
+ from eugene.agents.equity_research import EquityResearchAgent
10
  from eugene.sources.yahoo import get_stock_prices, get_earnings_data
11
  from eugene.sources.insider import get_insider_transactions
12
 
13
  config = Config()
14
  health = HealthMonitor(config)
15
  earnings_agent = EarningsAgent(config)
16
+ equity_agent = EquityResearchAgent(config)
17
 
18
+ def analyze_fundamentals(tickers_input):
19
+ if not tickers_input.strip(): return "Enter a ticker"
 
20
  tickers = [t.strip().upper() for t in tickers_input.split(",")]
21
  if len(tickers) == 1:
22
  report = health.analyze(tickers[0])
23
  return f"## {report.company_name} ({report.ticker})\n\n" + "\n".join([f"**{k}:** {v}" for k, v in report.to_dict().get("metrics", {}).items()])
24
+ return str(health.compare(tickers[:5]))
 
 
25
 
26
+ def analyze_prices(ticker, period="1y"):
27
+ if not ticker.strip(): return "Enter a ticker"
 
28
  data = get_stock_prices(ticker.strip().upper(), period, "1d")
29
+ if "error" in data: return f"❌ {data['error']}"
30
+ quote, stats = data.get("current_quote", {}), data.get("summary_stats", {})
 
 
 
 
31
  output = f"## {data.get('company_name', ticker)} ({data['ticker']})\n"
32
  output += f"**Price:** ${quote.get('price', 'N/A')} | **P/E:** {quote.get('pe_ratio', 'N/A')} | **Beta:** {quote.get('beta', 'N/A')}\n\n"
33
+ output += f"**Return:** {stats.get('total_return_pct', 'N/A')}% | **52W:** ${stats.get('52_week_low', 'N/A')} - ${stats.get('52_week_high', 'N/A')}\n"
 
 
 
 
 
 
 
 
 
34
  return output
35
 
36
+ def analyze_earnings(ticker):
37
+ if not ticker.strip(): return "Enter a ticker"
 
38
  data = get_earnings_data(ticker.strip().upper())
39
+ if "error" in data: return f"❌ {data['error']}"
 
 
40
  track = data.get("track_record", {})
41
  output = f"## {data.get('company_name', ticker)} — Earnings\n\n"
42
+ output += f"**Beat Rate:** {track.get('beat_rate_pct', 'N/A')}% | **Next:** {data.get('next_earnings', {}).get('date', 'TBD')}\n"
 
 
 
 
 
 
 
 
 
 
43
  return output
44
 
45
+ def analyze_insider(ticker, days=365):
46
+ if not ticker.strip(): return "Enter a ticker"
 
47
  data = get_insider_transactions(ticker.strip().upper(), int(days))
48
+ if "error" in data: return f"❌ {data['error']}"
 
 
49
  s = data.get("summary", {})
50
  signal = s.get("signal", "neutral")
51
  emoji = "🟢" if signal == "net_buying" else "🔴" if signal == "net_selling" else "⚪"
 
52
  output = f"## {data.get('company_name', ticker)} — Insider Activity\n\n"
53
  output += f"### Signal: {emoji} {signal.upper()}\n\n"
54
+ output += f"**Buys:** {s.get('total_buys', 0)} (${s.get('total_buy_value', 0):,.0f}) | **Sells:** {s.get('total_sells', 0)} (${s.get('total_sell_value', 0):,.0f})\n"
 
 
 
 
 
 
 
 
 
55
  return output
56
 
57
+ def analyze_earnings_calendar(tickers_input):
58
+ if not tickers_input.strip(): return "Enter tickers"
 
59
  tickers = [t.strip().upper() for t in tickers_input.split(",")]
60
  data = earnings_agent.get_earnings_calendar(tickers)
61
+ output = f"## Earnings Calendar\n\n| Ticker | Date | Days | Beat Rate |\n|--------|------|------|----------|\n"
 
 
 
62
  for e in data.get("calendar", []):
63
+ output += f"| {e['ticker']} | {e['earnings_date']} | {e['days_until']} | {e.get('beat_rate_pct', 'N/A')}% |\n"
 
64
  return output
65
 
66
+ def analyze_post_earnings(ticker):
67
+ if not ticker.strip(): return "Enter a ticker"
 
68
  data = earnings_agent.analyze_post_earnings_moves(ticker.strip().upper(), 8)
69
+ if "error" in data: return f"❌ {data['error']}"
 
 
70
  s = data.get("summary", {})
71
  output = f"## {data.get('company_name', ticker)} — Post-Earnings Moves\n\n"
72
+ output += f"**Avg on Beat:** {s.get('avg_move_on_beat_pct', 'N/A')}% | **Avg on Miss:** {s.get('avg_move_on_miss_pct', 'N/A')}%\n\n"
73
+ output += "| Date | Result | 1D Move | 5D Move |\n|------|--------|---------|--------|\n"
74
+ for h in data.get("history", [])[:6]:
75
+ emoji = "✅" if h["result"] == "beat" else "❌" if h["result"] == "miss" else "➖"
76
+ output += f"| {h['date']} | {emoji} | {h.get('move_1d_pct', 'N/A')}% | {h.get('move_5d_pct', 'N/A')}% |\n"
77
+ return output
78
+
79
+ def generate_equity_report(ticker, peers_input=""):
80
+ if not ticker.strip(): return "Enter a ticker"
81
+ peers = [p.strip().upper() for p in peers_input.split(",") if p.strip()] if peers_input else []
82
+ report = equity_agent.generate_report(ticker.strip().upper(), peers)
83
+
84
+ o = report.get("overview", {})
85
+ fh = report.get("financial_health", {})
86
+ prof = fh.get("profitability", {})
87
+ lev = fh.get("leverage", {})
88
+ perf = report.get("price_performance", {})
89
+ val = report.get("valuation", {})
90
+ earn = report.get("earnings", {})
91
+ ins = report.get("insider_activity", {})
92
+ risk = report.get("risks", {})
93
+ thesis = report.get("thesis", {})
94
+
95
+ output = f"# {o.get('company_name', ticker)} ({report['ticker']}) — Equity Research\n\n"
96
+ output += f"*Generated: {report['generated_at']}*\n\n"
97
+
98
+ # Overview
99
+ mcap = o.get('market_cap')
100
+ mcap_str = f"${mcap/1e9:.1f}B" if mcap and mcap > 1e9 else f"${mcap/1e6:.0f}M" if mcap else "N/A"
101
+ output += f"**Market Cap:** {mcap_str} | **Price:** ${o.get('current_price', 'N/A')}\n\n"
102
+
103
+ # Financial Health
104
+ output += "## Financial Health\n"
105
+ output += f"| Metric | Value |\n|--------|-------|\n"
106
+ output += f"| ROE | {prof.get('roe_pct', 'N/A')}% |\n"
107
+ output += f"| ROA | {prof.get('roa_pct', 'N/A')}% |\n"
108
+ output += f"| Net Margin | {prof.get('net_margin_pct', 'N/A')}% |\n"
109
+ output += f"| Debt/Equity | {lev.get('debt_to_equity', 'N/A')}x |\n\n"
110
+
111
+ # Price Performance
112
+ output += "## Price Performance\n"
113
+ output += f"| 1M | 3M | 6M | 1Y | 5Y |\n|----|----|----|----|----|\n"
114
+ output += f"| {perf.get('return_1M', 'N/A')}% | {perf.get('return_3M', 'N/A')}% | {perf.get('return_6M', 'N/A')}% | {perf.get('return_1Y', 'N/A')}% | {perf.get('return_5Y', 'N/A')}% |\n\n"
115
+ output += f"**52W Range:** ${perf.get('52_week_low', 'N/A')} - ${perf.get('52_week_high', 'N/A')} | **Beta:** {perf.get('beta', 'N/A')}\n\n"
116
+
117
+ # Valuation
118
+ output += "## Valuation\n"
119
+ output += f"**P/E:** {val.get('pe_ratio', 'N/A')}x | **Fwd P/E:** {val.get('forward_pe', 'N/A')}x | **P/S:** {val.get('ps_ratio', 'N/A')}x | **Div Yield:** {val.get('dividend_yield_pct', 'N/A')}%\n\n"
120
+
121
+ # Earnings
122
+ output += "## Earnings Quality\n"
123
+ output += f"**Beat Rate:** {earn.get('beat_rate_pct', 'N/A')}% | **Next Earnings:** {earn.get('next_earnings', 'TBD')}\n"
124
+ output += f"**EPS Growth:** {earn.get('earnings_growth_pct', 'N/A')}% | **Rev Growth:** {earn.get('revenue_growth_pct', 'N/A')}%\n\n"
125
+
126
+ # Insider
127
+ signal = ins.get('signal', 'neutral')
128
+ emoji = "🟢" if signal == "net_buying" else "🔴" if signal == "net_selling" else "⚪"
129
+ output += f"## Insider Activity: {emoji} {signal.upper()}\n"
130
+ output += f"**Net Value:** ${ins.get('net_value', 0):,.0f} ({ins.get('total_buys', 0)} buys, {ins.get('total_sells', 0)} sells)\n\n"
131
+
132
+ # Risk
133
+ output += f"## Risk Assessment: {risk.get('risk_level', 'N/A')} (Score: {risk.get('risk_score', 0)})\n"
134
+ for r in risk.get("identified_risks", []):
135
+ output += f"- ⚠️ **{r['type']}:** {r['detail']}\n"
136
+ output += "\n"
137
+
138
+ # Thesis
139
+ output += "## Investment Thesis\n"
140
+ output += "### Bull Case 🐂\n"
141
+ for b in thesis.get("bull_case", []):
142
+ output += f"- {b}\n"
143
+ output += "\n### Bear Case 🐻\n"
144
+ for b in thesis.get("bear_case", []):
145
+ output += f"- {b}\n"
146
+ output += f"\n### Summary\n{thesis.get('summary', '')}\n"
147
+
148
+ # Peer comparison
149
+ if "peer_comparison" in report:
150
+ pc = report["peer_comparison"]
151
+ output += "\n## Peer Comparison\n"
152
+ output += "| Ticker | Mkt Cap | P/E | 1Y Ret | ROE | Margin |\n"
153
+ output += "|--------|---------|-----|--------|-----|--------|\n"
154
+ for t, m in pc.get("metrics", {}).items():
155
+ if "error" in m: continue
156
+ mc = m.get('market_cap')
157
+ mc_str = f"${mc/1e9:.0f}B" if mc and mc > 1e9 else "N/A"
158
+ output += f"| {t} | {mc_str} | {m.get('pe_ratio', 'N/A')}x | {m.get('return_1y_pct', 'N/A')}% | {m.get('roe_pct', 'N/A')}% | {m.get('net_margin_pct', 'N/A')}% |\n"
159
 
160
  return output
161
 
 
163
  gr.Markdown("# 🏛️ Eugene Intelligence\n**Financial data infrastructure for AI agents.**")
164
 
165
  with gr.Tabs():
166
+ with gr.TabItem("🔬 Equity Research"):
167
+ gr.Markdown("Comprehensive equity research report with financials, valuation, earnings, insider activity, risk assessment, and investment thesis.")
168
+ with gr.Row():
169
+ er_ticker = gr.Textbox(label="Ticker", placeholder="NVDA")
170
+ er_peers = gr.Textbox(label="Peers (optional)", placeholder="AMD, INTC")
171
+ er_btn = gr.Button("Generate Report", variant="primary")
172
+ er_output = gr.Markdown()
173
+ er_btn.click(generate_equity_report, inputs=[er_ticker, er_peers], outputs=er_output)
174
+
175
  with gr.TabItem("📊 Fundamentals"):
 
176
  f_input = gr.Textbox(label="Ticker(s)", placeholder="AAPL or JPM, BAC, GS")
177
  f_btn = gr.Button("Analyze", variant="primary")
178
  f_output = gr.Markdown()
179
  f_btn.click(analyze_fundamentals, inputs=f_input, outputs=f_output)
180
 
181
  with gr.TabItem("📈 Stock Prices"):
 
182
  with gr.Row():
183
  p_ticker = gr.Textbox(label="Ticker", placeholder="AAPL")
184
  p_period = gr.Dropdown(choices=["1mo", "3mo", "6mo", "1y", "2y", "5y", "max"], value="1y", label="Period")
 
187
  p_btn.click(analyze_prices, inputs=[p_ticker, p_period], outputs=p_output)
188
 
189
  with gr.TabItem("💰 Earnings"):
 
190
  e_input = gr.Textbox(label="Ticker", placeholder="NVDA")
191
  e_btn = gr.Button("Get Earnings", variant="primary")
192
  e_output = gr.Markdown()
193
  e_btn.click(analyze_earnings, inputs=e_input, outputs=e_output)
194
 
195
  with gr.TabItem("📅 Earnings Calendar"):
 
196
  cal_input = gr.Textbox(label="Tickers", placeholder="AAPL, NVDA, TSLA, META, GOOGL")
197
  cal_btn = gr.Button("Get Calendar", variant="primary")
198
  cal_output = gr.Markdown()
199
  cal_btn.click(analyze_earnings_calendar, inputs=cal_input, outputs=cal_output)
200
 
201
  with gr.TabItem("📉 Post-Earnings Moves"):
 
202
  pe_input = gr.Textbox(label="Ticker", placeholder="NVDA")
203
+ pe_btn = gr.Button("Analyze", variant="primary")
204
  pe_output = gr.Markdown()
205
  pe_btn.click(analyze_post_earnings, inputs=pe_input, outputs=pe_output)
206
 
207
  with gr.TabItem("🕵️ Insider Trades"):
 
208
  with gr.Row():
209
  i_ticker = gr.Textbox(label="Ticker", placeholder="TSLA")
210
  i_days = gr.Slider(minimum=30, maximum=730, value=365, label="Days Back")
211
+ i_btn = gr.Button("Get Activity", variant="primary")
212
  i_output = gr.Markdown()
213
  i_btn.click(analyze_insider, inputs=[i_ticker, i_days], outputs=i_output)
214
 
eugene/agents/equity_research.py ADDED
@@ -0,0 +1,527 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Eugene Intelligence — Equity Research Agent
3
+ Comprehensive equity research reports combining fundamentals,
4
+ price performance, earnings quality, insider activity, and peer analysis.
5
+ """
6
+
7
+ from datetime import datetime, timedelta
8
+ from typing import List, Optional, Dict
9
+ from eugene.sources.yahoo import get_stock_prices, get_earnings_data
10
+ from eugene.sources.insider import get_insider_transactions
11
+
12
+
13
+ class EquityResearchAgent:
14
+ """Comprehensive equity research agent."""
15
+
16
+ def __init__(self, config):
17
+ self.config = config
18
+ self._health = None
19
+ self._earnings = None
20
+
21
+ @property
22
+ def health(self):
23
+ if self._health is None:
24
+ from eugene.agents.health import HealthMonitor
25
+ self._health = HealthMonitor(self.config)
26
+ return self._health
27
+
28
+ @property
29
+ def earnings_agent(self):
30
+ if self._earnings is None:
31
+ from eugene.agents.earnings import EarningsAgent
32
+ self._earnings = EarningsAgent(self.config)
33
+ return self._earnings
34
+
35
+ def generate_report(self, ticker: str, peers: List[str] = None) -> dict:
36
+ """
37
+ Generate comprehensive equity research report.
38
+
39
+ Args:
40
+ ticker: Stock symbol
41
+ peers: Optional list of peer tickers for comparison
42
+
43
+ Returns:
44
+ Full equity research report
45
+ """
46
+ ticker = ticker.upper().strip()
47
+ peers = [p.upper().strip() for p in (peers or [])][:5]
48
+
49
+ report = {
50
+ "ticker": ticker,
51
+ "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
52
+ "source": "Eugene Intelligence",
53
+ }
54
+
55
+ # 1. Company Overview & Fundamentals
56
+ report["overview"] = self._get_overview(ticker)
57
+
58
+ # 2. Financial Health
59
+ report["financial_health"] = self._get_financial_health(ticker)
60
+
61
+ # 3. Growth Analysis
62
+ report["growth"] = self._get_growth_analysis(ticker)
63
+
64
+ # 4. Price Performance
65
+ report["price_performance"] = self._get_price_performance(ticker)
66
+
67
+ # 5. Valuation
68
+ report["valuation"] = self._get_valuation(ticker)
69
+
70
+ # 6. Earnings Quality
71
+ report["earnings"] = self._get_earnings_quality(ticker)
72
+
73
+ # 7. Insider Sentiment
74
+ report["insider_activity"] = self._get_insider_sentiment(ticker)
75
+
76
+ # 8. Peer Comparison
77
+ if peers:
78
+ report["peer_comparison"] = self._get_peer_comparison(ticker, peers)
79
+
80
+ # 9. Risk Assessment
81
+ report["risks"] = self._get_risk_assessment(report)
82
+
83
+ # 10. Investment Thesis
84
+ report["thesis"] = self._generate_thesis(report)
85
+
86
+ return report
87
+
88
+ def _get_overview(self, ticker: str) -> dict:
89
+ """Company overview from Yahoo + XBRL."""
90
+ prices = get_stock_prices(ticker, "1d", "1d")
91
+
92
+ overview = {
93
+ "company_name": prices.get("company_name", ticker),
94
+ "ticker": ticker,
95
+ "exchange": prices.get("exchange", ""),
96
+ "currency": prices.get("currency", "USD"),
97
+ }
98
+
99
+ quote = prices.get("current_quote", {})
100
+ overview["market_cap"] = quote.get("market_cap")
101
+ overview["current_price"] = quote.get("price")
102
+
103
+ # Get fundamentals for additional context
104
+ try:
105
+ health_data = self.health.analyze(ticker)
106
+ if hasattr(health_data, 'to_dict'):
107
+ metrics = health_data.to_dict().get("metrics", {})
108
+ overview["revenue"] = metrics.get("_revenue", {}).get("value")
109
+ overview["net_income"] = metrics.get("_net_income", {}).get("value")
110
+ overview["total_assets"] = metrics.get("_total_assets", {}).get("value")
111
+ except:
112
+ pass
113
+
114
+ return overview
115
+
116
+ def _get_financial_health(self, ticker: str) -> dict:
117
+ """Financial ratios and health metrics."""
118
+ try:
119
+ health_data = self.health.analyze(ticker)
120
+ if hasattr(health_data, 'to_dict'):
121
+ metrics = health_data.to_dict().get("metrics", {})
122
+
123
+ return {
124
+ "liquidity": {
125
+ "current_ratio": metrics.get("current_ratio", {}).get("value"),
126
+ },
127
+ "leverage": {
128
+ "debt_to_assets": metrics.get("debt_to_assets", {}).get("value"),
129
+ "debt_to_equity": metrics.get("debt_to_equity", {}).get("value"),
130
+ "net_debt_to_ebitda": metrics.get("net_debt_to_ebitda", {}).get("value"),
131
+ },
132
+ "profitability": {
133
+ "roe_pct": metrics.get("roe_pct", {}).get("value"),
134
+ "roa_pct": metrics.get("roa_pct", {}).get("value"),
135
+ "net_margin_pct": metrics.get("net_margin_pct", {}).get("value"),
136
+ },
137
+ "balance_sheet": {
138
+ "cash": metrics.get("_cash", {}).get("value"),
139
+ "total_debt": metrics.get("_total_debt", {}).get("value"),
140
+ "total_assets": metrics.get("_total_assets", {}).get("value"),
141
+ },
142
+ }
143
+ except Exception as e:
144
+ return {"error": str(e)}
145
+
146
+ return {}
147
+
148
+ def _get_growth_analysis(self, ticker: str) -> dict:
149
+ """5-year growth trends."""
150
+ try:
151
+ health_data = self.health.analyze(ticker, include_trends=True)
152
+ if hasattr(health_data, 'to_dict'):
153
+ data = health_data.to_dict()
154
+ trends = data.get("trends", {})
155
+
156
+ return {
157
+ "revenue_cagr_5y": trends.get("revenue_cagr"),
158
+ "net_income_cagr_5y": trends.get("net_income_cagr"),
159
+ "eps_cagr_5y": trends.get("eps_cagr"),
160
+ "asset_growth_5y": trends.get("asset_growth"),
161
+ "revenue_history": trends.get("revenue_history", [])[-5:],
162
+ "net_income_history": trends.get("net_income_history", [])[-5:],
163
+ }
164
+ except:
165
+ pass
166
+
167
+ # Fallback to Yahoo
168
+ earnings = get_earnings_data(ticker)
169
+ return {
170
+ "annual_financials": earnings.get("annual_financials", [])[:5],
171
+ "quarterly_revenue": earnings.get("quarterly_revenue", [])[:8],
172
+ }
173
+
174
+ def _get_price_performance(self, ticker: str) -> dict:
175
+ """Price performance and technicals."""
176
+ # Multiple timeframes
177
+ perf = {}
178
+
179
+ for period, label in [("1mo", "1M"), ("3mo", "3M"), ("6mo", "6M"), ("1y", "1Y"), ("5y", "5Y")]:
180
+ data = get_stock_prices(ticker, period, "1d")
181
+ if "error" not in data:
182
+ perf[f"return_{label}"] = data.get("summary_stats", {}).get("total_return_pct")
183
+
184
+ # Get current data
185
+ current = get_stock_prices(ticker, "1y", "1d")
186
+ if "error" not in current:
187
+ stats = current.get("summary_stats", {})
188
+ quote = current.get("current_quote", {})
189
+
190
+ perf["current_price"] = quote.get("price")
191
+ perf["52_week_high"] = stats.get("52_week_high")
192
+ perf["52_week_low"] = stats.get("52_week_low")
193
+ perf["sma_50"] = stats.get("sma_50")
194
+ perf["sma_200"] = stats.get("sma_200")
195
+ perf["beta"] = quote.get("beta")
196
+
197
+ # Price vs SMAs
198
+ price = quote.get("price", 0)
199
+ if stats.get("sma_50") and price:
200
+ perf["vs_sma_50_pct"] = round(((price - stats["sma_50"]) / stats["sma_50"]) * 100, 2)
201
+ if stats.get("sma_200") and price:
202
+ perf["vs_sma_200_pct"] = round(((price - stats["sma_200"]) / stats["sma_200"]) * 100, 2)
203
+
204
+ # 52W range position
205
+ high = stats.get("52_week_high", 0)
206
+ low = stats.get("52_week_low", 0)
207
+ if high and low and high != low:
208
+ perf["52w_range_position_pct"] = round(((price - low) / (high - low)) * 100, 2)
209
+
210
+ return perf
211
+
212
+ def _get_valuation(self, ticker: str) -> dict:
213
+ """Valuation metrics."""
214
+ prices = get_stock_prices(ticker, "1d", "1d")
215
+ quote = prices.get("current_quote", {})
216
+
217
+ valuation = {
218
+ "pe_ratio": quote.get("pe_ratio"),
219
+ "forward_pe": quote.get("forward_pe"),
220
+ "dividend_yield_pct": quote.get("dividend_yield"),
221
+ }
222
+
223
+ # Calculate P/S if we have data
224
+ market_cap = quote.get("market_cap")
225
+ try:
226
+ health_data = self.health.analyze(ticker)
227
+ if hasattr(health_data, 'to_dict'):
228
+ metrics = health_data.to_dict().get("metrics", {})
229
+ revenue = metrics.get("_revenue", {}).get("value")
230
+ if market_cap and revenue and revenue > 0:
231
+ valuation["ps_ratio"] = round(market_cap / revenue, 2)
232
+ except:
233
+ pass
234
+
235
+ return valuation
236
+
237
+ def _get_earnings_quality(self, ticker: str) -> dict:
238
+ """Earnings quality and predictability."""
239
+ earnings = get_earnings_data(ticker)
240
+ if "error" in earnings:
241
+ return earnings
242
+
243
+ track = earnings.get("track_record", {})
244
+
245
+ # Get post-earnings moves
246
+ try:
247
+ moves = self.earnings_agent.analyze_post_earnings_moves(ticker, 8)
248
+ post_moves = moves.get("summary", {})
249
+ except:
250
+ post_moves = {}
251
+
252
+ return {
253
+ "beat_rate_pct": track.get("beat_rate_pct"),
254
+ "total_beats": track.get("beats"),
255
+ "total_misses": track.get("misses"),
256
+ "next_earnings": earnings.get("next_earnings", {}).get("date"),
257
+ "trailing_eps": earnings.get("current_estimates", {}).get("trailing_eps"),
258
+ "forward_eps": earnings.get("current_estimates", {}).get("forward_eps"),
259
+ "earnings_growth_pct": earnings.get("current_estimates", {}).get("earnings_growth"),
260
+ "revenue_growth_pct": earnings.get("current_estimates", {}).get("revenue_growth"),
261
+ "peg_ratio": earnings.get("current_estimates", {}).get("peg_ratio"),
262
+ "avg_move_on_beat_pct": post_moves.get("avg_move_on_beat_pct"),
263
+ "avg_move_on_miss_pct": post_moves.get("avg_move_on_miss_pct"),
264
+ "recent_history": earnings.get("earnings_history", [])[:4],
265
+ }
266
+
267
+ def _get_insider_sentiment(self, ticker: str) -> dict:
268
+ """Insider trading activity and sentiment."""
269
+ insider = get_insider_transactions(ticker, days_back=180)
270
+ if "error" in insider:
271
+ return insider
272
+
273
+ s = insider.get("summary", {})
274
+
275
+ return {
276
+ "signal": s.get("signal"),
277
+ "total_buys": s.get("total_buys"),
278
+ "total_sells": s.get("total_sells"),
279
+ "total_buy_value": s.get("total_buy_value"),
280
+ "total_sell_value": s.get("total_sell_value"),
281
+ "net_value": s.get("net_value"),
282
+ "unique_buyers": s.get("unique_buyers", 0),
283
+ "unique_sellers": s.get("unique_sellers", 0),
284
+ "notable_transactions": insider.get("notable_transactions", [])[:3],
285
+ }
286
+
287
+ def _get_peer_comparison(self, ticker: str, peers: List[str]) -> dict:
288
+ """Side-by-side peer comparison."""
289
+ all_tickers = [ticker] + peers
290
+
291
+ comparison = {"tickers": all_tickers, "metrics": {}}
292
+
293
+ for t in all_tickers:
294
+ try:
295
+ # Get key metrics for each
296
+ prices = get_stock_prices(t, "1y", "1d")
297
+ quote = prices.get("current_quote", {})
298
+ stats = prices.get("summary_stats", {})
299
+
300
+ health_data = self.health.analyze(t)
301
+ metrics = {}
302
+ if hasattr(health_data, 'to_dict'):
303
+ metrics = health_data.to_dict().get("metrics", {})
304
+
305
+ comparison["metrics"][t] = {
306
+ "market_cap": quote.get("market_cap"),
307
+ "pe_ratio": quote.get("pe_ratio"),
308
+ "return_1y_pct": stats.get("total_return_pct"),
309
+ "roe_pct": metrics.get("roe_pct", {}).get("value"),
310
+ "net_margin_pct": metrics.get("net_margin_pct", {}).get("value"),
311
+ "debt_to_equity": metrics.get("debt_to_equity", {}).get("value"),
312
+ }
313
+ except:
314
+ comparison["metrics"][t] = {"error": "Could not retrieve data"}
315
+
316
+ return comparison
317
+
318
+ def _get_risk_assessment(self, report: dict) -> dict:
319
+ """Assess key risks based on collected data."""
320
+ risks = []
321
+ risk_score = 0
322
+
323
+ # Leverage risk
324
+ leverage = report.get("financial_health", {}).get("leverage", {})
325
+ debt_to_equity = leverage.get("debt_to_equity")
326
+ if debt_to_equity and debt_to_equity > 2:
327
+ risks.append({"type": "HIGH_LEVERAGE", "detail": f"Debt/Equity of {debt_to_equity}x is elevated"})
328
+ risk_score += 2
329
+ elif debt_to_equity and debt_to_equity > 1:
330
+ risks.append({"type": "MODERATE_LEVERAGE", "detail": f"Debt/Equity of {debt_to_equity}x"})
331
+ risk_score += 1
332
+
333
+ # Earnings volatility
334
+ earnings = report.get("earnings", {})
335
+ beat_rate = earnings.get("beat_rate_pct")
336
+ if beat_rate and beat_rate < 50:
337
+ risks.append({"type": "EARNINGS_VOLATILITY", "detail": f"Only {beat_rate}% beat rate"})
338
+ risk_score += 2
339
+
340
+ # Insider selling
341
+ insider = report.get("insider_activity", {})
342
+ if insider.get("signal") == "net_selling":
343
+ net_sell = insider.get("net_value", 0)
344
+ risks.append({"type": "INSIDER_SELLING", "detail": f"Net insider selling of ${abs(net_sell):,.0f}"})
345
+ risk_score += 1
346
+
347
+ # Valuation risk
348
+ valuation = report.get("valuation", {})
349
+ pe = valuation.get("pe_ratio")
350
+ if pe and pe > 50:
351
+ risks.append({"type": "HIGH_VALUATION", "detail": f"P/E of {pe}x is elevated"})
352
+ risk_score += 1
353
+
354
+ # Price momentum
355
+ perf = report.get("price_performance", {})
356
+ vs_sma_200 = perf.get("vs_sma_200_pct")
357
+ if vs_sma_200 and vs_sma_200 < -20:
358
+ risks.append({"type": "NEGATIVE_MOMENTUM", "detail": f"Trading {abs(vs_sma_200)}% below 200-day SMA"})
359
+ risk_score += 1
360
+
361
+ # Liquidity risk
362
+ liquidity = report.get("financial_health", {}).get("liquidity", {})
363
+ current_ratio = liquidity.get("current_ratio")
364
+ if current_ratio and current_ratio < 1:
365
+ risks.append({"type": "LIQUIDITY_CONCERN", "detail": f"Current ratio of {current_ratio}x below 1"})
366
+ risk_score += 2
367
+
368
+ return {
369
+ "risk_score": risk_score,
370
+ "risk_level": "HIGH" if risk_score >= 5 else "MODERATE" if risk_score >= 2 else "LOW",
371
+ "identified_risks": risks,
372
+ }
373
+
374
+ def _generate_thesis(self, report: dict) -> dict:
375
+ """Generate bull/bear thesis and key catalysts."""
376
+ bull_points = []
377
+ bear_points = []
378
+ catalysts = []
379
+
380
+ # Analyze the data for thesis points
381
+
382
+ # Profitability
383
+ prof = report.get("financial_health", {}).get("profitability", {})
384
+ roe = prof.get("roe_pct")
385
+ margin = prof.get("net_margin_pct")
386
+
387
+ if roe and roe > 15:
388
+ bull_points.append(f"Strong ROE of {roe}% indicates efficient capital deployment")
389
+ elif roe and roe < 10:
390
+ bear_points.append(f"Below-average ROE of {roe}%")
391
+
392
+ if margin and margin > 20:
393
+ bull_points.append(f"High profit margins of {margin}% provide cushion")
394
+ elif margin and margin < 5:
395
+ bear_points.append(f"Thin margins of {margin}% limit flexibility")
396
+
397
+ # Growth
398
+ growth = report.get("growth", {})
399
+ rev_cagr = growth.get("revenue_cagr_5y")
400
+ if rev_cagr and rev_cagr > 10:
401
+ bull_points.append(f"Strong revenue growth of {rev_cagr}% CAGR over 5 years")
402
+ elif rev_cagr and rev_cagr < 0:
403
+ bear_points.append(f"Declining revenue ({rev_cagr}% CAGR)")
404
+
405
+ # Earnings
406
+ earnings = report.get("earnings", {})
407
+ beat_rate = earnings.get("beat_rate_pct")
408
+ if beat_rate and beat_rate > 75:
409
+ bull_points.append(f"Consistent earnings beats ({beat_rate}% beat rate)")
410
+
411
+ earnings_growth = earnings.get("earnings_growth_pct")
412
+ if earnings_growth and earnings_growth > 15:
413
+ bull_points.append(f"Strong earnings growth outlook ({earnings_growth}%)")
414
+ elif earnings_growth and earnings_growth < 0:
415
+ bear_points.append(f"Negative earnings growth expected ({earnings_growth}%)")
416
+
417
+ # Insider activity
418
+ insider = report.get("insider_activity", {})
419
+ if insider.get("signal") == "net_buying":
420
+ bull_points.append("Insider buying signals management confidence")
421
+ elif insider.get("signal") == "net_selling":
422
+ bear_points.append("Notable insider selling activity")
423
+
424
+ # Valuation
425
+ valuation = report.get("valuation", {})
426
+ pe = valuation.get("pe_ratio")
427
+ forward_pe = valuation.get("forward_pe")
428
+ if pe and forward_pe and forward_pe < pe:
429
+ bull_points.append(f"Forward P/E ({forward_pe}x) below trailing ({pe}x) suggests growth")
430
+ if pe and pe > 40:
431
+ bear_points.append(f"Premium valuation at {pe}x P/E")
432
+
433
+ # Catalysts
434
+ next_earnings = earnings.get("next_earnings")
435
+ if next_earnings:
436
+ catalysts.append(f"Upcoming earnings: {next_earnings}")
437
+
438
+ # Technical
439
+ perf = report.get("price_performance", {})
440
+ if perf.get("vs_sma_200_pct") and perf["vs_sma_200_pct"] > 0:
441
+ catalysts.append("Trading above 200-day moving average (bullish trend)")
442
+
443
+ return {
444
+ "bull_case": bull_points[:5],
445
+ "bear_case": bear_points[:5],
446
+ "catalysts": catalysts[:3],
447
+ "summary": self._generate_summary(report, bull_points, bear_points),
448
+ }
449
+
450
+ def _generate_summary(self, report: dict, bull_points: list, bear_points: list) -> str:
451
+ """Generate one-paragraph summary."""
452
+ ticker = report.get("ticker", "")
453
+ name = report.get("overview", {}).get("company_name", ticker)
454
+
455
+ # Build summary based on data
456
+ parts = [f"{name} ({ticker})"]
457
+
458
+ # Market cap context
459
+ mcap = report.get("overview", {}).get("market_cap")
460
+ if mcap:
461
+ if mcap > 200e9:
462
+ parts.append("is a mega-cap company")
463
+ elif mcap > 10e9:
464
+ parts.append("is a large-cap company")
465
+ elif mcap > 2e9:
466
+ parts.append("is a mid-cap company")
467
+ else:
468
+ parts.append("is a small-cap company")
469
+
470
+ # Financial health
471
+ risk_level = report.get("risks", {}).get("risk_level", "MODERATE")
472
+ if risk_level == "LOW":
473
+ parts.append("with a solid financial profile.")
474
+ elif risk_level == "HIGH":
475
+ parts.append("with elevated risk factors.")
476
+ else:
477
+ parts.append("with a moderate risk profile.")
478
+
479
+ # Key metrics
480
+ roe = report.get("financial_health", {}).get("profitability", {}).get("roe_pct")
481
+ margin = report.get("financial_health", {}).get("profitability", {}).get("net_margin_pct")
482
+ if roe and margin:
483
+ parts.append(f"Profitability metrics show {roe}% ROE and {margin}% net margins.")
484
+
485
+ # Verdict hint
486
+ if len(bull_points) > len(bear_points) + 2:
487
+ parts.append("The overall picture skews positive.")
488
+ elif len(bear_points) > len(bull_points) + 2:
489
+ parts.append("Several concerns warrant caution.")
490
+ else:
491
+ parts.append("The investment case is balanced.")
492
+
493
+ return " ".join(parts)
494
+
495
+ def quick_screen(self, tickers: List[str]) -> dict:
496
+ """
497
+ Quick screening of multiple stocks.
498
+ Returns key metrics for comparison.
499
+ """
500
+ results = []
501
+
502
+ for ticker in tickers[:20]:
503
+ try:
504
+ prices = get_stock_prices(ticker, "1y", "1d")
505
+ quote = prices.get("current_quote", {})
506
+ stats = prices.get("summary_stats", {})
507
+
508
+ insider = get_insider_transactions(ticker, days_back=90)
509
+ insider_signal = insider.get("summary", {}).get("signal", "unknown")
510
+
511
+ results.append({
512
+ "ticker": ticker.upper(),
513
+ "price": quote.get("price"),
514
+ "market_cap": quote.get("market_cap"),
515
+ "pe_ratio": quote.get("pe_ratio"),
516
+ "return_1y_pct": stats.get("total_return_pct"),
517
+ "vs_sma_200_pct": None, # Would need calculation
518
+ "insider_signal": insider_signal,
519
+ })
520
+ except:
521
+ results.append({"ticker": ticker.upper(), "error": "Failed to retrieve"})
522
+
523
+ return {
524
+ "source": "Eugene Intelligence",
525
+ "tickers_screened": len(tickers),
526
+ "results": results,
527
+ }