File size: 11,557 Bytes
07ff2cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
"""
Fundamental Analyst Tools for FinAgent.

Provides tools for fundamental analysis agents:
- get_financials: Retrieve key financial metrics
- get_earnings: Retrieve earnings history with surprise calculations
- get_peers: Retrieve sector/industry peers
"""

import math

import yfinance
from crewai.tools import tool

from tools.cache import TTLCache
from tools.utils import validate_ticker, format_currency, format_percent, safe_get

cache = TTLCache()


@tool("Get Financials")
def get_financials(ticker: str) -> str:
    """Retrieve key financial metrics for a company including market cap, P/E ratio, revenue growth, profit margin, and debt-to-equity ratio."""
    try:
        # 1. Input validation
        valid, result = validate_ticker(ticker)
        if not valid:
            return result

        normalized_ticker = result

        # 2. Cache check
        cache_key = cache.make_key("get_financials", ticker=normalized_ticker)
        cached = cache.get(cache_key)
        if cached:
            return cached

        # 3. External API call
        info = yfinance.Ticker(normalized_ticker).info

        if not info or info.get("regularMarketPrice") is None and info.get("currentPrice") is None:
            return f"Error: Ticker '{normalized_ticker}' not found. Please verify the symbol."

        # 4. Format response — missing fields show "N/A", not an error
        market_cap = format_currency(info.get("marketCap"))
        pe_ratio = safe_get(info, "trailingPE")
        revenue_growth = format_percent(info.get("revenueGrowth"))
        profit_margin = format_percent(info.get("profitMargins"))
        debt_equity = safe_get(info, "debtToEquity")

        response = (
            f"Financial Metrics for {normalized_ticker}:\n"
            f"Market Cap: {market_cap}\n"
            f"P/E Ratio: {pe_ratio}\n"
            f"Revenue Growth: {revenue_growth}\n"
            f"Profit Margin: {profit_margin}\n"
            f"Debt/Equity: {debt_equity}"
        )

        # 5. Cache and return
        cache.set(cache_key, response)
        return response

    except Exception as e:
        return f"Error: An unexpected error occurred while processing {ticker}: {str(e)}"


@tool("Get Earnings")
def get_earnings(ticker: str) -> str:
    """Retrieve earnings history with surprise calculations for a given ticker.

    Args:
        ticker: Stock symbol (e.g., AAPL) or crypto pair (e.g., BTC-USD).

    Returns:
        A formatted string with the last 4 quarters of earnings data including
        reported EPS, estimated EPS, and surprise percentage, or an error message.
    """
    try:
        # 1. Input validation
        valid, result = validate_ticker(ticker)
        if not valid:
            return result

        normalized_ticker = result

        # 2. Detect crypto tickers (contain "-" like BTC-USD, ETH-USD)
        if "-" in normalized_ticker:
            return "Earnings data is not available for this instrument type."

        # 3. Cache check
        cache_key = cache.make_key("get_earnings", ticker=normalized_ticker)
        cached = cache.get(cache_key)
        if cached:
            return cached

        # 4. External API call
        stock = yfinance.Ticker(normalized_ticker)
        earnings_dates = stock.earnings_dates

        # 5. Check if earnings data is available
        if earnings_dates is None or earnings_dates.empty:
            return "Earnings data is not available for this instrument type."

        # 6. Filter to rows that have reported EPS (past earnings only)
        relevant_cols = ["EPS Estimate", "Reported EPS"]
        if not all(col in earnings_dates.columns for col in relevant_cols):
            return "Earnings data is not available for this instrument type."

        earnings_data = earnings_dates.dropna(subset=["Reported EPS"])

        if earnings_data.empty:
            return "Earnings data is not available for this instrument type."

        # Take the last 4 quarters (most recent first)
        earnings_data = earnings_data.head(4)

        # 7. Format response
        lines = [f"Earnings History for {normalized_ticker} (Last 4 Quarters):"]

        for date_idx, row in earnings_data.iterrows():
            reported_eps = row["Reported EPS"]
            estimated_eps = row.get("EPS Estimate")

            # Determine quarter label from the date index
            quarter_date = date_idx
            quarter_num = (quarter_date.month - 1) // 3 + 1
            quarter_label = f"Q{quarter_num} {quarter_date.year}"

            # Calculate surprise percentage
            if estimated_eps is not None and estimated_eps != 0:
                if not math.isnan(estimated_eps):
                    surprise = round(((reported_eps - estimated_eps) / abs(estimated_eps)) * 100, 2)
                    surprise_str = f"+{surprise:.2f}%" if surprise >= 0 else f"{surprise:.2f}%"
                    lines.append(
                        f"{quarter_label}: EPS ${reported_eps:.2f} "
                        f"(Est: ${estimated_eps:.2f}) | Surprise: {surprise_str}"
                    )
                else:
                    lines.append(
                        f"{quarter_label}: EPS ${reported_eps:.2f} "
                        f"(Est: N/A) | Surprise: N/A"
                    )
            else:
                lines.append(
                    f"{quarter_label}: EPS ${reported_eps:.2f} "
                    f"(Est: N/A) | Surprise: N/A"
                )

        response = "\n".join(lines)

        # 8. Cache and return
        cache.set(cache_key, response)
        return response

    except Exception as e:
        return f"Error: An unexpected error occurred while processing {ticker}: {str(e)}"


