ellements 0.2.0__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.
Files changed (130) hide show
  1. ellements/__init__.py +57 -0
  2. ellements/agents/__init__.py +45 -0
  3. ellements/agents/backend.py +100 -0
  4. ellements/agents/builder.py +303 -0
  5. ellements/agents/claude_backend.py +200 -0
  6. ellements/agents/controller.py +237 -0
  7. ellements/agents/openai_backend.py +187 -0
  8. ellements/agents/py.typed +0 -0
  9. ellements/agents/runner.py +358 -0
  10. ellements/agents/tools.py +30 -0
  11. ellements/benchmarking/__init__.py +52 -0
  12. ellements/benchmarking/harness.py +342 -0
  13. ellements/benchmarking/py.typed +0 -0
  14. ellements/benchmarking/results.py +173 -0
  15. ellements/cli/__init__.py +80 -0
  16. ellements/cli/adapters.py +73 -0
  17. ellements/cli/agent_tui.py +1178 -0
  18. ellements/cli/components.py +411 -0
  19. ellements/cli/printer.py +229 -0
  20. ellements/cli/py.typed +0 -0
  21. ellements/core/__init__.py +112 -0
  22. ellements/core/async_utils.py +42 -0
  23. ellements/core/budgeting/__init__.py +43 -0
  24. ellements/core/budgeting/client.py +276 -0
  25. ellements/core/budgeting/protocol.py +51 -0
  26. ellements/core/budgeting/trackers.py +177 -0
  27. ellements/core/caching/__init__.py +51 -0
  28. ellements/core/caching/cache.py +73 -0
  29. ellements/core/caching/client.py +300 -0
  30. ellements/core/caching/disk.py +128 -0
  31. ellements/core/caching/keys.py +97 -0
  32. ellements/core/caching/memory.py +104 -0
  33. ellements/core/chunking.py +262 -0
  34. ellements/core/config.py +180 -0
  35. ellements/core/exceptions.py +145 -0
  36. ellements/core/llm/__init__.py +46 -0
  37. ellements/core/llm/client.py +1124 -0
  38. ellements/core/llm/images.py +226 -0
  39. ellements/core/llm/messages.py +202 -0
  40. ellements/core/llm/model_params.py +66 -0
  41. ellements/core/llm/protocol.py +146 -0
  42. ellements/core/llm/requests.py +100 -0
  43. ellements/core/llm/structured.py +91 -0
  44. ellements/core/llm/wrapper.py +79 -0
  45. ellements/core/observability/__init__.py +39 -0
  46. ellements/core/observability/events.py +169 -0
  47. ellements/core/observability/jsonl_logger.py +244 -0
  48. ellements/core/observability/markdown_formatter.py +197 -0
  49. ellements/core/observability/observer.py +56 -0
  50. ellements/core/prompting/__init__.py +14 -0
  51. ellements/core/prompting/context.py +185 -0
  52. ellements/core/prompting/guideline.py +133 -0
  53. ellements/core/prompting/persona.py +267 -0
  54. ellements/core/prompting/sources.py +92 -0
  55. ellements/core/py.typed +0 -0
  56. ellements/core/rate_limit/__init__.py +44 -0
  57. ellements/core/rate_limit/bucket.py +85 -0
  58. ellements/core/rate_limit/client.py +216 -0
  59. ellements/core/rate_limit/protocol.py +27 -0
  60. ellements/core/templating.py +126 -0
  61. ellements/core/tools/__init__.py +51 -0
  62. ellements/core/tools/dialects.py +136 -0
  63. ellements/core/tools/executor.py +48 -0
  64. ellements/core/tools/protocol.py +36 -0
  65. ellements/core/tools/records.py +28 -0
  66. ellements/core/tools/registry.py +205 -0
  67. ellements/core/tools/schemas.py +80 -0
  68. ellements/core/tools/simple.py +119 -0
  69. ellements/core/tools/spec.py +33 -0
  70. ellements/domain_specific/__init__.py +7 -0
  71. ellements/domain_specific/finance/__init__.py +188 -0
  72. ellements/domain_specific/finance/calculations.py +837 -0
  73. ellements/domain_specific/finance/charts.py +426 -0
  74. ellements/domain_specific/finance/portfolio.py +129 -0
  75. ellements/domain_specific/finance/quant_analysis.py +279 -0
  76. ellements/domain_specific/finance/risk.py +362 -0
  77. ellements/domain_specific/finance/technical_indicators.py +241 -0
  78. ellements/domain_specific/finance/tools.py +483 -0
  79. ellements/domain_specific/finance/valuation.py +312 -0
  80. ellements/domain_specific/finance/yahoo_finance.py +1523 -0
  81. ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
  82. ellements/domain_specific/py.typed +0 -0
  83. ellements/execution/__init__.py +56 -0
  84. ellements/execution/callbacks.py +149 -0
  85. ellements/execution/catalog.py +70 -0
  86. ellements/execution/collaborative.py +191 -0
  87. ellements/execution/config.py +135 -0
  88. ellements/execution/py.typed +0 -0
  89. ellements/execution/reflection.py +156 -0
  90. ellements/execution/self_consistency.py +189 -0
  91. ellements/execution/single_call.py +48 -0
  92. ellements/execution/strategies.py +237 -0
  93. ellements/execution/tree_of_thought.py +541 -0
  94. ellements/fslm/__init__.py +108 -0
  95. ellements/fslm/builtins.py +103 -0
  96. ellements/fslm/cli.py +199 -0
  97. ellements/fslm/context.py +50 -0
  98. ellements/fslm/definition.py +163 -0
  99. ellements/fslm/det.py +173 -0
  100. ellements/fslm/dsl.py +458 -0
  101. ellements/fslm/errors.py +38 -0
  102. ellements/fslm/evaluators.py +141 -0
  103. ellements/fslm/kernel.py +603 -0
  104. ellements/fslm/loading.py +123 -0
  105. ellements/fslm/models.py +623 -0
  106. ellements/fslm/nl.py +87 -0
  107. ellements/fslm/observers.py +107 -0
  108. ellements/fslm/persistence.py +72 -0
  109. ellements/fslm/py.typed +0 -0
  110. ellements/fslm/rendering.py +551 -0
  111. ellements/fslm/visualization.py +25 -0
  112. ellements/reporting/__init__.py +12 -0
  113. ellements/reporting/charts.py +364 -0
  114. ellements/reporting/html_generation.py +132 -0
  115. ellements/reporting/py.typed +0 -0
  116. ellements/reporting/visualization.py +73 -0
  117. ellements/standard_tools/__init__.py +22 -0
  118. ellements/standard_tools/py.typed +0 -0
  119. ellements/standard_tools/terminal.py +205 -0
  120. ellements/standard_tools/web/__init__.py +37 -0
  121. ellements/standard_tools/web/browser_viewer.py +214 -0
  122. ellements/standard_tools/web/crawler.py +454 -0
  123. ellements/standard_tools/web/search.py +399 -0
  124. ellements/standard_tools/web/youtube.py +1297 -0
  125. ellements-0.2.0.dist-info/METADATA +368 -0
  126. ellements-0.2.0.dist-info/RECORD +130 -0
  127. ellements-0.2.0.dist-info/WHEEL +5 -0
  128. ellements-0.2.0.dist-info/entry_points.txt +2 -0
  129. ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
  130. ellements-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1523 @@
