---
title: "1.3.2 Free market data sources"
description: "yfinance, Stooq, FRED, SEC EDGAR, ECB, Banco de España and CNMV: what each free source provides, its access terms and how to read its responses."
url: "https://pythonparatrading.com/en/python-trading-course/level-1/free-market-data-sources/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-1/free-market-data-sources.png"
---

# 1.3.2 · 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.

- Course: [Level 1: Financial data, markets and analysis](https://pythonparatrading.com/en/python-trading-course/level-1/)
- Module: 1.3 · Market data sources
- Duration: 50 min
- Prerequisites: [1.3.1 · Types of market data: EOD, bars, ticks, order book and reference](https://pythonparatrading.com/en/python-trading-course/level-1/types-of-market-data/)

## By the end of this lesson

- Distinguish an official source with a documented API (FRED, EDGAR, ECB, Banco de España) from an unofficial tool (yfinance) and know what that implies for a system.
- Comply with each source's access rules: FRED's key, EDGAR's User-Agent header and maximum request rate, the ECB's attribution.
- Convert the JSON and CSV responses of FRED, EDGAR, the ECB and Banco de España into DataFrames with correct types.
- Choose the right free source for prices, macro, exchange rates, fundamentals or regulatory information.

## 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
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 explicit[^1]: 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.

```python skip title="yfinance_descarga.py"
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.** `download` has `auto_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 `YFRateLimitError`
  exception (“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).

<aside class="callout" data-kind="aviso">

For years Stooq was the other free source of daily prices in CSV. When checked on October 21, 2026, the direct
download of a CSV (`https://stooq.com/q/d/l/?s=aapl.us&i=d`) returns a page that asks for JavaScript to verify the
browser, not the file. A free source can stop serving from one day to the next: that is the argument for storing
what you download (lesson 1.4.7) and not depending on requesting it again.

</aside>

## 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 block[^2].

```python skip title="fred_observaciones.py"
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:

```python
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"]]
```
```output
{'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.62
```

Without `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:

```python
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"))
```
```output
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 permission[^3].

## 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 authentication[^4]; 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 rules[^5]: 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.

```python
def url_submissions(cik):
    return f"https://data.sec.gov/submissions/CIK{int(cik):010d}.json"

CABECERAS = {
    "User-Agent": "My Research Project contact@example.com",
    "Accept-Encoding": "gzip, deflate",
}
print(url_submissions(320193))
print(url_submissions("0000320193"))
```
```output
https://data.sec.gov/submissions/CIK0000320193.json
https://data.sec.gov/submissions/CIK0000320193.json
```

```python skip title="edgar_submissions.py"
import 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:

```python
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"]]
```
```output
   form  reportDate filingDate
0  10-Q  2025-09-30 2025-11-05
2  10-Q  2025-06-30 2025-08-06
```

The 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
flowchart LR
  P[Request] --> L{10 in 1 s?}
  L -- yes --> W[Wait]
  W --> L
  L -- no --> S[data.sec.gov]
```

```python
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))
```
```output
without limiter: 1.98 s, max in one second: 50
with limiter: 9.18 s, max in one second: 10
```

*Fig. — One hundred EDGAR requests on a virtual clock, with and without a 10-requests-per-second limiter (20 ms response).* Two step curves of cumulative requests against time. Without a limiter, in orange, the 100 requests go out in 2 seconds, with 50 in a single second. With the limiter, highlighted, they rise in steps of 10 every second and finish at 9.18 seconds, always below the dashed line of the SEC limit.

Without 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 `lastNObservations`[^6]. It requires no key.

```python skip title="bce_exr.py"
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:

```python
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%}")
```
```output
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 it[^7].

**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 Euribor[^8]:

```python skip title="bde_ultimo_dato.py"
url = ("https://app.bde.es/bierest/resources/srdatosapp/favoritas"
       "?idioma=es&series=D_1NBAF472")
```

```python
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"]]
```
```output
        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 files[^9]:

- **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:

```python
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"])
```
```output
{'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     3
```

The 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

1. Add to `versiones` a fourth release of the March figure, 199.0, current from 2025-09-05. Adjust the
   `realtime_end` of the previous one and check which value was known on 2025-07-01 and on 2025-10-01.

**Solution.**

```python
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())
```
```output
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.

2. 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.**

```python
lentas = simular(100, latencia_ms=150)
print("max in one second:", peor_segundo(lentas))
```
```output
max in one second: 7
```

With 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.

3. 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).

[^1]: [yfinance README](https://github.com/ranaroussi/yfinance) and [documentation](https://ranaroussi.github.io/yfinance/), accessed on October 21, 2026; version 1.7.0 on [PyPI](https://pypi.org/project/yfinance/).
[^2]: FRED API: [series/observations](https://fred.stlouisfed.org/docs/api/fred/series_observations.html), [API key](https://fred.stlouisfed.org/docs/api/api_key.html) and [errors](https://fred.stlouisfed.org/docs/api/fred/errors.html) (“Up to 120 requests per minute”), accessed on October 21, 2026.
[^3]: [FRED: Legal Notices, Information and Disclaimers](https://fred.stlouisfed.org/legal/) and [FRED API Terms of Use](https://fred.stlouisfed.org/docs/api/terms_of_use.html).
[^4]: [SEC: EDGAR Application Programming Interfaces](https://www.sec.gov/search-filings/edgar-application-programming-interfaces).
[^5]: [SEC: Accessing EDGAR Data](https://www.sec.gov/search-filings/edgar-search-assistance/accessing-edgar-data), accessed on October 21, 2026.
[^6]: [ECB Data Portal: API, Overview](https://data.ecb.europa.eu/help/api/overview) and [Data](https://data.ecb.europa.eu/help/api/data).
[^7]: [ECB: Policy regarding the reuse of ESCB statistics](https://www.ecb.europa.eu/stats/ecb_statistics/governance_and_quality_framework/html/usage_policy.en.html).
[^8]: [Banco de España: Statistics web service (API)](https://www.bde.es/webbe/en/estadisticas/recursos/api-estadisticas-bde.html) and [time-series bulk download](https://www.bde.es/webbe/en/estadisticas/recursos/descargas-completas.html), accessed on October 21, 2026.
[^9]: CNMV: [short positions](https://www.cnmv.es/portal/consultas/busqueda.aspx?id=29), [other relevant information](https://www.cnmv.es/portal/Otra-Informacion-Relevante/resultado-oir.aspx?dias=30), [official registers](https://www.cnmv.es/portal/Menu/Registros-Oficiales-Emisores.aspx) and [statistics](https://www.cnmv.es/portal/Publicaciones/estadisticas.aspx).

## Self-assessment

1. What does the yfinance README itself say about its relationship with Yahoo?

   - A. That it is Yahoo Finance's official API
   - B. That the data is in the public domain
   - C. That it has a commercial agreement with Yahoo
   - 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

   *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.

2. What does the SEC require to download from EDGAR in an automated way?

   - A. Declaring a User-Agent with a name and contact email and not exceeding 10 requests per second
   - B. A paid API key
   - C. Registering as a professional
   - D. Nothing

   *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.

3. In the FRED API, the `value` field of each observation arrives as `"4.33"`. What do you have to do?

   - A. Nothing, it is already a number
   - B. Convert it with `pd.to_numeric(..., errors="coerce")`, because it is text and there may be non-numeric values
   - C. Multiply it by 100
   - D. Read it as a date

   *Answer B: Convert it with `pd.to_numeric(..., errors="coerce")`, because it is text and there may be non-numeric values.* FRED's JSON brings the values in quotes. If they are not converted, arithmetic operations fail or concatenate text.

4. What does FRED's `realtime_start` / `realtime_end` parameter allow?

   - A. Requesting real-time data
   - B. Changing the time zone
   - C. Requesting the data as it was known on a past date, with later revisions excluded (ALFRED)
   - D. Limiting the number of observations

   *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).

5. What condition does the ECB set for reusing its public statistics?

   - A. Citing the source and not modifying the statistics (or their metadata); third-party data needs its author's permission
   - B. Paying for a license
   - C. Non-commercial use only
   - D. None

   *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.

## Resources

### Research

- [Python for Trading Course - Part 1: Market Data with OpenBB](https://pythonparatrading.com/en/openbb-free-market-data/) · Research-0014 · 10 min

### Documentation

- [yfinance: README (legal disclaimer and usage)](https://github.com/ranaroussi/yfinance) · github.com
- [FRED API: fred/series/observations](https://fred.stlouisfed.org/docs/api/fred/series_observations.html) · fred.stlouisfed.org
- [SEC: Accessing EDGAR Data (fair access, User-Agent)](https://www.sec.gov/search-filings/edgar-search-assistance/accessing-edgar-data) · sec.gov
- [ECB: ECB Data Portal, data API](https://data.ecb.europa.eu/help/api/data) · data.ecb.europa.eu
- [Banco de España: statistics web service (API)](https://www.bde.es/webbe/en/estadisticas/recursos/api-estadisticas-bde.html) · bde.es
- [CNMV: short positions, notification search](https://www.cnmv.es/portal/consultas/busqueda.aspx?id=29) · cnmv.es

Previous lesson: [1.3.1 · Types of market data: EOD, bars, ticks, order book and reference](https://pythonparatrading.com/en/python-trading-course/level-1/types-of-market-data/)

Next lesson: [1.3.3 · Retail and prosumer market data APIs](https://pythonparatrading.com/en/python-trading-course/level-1/retail-and-prosumer-market-data-apis/)
