Lesson 1.3.2 · 50 min
Free market data sources
For learning and for research there is plenty of free data, but not all of it is alike: some sources are official and stable, others depend on a website that does not offer them as a service. This lesson goes through seven, with their access rules read in their own documentation, and processes the response format of each API.
- Module
- 1.3 · Market data sources
- Duration
- 50 min
- Author
- Jesús Cuesta
- Updated
Level 1 lessons
1.1 · How markets work
1.2 · Asset classes
- 1.2.1 Stocks: splits, dividends and corporate actions
- 1.2.2 ETFs: replication, tracking error, leveraged ETFs and UCITS
- 1.2.3 Futures: specifications, margin, roll and contango
- 1.2.4 Forex: pairs, pips, lots, rollover and CFDs
- 1.2.5 Options: calls, puts, intrinsic value and time value
- 1.2.6 Crypto: spot, perpetuals, funding and custody
- 1.2.7 Fixed income and interest rates; CFDs and leveraged products
1.3 · Market data sources
- 1.3.1 Types of market data: EOD, bars, ticks, order book and reference
- 1.3.2 Free market data sources
- 1.3.3 Retail and prosumer market data APIs
- 1.3.4 Professional market data providers
- 1.3.5 Crypto and on-chain data
- 1.3.6 Brokers as market data sources
- 1.3.7 Market data licensing and terms of use
- 1.3.8 How to choose a market data provider
1.4 · Financial data engineering
- 1.4.1 Downloading over REST and websockets: pagination, rate limits and retries
- 1.4.2 Data cleaning: missing values, bad ticks, gaps and time zones
- 1.4.3 Adjusting for splits and dividends: adjusted and unadjusted prices
- 1.4.4 Universes without survivorship bias: delistings, mergers and symbol changes
- 1.4.5 Building bars: time, tick, volume, dollar and imbalance
- 1.4.6 Continuous futures series: roll rules, difference adjustment and ratio adjustment
- 1.4.7 Storage: CSV, Parquet, partitioning, DuckDB, TimescaleDB and ArcticDB
- 1.4.8 Reproducible pipeline: download, validation, incremental updates and quality tests
1.5 · Fundamental and macro data
1.6 · Financial exploratory analysis
1.7 · Programmed technical analysis
Two kinds of “free”
There is a lot of free financial data, and almost everyone who learns starts with it. But under the same word there are two very different things:
- Official services: a central bank, a regulator or a statistics agency publishes its data with a documented API, terms of use and an intention to maintain it. FRED (Federal Reserve Bank of St. Louis), EDGAR (SEC), the ECB, Banco de España (Spain's central bank) and the CNMV (Spain's securities regulator) are of this kind.
- Unofficial tools: a library reads the data from a website that does not offer it as a service to third parties. yfinance is the best known. It works, and well, until the day the website changes.
The difference matters for a system. An official service can change version, but it announces and documents it. An unofficial tool can stop working without warning, change its format or return request limits that nobody has documented.
Mermaid source
flowchart LR
Q{What do you need?} --> P[Prices]
Q --> M[Macro and rates]
Q --> E[Companies]
P --> Y[yfinance]
M --> F[FRED]
M --> B[ECB]
M --> BE[Banco de España]
E --> S[SEC EDGAR]
E --> C[CNMV]The matrix in the header summarizes what type of data each one provides. The gaps matter as much as the squares: no free official source provides stock prices with long history and reliable corporate action adjustments. That is what the providers of lessons 1.3.3 and 1.3.4 are for.
yfinance: useful for learning, fragile for trading
yfinance is an open-source library (Apache license) that downloads prices, dividends, splits and some company data from Yahoo Finance. Its README is explicit1Note 1 · yfinance README and documentation, accessed on October 21, 2026; version 1.7.0 on PyPI.: it is not affiliated with, endorsed or vetted by Yahoo; it uses the APIs that Yahoo exposes publicly; it is intended for research and educational purposes; and you have to check Yahoo's terms of use to know what rights you have over the downloaded data, bearing in mind that the Yahoo Finance API is intended for personal use only. Lesson 1.3.7 explains what that means when publishing or sharing results.
import yfinance as yf # uv add yfinance (needs network)
aapl = yf.Ticker("AAPL")
diario = aapl.history(period="1mo")
varios = yf.download(["MSFT", "AAPL"], start="2025-01-01",
end="2025-12-31", auto_adjust=True)Three things worth knowing before using it:
- It adjusts by default.
downloadhasauto_adjust=True: prices come adjusted for dividends and splits. If they are compared with traded prices from another source, they do not match (lessons 1.3.8 and 1.4.3). - Undocumented limits. The README says nothing about request limits, but the code defines a
YFRateLimitErrorexception (“Too Many Requests”) raised when Yahoo responds with 429. A download of hundreds of tickers in a row can be cut off halfway. - Frequent releases. Version 1.7.0 is from August 26, 2026, after two others in July and August. When Yahoo
changes something, the library has to adapt; pin the version in
pyproject.toml(lesson 0.2.2).
FRED: macro and rates, with versions
FRED, from the Federal Reserve Bank of St. Louis, gathers economic series from many agencies: interest rates, inflation, employment, exchange rates. Its API requires a free key (a 32-character string) in every request and allows up to 120 requests per minute; above that it responds with 429, and not respecting the limit can end in a temporary block2Note 2 · FRED API: series/observations, API key and errors (“Up to 120 requests per minute”), accessed on October 21, 2026..
import os
from urllib.parse import urlencode
from urllib.request import urlopen
params = {"series_id": "DGS10", "file_type": "json",
"observation_start": "2025-01-01",
"api_key": os.environ["FRED_API_KEY"]}
url = ("https://api.stlouisfed.org/fred/series/observations?"
+ urlencode(params))
with urlopen(url, timeout=10) as r:
datos = r.read()The JSON response has query metadata (realtime_start, realtime_end, observation_start, count, offset,
limit…) and an observations list with one dictionary per date: date, value and the real-time period during
which that value was current. The detail that causes the most errors: value arrives as text. An illustrative
sample with that structure:
import json
import pandas as pd
fred_txt = """{
"realtime_start": "2026-10-21", "realtime_end": "2026-10-21",
"observation_start": "2025-01-01", "count": 3, "offset": 0,
"limit": 100000,
"observations": [
{"realtime_start": "2026-10-21", "realtime_end": "2026-10-21",
"date": "2025-01-02", "value": "4.57"},
{"realtime_start": "2026-10-21", "realtime_end": "2026-10-21",
"date": "2025-01-03", "value": "4.60"},
{"realtime_start": "2026-10-21", "realtime_end": "2026-10-21",
"date": "2025-01-06", "value": "4.62"}
]
}"""
fred = json.loads(fred_txt)
obs = pd.DataFrame(fred["observations"])
print(obs.dtypes.to_dict())
obs["date"] = pd.to_datetime(obs["date"])
obs["value"] = pd.to_numeric(obs["value"], errors="coerce")
obs[["date", "value"]]{'realtime_start': <StringDtype(na_value=nan)>, 'realtime_end': <StringDtype(na_value=nan)>, 'date': <StringDtype(na_value=nan)>, 'value': <StringDtype(na_value=nan)>}
date value
0 2025-01-02 4.57
1 2025-01-03 4.60
2 2025-01-06 4.62Without to_numeric, obs["value"].mean() would fail and obs["value"] + obs["value"] would concatenate strings.
With errors="coerce", any value that is not a number becomes NaN instead of breaking the conversion.
The two realtime_* fields are the door to one of FRED's most valuable features: vintages. Macro series get
revised (GDP, employment); realtime_start, realtime_end and vintage_dates let you request the data as it existed
on a past date, the basis of ALFRED, FRED's version archive. With an illustrative sample of a revised series:
versiones = pd.DataFrame({
"date": ["2025-03-01", "2025-03-01", "2025-03-01"],
"value": [151.0, 228.0, 205.0],
"realtime_start": ["2025-04-04", "2025-05-02", "2025-06-06"],
"realtime_end": ["2025-05-01", "2025-06-05", "9999-12-31"],
})
def conocido_el(v, fecha):
desde, hasta = v["realtime_start"], v["realtime_end"]
vigente = (desde <= fecha) & (hasta >= fecha)
return v.loc[vigente, ["date", "value"]]
for f in ["2025-04-10", "2025-05-20", "2026-01-01"]:
print(f, conocido_el(versiones, f).to_dict("records"))2025-04-10 [{'date': '2025-03-01', 'value': 151.0}]
2025-05-20 [{'date': '2025-03-01', 'value': 228.0}]
2026-01-01 [{'date': '2025-03-01', 'value': 205.0}]The same March figure was 151 for whoever looked at it in April, 228 in May and 205 from June on. A backtest that uses
today's series is using in April a 205 that nobody knew. Dates are compared as text because they are in ISO format
(YYYY-MM-DD), which sorts the same as dates; 9999-12-31 marks “still current”. Lesson 1.5.2 works through this
with full macro data.
A license warning: FRED gathers series from many agencies, and those carrying a copyright notice belong to third parties with their own restrictions; to use them beyond personal use you have to ask their owner for permission3Note 3 · FRED: Legal Notices, Information and Disclaimers and FRED API Terms of Use..
SEC EDGAR: US fundamentals and filings
EDGAR is the SEC system where US listed companies file their reports (10-K, 10-Q, 8-K…). Its JSON APIs at
data.sec.gov require no key or authentication4Note 4 · SEC: EDGAR Application Programming Interfaces.; the two most useful:
https://data.sec.gov/submissions/CIK##########.json: a company's filing history.https://data.sec.gov/api/xbrl/companyfacts/CIK##########.json: all of the company's XBRL accounting data.
########## is the CIK (the SEC's Central Index Key) with ten digits, leading zeros included. In exchange for not
requiring a key, the SEC sets fair access rules5Note 5 · SEC: Accessing EDGAR Data, accessed on October 21, 2026.: a current maximum of 10 requests per second and declaring a
User-Agent with the requester's name and a contact email, with this sample format:
User-Agent: Sample Company Name AdminContact@<sample company domain>.com. Requests without it can receive a 403.
def url_submissions(cik):
return f"https://data.sec.gov/submissions/CIK{int(cik):010d}.json"
CABECERAS = {
"User-Agent": "My Research Project [email protected]",
"Accept-Encoding": "gzip, deflate",
}
print(url_submissions(320193))
print(url_submissions("0000320193"))https://data.sec.gov/submissions/CIK0000320193.json
https://data.sec.gov/submissions/CIK0000320193.jsonimport gzip
import json
from urllib.request import Request, urlopen
req = Request(url_submissions(320193), headers=CABECERAS)
with urlopen(req, timeout=10) as r:
crudo = r.read()
if r.headers.get("Content-Encoding") == "gzip":
crudo = gzip.decompress(crudo)
sub = json.loads(crudo)The submissions response has company data (cik, name, tickers, exchanges…) and, in filings.recent, the
recent filings in columnar format: a dictionary of parallel lists (accessionNumber, filingDate, reportDate,
form, primaryDocument…), not a list of records. That is exactly what pd.DataFrame expects. With an illustrative
sample of that structure, for a fictitious company:
sub = {
"cik": "0000000042", "name": "NOVA CORP", "tickers": ["NOVA"],
"filings": {"recent": {
"accessionNumber": ["0000000042-25-000031",
"0000000042-25-000027",
"0000000042-25-000012"],
"filingDate": ["2025-11-05", "2025-10-24", "2025-08-06"],
"reportDate": ["2025-09-30", "", "2025-06-30"],
"form": ["10-Q", "8-K", "10-Q"],
"primaryDocument": ["nova-10q.htm", "nova-8k.htm",
"nova-10q.htm"],
}},
}
pres = pd.DataFrame(sub["filings"]["recent"])
pres["filingDate"] = pd.to_datetime(pres["filingDate"])
pres[pres["form"].isin(["10-K", "10-Q"])][
["form", "reportDate", "filingDate"]] form reportDate filingDate
0 10-Q 2025-09-30 2025-11-05
2 10-Q 2025-06-30 2025-08-06The two dates are the key to using fundamentals without look-ahead bias: reportDate is the end of the period
(September 30) and filingDate, the day the report was made public (November 5). A backtest can only use the
quarter's figures from the second one on (lesson 1.5.1).
Respecting EDGAR's rate
Ten requests per second seem like a lot until you download thousands of documents in a loop. The simplest loop fires requests as fast as the network responds and exceeds the limit in the first second. The solution is a rate limiter that waits as long as needed between requests. To run it without a network, with a virtual clock in integer milliseconds (so there are no rounding errors in the comparisons) in which each request takes 20 ms to respond:
Mermaid source
flowchart LR
P[Request] --> L{10 in 1 s?}
L -- yes --> W[Wait]
W --> L
L -- no --> S[data.sec.gov]from collections import deque
def simular(n, por_segundo=None, latencia_ms=20):
t, hechas, horas = 0, deque(), [] # integer milliseconds
for _ in range(n):
if por_segundo is not None:
while hechas and t - hechas[0] >= 1000:
hechas.popleft()
if len(hechas) >= por_segundo:
t = hechas[0] + 1000
hechas.popleft()
hechas.append(t)
horas.append(t)
t += latencia_ms
return pd.Series(horas)
def peor_segundo(ms):
return max(((ms >= h) & (ms < h + 1000)).sum() for h in ms)
sin_limite, con_limite = simular(100), simular(100, por_segundo=10)
print("without limiter:", f"{sin_limite.iloc[-1] / 1000:.2f} s,",
"max in one second:", peor_segundo(sin_limite))
print("with limiter:", f"{con_limite.iloc[-1] / 1000:.2f} s,",
"max in one second:", peor_segundo(con_limite))without limiter: 1.98 s, max in one second: 50
with limiter: 9.18 s, max in one second: 10Without a limiter, the 100 requests go out in 2 seconds and the worst second has 50: five times the maximum. With the limiter they take 9.18 seconds and there are never more than 10 in any one-second window. It is slower, but it is the difference between a download that finishes and a blocked IP. Lesson 1.4.1 generalizes the limiter and adds retries.
ECB and Banco de España: European data without a key
The ECB Data Portal offers an SDMX 2.1 web service with its entry point at data-api.ecb.europa.eu/service/. Each
series is identified by a key that joins the values of its dimensions with dots: the daily reference exchange rate of
the dollar against the euro is EXR/D.USD.EUR.SP00.A (dataset EXR; frequency D, currency USD, reference
currency EUR, type SP00, variant A). It accepts csvdata and jsondata, among other formats, and parameters
such as startPeriod or lastNObservations6Note 6 · ECB Data Portal: API, Overview and Data.. It requires no key.
url = ("https://data-api.ecb.europa.eu/service/data/EXR/"
"D.USD.EUR.SP00.A?lastNObservations=3&format=csvdata")
bce = pd.read_csv(url)The CSV response has one row per observation and more than thirty columns: the key's dimensions, the date
(TIME_PERIOD), the value (OBS_VALUE) and many metadata fields (observation status, title, unit, decimals…). A
sample with the main columns and the three values the previous query returned on October 21, 2026:
from io import StringIO
bce_csv = """KEY,FREQ,CURRENCY,TIME_PERIOD,OBS_VALUE,OBS_STATUS,TITLE
EXR.D.USD.EUR.SP00.A,D,USD,2026-10-16,1.1253,A,US dollar/Euro
EXR.D.USD.EUR.SP00.A,D,USD,2026-10-19,1.1225,A,US dollar/Euro
EXR.D.USD.EUR.SP00.A,D,USD,2026-10-20,1.1215,A,US dollar/Euro
"""
bce = pd.read_csv(StringIO(bce_csv), parse_dates=["TIME_PERIOD"])
eurusd = bce.set_index("TIME_PERIOD")["OBS_VALUE"].rename("EURUSD")
print(eurusd)
print(f"Change: {eurusd.pct_change().iloc[-1]:.3%}")TIME_PERIOD
2026-10-16 1.1253
2026-10-19 1.1225
2026-10-20 1.1215
Name: EURUSD, dtype: float64
Change: -0.089%The ECB reference rate is a figure of the day (the series title itself places it at 14:15 CET), not a tradable price: it is useful for valuation, for converting currencies in a report or for macro, not for an intraday currency system (lesson 1.2.4). Its reuse is free under two conditions: citing the source and not modifying the statistics or their metadata; third-party data published by the ECB needs permission from whoever produces it7Note 7 · ECB: Policy regarding the reuse of ESCB statistics..
Banco de España has a JSON web service that gives access to all the information in its statistics section and in
the HERMES application, with two types of request: the latest figure of one or several series (favoritas) and a
listing of the series over a range (listaSeries). For bulk downloads it also offers all the time series CSV files
in a ZIP. The series D_1NBAF472 is the one-year Euribor8Note 8 · Banco de España: Statistics web service (API) and time-series bulk download, accessed on October 21, 2026.:
url = ("https://app.bde.es/bierest/resources/srdatosapp/favoritas"
"?idioma=es&series=D_1NBAF472")bde_txt = """[{"serie": "D_1NBAF472",
"descripcionCorta": "Euribor a un año", "codFrecuencia": "M",
"decimales": 3, "simbolo": "%", "tendencia": "+",
"fechaValor": "2026-09-01T08:15:00Z", "valor": 3.247}]"""
bde = pd.DataFrame(json.loads(bde_txt))
bde["fechaValor"] = pd.to_datetime(bde["fechaValor"])
bde[["serie", "descripcionCorta", "fechaValor", "valor", "simbolo"]] serie descripcionCorta fechaValor valor simbolo
0 D_1NBAF472 Euribor a un año 2026-09-01 08:15:00+00:00 3.247 %This is the response the service returned on October 21, 2026: the monthly figure for September 2026, 3.247%. The
fechaValor timestamp is in UTC (Z) and the frequency is monthly (M): it is worth keeping both, because a monthly
series joined to daily prices needs an explicit alignment rule (lesson 1.4.2).
CNMV: regulatory information
The CNMV has no documented public API like the previous ones, but it publishes useful information on its website, in searches and downloadable files9Note 9 · CNMV: short positions, other relevant information, official registers and statistics.:
- Short positions: notifications of net short positions in Spanish stocks, with a downloadable file of the published and historical ones. Useful for studying bearish pressure or short squeeze risk.
- Other relevant information: issuers' announcements (formerly “hechos relevantes”, material events, until February 2020), notifications from executives and significant shareholdings.
- Official registers of issuers and listed companies, and statistics on issuers, funds and investment firms.
Since there is no API, access is by downloading files or reading pages, and any automation has to respect the website's terms of use. It is information for research and context, not a feed.
A common format for different sources
Each source returns a different format. Before storing, it is best to bring everything to a common shape, with the source and the series as columns, so that the rest of the code does not depend on who provided each figure:
largo = pd.concat([
pd.DataFrame({"fuente": "FRED", "serie": "DGS10",
"fecha": obs["date"], "valor": obs["value"]}),
pd.DataFrame({"fuente": "ECB", "serie": "EXR.D.USD.EUR",
"fecha": eurusd.index, "valor": eurusd.values}),
pd.DataFrame({"fuente": "BdE", "serie": bde["serie"],
"fecha": bde["fechaValor"].dt.tz_localize(None)
.dt.normalize(), "valor": bde["valor"]}),
], ignore_index=True)
print(largo.dtypes.to_dict())
largo.groupby(["fuente", "serie"])["fecha"].agg(["min", "max", "size"]){'fuente': <StringDtype(na_value=nan)>, 'serie': <StringDtype(na_value=nan)>, 'fecha': dtype('<M8[us]'), 'valor': dtype('float64')}
min max size
fuente serie
BdE D_1NBAF472 2026-09-01 2026-09-01 1
ECB EXR.D.USD.EUR 2026-10-16 2026-10-20 3
FRED DGS10 2025-01-02 2025-01-06 3The long format (one row per source, series and date) is easy to store in Parquet, to filter and to pivot when needed
(lesson 1.4.7). What is lost when normalizing (the units, the frequency, the status of each observation) is stored
separately, in a metadata table per series: without it, a year from now nobody will know whether 3.247 was
percentage points or a fraction.
Exercises
- Add to
versionesa fourth release of the March figure, 199.0, current from 2025-09-05. Adjust therealtime_endof the previous one and check which value was known on 2025-07-01 and on 2025-10-01.
Solution
v2 = versiones.copy()
v2.loc[2, "realtime_end"] = "2025-09-04"
v2.loc[3] = ["2025-03-01", 199.0, "2025-09-05", "9999-12-31"]
for f in ["2025-07-01", "2025-10-01"]:
print(f, conocido_el(v2, f)["value"].tolist())2025-07-01 [205.0]
2025-10-01 [199.0]On July 1, 205 was still current; from September 5, 199. Each revision closes the period of the previous one: if you
forget to adjust realtime_end, two versions appear to be current at the same time and conocido_el returns two
values.
- With
simular, what is the maximum number of requests per second that would go out if each EDGAR response took 150 ms and there were no limiter? Is the limiter needed then?
Solution
lentas = simular(100, latencia_ms=150)
print("max in one second:", peor_segundo(lentas))max in one second: 7With 150 ms per response, a sequential loop does not exceed 7 requests per second: below the limit. But the limiter is still needed: latency changes (a cache, another network) and, as soon as the code is parallelized with threads, the count multiplies. The limit is enforced in the code, not entrusted to the slowness of the network.
- You are going to study whether the spread between the one-year Euribor and the US 10-year bond anticipates anything in EURUSD. Say which sources you would take each series from, at what frequency they come and what you would have to resolve to join them.
Solution
One-year Euribor: Banco de España (or the ECB), monthly. US 10-year bond yield: FRED, daily. EURUSD: the ECB's daily reference rate (EXR), a closing figure from mid-afternoon in Frankfurt. To join them you have to decide the common frequency (monthly, if the Euribor is monthly), which daily value represents each month (the last one known at month end, not the average that is only known afterwards), the times of each figure (the ECB rate is set before the New York close) and, if revised data is used, the version that was known on each date (FRED vintages). In addition, cite the sources and check the terms of use of each series (lesson 1.3.7).
Footnotes
-
yfinance README and documentation, accessed on October 21, 2026; version 1.7.0 on PyPI. ↩
-
FRED API: series/observations, API key and errors (“Up to 120 requests per minute”), accessed on October 21, 2026. ↩
-
FRED: Legal Notices, Information and Disclaimers and FRED API Terms of Use. ↩
-
SEC: Accessing EDGAR Data, accessed on October 21, 2026. ↩
-
Banco de España: Statistics web service (API) and time-series bulk download, accessed on October 21, 2026. ↩
-
CNMV: short positions, other relevant information, official registers and statistics. ↩
Each answer is folded under its question
Self-assessment
What does the yfinance README itself say about its relationship with Yahoo?
- AThat it is Yahoo Finance's official API
- BThat the data is in the public domain
- CThat it has a commercial agreement with Yahoo
- DThat it is not affiliated with, endorsed or vetted by Yahoo, that it is meant for research and education and that you must check Yahoo's terms on the use of the data
Show answer
Answer D. That it is not affiliated with, endorsed or vetted by Yahoo, that it is meant for research and education and that you must check Yahoo's terms on the use of the data
yfinance is an open-source tool that uses Yahoo's publicly available APIs. The README refers to Yahoo's terms of use and reminds you that its API is intended for personal use.
What does the SEC require to download from EDGAR in an automated way?
- ADeclaring a User-Agent with a name and contact email and not exceeding 10 requests per second
- BA paid API key
- CRegistering as a professional
- DNothing
Show answer
Answer A. Declaring a User-Agent with a name and contact email and not exceeding 10 requests per second
The “Accessing EDGAR Data” page sets a current maximum of 10 requests per second and asks you to declare the User-Agent. The data.sec.gov APIs need no key.
In the FRED API, the
valuefield of each observation arrives as"4.33". What do you have to do?- ANothing, it is already a number
- BConvert it with
pd.to_numeric(..., errors="coerce"), because it is text and there may be non-numeric values - CMultiply it by 100
- DRead it as a date
Show answer
Answer B. Convert it with
pd.to_numeric(..., errors="coerce"), because it is text and there may be non-numeric valuesFRED's JSON brings the values in quotes. If they are not converted, arithmetic operations fail or concatenate text.
What does FRED's
realtime_start/realtime_endparameter allow?- ARequesting real-time data
- BChanging the time zone
- CRequesting the data as it was known on a past date, with later revisions excluded (ALFRED)
- DLimiting the number of observations
Show answer
Answer C. Requesting the data as it was known on a past date, with later revisions excluded (ALFRED)
Macro series get revised. With the real-time period (or
vintage_dates) you get the versions that existed on each date, which is what a backtest without look-ahead bias needs (lesson 1.5.2).What condition does the ECB set for reusing its public statistics?
- ACiting the source and not modifying the statistics (or their metadata); third-party data needs its author's permission
- BPaying for a license
- CNon-commercial use only
- DNone
Show answer
Answer A. Citing the source and not modifying the statistics (or their metadata); third-party data needs its author's permission
The ESCB statistics reuse policy allows free reuse citing the source (“Source: ECB statistics.”) and without modifying the data.
Key points
- “Free” covers two different things: official services with a documented API (FRED, EDGAR, ECB, Banco de España, CNMV) and unofficial tools that read a website (yfinance).
- yfinance is not affiliated with Yahoo, adjusts prices by default (
auto_adjust=True) and can return rate-limit errors (YFRateLimitError): it is fine for learning, not as the foundation of a system. - Each official source has its rules: FRED requires a key and allows up to 120 requests per minute; EDGAR, a declared User-Agent and 10 per second; the ECB, citing the source.
- Responses have to be typed: values as text in FRED, lists in columns in EDGAR, dozens of metadata columns in the ECB's CSV.
- Sources change: Stooq's direct CSV download, widely used for years, returned a browser check instead of the file on October 21, 2026.