1
+ """Yahoo Finance asset search and data retrieval functionality."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Coroutine
7
+ from datetime import datetime
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ import yfinance as yf
11
+ from ellements.core import ToolRegistry
12
+ from ellements.core.exceptions import LLMError
13
+
14
+ from .charts import technical_chart_assets
15
+ from .technical_indicators import (
16
+ compute_technical_indicators,
17
+ talib_available,
18
+ )
19
+ from .yahoo_finance_models import (
20
+ AssetProfile,
21
+ AssetQuote,
22
+ BalanceSheet,
23
+ CashFlowStatement,
24
+ EarningsData,
25
+ FinancialMetrics,
26
+ Fundamentals,
27
+ HistoricalData,
28
+ HistoricalPrice,
29
+ IncomeStatement,
30
+ SearchResult,
31
+ SearchResults,
32
+ _serialize_indicator_rows,
33
+ )
34
+
35
+ if TYPE_CHECKING:
36
+ pass
37
+
38
+
39
+ class YahooFinanceSearcher:
40
+ """Yahoo Finance asset search and data retrieval."""
41
+
42
+ # Common exchange suffixes to try when a bare ticker is not found.
43
+ # Ordered by likelihood for international portfolios.
44
+ EXCHANGE_SUFFIXES = [
45
+ ".SA", # B3 (Brazil)
46
+ ".L", # London
47
+ ".TO", # Toronto
48
+ ".AX", # ASX (Australia)
49
+ ".HK", # Hong Kong
50
+ ".DE", # XETRA (Germany)
51
+ ".PA", # Euronext Paris
52
+ ".MI", # Milan
53
+ ".AS", # Amsterdam
54
+ ".MC", # Madrid
55
+ ".SI", # SGX (Singapore)
56
+ ".KS", # KRX (Korea)
57
+ ".TW", # TWSE (Taiwan)
58
+ ".MX", # BMV (Mexico)
59
+ ]
60
+
61
+ def __init__(self, **kwargs: Any) -> None:
62
+ """Initialize the Yahoo Finance searcher.
63
+
64
+ Args:
65
+ **kwargs: Additional configuration
66
+ """
67
+ self.config = kwargs
68
+ # Cache of resolved symbols: bare_ticker -> resolved_symbol
69
+ self._symbol_cache: dict[str, str] = {}
70
+ # Set of bare symbols that were verified to have data
71
+ self._verified_bare: set[str] = set()
72
+
73
+ def resolve_symbol(self, symbol: str) -> str:
74
+ """Resolve a ticker symbol, trying exchange suffixes if needed.
75
+
76
+ If the bare symbol already contains a dot (e.g., "BBAS3.SA"),
77
+ it is returned as-is. Purely alphabetic symbols (e.g., "GOOG",
78
+ "MSFT") are assumed to be valid international tickers and are
79
+ returned without probing, since Yahoo Finance API probing is
80
+ unreliable for well-known symbols. Symbols containing digits
81
+ (e.g., "BBAS3") are probed with exchange suffixes.
82
+
83
+ Args:
84
+ symbol: Ticker symbol (e.g., "BBAS3" or "BBAS3.SA")
85
+
86
+ Returns:
87
+ The resolved symbol that works with Yahoo Finance.
88
+ """
89
+ if symbol in self._symbol_cache:
90
+ return self._symbol_cache[symbol]
91
+
92
+ # If symbol already has a suffix, use it directly
93
+ if "." in symbol:
94
+ self._symbol_cache[symbol] = symbol
95
+ return symbol
96
+
97
+ # Purely alphabetic symbols with typical US/intl length (1–5 letters:
98
+ # GOOG, MSFT, META, NU etc.) are almost certainly valid tickers.
99
+ # Longer all-alpha strings (KNCALL, BOVAL etc.) may be broker
100
+ # codes that need resolution, so we still probe for them.
101
+ if symbol.isalpha() and len(symbol) <= 5:
102
+ self._symbol_cache[symbol] = symbol
103
+ self._verified_bare.add(symbol)
104
+ return symbol
105
+
106
+ # Try the bare symbol first
107
+ if self._symbol_has_data(symbol):
108
+ self._symbol_cache[symbol] = symbol
109
+ self._verified_bare.add(symbol)
110
+ return symbol
111
+
112
+ # Try common exchange suffixes
113
+ for suffix in self.EXCHANGE_SUFFIXES:
114
+ candidate = f"{symbol}{suffix}"
115
+ if self._symbol_has_data(candidate):
116
+ self._symbol_cache[symbol] = candidate
117
+ return candidate
118
+
119
+ # Nothing worked — return original symbol so the caller gets
120
+ # the normal error for the bare ticker.
121
+ self._symbol_cache[symbol] = symbol
122
+ return symbol
123
+
124
+ def resolve_symbol_by_name(
125
+ self, symbol: str, name: str, asset_type: str = ""
126
+ ) -> str:
127
+ """Resolve a ticker via Yahoo Finance name search as fallback.
128
+
129
+ First tries ``resolve_symbol`` (exchange-suffix probing).
130
+ If that returns the bare symbol unchanged *and* a descriptive
131
+ ``name`` is provided, performs a Yahoo Finance text search
132
+ using the name and picks the best match.
133
+
134
+ Args:
135
+ symbol: Bare ticker (e.g. ``"BOVAL1"``).
136
+ name: Human-readable name (e.g. ``"ETF BOVAL1"``).
137
+ asset_type: Optional hint like ``"etf"``, ``"bdr"`` etc.
138
+
139
+ Returns:
140
+ Resolved symbol that (hopefully) works with Yahoo Finance.
141
+ """
142
+ # Fast path: already in cache
143
+ if symbol in self._symbol_cache:
144
+ return self._symbol_cache[symbol]
145
+
146
+ resolved = self.resolve_symbol(symbol)
147
+ # If resolve_symbol found a different symbol or verified the bare one, done
148
+ if resolved != symbol or symbol in self._verified_bare:
149
+ return resolved
150
+
151
+ # Try Yahoo search using the descriptive name
152
+ if name:
153
+ try:
154
+ search_results = self.search(name, max_results=5)
155
+ for sr in search_results.results:
156
+ candidate = sr.symbol
157
+ if self._symbol_has_data(candidate):
158
+ self._symbol_cache[symbol] = candidate
159
+ return candidate
160
+ except Exception:
161
+ pass
162
+
163
+ # Try a more specific search if asset_type is a BDR
164
+ if asset_type and asset_type.lower() == "bdr":
165
+ # BDRs track an underlying foreign asset; search for
166
+ # the name without "BDR" to find the original ticker.
167
+ clean_name = (
168
+ name.replace("BDR", "")
169
+ .replace("bdr", "")
170
+ .replace("(Nubank)", "")
171
+ .replace("(nubank)", "")
172
+ .strip()
173
+ )
174
+ if clean_name:
175
+ try:
176
+ search_results = self.search(clean_name, max_results=3)
177
+ for sr in search_results.results:
178
+ candidate = sr.symbol
179
+ if self._symbol_has_data(candidate):
180
+ self._symbol_cache[symbol] = candidate
181
+ return candidate
182
+ except Exception:
183
+ pass
184
+
185
+ return resolved
186
+
187
+ def resolve_symbol_with_web_search(
188
+ self, symbol: str, name: str, asset_type: str = ""
189
+ ) -> str:
190
+ """Resolve ticker using name search and web search as final fallback.
191
+
192
+ Extends :meth:`resolve_symbol_by_name` with a DuckDuckGo web
193
+ search to discover the correct ticker when Yahoo Finance search
194
+ alone doesn't find it (e.g., mistyped Brazilian B3 tickers).
195
+
196
+ Args:
197
+ symbol: Bare ticker
198
+ name: Human-readable asset name
199
+ asset_type: Optional hint (``"etf"``, ``"fii"``, ``"bdr"`` …)
200
+
201
+ Returns:
202
+ Best resolved ticker found.
203
+ """
204
+ resolved = self.resolve_symbol_by_name(symbol, name, asset_type)
205
+ if resolved != symbol or symbol in self._verified_bare:
206
+ return resolved
207
+
208
+ # Try common B3 ticker variations before web search.
209
+ # Some broker codes append an extra letter to standard tickers
210
+ # (e.g. BOVAL1 → BOVA11, VGIAL1 → VGIA11, KNCALL → KNCA11).
211
+ b3_variants = self._generate_b3_variants(symbol)
212
+ for variant in b3_variants:
213
+ if self._symbol_has_data(variant):
214
+ self._symbol_cache[symbol] = variant
215
+ return variant
216
+ sa = f"{variant}.SA"
217
+ if self._symbol_has_data(sa):
218
+ self._symbol_cache[symbol] = sa
219
+ return sa
220
+
221
+ # Web search fallback — look for the correct ticker
222
+ try:
223
+ from ellements.standard_tools.web.search import WebSearcher
224
+ except ImportError:
225
+ return resolved
226
+
227
+ ws = WebSearcher(max_results=5)
228
+ queries = [
229
+ f"{symbol} ticker B3 bolsa de valores",
230
+ f"{name} ticker stock exchange",
231
+ ]
232
+ for query in queries:
233
+ try:
234
+ results = ws.search(query, max_results=5)
235
+ for r in results.results:
236
+ # Look for ticker-like patterns in snippets
237
+ candidate = self._extract_ticker_from_text(
238
+ r.snippet + " " + r.title, symbol
239
+ )
240
+ if candidate and self._symbol_has_data(candidate):
241
+ self._symbol_cache[symbol] = candidate
242
+ return candidate
243
+ # Try with .SA suffix
244
+ if candidate:
245
+ sa = f"{candidate}.SA"
246
+ if self._symbol_has_data(sa):
247
+ self._symbol_cache[symbol] = sa
248
+ return sa
249
+ except Exception:
250
+ continue
251
+
252
+ return resolved
253
+
254
+ @staticmethod
255
+ def _generate_b3_variants(symbol: str) -> list[str]:
256
+ """Generate plausible B3 ticker variations for a broker code.
257
+
258
+ Common pattern: brokers append an extra letter to standard tickers
259
+ (e.g. BOVAL1 → BOVA11, VGIAL1 → VGIA11, KNCALL → KNCA11).
260
+ """
261
+ import re
262
+
263
+ sym = symbol.upper()
264
+ variants: list[str] = []
265
+
266
+ # Pattern: 4+ letters followed by 1-2 digits
267
+ m = re.match(r"^([A-Z]+?)([A-Z])(\d{1,2})$", sym)
268
+ if m:
269
+ prefix, extra, num = m.groups()
270
+ # Try dropping the extra letter and adjusting number
271
+ # BOVAL1 → BOV + A + L + 1 → BOVA + 11
272
+ if len(prefix) >= 3:
273
+ # Try prefix + extra letter + "11" (most common B3 suffix)
274
+ for suffix in ["11", "3", "4", "5", "6"]:
275
+ variants.append(f"{prefix}{extra}{suffix}")
276
+ variants.append(f"{prefix}{suffix}")
277
+
278
+ # Also try 4-char prefix + common suffixes
279
+ if len(sym) >= 5:
280
+ base4 = sym[:4]
281
+ for suffix in ["11", "3", "4", "5", "6"]:
282
+ candidate = f"{base4}{suffix}"
283
+ if candidate != sym:
284
+ variants.append(candidate)
285
+
286
+ return variants
287
+
288
+ @staticmethod
289
+ def _extract_ticker_from_text(text: str, original: str) -> str | None:
290
+ """Try to extract a plausible ticker from search result text.
291
+
292
+ Looks for B3-style tickers (4 letters + 2 digits) that resemble
293
+ the original symbol.
294
+ """
295
+ import re
296
+
297
+ # Common pattern: 4 uppercase letters + 1-2 digits (B3 style)
298
+ candidates = re.findall(r"\b([A-Z]{4}\d{1,2})\b", text.upper())
299
+ # Prefer candidates that share prefix with original
300
+ prefix = original[:3].upper()
301
+ for c in candidates:
302
+ if c.startswith(prefix) and c != original.upper():
303
+ return str(c)
304
+ # Any B3-style ticker as fallback
305
+ for c in candidates:
306
+ if c != original.upper():
307
+ return str(c)
308
+ return None
309
+
310
+ async def resolve_symbol_with_web_search_async(
311
+ self, symbol: str, name: str, asset_type: str = ""
312
+ ) -> str:
313
+ """Async version of :meth:`resolve_symbol_with_web_search`."""
314
+ loop = asyncio.get_event_loop()
315
+ return await loop.run_in_executor(
316
+ None,
317
+ lambda: self.resolve_symbol_with_web_search(symbol, name, asset_type),
318
+ )
319
+
320
+ async def resolve_symbol_by_name_async(
321
+ self, symbol: str, name: str, asset_type: str = ""
322
+ ) -> str:
323
+ """Async version of :meth:`resolve_symbol_by_name`."""
324
+ loop = asyncio.get_event_loop()
325
+ return await loop.run_in_executor(
326
+ None,
327
+ lambda: self.resolve_symbol_by_name(symbol, name, asset_type),
328
+ )
329
+
330
+ def _symbol_has_data(self, symbol: str) -> bool:
331
+ """Quick check whether Yahoo Finance recognises a symbol."""
332
+ import logging
333
+
334
+ # Suppress noisy HTTP 404 warnings during probing
335
+ loggers_to_quiet = [
336
+ logging.getLogger(name)
337
+ for name in ("yfinance", "urllib3", "peewee", "requests")
338
+ ]
339
+ prev_levels = [(lg, lg.level) for lg in loggers_to_quiet]
340
+ for lg in loggers_to_quiet:
341
+ lg.setLevel(logging.CRITICAL)
342
+ try:
343
+ ticker = yf.Ticker(symbol)
344
+ # Try fast_info first (less API-intensive)
345
+ try:
346
+ fi = ticker.fast_info
347
+ if hasattr(fi, "last_price") and fi.last_price is not None and fi.last_price > 0:
348
+ return True
349
+ except Exception:
350
+ pass
351
+
352
+ info = ticker.info or {}
353
+ if info.get("currentPrice") or info.get("regularMarketPrice"):
354
+ return True
355
+ if info.get("quoteType") and info["quoteType"] != "NONE":
356
+ return True
357
+ # Fallback: try recent history
358
+ hist = ticker.history(period="5d")
359
+ return not hist.empty
360
+ except Exception:
361
+ return False
362
+ finally:
363
+ for lg, level in prev_levels:
364
+ lg.setLevel(level)
365
+
366
+ def search(
367
+ self,
368
+ query: str,
369
+ max_results: int = 10,
370
+ **kwargs: Any,
371
+ ) -> SearchResults:
372
+ """Search for financial assets by name or symbol.
373
+
374
+ Args:
375
+ query: Search query (company name or ticker symbol)
376
+ max_results: Maximum number of results to return
377
+ **kwargs: Additional search parameters
378
+
379
+ Returns:
380
+ SearchResults object containing the results
381
+ """
382
+ try:
383
+ # Use yfinance Lookup to search
384
+ lookup = yf.Lookup(query, **kwargs)
385
+
386
+ # Get all results (returns a DataFrame)
387
+ all_results_df = lookup.all
388
+
389
+ # Convert DataFrame to SearchResult objects
390
+ results = []
391
+ for idx, row in all_results_df.head(max_results).iterrows():
392
+ result = SearchResult(
393
+ symbol=str(idx) if isinstance(idx, str) else row.get('symbol', str(idx)),
394
+ name=row.get('longName', row.get('shortName', row.get('name', ''))),
395
+ type=row.get('typeDisp', row.get('quoteType', 'Unknown')),
396
+ exchange=row.get('exchange', row.get('exchDisp', ''))
397
+ )
398
+ results.append(result)
399
+
400
+ return SearchResults(
401
+ query=query,
402
+ results=results,
403
+ total_results=len(results)
404
+ )
405
+
406
+ except Exception as e:
407
+ raise LLMError(f"Asset search failed: {e}") from e
408
+
409
+ async def search_async(
410
+ self,
411
+ query: str,
412
+ max_results: int = 10,
413
+ **kwargs: Any,
414
+ ) -> SearchResults:
415
+ """Async version of search.
416
+
417
+ Args:
418
+ query: Search query
419
+ max_results: Maximum number of results
420
+ **kwargs: Additional parameters
421
+
422
+ Returns:
423
+ SearchResults object
424
+ """
425
+ loop = asyncio.get_event_loop()
426
+ return await loop.run_in_executor(
427
+ None,
428
+ lambda: self.search(query=query, max_results=max_results, **kwargs)
429
+ )
430
+
431
+ def get_quote(
432
+ self,
433
+ symbol: str,
434
+ **kwargs: Any,
435
+ ) -> AssetQuote:
436
+ """Get current quote data for an asset.
437
+
438
+ Args:
439
+ symbol: Ticker symbol
440
+ **kwargs: Additional parameters
441
+
442
+ Returns:
443
+ AssetQuote with current market data
444
+ """
445
+ try:
446
+ resolved = self.resolve_symbol(symbol)
447
+ ticker = yf.Ticker(resolved)
448
+ info = ticker.info
449
+
450
+ # Try to get current price from info first
451
+ current_price = info.get('currentPrice') or info.get('regularMarketPrice')
452
+
453
+ # Fallback: If info doesn't have price, use history (more reliable)
454
+ if current_price is None:
455
+ hist = ticker.history(period='1d')
456
+ if not hist.empty:
457
+ current_price = float(hist['Close'].iloc[-1])
458
+ # Also get previous close from history if available
459
+ if len(hist) > 1:
460
+ previous_close = float(hist['Close'].iloc[-2])
461
+ else:
462
+ previous_close = None
463
+ else:
464
+ current_price = None
465
+ previous_close = None
466
+ else:
467
+ previous_close = info.get('previousClose') or info.get('regularMarketPreviousClose')
468
+
469
+ return AssetQuote(
470
+ symbol=symbol,
471
+ name=info.get('longName', info.get('shortName', symbol)),
472
+ current_price=current_price,
473
+ previous_close=previous_close,
474
+ open_price=info.get('open') or info.get('regularMarketOpen'),
475
+ day_high=info.get('dayHigh') or info.get('regularMarketDayHigh'),
476
+ day_low=info.get('dayLow') or info.get('regularMarketDayLow'),
477
+ volume=info.get('volume') or info.get('regularMarketVolume'),
478
+ market_cap=info.get('marketCap'),
479
+ pe_ratio=info.get('trailingPE') or info.get('forwardPE'),
480
+ dividend_yield=info.get('dividendYield'),
481
+ fifty_two_week_high=info.get('fiftyTwoWeekHigh'),
482
+ fifty_two_week_low=info.get('fiftyTwoWeekLow'),
483
+ currency=info.get('currency', 'USD'),
484
+ exchange=info.get('exchange', '')
485
+ )
486
+
487
+ except Exception as e:
488
+ raise LLMError(f"Failed to get quote for {symbol}: {e}") from e
489
+
490
+ async def get_quote_async(
491
+ self,
492
+ symbol: str,
493
+ **kwargs: Any,
494
+ ) -> AssetQuote:
495
+ """Async version of get_quote.
496
+
497
+ Args:
498
+ symbol: Ticker symbol
499
+ **kwargs: Additional parameters
500
+
501
+ Returns:
502
+ AssetQuote object
503
+ """
504
+ loop = asyncio.get_event_loop()
505
+ return await loop.run_in_executor(
506
+ None,
507
+ lambda: self.get_quote(symbol, **kwargs)
508
+ )
509
+
510
+ def get_profile(
511
+ self,
512
+ symbol: str,
513
+ **kwargs: Any,
514
+ ) -> AssetProfile:
515
+ """Get detailed profile information for an asset.
516
+
517
+ Args:
518
+ symbol: Ticker symbol
519
+ **kwargs: Additional parameters
520
+
521
+ Returns:
522
+ AssetProfile with company details
523
+ """
524
+ try:
525
+ resolved = self.resolve_symbol(symbol)
526
+ ticker = yf.Ticker(resolved)
527
+ info = ticker.info
528
+
529
+ return AssetProfile(
530
+ symbol=symbol,
531
+ name=info.get('longName', info.get('shortName', symbol)),
532
+ sector=info.get('sector', ''),
533
+ industry=info.get('industry', ''),
534
+ description=info.get('longBusinessSummary', ''),
535
+ website=info.get('website', ''),
536
+ country=info.get('country', ''),
537
+ employees=info.get('fullTimeEmployees'),
538
+ address=f"{info.get('address1', '')} {info.get('city', '')} {info.get('state', '')} {info.get('zip', '')}".strip()
539
+ )
540
+
541
+ except Exception as e:
542
+ raise LLMError(f"Failed to get profile for {symbol}: {e}") from e
543
+
544
+ async def get_profile_async(
545
+ self,
546
+ symbol: str,
547
+ **kwargs: Any,
548
+ ) -> AssetProfile:
549
+ """Async version of get_profile.
550
+
551
+ Args:
552
+ symbol: Ticker symbol
553
+ **kwargs: Additional parameters
554
+
555
+ Returns:
556
+ AssetProfile object
557
+ """
558
+ loop = asyncio.get_event_loop()
559
+ return await loop.run_in_executor(
560
+ None,
561
+ lambda: self.get_profile(symbol, **kwargs)
562
+ )
563
+
564
+ def get_historical_data(
565
+ self,
566
+ symbol: str,
567
+ period: str = "1mo",
568
+ interval: str = "1d",
569
+ start: datetime | None = None,
570
+ end: datetime | None = None,
571
+ **kwargs: Any,
572
+ ) -> HistoricalData:
573
+ """Get historical price data for an asset.
574
+
575
+ Args:
576
+ symbol: Ticker symbol
577
+ period: Time period ('1d', '5d', '1mo', '3mo', '6mo', '1y', '2y', '5y', '10y', 'ytd', 'max')
578
+ interval: Data interval ('1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1wk', '1mo', '3mo')
579
+ start: Start date (alternative to period)
580
+ end: End date (alternative to period)
581
+ **kwargs: Additional parameters
582
+
583
+ Returns:
584
+ HistoricalData with price history
585
+ """
586
+ try:
587
+ resolved = self.resolve_symbol(symbol)
588
+ ticker = yf.Ticker(resolved)
589
+
590
+ # Fetch history
591
+ if start and end:
592
+ hist = ticker.history(start=start, end=end, interval=interval, **kwargs)
593
+ else:
594
+ hist = ticker.history(period=period, interval=interval, **kwargs)
595
+
596
+ # Convert to HistoricalPrice objects
597
+ prices = []
598
+ for date, row in hist.iterrows():
599
+ price = HistoricalPrice(
600
+ date=date.to_pydatetime(),
601
+ open=row.get('Open'),
602
+ high=row.get('High'),
603
+ low=row.get('Low'),
604
+ close=row.get('Close'),
605
+ volume=int(row.get('Volume')) if row.get('Volume') is not None else None,
606
+ adj_close=row.get('Adj Close') if 'Adj Close' in row else row.get('Close')
607
+ )
608
+ prices.append(price)
609
+
610
+ return HistoricalData(
611
+ symbol=symbol,
612
+ prices=prices,
613
+ period=period if not start else f"{start.date()} to {end.date() if end else 'now'}",
614
+ interval=interval
615
+ )
616
+
617
+ except Exception as e:
618
+ raise LLMError(f"Failed to get historical data for {symbol}: {e}") from e
619
+
620
+ async def get_historical_data_async(
621
+ self,
622
+ symbol: str,
623
+ period: str = "1mo",
624
+ interval: str = "1d",
625
+ start: datetime | None = None,
626
+ end: datetime | None = None,
627
+ **kwargs: Any,
628
+ ) -> HistoricalData:
629
+ """Async version of get_historical_data.
630
+
631
+ Args:
632
+ symbol: Ticker symbol
633
+ period: Time period
634
+ interval: Data interval
635
+ start: Start date
636
+ end: End date
637
+ **kwargs: Additional parameters
638
+
639
+ Returns:
640
+ HistoricalData object
641
+ """
642
+ loop = asyncio.get_event_loop()
643
+ return await loop.run_in_executor(
644
+ None,
645
+ lambda: self.get_historical_data(
646
+ symbol=symbol,
647
+ period=period,
648
+ interval=interval,
649
+ start=start,
650
+ end=end,
651
+ **kwargs
652
+ )
653
+ )
654
+
655
+ def get_asset_info(
656
+ self,
657
+ symbol: str,
658
+ include_profile: bool = True,
659
+ include_quote: bool = True,
660
+ include_history: bool = False,
661
+ history_period: str = "1mo",
662
+ **kwargs: Any,
663
+ ) -> dict[str, Any]:
664
+ """Get comprehensive information about an asset.
665
+
666
+ Args:
667
+ symbol: Ticker symbol
668
+ include_profile: Include profile information
669
+ include_quote: Include current quote
670
+ include_history: Include historical data
671
+ history_period: Period for historical data
672
+ **kwargs: Additional parameters
673
+
674
+ Returns:
675
+ Dictionary with requested information
676
+ """
677
+ result: dict[str, Any] = {"symbol": symbol}
678
+
679
+ try:
680
+ if include_quote:
681
+ result["quote"] = self.get_quote(symbol, **kwargs)
682
+
683
+ if include_profile:
684
+ result["profile"] = self.get_profile(symbol, **kwargs)
685
+
686
+ if include_history:
687
+ result["history"] = self.get_historical_data(
688
+ symbol,
689
+ period=history_period,
690
+ **kwargs
691
+ )
692
+
693
+ return result
694
+
695
+ except Exception as e:
696
+ raise LLMError(f"Failed to get asset info for {symbol}: {e}") from e
697
+
698
+ async def get_asset_info_async(
699
+ self,
700
+ symbol: str,
701
+ include_profile: bool = True,
702
+ include_quote: bool = True,
703
+ include_history: bool = False,
704
+ history_period: str = "1mo",
705
+ **kwargs: Any,
706
+ ) -> dict[str, Any]:
707
+ """Async version of get_asset_info.
708
+
709
+ Args:
710
+ symbol: Ticker symbol
711
+ include_profile: Include profile information
712
+ include_quote: Include current quote
713
+ include_history: Include historical data
714
+ history_period: Period for historical data
715
+ **kwargs: Additional parameters
716
+
717
+ Returns:
718
+ Dictionary with requested information
719
+ """
720
+ result: dict[str, Any] = {"symbol": symbol}
721
+
722
+ # Collect tasks for concurrent execution
723
+ tasks: list[Coroutine[Any, Any, Any]] = []
724
+ task_names: list[str] = []
725
+
726
+ if include_quote:
727
+ tasks.append(self.get_quote_async(symbol, **kwargs))
728
+ task_names.append("quote")
729
+
730
+ if include_profile:
731
+ tasks.append(self.get_profile_async(symbol, **kwargs))
732
+ task_names.append("profile")
733
+
734
+ if include_history:
735
+ tasks.append(self.get_historical_data_async(
736
+ symbol,
737
+ period=history_period,
738
+ **kwargs
739
+ ))
740
+ task_names.append("history")
741
+
742
+ # Execute all tasks concurrently
743
+ if tasks:
744
+ results = await asyncio.gather(*tasks)
745
+ for name, data in zip(task_names, results, strict=False):
746
+ result[name] = data
747
+
748
+ return result
749
+
750
+ def compare_assets(
751
+ self,
752
+ symbols: list[str],
753
+ period: str = "1mo",
754
+ **kwargs: Any,
755
+ ) -> dict[str, HistoricalData]:
756
+ """Get historical data for multiple assets for comparison.
757
+
758
+ Args:
759
+ symbols: List of ticker symbols
760
+ period: Time period
761
+ **kwargs: Additional parameters
762
+
763
+ Returns:
764
+ Dictionary mapping symbols to historical data
765
+ """
766
+ result = {}
767
+ for symbol in symbols:
768
+ try:
769
+ data = self.get_historical_data(symbol, period=period, **kwargs)
770
+ except Exception:
771
+ continue
772
+ if data.prices:
773
+ result[symbol] = data
774
+ return result
775
+
776
+ async def compare_assets_async(
777
+ self,
778
+ symbols: list[str],
779
+ period: str = "1mo",
780
+ max_concurrent: int = 5,
781
+ **kwargs: Any,
782
+ ) -> dict[str, HistoricalData]:
783
+ """Async version of compare_assets with concurrency control.
784
+
785
+ Args:
786
+ symbols: List of ticker symbols
787
+ period: Time period
788
+ max_concurrent: Maximum concurrent requests
789
+ **kwargs: Additional parameters
790
+
791
+ Returns:
792
+ Dictionary mapping symbols to historical data
793
+ """
794
+ semaphore = asyncio.Semaphore(max_concurrent)
795
+
796
+ async def fetch_history(symbol: str) -> tuple[str, HistoricalData | None]:
797
+ async with semaphore:
798
+ try:
799
+ data = await self.get_historical_data_async(symbol, period=period, **kwargs)
800
+ return symbol, data
801
+ except Exception:
802
+ return symbol, None
803
+
804
+ tasks = [fetch_history(symbol) for symbol in symbols]
805
+ results = await asyncio.gather(*tasks)
806
+
807
+ return {
808
+ symbol: data
809
+ for symbol, data in results
810
+ if data is not None and data.prices
811
+ }
812
+
813
+ def get_financial_metrics(
814
+ self,
815
+ symbol: str,
816
+ **kwargs: Any,
817
+ ) -> FinancialMetrics:
818
+ """Get comprehensive financial metrics and ratios.
819
+
820
+ Args:
821
+ symbol: Ticker symbol
822
+ **kwargs: Additional parameters
823
+
824
+ Returns:
825
+ FinancialMetrics with all available metrics
826
+ """
827
+ try:
828
+ resolved = self.resolve_symbol(symbol)
829
+ ticker = yf.Ticker(resolved)
830
+ info = ticker.info
831
+
832
+ return FinancialMetrics(
833
+ symbol=symbol,
834
+ # Valuation Ratios
835
+ pe_ratio=info.get('trailingPE') or info.get('forwardPE'),
836
+ forward_pe=info.get('forwardPE'),
837
+ peg_ratio=info.get('pegRatio'),
838
+ price_to_book=info.get('priceToBook'),
839
+ price_to_sales=info.get('priceToSalesTrailing12Months'),
840
+ enterprise_value=info.get('enterpriseValue'),
841
+ ev_to_revenue=info.get('enterpriseToRevenue'),
842
+ ev_to_ebitda=info.get('enterpriseToEbitda'),
843
+ # Profitability
844
+ profit_margin=info.get('profitMargins'),
845
+ operating_margin=info.get('operatingMargins'),
846
+ gross_margin=info.get('grossMargins'),
847
+ return_on_assets=info.get('returnOnAssets'),
848
+ return_on_equity=info.get('returnOnEquity'),
849
+ # Growth
850
+ revenue_growth=info.get('revenueGrowth'),
851
+ earnings_growth=info.get('earningsGrowth'),
852
+ # Financial Health
853
+ current_ratio=info.get('currentRatio'),
854
+ quick_ratio=info.get('quickRatio'),
855
+ debt_to_equity=info.get('debtToEquity'),
856
+ total_debt=info.get('totalDebt'),
857
+ total_cash=info.get('totalCash'),
858
+ # Per Share
859
+ book_value_per_share=info.get('bookValue'),
860
+ revenue_per_share=info.get('revenuePerShare'),
861
+ earnings_per_share=info.get('trailingEps'),
862
+ # Dividends
863
+ dividend_rate=info.get('dividendRate'),
864
+ dividend_yield=info.get('dividendYield'),
865
+ payout_ratio=info.get('payoutRatio'),
866
+ # Trading
867
+ beta=info.get('beta'),
868
+ shares_outstanding=info.get('sharesOutstanding'),
869
+ float_shares=info.get('floatShares'),
870
+ shares_short=info.get('sharesShort'),
871
+ short_ratio=info.get('shortRatio'),
872
+ )
873
+
874
+ except Exception as e:
875
+ raise LLMError(f"Failed to get financial metrics for {symbol}: {e}") from e
876
+
877
+ async def get_financial_metrics_async(
878
+ self,
879
+ symbol: str,
880
+ **kwargs: Any,
881
+ ) -> FinancialMetrics:
882
+ """Async version of get_financial_metrics."""
883
+ loop = asyncio.get_event_loop()
884
+ return await loop.run_in_executor(
885
+ None,
886
+ lambda: self.get_financial_metrics(symbol, **kwargs)
887
+ )
888
+
889
+ def get_financials(
890
+ self,
891
+ symbol: str,
892
+ quarterly: bool = False,
893
+ **kwargs: Any,
894
+ ) -> Fundamentals:
895
+ """Get comprehensive fundamental data including financial statements.
896
+
897
+ Args:
898
+ symbol: Ticker symbol
899
+ quarterly: Get quarterly data (default: annual)
900
+ **kwargs: Additional parameters
901
+
902
+ Returns:
903
+ Fundamentals object with all financial data
904
+ """
905
+ try:
906
+ resolved = self.resolve_symbol(symbol)
907
+ ticker = yf.Ticker(resolved)
908
+
909
+ # Get financial metrics
910
+ metrics = self.get_financial_metrics(symbol, **kwargs)
911
+
912
+ # Get income statements
913
+ income_statements = []
914
+ income_df = ticker.quarterly_financials if quarterly else ticker.financials
915
+ if income_df is not None and not income_df.empty:
916
+ for col in income_df.columns:
917
+ row_data = income_df[col]
918
+ stmt = IncomeStatement(
919
+ date=col.to_pydatetime(),
920
+ total_revenue=row_data.get('Total Revenue'),
921
+ cost_of_revenue=row_data.get('Cost Of Revenue'),
922
+ gross_profit=row_data.get('Gross Profit'),
923
+ operating_expense=row_data.get('Operating Expense'),
924
+ operating_income=row_data.get('Operating Income'),
925
+ net_income=row_data.get('Net Income'),
926
+ ebitda=row_data.get('EBITDA'),
927
+ basic_eps=row_data.get('Basic EPS'),
928
+ diluted_eps=row_data.get('Diluted EPS'),
929
+ )
930
+ income_statements.append(stmt)
931
+
932
+ # Get balance sheets
933
+ balance_sheets = []
934
+ balance_df = ticker.quarterly_balance_sheet if quarterly else ticker.balance_sheet
935
+ if balance_df is not None and not balance_df.empty:
936
+ for col in balance_df.columns:
937
+ row_data = balance_df[col]
938
+ sheet = BalanceSheet(
939
+ date=col.to_pydatetime(),
940
+ total_assets=row_data.get('Total Assets'),
941
+ current_assets=row_data.get('Current Assets'),
942
+ cash_and_equivalents=row_data.get('Cash And Cash Equivalents'),
943
+ total_liabilities=row_data.get('Total Liabilities Net Minority Interest'),
944
+ current_liabilities=row_data.get('Current Liabilities'),
945
+ long_term_debt=row_data.get('Long Term Debt'),
946
+ stockholders_equity=row_data.get('Stockholders Equity'),
947
+ retained_earnings=row_data.get('Retained Earnings'),
948
+ )
949
+ balance_sheets.append(sheet)
950
+
951
+ # Get cash flow statements
952
+ cash_flows = []
953
+ cashflow_df = ticker.quarterly_cashflow if quarterly else ticker.cashflow
954
+ if cashflow_df is not None and not cashflow_df.empty:
955
+ for col in cashflow_df.columns:
956
+ row_data = cashflow_df[col]
957
+ cf = CashFlowStatement(
958
+ date=col.to_pydatetime(),
959
+ operating_cash_flow=row_data.get('Operating Cash Flow'),
960
+ investing_cash_flow=row_data.get('Investing Cash Flow'),
961
+ financing_cash_flow=row_data.get('Financing Cash Flow'),
962
+ free_cash_flow=row_data.get('Free Cash Flow'),
963
+ capital_expenditures=row_data.get('Capital Expenditure'),
964
+ )
965
+ cash_flows.append(cf)
966
+
967
+ # Get earnings data
968
+ earnings_data = None
969
+ try:
970
+ earnings_dates = []
971
+ if hasattr(ticker, 'earnings_dates') and ticker.earnings_dates is not None:
972
+ for idx in ticker.earnings_dates.index:
973
+ if isinstance(idx, datetime):
974
+ earnings_dates.append(idx)
975
+
976
+ earnings_history = []
977
+ if hasattr(ticker, 'earnings_history') and ticker.earnings_history is not None:
978
+ earnings_history = ticker.earnings_history.to_dict('records')
979
+
980
+ earnings_estimates = {}
981
+ if hasattr(ticker, 'earnings_estimate') and ticker.earnings_estimate is not None:
982
+ earnings_estimates = ticker.earnings_estimate.to_dict()
983
+
984
+ earnings_data = EarningsData(
985
+ symbol=symbol,
986
+ earnings_dates=earnings_dates,
987
+ earnings_history=earnings_history,
988
+ earnings_estimates=earnings_estimates,
989
+ )
990
+ except Exception:
991
+ # Earnings data is optional
992
+ pass
993
+
994
+ return Fundamentals(
995
+ symbol=symbol,
996
+ metrics=metrics,
997
+ income_statements=income_statements,
998
+ balance_sheets=balance_sheets,
999
+ cash_flows=cash_flows,
1000
+ earnings=earnings_data,
1001
+ )
1002
+
1003
+ except Exception as e:
1004
+ raise LLMError(f"Failed to get financials for {symbol}: {e}") from e
1005
+
1006
+ async def get_financials_async(
1007
+ self,
1008
+ symbol: str,
1009
+ quarterly: bool = False,
1010
+ **kwargs: Any,
1011
+ ) -> Fundamentals:
1012
+ """Async version of get_financials."""
1013
+ loop = asyncio.get_event_loop()
1014
+ return await loop.run_in_executor(
1015
+ None,
1016
+ lambda: self.get_financials(symbol, quarterly=quarterly, **kwargs)
1017
+ )
1018
+
1019
+ def get_analyst_recommendations(
1020
+ self,
1021
+ symbol: str,
1022
+ **kwargs: Any,
1023
+ ) -> dict[str, Any]:
1024
+ """Get analyst recommendations and price targets.
1025
+
1026
+ Args:
1027
+ symbol: Ticker symbol
1028
+ **kwargs: Additional parameters
1029
+
1030
+ Returns:
1031
+ Dictionary with recommendations data
1032
+ """
1033
+ try:
1034
+ resolved = self.resolve_symbol(symbol)
1035
+ ticker = yf.Ticker(resolved)
1036
+ info = ticker.info
1037
+
1038
+ recommendations = {
1039
+ "symbol": symbol,
1040
+ "target_high_price": info.get('targetHighPrice'),
1041
+ "target_low_price": info.get('targetLowPrice'),
1042
+ "target_mean_price": info.get('targetMeanPrice'),
1043
+ "target_median_price": info.get('targetMedianPrice'),
1044
+ "recommendation_key": info.get('recommendationKey'),
1045
+ "recommendation_mean": info.get('recommendationMean'),
1046
+ "number_of_analyst_opinions": info.get('numberOfAnalystOpinions'),
1047
+ }
1048
+
1049
+ # Get historical recommendations if available
1050
+ if hasattr(ticker, 'recommendations') and ticker.recommendations is not None:
1051
+ recommendations["history"] = ticker.recommendations.to_dict('records')
1052
+
1053
+ return recommendations
1054
+
1055
+ except Exception as e:
1056
+ raise LLMError(f"Failed to get analyst recommendations for {symbol}: {e}") from e
1057
+
1058
+ async def get_analyst_recommendations_async(
1059
+ self,
1060
+ symbol: str,
1061
+ **kwargs: Any,
1062
+ ) -> dict[str, Any]:
1063
+ """Async version of get_analyst_recommendations."""
1064
+ loop = asyncio.get_event_loop()
1065
+ return await loop.run_in_executor(
1066
+ None,
1067
+ lambda: self.get_analyst_recommendations(symbol, **kwargs)
1068
+ )
1069
+
1070
+ def get_institutional_holders(
1071
+ self,
1072
+ symbol: str,
1073
+ **kwargs: Any,
1074
+ ) -> dict[str, Any]:
1075
+ """Get institutional holders information.
1076
+
1077
+ Args:
1078
+ symbol: Ticker symbol
1079
+ **kwargs: Additional parameters
1080
+
1081
+ Returns:
1082
+ Dictionary with institutional holders data
1083
+ """
1084
+ try:
1085
+ resolved = self.resolve_symbol(symbol)
1086
+ ticker = yf.Ticker(resolved)
1087
+
1088
+ result = {"symbol": symbol}
1089
+
1090
+ if hasattr(ticker, 'institutional_holders') and ticker.institutional_holders is not None:
1091
+ result["institutional_holders"] = ticker.institutional_holders.to_dict('records')
1092
+
1093
+ if hasattr(ticker, 'major_holders') and ticker.major_holders is not None:
1094
+ result["major_holders"] = ticker.major_holders.to_dict()
1095
+
1096
+ if hasattr(ticker, 'mutualfund_holders') and ticker.mutualfund_holders is not None:
1097
+ result["mutualfund_holders"] = ticker.mutualfund_holders.to_dict('records')
1098
+
1099
+ return result
1100
+
1101
+ except Exception as e:
1102
+ raise LLMError(f"Failed to get institutional holders for {symbol}: {e}") from e
1103
+
1104
+ async def get_institutional_holders_async(
1105
+ self,
1106
+ symbol: str,
1107
+ **kwargs: Any,
1108
+ ) -> dict[str, Any]:
1109
+ """Async version of get_institutional_holders."""
1110
+ loop = asyncio.get_event_loop()
1111
+ return await loop.run_in_executor(
1112
+ None,
1113
+ lambda: self.get_institutional_holders(symbol, **kwargs)
1114
+ )
1115
+
1116
+ def get_all_data(
1117
+ self,
1118
+ symbol: str,
1119
+ include_financials: bool = True,
1120
+ include_recommendations: bool = True,
1121
+ include_holders: bool = True,
1122
+ quarterly: bool = False,
1123
+ **kwargs: Any,
1124
+ ) -> dict[str, Any]:
1125
+ """Get all available data for comprehensive fundamental analysis.
1126
+
1127
+ Args:
1128
+ symbol: Ticker symbol
1129
+ include_financials: Include financial statements
1130
+ include_recommendations: Include analyst recommendations
1131
+ include_holders: Include institutional holders
1132
+ quarterly: Use quarterly data for financials
1133
+ **kwargs: Additional parameters
1134
+
1135
+ Returns:
1136
+ Dictionary with all available data
1137
+ """
1138
+ result = {
1139
+ "symbol": symbol,
1140
+ "quote": self.get_quote(symbol, **kwargs),
1141
+ "profile": self.get_profile(symbol, **kwargs),
1142
+ }
1143
+
1144
+ if include_financials:
1145
+ result["fundamentals"] = self.get_financials(symbol, quarterly=quarterly, **kwargs)
1146
+
1147
+ if include_recommendations:
1148
+ result["analyst_recommendations"] = self.get_analyst_recommendations(symbol, **kwargs)
1149
+
1150
+ if include_holders:
1151
+ result["institutional_holders"] = self.get_institutional_holders(symbol, **kwargs)
1152
+
1153
+ return result
1154
+
1155
+ async def get_all_data_async(
1156
+ self,
1157
+ symbol: str,
1158
+ include_financials: bool = True,
1159
+ include_recommendations: bool = True,
1160
+ include_holders: bool = True,
1161
+ quarterly: bool = False,
1162
+ **kwargs: Any,
1163
+ ) -> dict[str, Any]:
1164
+ """Async version of get_all_data with concurrent fetching.
1165
+
1166
+ Args:
1167
+ symbol: Ticker symbol
1168
+ include_financials: Include financial statements
1169
+ include_recommendations: Include analyst recommendations
1170
+ include_holders: Include institutional holders
1171
+ quarterly: Use quarterly data for financials
1172
+ **kwargs: Additional parameters
1173
+
1174
+ Returns:
1175
+ Dictionary with all available data
1176
+ """
1177
+ # Fetch core data concurrently
1178
+ tasks: list[Coroutine[Any, Any, Any]] = []
1179
+ task_names: list[str] = []
1180
+
1181
+ tasks.append(self.get_quote_async(symbol, **kwargs))
1182
+ task_names.append("quote")
1183
+
1184
+ tasks.append(self.get_profile_async(symbol, **kwargs))
1185
+ task_names.append("profile")
1186
+
1187
+ if include_financials:
1188
+ tasks.append(self.get_financials_async(symbol, quarterly=quarterly, **kwargs))
1189
+ task_names.append("fundamentals")
1190
+
1191
+ if include_recommendations:
1192
+ tasks.append(self.get_analyst_recommendations_async(symbol, **kwargs))
1193
+ task_names.append("analyst_recommendations")
1194
+
1195
+ if include_holders:
1196
+ tasks.append(self.get_institutional_holders_async(symbol, **kwargs))
1197
+ task_names.append("institutional_holders")
1198
+
1199
+ # Execute all tasks concurrently
1200
+ results = await asyncio.gather(*tasks)
1201
+
1202
+ # Build result dictionary
1203
+ result: dict[str, Any] = {"symbol": symbol}
1204
+ for name, data in zip(task_names, results, strict=False):
1205
+ result[name] = data
1206
+
1207
+ return result
1208
+
1209
+
1210
+ # Agent Tools
1211
+ # ---------------------------------------------------------------------------
1212
+
1213
+ def finance_tools() -> ToolRegistry:
1214
+ """Create Yahoo Finance tools for agents.
1215
+
1216
+ These tools are returned as plain callables. Framework-specific backends
1217
+ adapt them when needed.
1218
+
1219
+ Returns:
1220
+ Dict of plain tool callables ready for adaptation by agent frameworks
1221
+
1222
+ Example:
1223
+ >>> tools = finance_tools()
1224
+ >>> agent = AgentBuilder("FinanceAnalyst").with_tools(tools).build()
1225
+ """
1226
+
1227
+ # Create searcher instance
1228
+ searcher = YahooFinanceSearcher()
1229
+ def search_asset(query: str, max_results: int = 10) -> SearchResults:
1230
+ """Search for financial assets by name or ticker symbol.
1231
+
1232
+ Args:
1233
+ query: Company name or ticker symbol to search for
1234
+ max_results: Maximum number of results to return
1235
+
1236
+ Returns:
1237
+ SearchResults object containing matching assets
1238
+ """
1239
+ return searcher.search(query, max_results=max_results)
1240
+ def get_asset_quote(symbol: str) -> AssetQuote:
1241
+ """Get current quote and market data for a financial asset.
1242
+
1243
+ Args:
1244
+ symbol: Ticker symbol (e.g., 'AAPL', 'MSFT', 'GOOGL')
1245
+
1246
+ Returns:
1247
+ AssetQuote with current price, volume, market cap, and other market data
1248
+ """
1249
+ return searcher.get_quote(symbol)
1250
+ def get_asset_profile(symbol: str) -> AssetProfile:
1251
+ """Get detailed company profile information.
1252
+
1253
+ Args:
1254
+ symbol: Ticker symbol
1255
+
1256
+ Returns:
1257
+ AssetProfile with company description, sector, industry, website, and business details
1258
+ """
1259
+ return searcher.get_profile(symbol)
1260
+ def get_financial_metrics(symbol: str) -> FinancialMetrics:
1261
+ """Get comprehensive financial metrics and ratios.
1262
+
1263
+ Includes valuation ratios (P/E, P/B, PEG), profitability metrics (ROE, ROA, margins),
1264
+ growth metrics, financial health indicators, and trading metrics.
1265
+
1266
+ Args:
1267
+ symbol: Ticker symbol
1268
+
1269
+ Returns:
1270
+ FinancialMetrics with all available financial ratios and metrics
1271
+ """
1272
+ return searcher.get_financial_metrics(symbol)
1273
+ def get_income_statement(symbol: str, quarterly: bool = False) -> str:
1274
+ """Get income statement data (revenue, expenses, profit).
1275
+
1276
+ Args:
1277
+ symbol: Ticker symbol
1278
+ quarterly: If True, get quarterly data; if False, get annual data
1279
+
1280
+ Returns:
1281
+ Formatted string with income statement data from recent periods
1282
+ """
1283
+ fundamentals = searcher.get_financials(symbol, quarterly=quarterly)
1284
+ if not fundamentals.income_statements:
1285
+ return f"No income statement data available for {symbol}"
1286
+
1287
+ lines = [f"Income Statements for {symbol} ({'Quarterly' if quarterly else 'Annual'}):"]
1288
+ for stmt in fundamentals.income_statements[:4]: # Show last 4 periods
1289
+ lines.append(f"\nPeriod ending {stmt.date.date()}:")
1290
+ if stmt.total_revenue:
1291
+ lines.append(f" Total Revenue: ${stmt.total_revenue:,.0f}")
1292
+ if stmt.gross_profit:
1293
+ lines.append(f" Gross Profit: ${stmt.gross_profit:,.0f}")
1294
+ if stmt.operating_income:
1295
+ lines.append(f" Operating Income: ${stmt.operating_income:,.0f}")
1296
+ if stmt.net_income:
1297
+ lines.append(f" Net Income: ${stmt.net_income:,.0f}")
1298
+ if stmt.diluted_eps:
1299
+ lines.append(f" Diluted EPS: ${stmt.diluted_eps:.2f}")
1300
+
1301
+ return "\n".join(lines)
1302
+ def get_balance_sheet(symbol: str, quarterly: bool = False) -> str:
1303
+ """Get balance sheet data (assets, liabilities, equity).
1304
+
1305
+ Args:
1306
+ symbol: Ticker symbol
1307
+ quarterly: If True, get quarterly data; if False, get annual data
1308
+
1309
+ Returns:
1310
+ Formatted string with balance sheet data from recent periods
1311
+ """
1312
+ fundamentals = searcher.get_financials(symbol, quarterly=quarterly)
1313
+ if not fundamentals.balance_sheets:
1314
+ return f"No balance sheet data available for {symbol}"
1315
+
1316
+ lines = [f"Balance Sheets for {symbol} ({'Quarterly' if quarterly else 'Annual'}):"]
1317
+ for sheet in fundamentals.balance_sheets[:4]: # Show last 4 periods
1318
+ lines.append(f"\nPeriod ending {sheet.date.date()}:")
1319
+ if sheet.total_assets:
1320
+ lines.append(f" Total Assets: ${sheet.total_assets:,.0f}")
1321
+ if sheet.current_assets:
1322
+ lines.append(f" Current Assets: ${sheet.current_assets:,.0f}")
1323
+ if sheet.cash_and_equivalents:
1324
+ lines.append(f" Cash & Equivalents: ${sheet.cash_and_equivalents:,.0f}")
1325
+ if sheet.total_liabilities:
1326
+ lines.append(f" Total Liabilities: ${sheet.total_liabilities:,.0f}")
1327
+ if sheet.long_term_debt:
1328
+ lines.append(f" Long-term Debt: ${sheet.long_term_debt:,.0f}")
1329
+ if sheet.stockholders_equity:
1330
+ lines.append(f" Stockholders' Equity: ${sheet.stockholders_equity:,.0f}")
1331
+
1332
+ return "\n".join(lines)
1333
+ def get_cash_flow(symbol: str, quarterly: bool = False) -> str:
1334
+ """Get cash flow statement data (operating, investing, financing cash flows).
1335
+
1336
+ Args:
1337
+ symbol: Ticker symbol
1338
+ quarterly: If True, get quarterly data; if False, get annual data
1339
+
1340
+ Returns:
1341
+ Formatted string with cash flow statement data from recent periods
1342
+ """
1343
+ fundamentals = searcher.get_financials(symbol, quarterly=quarterly)
1344
+ if not fundamentals.cash_flows:
1345
+ return f"No cash flow data available for {symbol}"
1346
+
1347
+ lines = [f"Cash Flow Statements for {symbol} ({'Quarterly' if quarterly else 'Annual'}):"]
1348
+ for cf in fundamentals.cash_flows[:4]: # Show last 4 periods
1349
+ lines.append(f"\nPeriod ending {cf.date.date()}:")
1350
+ if cf.operating_cash_flow:
1351
+ lines.append(f" Operating Cash Flow: ${cf.operating_cash_flow:,.0f}")
1352
+ if cf.investing_cash_flow:
1353
+ lines.append(f" Investing Cash Flow: ${cf.investing_cash_flow:,.0f}")
1354
+ if cf.financing_cash_flow:
1355
+ lines.append(f" Financing Cash Flow: ${cf.financing_cash_flow:,.0f}")
1356
+ if cf.free_cash_flow:
1357
+ lines.append(f" Free Cash Flow: ${cf.free_cash_flow:,.0f}")
1358
+ if cf.capital_expenditures:
1359
+ lines.append(f" Capital Expenditures: ${cf.capital_expenditures:,.0f}")
1360
+
1361
+ return "\n".join(lines)
1362
+ def get_analyst_recommendations(symbol: str) -> str:
1363
+ """Get analyst recommendations and price targets.
1364
+
1365
+ Args:
1366
+ symbol: Ticker symbol
1367
+
1368
+ Returns:
1369
+ Formatted string with analyst recommendations and price targets
1370
+ """
1371
+ recs = searcher.get_analyst_recommendations(symbol)
1372
+
1373
+ lines = [f"Analyst Recommendations for {symbol}:"]
1374
+ if recs.get('recommendation_key'):
1375
+ lines.append(f" Recommendation: {recs['recommendation_key'].upper()}")
1376
+ if recs.get('recommendation_mean'):
1377
+ lines.append(f" Recommendation Mean: {recs['recommendation_mean']:.2f} (1=Strong Buy, 5=Sell)")
1378
+ if recs.get('number_of_analyst_opinions'):
1379
+ lines.append(f" Number of Analysts: {recs['number_of_analyst_opinions']}")
1380
+
1381
+ lines.append("\nPrice Targets:")
1382
+ if recs.get('target_mean_price'):
1383
+ lines.append(f" Mean: ${recs['target_mean_price']:.2f}")
1384
+ if recs.get('target_median_price'):
1385
+ lines.append(f" Median: ${recs['target_median_price']:.2f}")
1386
+ if recs.get('target_high_price'):
1387
+ lines.append(f" High: ${recs['target_high_price']:.2f}")
1388
+ if recs.get('target_low_price'):
1389
+ lines.append(f" Low: ${recs['target_low_price']:.2f}")
1390
+
1391
+ if recs.get('history'):
1392
+ lines.append(f"\nRecent Recommendations History: {len(recs['history'])} entries available")
1393
+
1394
+ return "\n".join(lines)
1395
+ def get_technical_indicators(
1396
+ symbol: str,
1397
+ period: str = "6mo",
1398
+ interval: str = "1d",
1399
+ rsi_period: int = 14,
1400
+ macd_fast: int = 12,
1401
+ macd_slow: int = 26,
1402
+ macd_signal: int = 9,
1403
+ sma_period: int = 20,
1404
+ ema_period: int = 20,
1405
+ bollinger_window: int = 20,
1406
+ bollinger_dev: float = 2.0,
1407
+ stochastic_window: int = 14,
1408
+ stochastic_smooth_window: int = 3,
1409
+ atr_window: int = 14,
1410
+ adx_window: int = 14,
1411
+ include_chart: bool = True,
1412
+ chart_image_format: str = "png",
1413
+ ) -> dict[str, Any]:
1414
+ """Compute technical indicators and optional chart asset for a ticker.
1415
+
1416
+ Args:
1417
+ symbol: Ticker symbol (e.g., AAPL).
1418
+ period: Yahoo period (e.g., 1mo, 6mo, 1y).
1419
+ interval: Yahoo interval (e.g., 1d, 1wk).
1420
+ include_chart: Include chart assets compatible with canvas-chart UI flow.
1421
+
1422
+ Returns:
1423
+ Dictionary with indicator snapshot, aligned series data, and optional chart assets.
1424
+ """
1425
+ history = searcher.get_historical_data(symbol, period=period, interval=interval)
1426
+ frame = history.to_dataframe()
1427
+ indicators_df, snapshot = compute_technical_indicators(
1428
+ frame,
1429
+ rsi_period=rsi_period,
1430
+ macd_fast=macd_fast,
1431
+ macd_slow=macd_slow,
1432
+ macd_signal=macd_signal,
1433
+ sma_period=sma_period,
1434
+ ema_period=ema_period,
1435
+ bollinger_window=bollinger_window,
1436
+ bollinger_dev=bollinger_dev,
1437
+ stochastic_window=stochastic_window,
1438
+ stochastic_smooth_window=stochastic_smooth_window,
1439
+ atr_window=atr_window,
1440
+ adx_window=adx_window,
1441
+ )
1442
+ series_rows = _serialize_indicator_rows(indicators_df)
1443
+
1444
+ result: dict[str, Any] = {
1445
+ "symbol": symbol,
1446
+ "period": period,
1447
+ "interval": interval,
1448
+ "talib_available": talib_available(),
1449
+ "snapshot": snapshot.model_dump(),
1450
+ "indicators": series_rows,
1451
+ }
1452
+ if include_chart:
1453
+ chart_assets = technical_chart_assets(
1454
+ history,
1455
+ title=f"{symbol} Technical Indicators ({period}, {interval})",
1456
+ image_format=chart_image_format,
1457
+ rsi_period=rsi_period,
1458
+ macd_fast=macd_fast,
1459
+ macd_slow=macd_slow,
1460
+ macd_signal=macd_signal,
1461
+ sma_period=sma_period,
1462
+ ema_period=ema_period,
1463
+ bollinger_window=bollinger_window,
1464
+ bollinger_dev=bollinger_dev,
1465
+ stochastic_window=stochastic_window,
1466
+ stochastic_smooth_window=stochastic_smooth_window,
1467
+ atr_window=atr_window,
1468
+ adx_window=adx_window,
1469
+ )
1470
+ result["chart"] = chart_assets
1471
+ return result
1472
+ def get_technical_chart(
1473
+ symbol: str,
1474
+ period: str = "6mo",
1475
+ interval: str = "1d",
1476
+ image_format: str = "png",
1477
+ include_ohlc_fallback: bool = False,
1478
+ rsi_period: int = 14,
1479
+ macd_fast: int = 12,
1480
+ macd_slow: int = 26,
1481
+ macd_signal: int = 9,
1482
+ sma_period: int = 20,
1483
+ ema_period: int = 20,
1484
+ bollinger_window: int = 20,
1485
+ bollinger_dev: float = 2.0,
1486
+ stochastic_window: int = 14,
1487
+ stochastic_smooth_window: int = 3,
1488
+ atr_window: int = 14,
1489
+ adx_window: int = 14,
1490
+ ) -> dict[str, Any]:
1491
+ """Render a technical analysis chart payload compatible with canvas-chart blocks."""
1492
+ history = searcher.get_historical_data(symbol, period=period, interval=interval)
1493
+ return technical_chart_assets(
1494
+ history,
1495
+ title=f"{symbol} Technical Indicators ({period}, {interval})",
1496
+ image_format=image_format,
1497
+ include_ohlc_fallback=include_ohlc_fallback,
1498
+ rsi_period=rsi_period,
1499
+ macd_fast=macd_fast,
1500
+ macd_slow=macd_slow,
1501
+ macd_signal=macd_signal,
1502
+ sma_period=sma_period,
1503
+ ema_period=ema_period,
1504
+ bollinger_window=bollinger_window,
1505
+ bollinger_dev=bollinger_dev,
1506
+ stochastic_window=stochastic_window,
1507
+ stochastic_smooth_window=stochastic_smooth_window,
1508
+ atr_window=atr_window,
1509
+ adx_window=adx_window,
1510
+ )
1511
+
1512
+ return ToolRegistry.from_mapping({
1513
+ "search_asset": search_asset,
1514
+ "get_asset_quote": get_asset_quote,
1515
+ "get_asset_profile": get_asset_profile,
1516
+ "get_financial_metrics": get_financial_metrics,
1517
+ "get_income_statement": get_income_statement,
1518
+ "get_balance_sheet": get_balance_sheet,
1519
+ "get_cash_flow": get_cash_flow,
1520
+ "get_analyst_recommendations": get_analyst_recommendations,
1521
+ "get_technical_indicators": get_technical_indicators,
1522
+ "get_technical_chart": get_technical_chart,
1523
+ })