parsimony-eodhd 0.4.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.
- parsimony_eodhd/__init__.py +479 -0
- parsimony_eodhd/_http.py +172 -0
- parsimony_eodhd/outputs.py +193 -0
- parsimony_eodhd/params.py +284 -0
- parsimony_eodhd/py.typed +0 -0
- parsimony_eodhd-0.4.0.dist-info/METADATA +118 -0
- parsimony_eodhd-0.4.0.dist-info/RECORD +10 -0
- parsimony_eodhd-0.4.0.dist-info/WHEEL +4 -0
- parsimony_eodhd-0.4.0.dist-info/entry_points.txt +2 -0
- parsimony_eodhd-0.4.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"""EODHD source: typed connectors per endpoint.
|
|
2
|
+
|
|
3
|
+
API docs: https://eodhd.com/financial-apis/api-for-historical-data-and-volumes/
|
|
4
|
+
Authentication: API token via ``?api_token=<key>`` query param.
|
|
5
|
+
Base URL: https://eodhd.com/api
|
|
6
|
+
|
|
7
|
+
Provides 17 connectors covering the full EODHD REST surface:
|
|
8
|
+
- Market data: EOD prices, live quotes, intraday, bulk EOD
|
|
9
|
+
- Corporate actions: dividends, splits
|
|
10
|
+
- Reference: search, exchanges, exchange symbol lists
|
|
11
|
+
- Fundamentals (raw dict — nested JSON blob)
|
|
12
|
+
- Calendars: earnings, IPO, trends
|
|
13
|
+
- News
|
|
14
|
+
- Macro indicators
|
|
15
|
+
- Technical indicators
|
|
16
|
+
- Insider transactions
|
|
17
|
+
- Screener
|
|
18
|
+
|
|
19
|
+
Internal layout (not part of the public contract):
|
|
20
|
+
|
|
21
|
+
* :mod:`parsimony_eodhd._http` — shared transport, unified error mapping,
|
|
22
|
+
URL redaction, Retry-After parsing, JSON fetch helper.
|
|
23
|
+
* :mod:`parsimony_eodhd.params` — Pydantic parameter models.
|
|
24
|
+
* :mod:`parsimony_eodhd.outputs` — declarative :class:`OutputConfig` schemas.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from parsimony.connector import Connectors, connector
|
|
33
|
+
from parsimony.result import Result
|
|
34
|
+
|
|
35
|
+
from parsimony_eodhd._http import eodhd_fetch as _eodhd_fetch
|
|
36
|
+
from parsimony_eodhd._http import make_http as _make_http
|
|
37
|
+
from parsimony_eodhd.outputs import BULK_EOD_OUTPUT as _BULK_EOD_OUTPUT
|
|
38
|
+
from parsimony_eodhd.outputs import CALENDAR_OUTPUT as _CALENDAR_OUTPUT
|
|
39
|
+
from parsimony_eodhd.outputs import DIVIDENDS_OUTPUT as _DIVIDENDS_OUTPUT
|
|
40
|
+
from parsimony_eodhd.outputs import EOD_OUTPUT as _EOD_OUTPUT
|
|
41
|
+
from parsimony_eodhd.outputs import EXCHANGE_SYMBOLS_OUTPUT as _EXCHANGE_SYMBOLS_OUTPUT
|
|
42
|
+
from parsimony_eodhd.outputs import EXCHANGES_OUTPUT as _EXCHANGES_OUTPUT
|
|
43
|
+
from parsimony_eodhd.outputs import INSIDER_OUTPUT as _INSIDER_OUTPUT
|
|
44
|
+
from parsimony_eodhd.outputs import INTRADAY_OUTPUT as _INTRADAY_OUTPUT
|
|
45
|
+
from parsimony_eodhd.outputs import LIVE_OUTPUT as _LIVE_OUTPUT
|
|
46
|
+
from parsimony_eodhd.outputs import MACRO_OUTPUT as _MACRO_OUTPUT
|
|
47
|
+
from parsimony_eodhd.outputs import NEWS_OUTPUT as _NEWS_OUTPUT
|
|
48
|
+
from parsimony_eodhd.outputs import SCREENER_OUTPUT as _SCREENER_OUTPUT
|
|
49
|
+
from parsimony_eodhd.outputs import SEARCH_OUTPUT as _SEARCH_OUTPUT
|
|
50
|
+
from parsimony_eodhd.outputs import SPLITS_OUTPUT as _SPLITS_OUTPUT
|
|
51
|
+
from parsimony_eodhd.outputs import TECHNICAL_OUTPUT as _TECHNICAL_OUTPUT
|
|
52
|
+
from parsimony_eodhd.params import (
|
|
53
|
+
EodhdBulkEodParams,
|
|
54
|
+
EodhdCalendarParams,
|
|
55
|
+
EodhdDividendsParams,
|
|
56
|
+
EodhdEodParams,
|
|
57
|
+
EodhdExchangesParams,
|
|
58
|
+
EodhdExchangeSymbolsParams,
|
|
59
|
+
EodhdFundamentalsParams,
|
|
60
|
+
EodhdInsiderParams,
|
|
61
|
+
EodhdIntradayParams,
|
|
62
|
+
EodhdLiveParams,
|
|
63
|
+
EodhdMacroBulkParams,
|
|
64
|
+
EodhdMacroParams,
|
|
65
|
+
EodhdNewsParams,
|
|
66
|
+
EodhdScreenerParams,
|
|
67
|
+
EodhdSearchParams,
|
|
68
|
+
EodhdSplitsParams,
|
|
69
|
+
EodhdTechnicalParams,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
_LATENCY_TIMEOUT: float = 10.0
|
|
73
|
+
_BULK_TIMEOUT: float = 60.0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Market Data — Connectors
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_EOD_OUTPUT, tags=["eodhd", "equity"])
|
|
82
|
+
async def eodhd_eod(params: EodhdEodParams, *, api_key: str) -> Result:
|
|
83
|
+
"""[Free+] Fetch end-of-day OHLCV prices for a ticker. Supports daily, weekly, and monthly
|
|
84
|
+
aggregation. Use from/to to limit the date range (ISO 8601). Empty result may indicate an
|
|
85
|
+
invalid ticker or exchange code — verify with eodhd_search first."""
|
|
86
|
+
http = _make_http(api_key)
|
|
87
|
+
p: dict[str, Any] = {"ticker": params.ticker}
|
|
88
|
+
if params.from_date:
|
|
89
|
+
p["from"] = params.from_date
|
|
90
|
+
if params.to_date:
|
|
91
|
+
p["to"] = params.to_date
|
|
92
|
+
if params.period:
|
|
93
|
+
p["period"] = params.period
|
|
94
|
+
return await _eodhd_fetch(http, path="/eod/{ticker}", params=p, op_name="eodhd_eod", output_config=_EOD_OUTPUT)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_LIVE_OUTPUT, tags=["eodhd", "equity", "tool"])
|
|
98
|
+
async def eodhd_live(params: EodhdLiveParams, *, api_key: str) -> Result:
|
|
99
|
+
"""[Free+] Fetch live (real-time or 15-min delayed) quote for a ticker. Use eodhd_search
|
|
100
|
+
to resolve a company name to its EODHD ticker format (e.g. AAPL.US)."""
|
|
101
|
+
http = _make_http(api_key, timeout=_LATENCY_TIMEOUT)
|
|
102
|
+
return await _eodhd_fetch(
|
|
103
|
+
http,
|
|
104
|
+
path="/real-time/{ticker}",
|
|
105
|
+
params={"ticker": params.ticker},
|
|
106
|
+
op_name="eodhd_live",
|
|
107
|
+
output_config=_LIVE_OUTPUT,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_INTRADAY_OUTPUT, tags=["eodhd", "equity"])
|
|
112
|
+
async def eodhd_intraday(params: EodhdIntradayParams, *, api_key: str) -> Result:
|
|
113
|
+
"""[EOD+Intraday+] Fetch intraday OHLCV data for a ticker. Intervals: 1m, 5m, 1h.
|
|
114
|
+
Provide from_unix / to_unix as Unix timestamps (seconds) to bound the range.
|
|
115
|
+
Returns at most the last 100 data points when no range is specified."""
|
|
116
|
+
http = _make_http(api_key, timeout=_LATENCY_TIMEOUT)
|
|
117
|
+
p: dict[str, Any] = {"ticker": params.ticker, "interval": params.interval}
|
|
118
|
+
if params.from_unix is not None:
|
|
119
|
+
p["from"] = params.from_unix
|
|
120
|
+
if params.to_unix is not None:
|
|
121
|
+
p["to"] = params.to_unix
|
|
122
|
+
return await _eodhd_fetch(
|
|
123
|
+
http, path="/intraday/{ticker}", params=p, op_name="eodhd_intraday", output_config=_INTRADAY_OUTPUT
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_BULK_EOD_OUTPUT, tags=["eodhd", "equity"])
|
|
128
|
+
async def eodhd_bulk_eod(params: EodhdBulkEodParams, *, api_key: str) -> Result:
|
|
129
|
+
"""[EOD Historical+] Fetch end-of-day prices for all symbols on an exchange in a single request.
|
|
130
|
+
Returns the last trading day by default; pass date to fetch a specific day.
|
|
131
|
+
Large response — use for batch ingestion, not per-ticker lookups."""
|
|
132
|
+
http = _make_http(api_key, timeout=_BULK_TIMEOUT)
|
|
133
|
+
p: dict[str, Any] = {"exchange": params.exchange}
|
|
134
|
+
if params.date:
|
|
135
|
+
p["date"] = params.date
|
|
136
|
+
return await _eodhd_fetch(
|
|
137
|
+
http, path="/eod/bulk_last_day/{exchange}", params=p, op_name="eodhd_bulk_eod", output_config=_BULK_EOD_OUTPUT
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# Corporate Actions — Connectors
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_DIVIDENDS_OUTPUT, tags=["eodhd", "equity"])
|
|
147
|
+
async def eodhd_dividends(params: EodhdDividendsParams, *, api_key: str) -> Result:
|
|
148
|
+
"""[Free+] Fetch dividend history for a ticker. Use from/to to limit the range."""
|
|
149
|
+
http = _make_http(api_key)
|
|
150
|
+
p: dict[str, Any] = {"ticker": params.ticker}
|
|
151
|
+
if params.from_date:
|
|
152
|
+
p["from"] = params.from_date
|
|
153
|
+
if params.to_date:
|
|
154
|
+
p["to"] = params.to_date
|
|
155
|
+
return await _eodhd_fetch(
|
|
156
|
+
http, path="/div/{ticker}", params=p, op_name="eodhd_dividends", output_config=_DIVIDENDS_OUTPUT
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_SPLITS_OUTPUT, tags=["eodhd", "equity"])
|
|
161
|
+
async def eodhd_splits(params: EodhdSplitsParams, *, api_key: str) -> Result:
|
|
162
|
+
"""[Free+] Fetch stock split history for a ticker. The split ratio column contains the
|
|
163
|
+
ratio string as returned by the API (e.g. "4/1" for a 4-for-1 split). Use from/to to limit the range."""
|
|
164
|
+
http = _make_http(api_key)
|
|
165
|
+
p: dict[str, Any] = {"ticker": params.ticker}
|
|
166
|
+
if params.from_date:
|
|
167
|
+
p["from"] = params.from_date
|
|
168
|
+
if params.to_date:
|
|
169
|
+
p["to"] = params.to_date
|
|
170
|
+
return await _eodhd_fetch(
|
|
171
|
+
http, path="/splits/{ticker}", params=p, op_name="eodhd_splits", output_config=_SPLITS_OUTPUT
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
# Reference Data — Connectors
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_SEARCH_OUTPUT, tags=["eodhd", "tool"])
|
|
181
|
+
async def eodhd_search(params: EodhdSearchParams, *, api_key: str) -> Result:
|
|
182
|
+
"""[Free+] Search for instruments by company name or partial ticker. Use to resolve company
|
|
183
|
+
names to EODHD ticker codes (format: TICKER.EXCHANGE, e.g. AAPL.US). Filter by type to
|
|
184
|
+
narrow results."""
|
|
185
|
+
http = _make_http(api_key)
|
|
186
|
+
p: dict[str, Any] = {"query": params.query, "limit": params.limit}
|
|
187
|
+
if params.type:
|
|
188
|
+
p["type"] = params.type
|
|
189
|
+
return await _eodhd_fetch(
|
|
190
|
+
http, path="/search/{query}", params=p, op_name="eodhd_search", output_config=_SEARCH_OUTPUT
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_EXCHANGES_OUTPUT, tags=["eodhd", "tool"])
|
|
195
|
+
async def eodhd_exchanges(params: EodhdExchangesParams, *, api_key: str) -> Result:
|
|
196
|
+
"""[Free+] List all exchanges supported by EODHD. Use to find valid exchange codes for
|
|
197
|
+
eodhd_bulk_eod and eodhd_exchange_symbols."""
|
|
198
|
+
http = _make_http(api_key)
|
|
199
|
+
return await _eodhd_fetch(
|
|
200
|
+
http, path="/exchanges-list", params={}, op_name="eodhd_exchanges", output_config=_EXCHANGES_OUTPUT
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_EXCHANGE_SYMBOLS_OUTPUT, tags=["eodhd"])
|
|
205
|
+
async def eodhd_exchange_symbols(params: EodhdExchangeSymbolsParams, *, api_key: str) -> Result:
|
|
206
|
+
"""[Free+] List all symbols traded on an exchange. Large response for major exchanges
|
|
207
|
+
(US has 20 000+ symbols) — use type filter to limit. Empty result may indicate an
|
|
208
|
+
invalid exchange code."""
|
|
209
|
+
http = _make_http(api_key, timeout=_BULK_TIMEOUT)
|
|
210
|
+
p: dict[str, Any] = {"exchange": params.exchange}
|
|
211
|
+
if params.type:
|
|
212
|
+
p["type"] = params.type
|
|
213
|
+
return await _eodhd_fetch(
|
|
214
|
+
http,
|
|
215
|
+
path="/exchange-symbol-list/{exchange}",
|
|
216
|
+
params=p,
|
|
217
|
+
op_name="eodhd_exchange_symbols",
|
|
218
|
+
output_config=_EXCHANGE_SYMBOLS_OUTPUT,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
# Fundamentals — Connector
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, tags=["eodhd", "equity"])
|
|
228
|
+
async def eodhd_fundamentals(params: EodhdFundamentalsParams, *, api_key: str) -> Result:
|
|
229
|
+
"""[Fundamentals+] Fetch full fundamentals for a stock or ETF. Returns a large nested dict
|
|
230
|
+
(not a DataFrame). Typical top-level keys for equities: General, Highlights, Valuation,
|
|
231
|
+
SharesStats, Technicals, SplitsDividends, AnalystRatings, Holders, InsiderTransactions,
|
|
232
|
+
Financials, Earnings. ETF top-level keys differ: General, Technicals, ETF_Data.
|
|
233
|
+
|
|
234
|
+
Navigate by key path, e.g.:
|
|
235
|
+
result.data['Highlights']['MarketCapitalization']
|
|
236
|
+
result.data['Financials']['Income_Statement']['annual']
|
|
237
|
+
|
|
238
|
+
Returns raw dict — use result.data to access the nested structure."""
|
|
239
|
+
http = _make_http(api_key, timeout=_BULK_TIMEOUT)
|
|
240
|
+
return await _eodhd_fetch(
|
|
241
|
+
http,
|
|
242
|
+
path="/fundamentals/{ticker}",
|
|
243
|
+
params={"ticker": params.ticker},
|
|
244
|
+
op_name="eodhd_fundamentals",
|
|
245
|
+
raw=True,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
# Calendars — Dispatch map + Connector
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
_CALENDAR_PATHS: dict[str, str] = {
|
|
254
|
+
"earnings": "calendar/earnings",
|
|
255
|
+
"ipo": "calendar/ipo",
|
|
256
|
+
"trends": "calendar/trends",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_CALENDAR_OUTPUT, tags=["eodhd", "equity"])
|
|
261
|
+
async def eodhd_calendar(params: EodhdCalendarParams, *, api_key: str) -> Result:
|
|
262
|
+
"""[Fundamentals+] Fetch market calendar data. Three types available:
|
|
263
|
+
- earnings: upcoming earnings announcements with EPS estimates and actuals
|
|
264
|
+
- ipo: upcoming and recent IPO listings
|
|
265
|
+
- trends: analyst recommendation trends by sector
|
|
266
|
+
|
|
267
|
+
Use from/to to narrow the date window (max 90 days recommended for earnings)."""
|
|
268
|
+
http = _make_http(api_key)
|
|
269
|
+
path = _CALENDAR_PATHS[params.type]
|
|
270
|
+
p: dict[str, Any] = {}
|
|
271
|
+
if params.from_date:
|
|
272
|
+
p["from"] = params.from_date
|
|
273
|
+
if params.to_date:
|
|
274
|
+
p["to"] = params.to_date
|
|
275
|
+
if params.symbols:
|
|
276
|
+
p["symbols"] = params.symbols
|
|
277
|
+
return await _eodhd_fetch(http, path=path, params=p, op_name="eodhd_calendar", output_config=_CALENDAR_OUTPUT)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# News — Connector
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_NEWS_OUTPUT, tags=["eodhd", "tool"])
|
|
286
|
+
async def eodhd_news(params: EodhdNewsParams, *, api_key: str) -> Result:
|
|
287
|
+
"""[Free+] Fetch financial news articles. Filter by ticker (e.g. AAPL.US) or leave
|
|
288
|
+
empty for broad market news. Use from/to for date filtering and limit/offset for pagination.
|
|
289
|
+
Empty result may indicate no news in the date range for the specified ticker."""
|
|
290
|
+
http = _make_http(api_key)
|
|
291
|
+
p: dict[str, Any] = {"limit": params.limit, "offset": params.offset}
|
|
292
|
+
if params.ticker:
|
|
293
|
+
p["s"] = params.ticker # EODHD uses 's=' for symbol filtering on news endpoint
|
|
294
|
+
if params.from_date:
|
|
295
|
+
p["from"] = params.from_date
|
|
296
|
+
if params.to_date:
|
|
297
|
+
p["to"] = params.to_date
|
|
298
|
+
return await _eodhd_fetch(http, path="/news", params=p, op_name="eodhd_news", output_config=_NEWS_OUTPUT)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# Macro Indicators — Connectors
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_MACRO_OUTPUT, tags=["eodhd", "macro"])
|
|
307
|
+
async def eodhd_macro(params: EodhdMacroParams, *, api_key: str) -> Result:
|
|
308
|
+
"""[Fundamentals+] Fetch a macro indicator time series for a country.
|
|
309
|
+
Country must be an ISO 3-letter code (e.g. USA, DEU). Common indicators:
|
|
310
|
+
gdp_current_usd, unemployment_total_percent, inflation_consumer_prices_annual,
|
|
311
|
+
real_interest_rate, population_total, exports_of_goods_and_services_usd."""
|
|
312
|
+
http = _make_http(api_key)
|
|
313
|
+
return await _eodhd_fetch(
|
|
314
|
+
http,
|
|
315
|
+
path="/macro-indicator/{country}",
|
|
316
|
+
params={"country": params.country, "indicator": params.indicator},
|
|
317
|
+
op_name="eodhd_macro",
|
|
318
|
+
output_config=_MACRO_OUTPUT,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_MACRO_OUTPUT, tags=["eodhd", "macro"])
|
|
323
|
+
async def eodhd_macro_bulk(params: EodhdMacroBulkParams, *, api_key: str) -> Result:
|
|
324
|
+
"""[Fundamentals+] Fetch all available macro indicators for a country in a single request.
|
|
325
|
+
Large response — use eodhd_macro for a specific indicator.
|
|
326
|
+
Country must be an ISO 3-letter code (e.g. USA)."""
|
|
327
|
+
http = _make_http(api_key, timeout=_BULK_TIMEOUT)
|
|
328
|
+
p: dict[str, Any] = {"country": params.country}
|
|
329
|
+
if params.topic:
|
|
330
|
+
p["topic"] = params.topic
|
|
331
|
+
return await _eodhd_fetch(
|
|
332
|
+
http, path="/macro-indicator/{country}", params=p, op_name="eodhd_macro_bulk", output_config=_MACRO_OUTPUT
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# ---------------------------------------------------------------------------
|
|
337
|
+
# Technical Indicators — Connector
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_TECHNICAL_OUTPUT, tags=["eodhd", "equity"])
|
|
342
|
+
async def eodhd_technical(params: EodhdTechnicalParams, *, api_key: str) -> Result:
|
|
343
|
+
"""[EOD+Intraday+] Fetch technical indicator values for a ticker alongside OHLCV data.
|
|
344
|
+
Indicator-specific output columns vary by function:
|
|
345
|
+
- sma/ema/wma → sma/ema/wma column
|
|
346
|
+
- macd → macd, macd_signal, macd_hist
|
|
347
|
+
- bbands → uband, mband, lband
|
|
348
|
+
- stochastic → stoch_kd, stoch_d
|
|
349
|
+
- adx/dmi → adx, plusDI, minusDI
|
|
350
|
+
|
|
351
|
+
Use period to control the lookback window (default 50)."""
|
|
352
|
+
http = _make_http(api_key)
|
|
353
|
+
p: dict[str, Any] = {
|
|
354
|
+
"ticker": params.ticker,
|
|
355
|
+
"function": params.function,
|
|
356
|
+
"period": params.period,
|
|
357
|
+
"order": params.order,
|
|
358
|
+
}
|
|
359
|
+
if params.from_date:
|
|
360
|
+
p["from"] = params.from_date
|
|
361
|
+
if params.to_date:
|
|
362
|
+
p["to"] = params.to_date
|
|
363
|
+
return await _eodhd_fetch(
|
|
364
|
+
http, path="/technicals/{ticker}", params=p, op_name="eodhd_technical", output_config=_TECHNICAL_OUTPUT
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ---------------------------------------------------------------------------
|
|
369
|
+
# Insider Transactions & Screener — Connectors
|
|
370
|
+
# ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_INSIDER_OUTPUT, tags=["eodhd", "equity"])
|
|
374
|
+
async def eodhd_insider(params: EodhdInsiderParams, *, api_key: str) -> Result:
|
|
375
|
+
"""[Fundamentals+] Fetch insider (executive and director) transactions. Filter by ticker
|
|
376
|
+
or omit for recent cross-market transactions. Use limit/offset to page."""
|
|
377
|
+
http = _make_http(api_key)
|
|
378
|
+
p: dict[str, Any] = {"limit": params.limit, "offset": params.offset}
|
|
379
|
+
if params.ticker:
|
|
380
|
+
p["code"] = params.ticker
|
|
381
|
+
return await _eodhd_fetch(
|
|
382
|
+
http, path="/insider-transactions", params=p, op_name="eodhd_insider", output_config=_INSIDER_OUTPUT
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@connector(env={"api_key": "EODHD_API_KEY"}, output=_SCREENER_OUTPUT, tags=["eodhd", "equity", "tool"])
|
|
387
|
+
async def eodhd_screener(params: EodhdScreenerParams, *, api_key: str) -> Result:
|
|
388
|
+
"""[EOD+Intraday+] Screen stocks by fundamental, price, and exchange criteria.
|
|
389
|
+
Filters are structured triples [field, operator, value] — see EodhdScreenerParams.filters.
|
|
390
|
+
Empty result may indicate invalid filter field or operator — verify against the EODHD
|
|
391
|
+
screener field list in their documentation."""
|
|
392
|
+
http = _make_http(api_key)
|
|
393
|
+
p: dict[str, Any] = {"limit": params.limit, "offset": params.offset, "order": params.order}
|
|
394
|
+
if params.filters:
|
|
395
|
+
p["filters"] = json.dumps([[f[0], f[1], f[2]] for f in params.filters])
|
|
396
|
+
if params.signals:
|
|
397
|
+
p["signals"] = params.signals
|
|
398
|
+
if params.sort:
|
|
399
|
+
p["sort"] = params.sort
|
|
400
|
+
return await _eodhd_fetch(
|
|
401
|
+
http, path="/screener", params=p, op_name="eodhd_screener", output_config=_SCREENER_OUTPUT
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# ---------------------------------------------------------------------------
|
|
406
|
+
# Connector collections
|
|
407
|
+
# ---------------------------------------------------------------------------
|
|
408
|
+
|
|
409
|
+
CONNECTORS = Connectors(
|
|
410
|
+
[
|
|
411
|
+
# Discovery
|
|
412
|
+
eodhd_search,
|
|
413
|
+
eodhd_exchanges,
|
|
414
|
+
eodhd_news,
|
|
415
|
+
eodhd_screener,
|
|
416
|
+
# Market data
|
|
417
|
+
eodhd_eod,
|
|
418
|
+
eodhd_live,
|
|
419
|
+
eodhd_intraday,
|
|
420
|
+
eodhd_bulk_eod,
|
|
421
|
+
# Corporate actions
|
|
422
|
+
eodhd_dividends,
|
|
423
|
+
eodhd_splits,
|
|
424
|
+
# Reference
|
|
425
|
+
eodhd_exchange_symbols,
|
|
426
|
+
# Fundamentals
|
|
427
|
+
eodhd_fundamentals,
|
|
428
|
+
# Calendars
|
|
429
|
+
eodhd_calendar,
|
|
430
|
+
# Macro
|
|
431
|
+
eodhd_macro,
|
|
432
|
+
eodhd_macro_bulk,
|
|
433
|
+
# Technical
|
|
434
|
+
eodhd_technical,
|
|
435
|
+
# Transactions
|
|
436
|
+
eodhd_insider,
|
|
437
|
+
]
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
__all__ = [
|
|
442
|
+
"CONNECTORS",
|
|
443
|
+
# Connectors
|
|
444
|
+
"eodhd_bulk_eod",
|
|
445
|
+
"eodhd_calendar",
|
|
446
|
+
"eodhd_dividends",
|
|
447
|
+
"eodhd_eod",
|
|
448
|
+
"eodhd_exchange_symbols",
|
|
449
|
+
"eodhd_exchanges",
|
|
450
|
+
"eodhd_fundamentals",
|
|
451
|
+
"eodhd_insider",
|
|
452
|
+
"eodhd_intraday",
|
|
453
|
+
"eodhd_live",
|
|
454
|
+
"eodhd_macro",
|
|
455
|
+
"eodhd_macro_bulk",
|
|
456
|
+
"eodhd_news",
|
|
457
|
+
"eodhd_screener",
|
|
458
|
+
"eodhd_search",
|
|
459
|
+
"eodhd_splits",
|
|
460
|
+
"eodhd_technical",
|
|
461
|
+
# Param classes
|
|
462
|
+
"EodhdBulkEodParams",
|
|
463
|
+
"EodhdCalendarParams",
|
|
464
|
+
"EodhdDividendsParams",
|
|
465
|
+
"EodhdEodParams",
|
|
466
|
+
"EodhdExchangeSymbolsParams",
|
|
467
|
+
"EodhdExchangesParams",
|
|
468
|
+
"EodhdFundamentalsParams",
|
|
469
|
+
"EodhdInsiderParams",
|
|
470
|
+
"EodhdIntradayParams",
|
|
471
|
+
"EodhdLiveParams",
|
|
472
|
+
"EodhdMacroBulkParams",
|
|
473
|
+
"EodhdMacroParams",
|
|
474
|
+
"EodhdNewsParams",
|
|
475
|
+
"EodhdScreenerParams",
|
|
476
|
+
"EodhdSearchParams",
|
|
477
|
+
"EodhdSplitsParams",
|
|
478
|
+
"EodhdTechnicalParams",
|
|
479
|
+
]
|
parsimony_eodhd/_http.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""EODHD transport — shared HTTP helpers and timeout handling.
|
|
2
|
+
|
|
3
|
+
Error mapping and Retry-After parsing are delegated to the kernel
|
|
4
|
+
(:func:`parsimony.transport.map_http_error`,
|
|
5
|
+
:func:`parsimony.transport.parse_retry_after`). API-token redaction is
|
|
6
|
+
delegated to :func:`parsimony.transport.redact_url` — the ``api_token``
|
|
7
|
+
query param is already in the kernel's sensitive-name set.
|
|
8
|
+
|
|
9
|
+
This module only owns what is EODHD-specific: bracket-syntax query
|
|
10
|
+
parameter encoding (``filter_x`` → ``filter[x]``) and 200-body error
|
|
11
|
+
detection.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
import pandas as pd
|
|
21
|
+
from parsimony.errors import (
|
|
22
|
+
EmptyDataError,
|
|
23
|
+
ParseError,
|
|
24
|
+
ProviderError,
|
|
25
|
+
)
|
|
26
|
+
from parsimony.result import OutputConfig, Provenance, Result
|
|
27
|
+
from parsimony.transport import HttpClient, map_http_error, map_timeout_error
|
|
28
|
+
|
|
29
|
+
# Per-request timeout. 15s is defensible for EODHD's REST endpoints, which
|
|
30
|
+
# are not streaming. Bulk endpoints (fundamentals, macro_bulk, bulk_eod,
|
|
31
|
+
# exchange_symbols) override this via the ``timeout`` kwarg on ``make_http``.
|
|
32
|
+
_DEFAULT_TIMEOUT_SECONDS: float = 15.0
|
|
33
|
+
|
|
34
|
+
_DEFAULT_BASE_URL: str = "https://eodhd.com/api"
|
|
35
|
+
|
|
36
|
+
_PROVIDER: str = "eodhd"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make_http(
|
|
40
|
+
api_key: str,
|
|
41
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
42
|
+
timeout: float = _DEFAULT_TIMEOUT_SECONDS,
|
|
43
|
+
) -> HttpClient:
|
|
44
|
+
"""Construct the standard EODHD transport.
|
|
45
|
+
|
|
46
|
+
The API token rides as a default query parameter (``api_token=<key>``),
|
|
47
|
+
alongside EODHD's ``fmt=json`` convention. Timeout defaults to 15s;
|
|
48
|
+
bulk endpoints pass a larger value explicitly.
|
|
49
|
+
"""
|
|
50
|
+
return HttpClient(
|
|
51
|
+
base_url,
|
|
52
|
+
query_params={"api_token": api_key, "fmt": "json"},
|
|
53
|
+
timeout=timeout,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _to_bracket_params(params: dict[str, Any]) -> dict[str, Any]:
|
|
58
|
+
"""Transform ``filter_x`` → ``filter[x]`` and ``page_x`` → ``page[x]`` for EODHD bracket syntax.
|
|
59
|
+
|
|
60
|
+
Pure function: does not mutate input. None values are dropped.
|
|
61
|
+
"""
|
|
62
|
+
result: dict[str, Any] = {}
|
|
63
|
+
for k, v in params.items():
|
|
64
|
+
if v is None:
|
|
65
|
+
continue
|
|
66
|
+
if k.startswith("filter_"):
|
|
67
|
+
result[f"filter[{k[7:]}]"] = v
|
|
68
|
+
elif k.startswith("page_"):
|
|
69
|
+
result[f"page[{k[5:]}]"] = v
|
|
70
|
+
else:
|
|
71
|
+
result[k] = v
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def eodhd_fetch(
|
|
76
|
+
http: HttpClient,
|
|
77
|
+
*,
|
|
78
|
+
path: str,
|
|
79
|
+
params: dict[str, Any],
|
|
80
|
+
op_name: str,
|
|
81
|
+
output_config: OutputConfig | None = None,
|
|
82
|
+
raw: bool = False,
|
|
83
|
+
) -> Result:
|
|
84
|
+
"""Shared EODHD fetch: path interpolation, bracket params, JSON extraction, Result building.
|
|
85
|
+
|
|
86
|
+
Error mapping is delegated to :func:`~parsimony.transport.map_http_error`:
|
|
87
|
+
401/403 → UnauthorizedError
|
|
88
|
+
402 → PaymentRequiredError
|
|
89
|
+
429 → RateLimitError (Retry-After parsed when present)
|
|
90
|
+
else → ProviderError
|
|
91
|
+
|
|
92
|
+
``httpx.TimeoutException`` is mapped to ``ProviderError(status_code=408)``.
|
|
93
|
+
The EODHD API token is never included in exception messages.
|
|
94
|
+
``asyncio.CancelledError`` propagates unchanged.
|
|
95
|
+
|
|
96
|
+
``raw=True`` bypasses the DataFrame pipeline and returns the parsed JSON
|
|
97
|
+
verbatim (used by ``eodhd_fundamentals``, which returns a nested dict).
|
|
98
|
+
"""
|
|
99
|
+
# Path template substitution: {key} → value; remainder → query params
|
|
100
|
+
rendered = path
|
|
101
|
+
query_params: dict[str, Any] = {}
|
|
102
|
+
|
|
103
|
+
for key, value in params.items():
|
|
104
|
+
if value is None:
|
|
105
|
+
continue
|
|
106
|
+
placeholder = f"{{{key}}}"
|
|
107
|
+
if placeholder in rendered:
|
|
108
|
+
rendered = rendered.replace(placeholder, str(value))
|
|
109
|
+
else:
|
|
110
|
+
query_params[key] = value
|
|
111
|
+
|
|
112
|
+
# Remove any unfilled optional placeholders
|
|
113
|
+
rendered = re.sub(r"\{[^}]+\}", "", rendered)
|
|
114
|
+
|
|
115
|
+
# Apply EODHD bracket syntax transformation (filter_x → filter[x], page_x → page[x])
|
|
116
|
+
query_params = _to_bracket_params(query_params)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
response = await http.request("GET", f"/{rendered.lstrip('/')}", params=query_params or None)
|
|
120
|
+
response.raise_for_status()
|
|
121
|
+
except httpx.HTTPStatusError as exc:
|
|
122
|
+
map_http_error(exc, provider=_PROVIDER, op_name=op_name)
|
|
123
|
+
except httpx.TimeoutException as exc:
|
|
124
|
+
map_timeout_error(exc, provider=_PROVIDER, op_name=op_name)
|
|
125
|
+
|
|
126
|
+
data = response.json()
|
|
127
|
+
prov = Provenance(source=op_name, params=dict(params))
|
|
128
|
+
|
|
129
|
+
# 200-body error detection (EODHD returns error strings in the body on some endpoints)
|
|
130
|
+
if isinstance(data, dict) and "error" in data and isinstance(data["error"], str):
|
|
131
|
+
raise ProviderError(
|
|
132
|
+
provider=_PROVIDER,
|
|
133
|
+
status_code=200,
|
|
134
|
+
message=f"EODHD error on '{op_name}': {data['error']}",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Raw return path (fundamentals): bypass DataFrame pipeline entirely
|
|
138
|
+
if raw:
|
|
139
|
+
return Result(data=data, provenance=prov)
|
|
140
|
+
|
|
141
|
+
# DataFrame construction
|
|
142
|
+
if isinstance(data, list):
|
|
143
|
+
df = pd.DataFrame(data)
|
|
144
|
+
elif isinstance(data, dict):
|
|
145
|
+
for key in ("earnings", "ipos", "splits", "trends", "data", "results"):
|
|
146
|
+
if key in data and isinstance(data[key], list):
|
|
147
|
+
df = pd.DataFrame(data[key])
|
|
148
|
+
break
|
|
149
|
+
else:
|
|
150
|
+
df = pd.DataFrame([data])
|
|
151
|
+
else:
|
|
152
|
+
raise ParseError(
|
|
153
|
+
provider=_PROVIDER,
|
|
154
|
+
message=f"Unexpected response type from EODHD '{op_name}': {type(data).__name__}",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if df.empty:
|
|
158
|
+
raise EmptyDataError(
|
|
159
|
+
provider=_PROVIDER,
|
|
160
|
+
message=f"No data returned from EODHD endpoint '{op_name}'",
|
|
161
|
+
query_params=dict(params),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if output_config is not None:
|
|
165
|
+
return output_config.build_table_result(df, provenance=prov, params=dict(params))
|
|
166
|
+
return Result.from_dataframe(df, prov)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
__all__ = [
|
|
170
|
+
"eodhd_fetch",
|
|
171
|
+
"make_http",
|
|
172
|
+
]
|