vibesurf 0.1.23__py3-none-any.whl → 0.1.25__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of vibesurf might be problematic. Click here for more details.

@@ -0,0 +1,629 @@
1
+ """
2
+ Comprehensive finance tools using Yahoo Finance API.
3
+ Provides access to stock market data, company financials, and trading information.
4
+ """
5
+ import pdb
6
+ from enum import Enum
7
+ from typing import Dict, List, Any, Optional, Union
8
+ from datetime import datetime, timedelta
9
+ import yfinance as yf
10
+ import pandas as pd
11
+ from datetime import datetime
12
+
13
+ from vibe_surf.logger import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class FinanceMethod(Enum):
19
+ """Available Yahoo Finance data methods"""
20
+ # Basic Information
21
+ GET_INFO = "get_info" # Company basic information
22
+ GET_FAST_INFO = "get_fast_info" # Quick stats like current price, volume
23
+
24
+ # Market Data & History
25
+ GET_HISTORY = "get_history" # Historical price and volume data
26
+ GET_ACTIONS = "get_actions" # Dividends and stock splits
27
+ GET_DIVIDENDS = "get_dividends" # Dividend history
28
+ GET_SPLITS = "get_splits" # Stock split history
29
+ GET_CAPITAL_GAINS = "get_capital_gains" # Capital gains distributions
30
+
31
+ # Financial Statements
32
+ GET_FINANCIALS = "get_financials" # Income statement (annual)
33
+ GET_QUARTERLY_FINANCIALS = "get_quarterly_financials" # Income statement (quarterly)
34
+ GET_BALANCE_SHEET = "get_balance_sheet" # Balance sheet (annual)
35
+ GET_QUARTERLY_BALANCE_SHEET = "get_quarterly_balance_sheet" # Balance sheet (quarterly)
36
+ GET_CASHFLOW = "get_cashflow" # Cash flow statement (annual)
37
+ GET_QUARTERLY_CASHFLOW = "get_quarterly_cashflow" # Cash flow statement (quarterly)
38
+
39
+ # Earnings & Analysis
40
+ GET_EARNINGS = "get_earnings" # Historical earnings data
41
+ GET_QUARTERLY_EARNINGS = "get_quarterly_earnings" # Quarterly earnings
42
+ GET_EARNINGS_DATES = "get_earnings_dates" # Upcoming earnings dates
43
+ GET_CALENDAR = "get_calendar" # Earnings calendar
44
+
45
+ # Recommendations & Analysis
46
+ GET_RECOMMENDATIONS = "get_recommendations" # Analyst recommendations
47
+ GET_RECOMMENDATIONS_SUMMARY = "get_recommendations_summary" # Summary of recommendations
48
+ GET_UPGRADES_DOWNGRADES = "get_upgrades_downgrades" # Rating changes
49
+ GET_ANALYSIS = "get_analysis" # Analyst analysis
50
+
51
+ # Ownership & Holdings
52
+ GET_MAJOR_HOLDERS = "get_major_holders" # Major shareholders
53
+ GET_INSTITUTIONAL_HOLDERS = "get_institutional_holders" # Institutional holdings
54
+ GET_MUTUALFUND_HOLDERS = "get_mutualfund_holders" # Mutual fund holdings
55
+ GET_INSIDER_PURCHASES = "get_insider_purchases" # Insider purchases
56
+ GET_INSIDER_TRANSACTIONS = "get_insider_transactions" # Insider transactions
57
+ GET_INSIDER_ROSTER_HOLDERS = "get_insider_roster_holders" # Insider roster
58
+
59
+ # Additional Data
60
+ GET_NEWS = "get_news" # Latest news
61
+ GET_SUSTAINABILITY = "get_sustainability" # ESG scores
62
+ GET_SEC_FILINGS = "get_sec_filings" # SEC filings
63
+ GET_SHARES = "get_shares" # Share count data
64
+
65
+ # Options (if applicable)
66
+ GET_OPTIONS = "get_options" # Option chain data
67
+
68
+
69
+ class FinanceDataRetriever:
70
+ """Main class for retrieving and formatting Yahoo Finance data"""
71
+
72
+ def __init__(self, symbol: str):
73
+ """Initialize with stock symbol"""
74
+ self.symbol = symbol.upper()
75
+ self.ticker = yf.Ticker(self.symbol)
76
+
77
+ def get_finance_data(self, methods: List[str], **kwargs) -> Dict[str, Any]:
78
+ """
79
+ Retrieve finance data using specified methods
80
+
81
+ Args:
82
+ methods: List of method names (FinanceMethod enum values)
83
+ **kwargs: Additional parameters (e.g., period, start_date, end_date, num_news)
84
+
85
+ Returns:
86
+ Dictionary with method names as keys and data as values
87
+ """
88
+ results = {}
89
+
90
+ for method in methods:
91
+ try:
92
+ if hasattr(self, f"_{method}"):
93
+ method_func = getattr(self, f"_{method}")
94
+ results[method] = method_func(**kwargs)
95
+ else:
96
+ results[method] = f"Error: Method {method} not implemented"
97
+ logger.warning(f"Method {method} not implemented for {self.symbol}")
98
+ except Exception as e:
99
+ error_msg = f"Error retrieving {method}: {str(e)}"
100
+ results[method] = error_msg
101
+ logger.error(f"Error retrieving {method} for {self.symbol}: {e}")
102
+
103
+ return results
104
+
105
+ # Basic Information Methods
106
+ def _get_info(self, **kwargs) -> Dict:
107
+ """Get basic company information"""
108
+ return self.ticker.info
109
+
110
+ def _get_fast_info(self, **kwargs) -> Dict:
111
+ """Get quick statistics"""
112
+ try:
113
+ fast_info = self.ticker.fast_info
114
+ return dict(fast_info) if hasattr(fast_info, '__dict__') else fast_info
115
+ except:
116
+ return self.ticker.get_fast_info()
117
+
118
+ # Market Data & History Methods
119
+ def _get_history(self, **kwargs) -> pd.DataFrame:
120
+ """Get historical price and volume data"""
121
+ period = kwargs.get('period', '1y')
122
+ start_date = kwargs.get('start_date')
123
+ end_date = kwargs.get('end_date')
124
+ interval = kwargs.get('interval', '1d')
125
+
126
+ if start_date and end_date:
127
+ return self.ticker.history(start=start_date, end=end_date, interval=interval)
128
+ else:
129
+ return self.ticker.history(period=period, interval=interval)
130
+
131
+ def _get_actions(self, **kwargs) -> pd.DataFrame:
132
+ """Get dividend and stock split history"""
133
+ return self.ticker.actions
134
+
135
+ def _get_dividends(self, **kwargs) -> pd.Series:
136
+ """Get dividend history"""
137
+ return self.ticker.dividends
138
+
139
+ def _get_splits(self, **kwargs) -> pd.Series:
140
+ """Get stock split history"""
141
+ return self.ticker.splits
142
+
143
+ def _get_capital_gains(self, **kwargs) -> pd.Series:
144
+ """Get capital gains distributions"""
145
+ return self.ticker.capital_gains
146
+
147
+ # Financial Statements Methods
148
+ def _get_financials(self, **kwargs) -> pd.DataFrame:
149
+ """Get annual income statement"""
150
+ return self.ticker.financials
151
+
152
+ def _get_quarterly_financials(self, **kwargs) -> pd.DataFrame:
153
+ """Get quarterly income statement"""
154
+ return self.ticker.quarterly_financials
155
+
156
+ def _get_balance_sheet(self, **kwargs) -> pd.DataFrame:
157
+ """Get annual balance sheet"""
158
+ return self.ticker.balance_sheet
159
+
160
+ def _get_quarterly_balance_sheet(self, **kwargs) -> pd.DataFrame:
161
+ """Get quarterly balance sheet"""
162
+ return self.ticker.quarterly_balance_sheet
163
+
164
+ def _get_cashflow(self, **kwargs) -> pd.DataFrame:
165
+ """Get annual cash flow statement"""
166
+ return self.ticker.cashflow
167
+
168
+ def _get_quarterly_cashflow(self, **kwargs) -> pd.DataFrame:
169
+ """Get quarterly cash flow statement"""
170
+ return self.ticker.quarterly_cashflow
171
+
172
+ # Earnings & Analysis Methods
173
+ def _get_earnings(self, **kwargs) -> pd.DataFrame:
174
+ """Get historical earnings data"""
175
+ return self.ticker.earnings
176
+
177
+ def _get_quarterly_earnings(self, **kwargs) -> pd.DataFrame:
178
+ """Get quarterly earnings data"""
179
+ return self.ticker.quarterly_earnings
180
+
181
+ def _get_earnings_dates(self, **kwargs) -> pd.DataFrame:
182
+ """Get earnings dates and estimates"""
183
+ return self.ticker.earnings_dates
184
+
185
+ def _get_calendar(self, **kwargs) -> Dict:
186
+ """Get earnings calendar"""
187
+ return self.ticker.calendar
188
+
189
+ # Recommendations & Analysis Methods
190
+ def _get_recommendations(self, **kwargs) -> pd.DataFrame:
191
+ """Get analyst recommendations history"""
192
+ return self.ticker.recommendations
193
+
194
+ def _get_recommendations_summary(self, **kwargs) -> pd.DataFrame:
195
+ """Get summary of analyst recommendations"""
196
+ return self.ticker.recommendations_summary
197
+
198
+ def _get_upgrades_downgrades(self, **kwargs) -> pd.DataFrame:
199
+ """Get analyst upgrades and downgrades"""
200
+ return self.ticker.upgrades_downgrades
201
+
202
+ def _get_analysis(self, **kwargs) -> pd.DataFrame:
203
+ """Get analyst analysis"""
204
+ return getattr(self.ticker, 'analysis', pd.DataFrame())
205
+
206
+ # Ownership & Holdings Methods
207
+ def _get_major_holders(self, **kwargs) -> pd.DataFrame:
208
+ """Get major shareholders"""
209
+ return self.ticker.major_holders
210
+
211
+ def _get_institutional_holders(self, **kwargs) -> pd.DataFrame:
212
+ """Get institutional holdings"""
213
+ return self.ticker.institutional_holders
214
+
215
+ def _get_mutualfund_holders(self, **kwargs) -> pd.DataFrame:
216
+ """Get mutual fund holdings"""
217
+ return self.ticker.mutualfund_holders
218
+
219
+ def _get_insider_purchases(self, **kwargs) -> pd.DataFrame:
220
+ """Get insider purchases"""
221
+ return getattr(self.ticker, 'insider_purchases', pd.DataFrame())
222
+
223
+ def _get_insider_transactions(self, **kwargs) -> pd.DataFrame:
224
+ """Get insider transactions"""
225
+ return getattr(self.ticker, 'insider_transactions', pd.DataFrame())
226
+
227
+ def _get_insider_roster_holders(self, **kwargs) -> pd.DataFrame:
228
+ """Get insider roster"""
229
+ return getattr(self.ticker, 'insider_roster_holders', pd.DataFrame())
230
+
231
+ # Additional Data Methods
232
+ def _get_news(self, **kwargs) -> List[Dict]:
233
+ """Get latest news"""
234
+ num_news = kwargs.get('num_news', 5)
235
+ news = self.ticker.news
236
+ return news[:num_news] if news else []
237
+
238
+ def _get_sustainability(self, **kwargs) -> pd.DataFrame:
239
+ """Get ESG sustainability data"""
240
+ return getattr(self.ticker, 'sustainability', pd.DataFrame())
241
+
242
+ def _get_sec_filings(self, **kwargs) -> pd.DataFrame:
243
+ """Get SEC filings"""
244
+ return getattr(self.ticker, 'sec_filings', pd.DataFrame())
245
+
246
+ def _get_shares(self, **kwargs) -> pd.DataFrame:
247
+ """Get share count data"""
248
+ return getattr(self.ticker, 'shares', pd.DataFrame())
249
+
250
+ def _get_options(self, **kwargs) -> Dict:
251
+ """Get options data"""
252
+ try:
253
+ option_dates = self.ticker.options
254
+ if option_dates:
255
+ # Get the first available expiration date
256
+ first_expiry = option_dates[0]
257
+ opt_chain = self.ticker.option_chain(first_expiry)
258
+ return {
259
+ 'expiration_dates': list(option_dates),
260
+ 'calls': opt_chain.calls,
261
+ 'puts': opt_chain.puts,
262
+ 'selected_expiry': first_expiry
263
+ }
264
+ return {'expiration_dates': [], 'calls': pd.DataFrame(), 'puts': pd.DataFrame()}
265
+ except:
266
+ return {'error': 'Options data not available for this ticker'}
267
+
268
+
269
+ class FinanceMarkdownFormatter:
270
+ """Formats finance data into markdown"""
271
+
272
+ @staticmethod
273
+ def format_finance_data(symbol: str, results: Dict[str, Any], methods: List[str]) -> str:
274
+ """Format all finance data as markdown"""
275
+ markdown = f"# 💹 Financial Data for {symbol.upper()}\n\n"
276
+
277
+ for method in methods:
278
+ data = results.get(method)
279
+
280
+ if isinstance(data, str) and data.startswith('Error'):
281
+ markdown += f"## ❌ {method.replace('_', ' ').title()}\n{data}\n\n"
282
+ continue
283
+
284
+ markdown += f"## 📊 {method.replace('_', ' ').title()}\n\n"
285
+
286
+ # Route to appropriate formatter
287
+ formatter_method = f"_format_{method}"
288
+ if hasattr(FinanceMarkdownFormatter, formatter_method):
289
+ formatter = getattr(FinanceMarkdownFormatter, formatter_method)
290
+ markdown += formatter(data)
291
+ else:
292
+ # Generic formatter for unhandled methods
293
+ markdown += FinanceMarkdownFormatter._format_generic(data)
294
+
295
+ markdown += "\n\n"
296
+
297
+ return markdown.strip()
298
+
299
+ @staticmethod
300
+ def _format_generic(data: Any) -> str:
301
+ """Generic formatter for any data type"""
302
+ if data is None or (hasattr(data, 'empty') and data.empty):
303
+ return "No data available.\n"
304
+
305
+ if isinstance(data, pd.DataFrame):
306
+ if len(data) == 0:
307
+ return "No data available.\n"
308
+ return f"```\n{data.to_string()}\n```\n"
309
+ elif isinstance(data, pd.Series):
310
+ if len(data) == 0:
311
+ return "No data available.\n"
312
+ return f"```\n{data.to_string()}\n```\n"
313
+ elif isinstance(data, (list, dict)):
314
+ import json
315
+ return f"```json\n{json.dumps(data, indent=2, default=str)}\n```\n"
316
+ else:
317
+ return f"```\n{str(data)}\n```\n"
318
+
319
+ @staticmethod
320
+ def _format_get_info(info: Dict) -> str:
321
+ """Format company info as markdown"""
322
+ if not info:
323
+ return "No company information available.\n"
324
+
325
+ markdown = ""
326
+
327
+ # Basic company info
328
+ if 'longName' in info:
329
+ markdown += f"**Company Name:** {info['longName']}\n"
330
+ if 'sector' in info:
331
+ markdown += f"**Sector:** {info['sector']}\n"
332
+ if 'industry' in info:
333
+ markdown += f"**Industry:** {info['industry']}\n"
334
+ if 'website' in info:
335
+ markdown += f"**Website:** {info['website']}\n"
336
+ if 'country' in info:
337
+ markdown += f"**Country:** {info['country']}\n"
338
+
339
+ markdown += "\n### 💰 Financial Metrics\n"
340
+
341
+ # Financial metrics
342
+ if 'marketCap' in info and info['marketCap']:
343
+ markdown += f"**Market Cap:** ${info['marketCap']:,.0f}\n"
344
+ if 'enterpriseValue' in info and info['enterpriseValue']:
345
+ markdown += f"**Enterprise Value:** ${info['enterpriseValue']:,.0f}\n"
346
+ if 'totalRevenue' in info and info['totalRevenue']:
347
+ markdown += f"**Total Revenue:** ${info['totalRevenue']:,.0f}\n"
348
+ if 'grossMargins' in info and info['grossMargins']:
349
+ markdown += f"**Gross Margin:** {info['grossMargins']:.2%}\n"
350
+ if 'profitMargins' in info and info['profitMargins']:
351
+ markdown += f"**Profit Margin:** {info['profitMargins']:.2%}\n"
352
+
353
+ markdown += "\n### 📈 Stock Price Info\n"
354
+
355
+ # Stock price info
356
+ if 'currentPrice' in info and info['currentPrice']:
357
+ markdown += f"**Current Price:** ${info['currentPrice']:.2f}\n"
358
+ if 'previousClose' in info and info['previousClose']:
359
+ markdown += f"**Previous Close:** ${info['previousClose']:.2f}\n"
360
+ if 'fiftyTwoWeekHigh' in info and info['fiftyTwoWeekHigh']:
361
+ markdown += f"**52 Week High:** ${info['fiftyTwoWeekHigh']:.2f}\n"
362
+ if 'fiftyTwoWeekLow' in info and info['fiftyTwoWeekLow']:
363
+ markdown += f"**52 Week Low:** ${info['fiftyTwoWeekLow']:.2f}\n"
364
+ if 'dividendYield' in info and info['dividendYield']:
365
+ markdown += f"**Dividend Yield:** {info['dividendYield']:.2%}\n"
366
+
367
+ # Business summary
368
+ if 'longBusinessSummary' in info:
369
+ summary = info['longBusinessSummary'][:500]
370
+ if len(info['longBusinessSummary']) > 500:
371
+ summary += "..."
372
+ markdown += f"\n### 📋 Business Summary\n{summary}\n"
373
+
374
+ return markdown
375
+
376
+ @staticmethod
377
+ def _format_get_fast_info(fast_info) -> str:
378
+ """Format fast info as markdown"""
379
+ if not fast_info:
380
+ return "No fast info available.\n"
381
+
382
+ markdown = ""
383
+
384
+ # Convert to dict if needed
385
+ if hasattr(fast_info, '__dict__'):
386
+ data = fast_info.__dict__
387
+ elif isinstance(fast_info, dict):
388
+ data = fast_info
389
+ else:
390
+ return f"Fast info data: {str(fast_info)}\n"
391
+
392
+ # Format key metrics
393
+ for key, value in data.items():
394
+ if value is not None:
395
+ key_formatted = key.replace('_', ' ').title()
396
+ if isinstance(value, (int, float)):
397
+ if 'price' in key.lower() or 'value' in key.lower():
398
+ markdown += f"**{key_formatted}:** ${value:,.2f}\n"
399
+ elif 'volume' in key.lower():
400
+ markdown += f"**{key_formatted}:** {value:,}\n"
401
+ else:
402
+ markdown += f"**{key_formatted}:** {value}\n"
403
+ else:
404
+ markdown += f"**{key_formatted}:** {value}\n"
405
+
406
+ return markdown
407
+
408
+ @staticmethod
409
+ def _format_get_history(history: pd.DataFrame) -> str:
410
+ """Format historical data as markdown"""
411
+ if history.empty:
412
+ return "No historical data available.\n"
413
+
414
+ markdown = f"**Period:** {history.index.min().strftime('%Y-%m-%d')} to {history.index.max().strftime('%Y-%m-%d')}\n"
415
+ markdown += f"**Total Records:** {len(history)}\n\n"
416
+
417
+ # Determine how much data to show based on total records
418
+ total_records = len(history)
419
+ if total_records <= 30:
420
+ # Show all data if 30 records or less
421
+ display_data = history
422
+ markdown += f"### 📈 Historical Data (All {total_records} Records)\n\n"
423
+ else:
424
+ # Show recent 30 records for larger datasets
425
+ display_data = history.tail(30)
426
+ markdown += f"### 📈 Recent Data (Last 30 Records)\n\n"
427
+
428
+ markdown += "| Date | Open | High | Low | Close | Volume |\n"
429
+ markdown += "|------|------|------|-----|-------|--------|\n"
430
+
431
+ for date, row in display_data.iterrows():
432
+ markdown += f"| {date.strftime('%Y-%m-%d')} | ${row['Open']:.2f} | ${row['High']:.2f} | ${row['Low']:.2f} | ${row['Close']:.2f} | {row['Volume']:,} |\n"
433
+
434
+ # Summary statistics
435
+ markdown += "\n### 📊 Summary Statistics\n"
436
+ markdown += f"**Highest Price:** ${history['High'].max():.2f}\n"
437
+ markdown += f"**Lowest Price:** ${history['Low'].min():.2f}\n"
438
+ markdown += f"**Average Volume:** {history['Volume'].mean():,.0f}\n"
439
+ markdown += f"**Total Volume:** {history['Volume'].sum():,}\n"
440
+
441
+ return markdown
442
+
443
+ @staticmethod
444
+ def _format_get_news(news: List[Dict]) -> str:
445
+ """Format news data as markdown"""
446
+ if not news:
447
+ return "No news available.\n"
448
+
449
+ markdown = f"**Total News Articles:** {len(news)}\n\n"
450
+ for i, article in enumerate(news, 1):
451
+ if isinstance(article, dict):
452
+ # Handle new yfinance news structure with nested 'content'
453
+ content = article.get('content', article) # Fallback to article itself for backwards compatibility
454
+
455
+ # Extract title
456
+ title = (content.get('title') or
457
+ content.get('headline') or
458
+ content.get('summary') or
459
+ article.get('title') or # Fallback to old format
460
+ 'No title available')
461
+
462
+ # Extract content type if available
463
+ content_type = content.get('contentType', '')
464
+ type_emoji = "🎥" if content_type == "VIDEO" else "📰"
465
+
466
+ # Extract link/URL - try new nested structure first
467
+ link = ''
468
+ if 'canonicalUrl' in content and isinstance(content['canonicalUrl'], dict):
469
+ link = content['canonicalUrl'].get('url', '')
470
+ elif 'clickThroughUrl' in content and isinstance(content['clickThroughUrl'], dict):
471
+ link = content['clickThroughUrl'].get('url', '')
472
+ else:
473
+ # Fallback to old format
474
+ link = (content.get('link') or
475
+ content.get('url') or
476
+ content.get('guid') or
477
+ article.get('link') or '')
478
+
479
+ # Extract publisher - try new nested structure first
480
+ publisher = 'Unknown'
481
+ if 'provider' in content and isinstance(content['provider'], dict):
482
+ publisher = content['provider'].get('displayName', 'Unknown')
483
+ else:
484
+ # Fallback to old format
485
+ publisher = (content.get('publisher') or
486
+ content.get('source') or
487
+ content.get('author') or
488
+ article.get('publisher') or
489
+ 'Unknown')
490
+
491
+ # Extract publication time
492
+ publish_time = (content.get('pubDate') or
493
+ content.get('providerPublishTime') or
494
+ content.get('timestamp') or
495
+ content.get('published') or
496
+ article.get('providerPublishTime') or '')
497
+
498
+ # Format the article
499
+ markdown += f"### {type_emoji} {i}. {title}\n"
500
+ if content_type:
501
+ markdown += f"**Type:** {content_type}\n"
502
+ markdown += f"**Publisher:** {publisher}\n"
503
+
504
+ if publish_time:
505
+ try:
506
+ # Handle different timestamp formats
507
+ if isinstance(publish_time, (int, float)):
508
+ dt = datetime.fromtimestamp(publish_time)
509
+ markdown += f"**Published:** {dt.strftime('%Y-%m-%d %H:%M')}\n"
510
+ elif isinstance(publish_time, str):
511
+ # Try to parse ISO format first (new format)
512
+ try:
513
+ if publish_time.endswith('Z'):
514
+ dt = datetime.fromisoformat(publish_time.replace('Z', '+00:00'))
515
+ markdown += f"**Published:** {dt.strftime('%Y-%m-%d %H:%M UTC')}\n"
516
+ else:
517
+ # Try to parse as Unix timestamp
518
+ publish_time_int = int(float(publish_time))
519
+ dt = datetime.fromtimestamp(publish_time_int)
520
+ markdown += f"**Published:** {dt.strftime('%Y-%m-%d %H:%M')}\n"
521
+ except:
522
+ markdown += f"**Published:** {publish_time}\n"
523
+ except Exception as e:
524
+ # If timestamp parsing fails, show raw value
525
+ markdown += f"**Published:** {publish_time}\n"
526
+
527
+ if link:
528
+ markdown += f"**Link:** {link}\n"
529
+
530
+ # Add summary or description if available
531
+ summary = (content.get('summary') or
532
+ content.get('description') or
533
+ content.get('snippet') or
534
+ article.get('summary') or '')
535
+ if summary and summary != title:
536
+ # Clean HTML tags from description if present
537
+ import re
538
+ clean_summary = re.sub(r'<[^>]+>', '', summary)
539
+ clean_summary = re.sub(r'\s+', ' ', clean_summary).strip()
540
+
541
+ # Limit summary length
542
+ if len(clean_summary) > 300:
543
+ clean_summary = clean_summary[:300] + "..."
544
+ markdown += f"**Summary:** {clean_summary}\n"
545
+
546
+ # Add metadata if available
547
+ if 'metadata' in content and isinstance(content['metadata'], dict):
548
+ if content['metadata'].get('editorsPick'):
549
+ markdown += f"**Editor's Pick:** ✅\n"
550
+
551
+ markdown += "\n"
552
+
553
+ return markdown
554
+
555
+ @staticmethod
556
+ def _format_get_dividends(dividends: pd.Series) -> str:
557
+ """Format dividend data as markdown"""
558
+ if dividends.empty:
559
+ return "No dividend data available.\n"
560
+
561
+ markdown = f"**Total Dividends Recorded:** {len(dividends)}\n"
562
+ markdown += f"**Date Range:** {dividends.index.min().strftime('%Y-%m-%d')} to {dividends.index.max().strftime('%Y-%m-%d')}\n\n"
563
+
564
+ # Recent dividends (last 10)
565
+ recent_dividends = dividends.tail(10)
566
+ markdown += "### 💰 Recent Dividends\n\n"
567
+ markdown += "| Date | Dividend Amount |\n"
568
+ markdown += "|------|----------------|\n"
569
+
570
+ for date, amount in recent_dividends.items():
571
+ markdown += f"| {date.strftime('%Y-%m-%d')} | ${amount:.4f} |\n"
572
+
573
+ # Summary
574
+ markdown += f"\n**Total Dividends Paid:** ${dividends.sum():.4f}\n"
575
+ markdown += f"**Average Dividend:** ${dividends.mean():.4f}\n"
576
+ if len(dividends) > 1:
577
+ yearly_frequency = len(dividends) / ((dividends.index.max() - dividends.index.min()).days / 365.25)
578
+ markdown += f"**Estimated Annual Frequency:** {yearly_frequency:.1f} times per year\n"
579
+
580
+ return markdown
581
+
582
+ @staticmethod
583
+ def _format_get_recommendations(recommendations: pd.DataFrame) -> str:
584
+ """Format recommendations as markdown"""
585
+ if recommendations.empty:
586
+ return "No recommendations available.\n"
587
+
588
+ markdown = f"**Total Recommendations:** {len(recommendations)}\n\n"
589
+
590
+ # Recent recommendations (last 15)
591
+ recent_recs = recommendations.tail(15)
592
+ markdown += "### 📊 Recent Analyst Recommendations\n\n"
593
+ markdown += "| Date | Firm | To Grade | From Grade | Action |\n"
594
+ markdown += "|------|------|----------|------------|--------|\n"
595
+
596
+ for _, rec in recent_recs.iterrows():
597
+ date = rec.get('Date', 'N/A')
598
+ firm = rec.get('Firm', 'N/A')
599
+ to_grade = rec.get('To Grade', 'N/A')
600
+ from_grade = rec.get('From Grade', 'N/A')
601
+ action = rec.get('Action', 'N/A')
602
+
603
+ markdown += f"| {date} | {firm} | {to_grade} | {from_grade} | {action} |\n"
604
+
605
+ return markdown
606
+
607
+ @staticmethod
608
+ def _format_get_earnings(earnings: pd.DataFrame) -> str:
609
+ """Format earnings as markdown"""
610
+ if earnings.empty:
611
+ return "No earnings data available.\n"
612
+
613
+ markdown = "### 💼 Annual Earnings History\n\n"
614
+ markdown += "| Year | Revenue | Earnings |\n"
615
+ markdown += "|------|---------|----------|\n"
616
+
617
+ for year, row in earnings.iterrows():
618
+ revenue = row.get('Revenue', 'N/A')
619
+ earnings_val = row.get('Earnings', 'N/A')
620
+
621
+ # Format numbers if they're numeric
622
+ if isinstance(revenue, (int, float)):
623
+ revenue = f"${revenue:,.0f}"
624
+ if isinstance(earnings_val, (int, float)):
625
+ earnings_val = f"${earnings_val:,.0f}"
626
+
627
+ markdown += f"| {year} | {revenue} | {earnings_val} |\n"
628
+
629
+ return markdown
@@ -2,11 +2,12 @@ from browser_use.tools.registry.service import Registry
2
2
  from vibe_surf.tools.vibesurf_tools import VibeSurfTools
3
3
  from vibe_surf.tools.file_system import CustomFileSystem
4
4
  from browser_use.tools.views import NoParamsAction
5
+ from vibe_surf.tools.vibesurf_registry import VibeSurfRegistry
5
6
 
6
7
 
7
8
  class ReportWriterTools(VibeSurfTools):
8
9
  def __init__(self, exclude_actions: list[str] = []):
9
- self.registry = Registry(exclude_actions)
10
+ self.registry = VibeSurfRegistry(exclude_actions)
10
11
  self._register_file_actions()
11
12
  self._register_done_action()
12
13