# Sector-to-peers mapping for common sectors
SECTOR_PEERS = {
    "Technology": [
        ("MSFT", "Microsoft Corporation"),
        ("AAPL", "Apple Inc."),
        ("GOOGL", "Alphabet Inc."),
        ("NVDA", "NVIDIA Corporation"),
        ("META", "Meta Platforms Inc."),
        ("AMZN", "Amazon.com Inc."),
        ("CRM", "Salesforce Inc."),
        ("ADBE", "Adobe Inc."),
        ("ORCL", "Oracle Corporation"),
        ("INTC", "Intel Corporation"),
    ],
    "Healthcare": [
        ("JNJ", "Johnson & Johnson"),
        ("UNH", "UnitedHealth Group Inc."),
        ("PFE", "Pfizer Inc."),
        ("ABBV", "AbbVie Inc."),
        ("MRK", "Merck & Co. Inc."),
        ("LLY", "Eli Lilly and Company"),
        ("TMO", "Thermo Fisher Scientific Inc."),
        ("ABT", "Abbott Laboratories"),
    ],
    "Financial Services": [
        ("JPM", "JPMorgan Chase & Co."),
        ("BAC", "Bank of America Corporation"),
        ("GS", "Goldman Sachs Group Inc."),
        ("MS", "Morgan Stanley"),
        ("WFC", "Wells Fargo & Company"),
        ("C", "Citigroup Inc."),
        ("BLK", "BlackRock Inc."),
        ("SCHW", "Charles Schwab Corporation"),
    ],
    "Consumer Cyclical": [
        ("AMZN", "Amazon.com Inc."),
        ("TSLA", "Tesla Inc."),
        ("HD", "The Home Depot Inc."),
        ("NKE", "Nike Inc."),
        ("MCD", "McDonald's Corporation"),
        ("SBUX", "Starbucks Corporation"),
        ("TGT", "Target Corporation"),
        ("LOW", "Lowe's Companies Inc."),
    ],
    "Consumer Defensive": [
        ("PG", "Procter & Gamble Company"),
        ("KO", "The Coca-Cola Company"),
        ("PEP", "PepsiCo Inc."),
        ("WMT", "Walmart Inc."),
        ("COST", "Costco Wholesale Corporation"),
        ("CL", "Colgate-Palmolive Company"),
        ("MDLZ", "Mondelez International Inc."),
    ],
    "Communication Services": [
        ("GOOGL", "Alphabet Inc."),
        ("META", "Meta Platforms Inc."),
        ("DIS", "The Walt Disney Company"),
        ("NFLX", "Netflix Inc."),
        ("CMCSA", "Comcast Corporation"),
        ("T", "AT&T Inc."),
        ("VZ", "Verizon Communications Inc."),
    ],
    "Industrials": [
        ("CAT", "Caterpillar Inc."),
        ("HON", "Honeywell International Inc."),
        ("UPS", "United Parcel Service Inc."),
        ("BA", "The Boeing Company"),
        ("GE", "General Electric Company"),
        ("RTX", "RTX Corporation"),
        ("DE", "Deere & Company"),
        ("LMT", "Lockheed Martin Corporation"),
    ],
    "Energy": [
        ("XOM", "Exxon Mobil Corporation"),
        ("CVX", "Chevron Corporation"),
        ("COP", "ConocoPhillips"),
        ("SLB", "Schlumberger Limited"),
        ("EOG", "EOG Resources Inc."),
        ("OXY", "Occidental Petroleum Corporation"),
        ("MPC", "Marathon Petroleum Corporation"),
    ],
    "Real Estate": [
        ("AMT", "American Tower Corporation"),
        ("PLD", "Prologis Inc."),
        ("CCI", "Crown Castle Inc."),
        ("EQIX", "Equinix Inc."),
        ("SPG", "Simon Property Group Inc."),
        ("O", "Realty Income Corporation"),
    ],
    "Utilities": [
        ("NEE", "NextEra Energy Inc."),
        ("DUK", "Duke Energy Corporation"),
        ("SO", "The Southern Company"),
        ("D", "Dominion Energy Inc."),
        ("AEP", "American Electric Power Company Inc."),
        ("SRE", "Sempra"),
    ],
    "Basic Materials": [
        ("LIN", "Linde plc"),
        ("APD", "Air Products and Chemicals Inc."),
        ("SHW", "The Sherwin-Williams Company"),
        ("FCX", "Freeport-McMoRan Inc."),
        ("NEM", "Newmont Corporation"),
        ("DOW", "Dow Inc."),
    ],
}


