Spaces:
Runtime error
Runtime error
Matthew-Anyiam commited on
Commit ·
a0cc0bb
1
Parent(s): 559cfa3
Add comprehensive Equity Research agent
Browse files- app.py +123 -93
- 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
|
| 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 |
-
|
| 25 |
-
result = health.compare(tickers[:5])
|
| 26 |
-
return f"```json\n{result}\n```"
|
| 27 |
|
| 28 |
-
def analyze_prices(ticker
|
| 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 |
-
|
| 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"
|
| 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
|
| 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')}%
|
| 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
|
| 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"
|
| 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
|
| 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['
|
| 110 |
-
|
| 111 |
return output
|
| 112 |
|
| 113 |
-
def analyze_post_earnings(ticker
|
| 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
|
| 123 |
-
output +=
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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 |
+
}
|