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.
- ellements/__init__.py +57 -0
- ellements/agents/__init__.py +45 -0
- ellements/agents/backend.py +100 -0
- ellements/agents/builder.py +303 -0
- ellements/agents/claude_backend.py +200 -0
- ellements/agents/controller.py +237 -0
- ellements/agents/openai_backend.py +187 -0
- ellements/agents/py.typed +0 -0
- ellements/agents/runner.py +358 -0
- ellements/agents/tools.py +30 -0
- ellements/benchmarking/__init__.py +52 -0
- ellements/benchmarking/harness.py +342 -0
- ellements/benchmarking/py.typed +0 -0
- ellements/benchmarking/results.py +173 -0
- ellements/cli/__init__.py +80 -0
- ellements/cli/adapters.py +73 -0
- ellements/cli/agent_tui.py +1178 -0
- ellements/cli/components.py +411 -0
- ellements/cli/printer.py +229 -0
- ellements/cli/py.typed +0 -0
- ellements/core/__init__.py +112 -0
- ellements/core/async_utils.py +42 -0
- ellements/core/budgeting/__init__.py +43 -0
- ellements/core/budgeting/client.py +276 -0
- ellements/core/budgeting/protocol.py +51 -0
- ellements/core/budgeting/trackers.py +177 -0
- ellements/core/caching/__init__.py +51 -0
- ellements/core/caching/cache.py +73 -0
- ellements/core/caching/client.py +300 -0
- ellements/core/caching/disk.py +128 -0
- ellements/core/caching/keys.py +97 -0
- ellements/core/caching/memory.py +104 -0
- ellements/core/chunking.py +262 -0
- ellements/core/config.py +180 -0
- ellements/core/exceptions.py +145 -0
- ellements/core/llm/__init__.py +46 -0
- ellements/core/llm/client.py +1124 -0
- ellements/core/llm/images.py +226 -0
- ellements/core/llm/messages.py +202 -0
- ellements/core/llm/model_params.py +66 -0
- ellements/core/llm/protocol.py +146 -0
- ellements/core/llm/requests.py +100 -0
- ellements/core/llm/structured.py +91 -0
- ellements/core/llm/wrapper.py +79 -0
- ellements/core/observability/__init__.py +39 -0
- ellements/core/observability/events.py +169 -0
- ellements/core/observability/jsonl_logger.py +244 -0
- ellements/core/observability/markdown_formatter.py +197 -0
- ellements/core/observability/observer.py +56 -0
- ellements/core/prompting/__init__.py +14 -0
- ellements/core/prompting/context.py +185 -0
- ellements/core/prompting/guideline.py +133 -0
- ellements/core/prompting/persona.py +267 -0
- ellements/core/prompting/sources.py +92 -0
- ellements/core/py.typed +0 -0
- ellements/core/rate_limit/__init__.py +44 -0
- ellements/core/rate_limit/bucket.py +85 -0
- ellements/core/rate_limit/client.py +216 -0
- ellements/core/rate_limit/protocol.py +27 -0
- ellements/core/templating.py +126 -0
- ellements/core/tools/__init__.py +51 -0
- ellements/core/tools/dialects.py +136 -0
- ellements/core/tools/executor.py +48 -0
- ellements/core/tools/protocol.py +36 -0
- ellements/core/tools/records.py +28 -0
- ellements/core/tools/registry.py +205 -0
- ellements/core/tools/schemas.py +80 -0
- ellements/core/tools/simple.py +119 -0
- ellements/core/tools/spec.py +33 -0
- ellements/domain_specific/__init__.py +7 -0
- ellements/domain_specific/finance/__init__.py +188 -0
- ellements/domain_specific/finance/calculations.py +837 -0
- ellements/domain_specific/finance/charts.py +426 -0
- ellements/domain_specific/finance/portfolio.py +129 -0
- ellements/domain_specific/finance/quant_analysis.py +279 -0
- ellements/domain_specific/finance/risk.py +362 -0
- ellements/domain_specific/finance/technical_indicators.py +241 -0
- ellements/domain_specific/finance/tools.py +483 -0
- ellements/domain_specific/finance/valuation.py +312 -0
- ellements/domain_specific/finance/yahoo_finance.py +1523 -0
- ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
- ellements/domain_specific/py.typed +0 -0
- ellements/execution/__init__.py +56 -0
- ellements/execution/callbacks.py +149 -0
- ellements/execution/catalog.py +70 -0
- ellements/execution/collaborative.py +191 -0
- ellements/execution/config.py +135 -0
- ellements/execution/py.typed +0 -0
- ellements/execution/reflection.py +156 -0
- ellements/execution/self_consistency.py +189 -0
- ellements/execution/single_call.py +48 -0
- ellements/execution/strategies.py +237 -0
- ellements/execution/tree_of_thought.py +541 -0
- ellements/fslm/__init__.py +108 -0
- ellements/fslm/builtins.py +103 -0
- ellements/fslm/cli.py +199 -0
- ellements/fslm/context.py +50 -0
- ellements/fslm/definition.py +163 -0
- ellements/fslm/det.py +173 -0
- ellements/fslm/dsl.py +458 -0
- ellements/fslm/errors.py +38 -0
- ellements/fslm/evaluators.py +141 -0
- ellements/fslm/kernel.py +603 -0
- ellements/fslm/loading.py +123 -0
- ellements/fslm/models.py +623 -0
- ellements/fslm/nl.py +87 -0
- ellements/fslm/observers.py +107 -0
- ellements/fslm/persistence.py +72 -0
- ellements/fslm/py.typed +0 -0
- ellements/fslm/rendering.py +551 -0
- ellements/fslm/visualization.py +25 -0
- ellements/reporting/__init__.py +12 -0
- ellements/reporting/charts.py +364 -0
- ellements/reporting/html_generation.py +132 -0
- ellements/reporting/py.typed +0 -0
- ellements/reporting/visualization.py +73 -0
- ellements/standard_tools/__init__.py +22 -0
- ellements/standard_tools/py.typed +0 -0
- ellements/standard_tools/terminal.py +205 -0
- ellements/standard_tools/web/__init__.py +37 -0
- ellements/standard_tools/web/browser_viewer.py +214 -0
- ellements/standard_tools/web/crawler.py +454 -0
- ellements/standard_tools/web/search.py +399 -0
- ellements/standard_tools/web/youtube.py +1297 -0
- ellements-0.2.0.dist-info/METADATA +368 -0
- ellements-0.2.0.dist-info/RECORD +130 -0
- ellements-0.2.0.dist-info/WHEEL +5 -0
- ellements-0.2.0.dist-info/entry_points.txt +2 -0
- ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
- 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
|
+
})
|