@tool("Get Peers")
def get_peers(ticker: str) -> str:
    """Retrieve sector/industry classification and peer companies for a given ticker.
    Returns sector, industry, and up to 5 peer companies in the same sector.
    Use this to contextualize a company's performance relative to competitors."""
    try:
        # 1. Input validation
        valid, result = validate_ticker(ticker)
        if not valid:
            return result

        normalized_ticker = result

        # 2. Cache check
        cache_key = cache.make_key("get_peers", ticker=normalized_ticker)
        cached = cache.get(cache_key)
        if cached:
            return cached

        # 3. Detect crypto/ETFs early by ticker pattern (e.g., BTC-USD, ETH-USD)
        if "-" in normalized_ticker:
            return "Peer comparison is not available for this instrument type."

        # 4. External API call
        yf_ticker = yfinance.Ticker(normalized_ticker)
        info = yf_ticker.info

        # 5. Check if sector is available (missing for crypto/ETFs)
        sector = info.get("sector")
        industry = info.get("industry")

        if not sector:
            return "Peer comparison is not available for this instrument type."

        # 6. Identify peers from sector mapping, excluding the ticker itself
        sector_companies = SECTOR_PEERS.get(sector, [])
        peers = [
            (sym, name)
            for sym, name in sector_companies
            if sym != normalized_ticker
        ][:5]

        # 7. Format response
        lines = [f"Peer Analysis for {normalized_ticker}:"]
        lines.append(f"Sector: {sector}")
        lines.append(f"Industry: {industry if industry else 'N/A'}")

        if peers:
            lines.append("Peers:")
            for sym, name in peers:
                lines.append(f"- {sym} ({name})")
        else:
            lines.append("Peers: No peer data available for this sector.")

        response = "\n".join(lines)

        # 8. Cache and return
        cache.set(cache_key, response)
        return response

    except Exception as e:
        return f"Error: An unexpected error occurred while processing {ticker}: {str(e)}"